shelving 1.201.0 → 1.202.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/markup/render.d.ts +4 -4
- package/markup/render.js +4 -4
- package/markup/rule/unordered.d.ts +2 -2
- package/markup/util/rule.d.ts +4 -4
- package/package.json +4 -2
- package/ui/app/App.d.ts +2 -2
- package/ui/app/App.js +4 -6
- package/ui/app/App.tsx +6 -8
- package/ui/block/Video.js +1 -1
- package/ui/block/Video.tsx +1 -1
- package/ui/layout/SidebarLayout.d.ts +17 -0
- package/ui/layout/SidebarLayout.js +15 -0
- package/ui/layout/SidebarLayout.module.css +47 -0
- package/ui/layout/SidebarLayout.tsx +36 -0
- package/ui/layout/index.d.ts +1 -0
- package/ui/layout/index.js +1 -0
- package/ui/layout/index.tsx +1 -0
- package/ui/misc/Meta.d.ts +2 -2
- package/ui/misc/Meta.tsx +2 -2
- package/ui/page/HTML.d.ts +10 -0
- package/ui/page/HTML.js +11 -0
- package/ui/page/HTML.tsx +20 -0
- package/ui/page/Head.d.ts +0 -5
- package/ui/page/Head.js +4 -2
- package/ui/page/Head.tsx +3 -8
- package/ui/page/Page.d.ts +4 -3
- package/ui/page/Page.js +2 -1
- package/ui/page/Page.tsx +4 -3
- package/ui/page/index.d.ts +1 -0
- package/ui/page/index.js +1 -0
- package/ui/page/index.ts +1 -0
- package/ui/router/Router.d.ts +13 -11
- package/ui/router/Router.js +13 -3
- package/ui/router/Router.tsx +19 -14
- package/ui/router/RouterStore.d.ts +1 -1
- package/ui/router/RouterStore.js +2 -2
- package/ui/router/RouterStore.tsx +2 -2
- package/ui/util/meta.d.ts +8 -8
- package/ui/util/meta.js +5 -5
- package/ui/util/meta.ts +11 -11
- package/util/element.d.ts +32 -0
- package/util/element.js +41 -0
- package/util/index.d.ts +1 -1
- package/util/index.js +1 -1
- package/util/jsx.d.ts +0 -32
- package/util/jsx.js +0 -41
package/markup/render.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Elements } from "../util/element.js";
|
|
2
2
|
import type { MarkupOptions } from "./util/options.js";
|
|
3
3
|
/**
|
|
4
|
-
* Parse a text string as Markdownish syntax and render it as
|
|
4
|
+
* Parse a text string as Markdownish syntax and render it as elements.
|
|
5
5
|
* - Syntax is not defined by this code, but by the rules supplied to it.
|
|
6
6
|
*
|
|
7
7
|
* @param input The string content possibly containing markup syntax, e.g. "This is a *bold* string.
|
|
8
8
|
* @param options An options object for the render.
|
|
9
9
|
* @param context The context to render in (defaults to `"block"`).
|
|
10
10
|
*
|
|
11
|
-
* @returns
|
|
11
|
+
* @returns Elements, i.e. either a complete `Element`, `null`, `undefined`, `string`, or an array of zero or more of those.
|
|
12
12
|
*/
|
|
13
|
-
export declare function renderMarkup(input: string, options: MarkupOptions, context?: string):
|
|
13
|
+
export declare function renderMarkup(input: string, options: MarkupOptions, context?: string): Elements;
|
package/markup/render.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Parse a text string as Markdownish syntax and render it as
|
|
2
|
+
* Parse a text string as Markdownish syntax and render it as elements.
|
|
3
3
|
* - Syntax is not defined by this code, but by the rules supplied to it.
|
|
4
4
|
*
|
|
5
5
|
* @param input The string content possibly containing markup syntax, e.g. "This is a *bold* string.
|
|
6
6
|
* @param options An options object for the render.
|
|
7
7
|
* @param context The context to render in (defaults to `"block"`).
|
|
8
8
|
*
|
|
9
|
-
* @returns
|
|
9
|
+
* @returns Elements, i.e. either a complete `Element`, `null`, `undefined`, `string`, or an array of zero or more of those.
|
|
10
10
|
*/
|
|
11
11
|
export function renderMarkup(input, options, context = "block") {
|
|
12
12
|
const arr = Array.from(_parseString(input, options, context));
|
|
13
13
|
return !arr.length ? null : arr.length === 1 ? arr[0] : arr;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Parse a string to its corresponding
|
|
17
|
-
* - This code is heavily inspired by `simple-markdown`, but intends to be even simpler (and faster) by always producing
|
|
16
|
+
* Parse a string to its corresponding elements in a given context.
|
|
17
|
+
* - This code is heavily inspired by `simple-markdown`, but intends to be even simpler (and faster) by always producing elements.
|
|
18
18
|
*/
|
|
19
19
|
function* _parseString(
|
|
20
20
|
/** The input string. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Element } from "../../util/element.js";
|
|
2
2
|
import type { MarkupOptions } from "../util/options.js";
|
|
3
3
|
export declare const UNORDERED_REGEXP: import("../../index.js").NamedRegExp<{
|
|
4
4
|
list?: string;
|
|
@@ -13,4 +13,4 @@ export declare const UNORDERED_REGEXP: import("../../index.js").NamedRegExp<{
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const UNORDERED_RULE: import("../util/rule.js").MarkupRule;
|
|
15
15
|
/** Parse a markdown list into a set of items elements. */
|
|
16
|
-
export declare function _getItems(list: string, options: MarkupOptions): Iterable<
|
|
16
|
+
export declare function _getItems(list: string, options: MarkupOptions): Iterable<Element>;
|
package/markup/util/rule.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Element } from "../../util/element.js";
|
|
2
2
|
import type { NamedRegExp, NamedRegExpExecArray } from "../../util/regexp.js";
|
|
3
3
|
import type { MarkupOptions } from "./options.js";
|
|
4
4
|
export type MarkupContexts = [string, ...string[]];
|
|
5
5
|
export interface MarkupRule {
|
|
6
6
|
/** Regular expression used for matching the rule. */
|
|
7
7
|
regexp: RegExp;
|
|
8
|
-
/** Use the matched data to render
|
|
9
|
-
render(match: RegExpExecArray, options: MarkupOptions, key: string):
|
|
8
|
+
/** Use the matched data to render an element. */
|
|
9
|
+
render(match: RegExpExecArray, options: MarkupOptions, key: string): Element;
|
|
10
10
|
/** One or more contexts this rule should render in. */
|
|
11
11
|
contexts: MarkupContexts;
|
|
12
12
|
/** Priority for this rule (higher priority rules override lower priority rules). */
|
|
@@ -14,4 +14,4 @@ export interface MarkupRule {
|
|
|
14
14
|
}
|
|
15
15
|
export type MarkupRules = readonly MarkupRule[];
|
|
16
16
|
/** Helper to make it easier to create typed `MarkupRule` instances using `NamedRegExp` regular expressions. */
|
|
17
|
-
export declare function getMarkupRule<T extends NamedRegExp | RegExp>(regexp: T, render: T extends NamedRegExp<infer X> ? (match: NamedRegExpExecArray<X>, options: MarkupOptions, key: string) =>
|
|
17
|
+
export declare function getMarkupRule<T extends NamedRegExp | RegExp>(regexp: T, render: T extends NamedRegExp<infer X> ? (match: NamedRegExpExecArray<X>, options: MarkupOptions, key: string) => Element : (match: RegExpExecArray, options: MarkupOptions, key: string) => Element, contexts: MarkupContexts, priority?: number): MarkupRule;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shelving",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.202.0",
|
|
4
4
|
"author": "Dave Houlbrooke <dave@shax.com>",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"@typescript/native-preview": "^7.0.0-dev.20260502.1",
|
|
19
19
|
"firebase": "^12.12.1",
|
|
20
20
|
"react": "canary",
|
|
21
|
-
"react-dom": "canary"
|
|
21
|
+
"react-dom": "canary",
|
|
22
|
+
"typescript": "^5"
|
|
22
23
|
},
|
|
23
24
|
"peerDependencies": {
|
|
24
25
|
"@google-cloud/firestore": ">=7.0.0",
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"test:unit": "bun test --concurrent --only-failures",
|
|
70
71
|
"fix": "bun run --sequential fix:*",
|
|
71
72
|
"fix:0:lint": "biome check --write .",
|
|
73
|
+
"docs": "bun ./scripts/docs.tsx",
|
|
72
74
|
"build": "bun run --sequential build:*",
|
|
73
75
|
"build:0:setup": "rm -rf ./dist && mkdir -p ./dist",
|
|
74
76
|
"build:1:copy": "cp package.json dist/package.json && cp LICENSE.md dist/LICENSE.md && cp README.md dist/README.md && cp .npmignore dist/.npmignore && cp -r modules/ui dist/ui",
|
package/ui/app/App.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
export interface AppProps extends
|
|
2
|
+
import type { PossibleMeta } from "../util/meta.js";
|
|
3
|
+
export interface AppProps extends PossibleMeta {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
}
|
|
6
6
|
/**
|
package/ui/app/App.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect } from "react";
|
|
3
3
|
import { Meta } from "../misc/Meta.js";
|
|
4
4
|
import APP_CSS from "./App.module.css";
|
|
5
|
-
const
|
|
5
|
+
const APP_CLASS = APP_CSS.app;
|
|
6
6
|
/**
|
|
7
7
|
* Root component for an application.
|
|
8
8
|
* - Adds the theme CSS class (which sets CSS token variables on `:root`) to `document.body` on mount and removes it on unmount.
|
|
@@ -10,12 +10,10 @@ const _appClass = APP_CSS.app;
|
|
|
10
10
|
*/
|
|
11
11
|
export function App({ children, ...metadata }) {
|
|
12
12
|
useEffect(() => {
|
|
13
|
-
if (!
|
|
13
|
+
if (!APP_CLASS)
|
|
14
14
|
return;
|
|
15
|
-
document.body.classList.add(
|
|
16
|
-
return () =>
|
|
17
|
-
document.body.classList.remove(_appClass);
|
|
18
|
-
};
|
|
15
|
+
document.body.classList.add(APP_CLASS);
|
|
16
|
+
return () => document.body.classList.remove(APP_CLASS);
|
|
19
17
|
}, []);
|
|
20
18
|
return _jsx(Meta, { ...metadata, children: children });
|
|
21
19
|
}
|
package/ui/app/App.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode, useEffect } from "react";
|
|
2
2
|
import { Meta } from "../misc/Meta.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { PossibleMeta } from "../util/meta.js";
|
|
4
4
|
import APP_CSS from "./App.module.css";
|
|
5
5
|
|
|
6
|
-
export interface AppProps extends
|
|
6
|
+
export interface AppProps extends PossibleMeta {
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const APP_CLASS = APP_CSS.app;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Root component for an application.
|
|
@@ -16,11 +16,9 @@ const _appClass = APP_CSS.app;
|
|
|
16
16
|
*/
|
|
17
17
|
export function App({ children, ...metadata }: AppProps): ReactElement {
|
|
18
18
|
useEffect(() => {
|
|
19
|
-
if (!
|
|
20
|
-
document.body.classList.add(
|
|
21
|
-
return () =>
|
|
22
|
-
document.body.classList.remove(_appClass);
|
|
23
|
-
};
|
|
19
|
+
if (!APP_CLASS) return;
|
|
20
|
+
document.body.classList.add(APP_CLASS);
|
|
21
|
+
return () => document.body.classList.remove(APP_CLASS);
|
|
24
22
|
}, []);
|
|
25
23
|
return <Meta {...metadata}>{children}</Meta>;
|
|
26
24
|
}
|
package/ui/block/Video.js
CHANGED
|
@@ -22,7 +22,7 @@ export function VideoButton({ children, title, onClick, disabled, ...variants })
|
|
|
22
22
|
}
|
|
23
23
|
/** Button to make a video element go fullscreen. */
|
|
24
24
|
export function FullscreenVideoButton() {
|
|
25
|
-
const [isFull, setFull] = useState(!!document.fullscreenElement);
|
|
25
|
+
const [isFull, setFull] = useState(() => typeof document !== "undefined" && !!document.fullscreenElement);
|
|
26
26
|
useEffect(() => {
|
|
27
27
|
const onChange = () => setFull(!!document.fullscreenElement);
|
|
28
28
|
document.addEventListener("fullscreenchange", onChange);
|
package/ui/block/Video.tsx
CHANGED
|
@@ -63,7 +63,7 @@ export interface FullscreenVideoButtonProps {
|
|
|
63
63
|
|
|
64
64
|
/** Button to make a video element go fullscreen. */
|
|
65
65
|
export function FullscreenVideoButton(): ReactElement | null {
|
|
66
|
-
const [isFull, setFull] = useState(!!document.fullscreenElement);
|
|
66
|
+
const [isFull, setFull] = useState(() => typeof document !== "undefined" && !!document.fullscreenElement);
|
|
67
67
|
|
|
68
68
|
useEffect(() => {
|
|
69
69
|
const onChange = () => setFull(!!document.fullscreenElement);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
|
|
3
|
+
export interface SidebarLayoutProps {
|
|
4
|
+
/** Content rendered in the fixed-width side column. */
|
|
5
|
+
sidebar: ReactNode;
|
|
6
|
+
/** Main content rendered in the scrollable content column. */
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
/** Render the sidebar on the right rather than the left. */
|
|
9
|
+
right?: boolean | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
|
|
13
|
+
* - The sidebar collapses above the main content on narrow viewports.
|
|
14
|
+
* - Use the `--sidebar-layout-width` and `--sidebar-layout-bg` custom properties to override defaults.
|
|
15
|
+
*/
|
|
16
|
+
export declare function SidebarLayout({ sidebar, children, right }: SidebarLayoutProps): ReactElement;
|
|
17
|
+
export { SIDEBAR_LAYOUT_CSS };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { getClass } from "../util/css.js";
|
|
3
|
+
import { LAYOUT_CSS } from "./Layout.js";
|
|
4
|
+
import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
|
|
5
|
+
/**
|
|
6
|
+
* Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
|
|
7
|
+
* - The sidebar collapses above the main content on narrow viewports.
|
|
8
|
+
* - Use the `--sidebar-layout-width` and `--sidebar-layout-bg` custom properties to override defaults.
|
|
9
|
+
*/
|
|
10
|
+
export function SidebarLayout({ sidebar, children, right = false }) {
|
|
11
|
+
const sidebarEl = (_jsx("aside", { className: SIDEBAR_LAYOUT_CSS.sidebar, children: sidebar }, "sidebar"));
|
|
12
|
+
const contentEl = (_jsx("div", { className: SIDEBAR_LAYOUT_CSS.content, children: _jsx("div", { className: SIDEBAR_LAYOUT_CSS.contentInner, children: children }) }, "content"));
|
|
13
|
+
return (_jsx("main", { className: getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout), children: right ? [contentEl, sidebarEl] : [sidebarEl, contentEl] }));
|
|
14
|
+
}
|
|
15
|
+
export { SIDEBAR_LAYOUT_CSS };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Sidebar layout: a fixed-width column on the left, scrollable main content on the right.
|
|
3
|
+
*
|
|
4
|
+
* The outer `.layout` class (from Layout.module.css) owns the overall page scroll,
|
|
5
|
+
* but here we override that: the sidebar and the main column each scroll independently.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.main {
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: var(--sidebar-layout-width, 17.5rem) 1fr;
|
|
11
|
+
gap: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.sidebar {
|
|
17
|
+
overflow-y: auto;
|
|
18
|
+
overscroll-behavior: contain;
|
|
19
|
+
padding: var(--spacing-block);
|
|
20
|
+
background: var(--sidebar-layout-bg, var(--color-surface));
|
|
21
|
+
border-right: 1px solid var(--color-border);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.content {
|
|
25
|
+
overflow-y: auto;
|
|
26
|
+
overscroll-behavior: contain;
|
|
27
|
+
padding: var(--spacing-block) var(--spacing-spacious);
|
|
28
|
+
min-width: 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.contentInner {
|
|
32
|
+
width: 100%;
|
|
33
|
+
max-width: var(--width-wide);
|
|
34
|
+
margin: 0 auto;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* On narrow viewports collapse to a single column with the sidebar above. */
|
|
38
|
+
@media (max-width: 48rem) {
|
|
39
|
+
.main {
|
|
40
|
+
grid-template-columns: 1fr;
|
|
41
|
+
}
|
|
42
|
+
.sidebar {
|
|
43
|
+
border-right: none;
|
|
44
|
+
border-bottom: 1px solid var(--color-border);
|
|
45
|
+
max-height: 50vh;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { getClass } from "../util/css.js";
|
|
3
|
+
import { LAYOUT_CSS } from "./Layout.js";
|
|
4
|
+
import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
|
|
5
|
+
|
|
6
|
+
export interface SidebarLayoutProps {
|
|
7
|
+
/** Content rendered in the fixed-width side column. */
|
|
8
|
+
sidebar: ReactNode;
|
|
9
|
+
/** Main content rendered in the scrollable content column. */
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
/** Render the sidebar on the right rather than the left. */
|
|
12
|
+
right?: boolean | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
|
|
17
|
+
* - The sidebar collapses above the main content on narrow viewports.
|
|
18
|
+
* - Use the `--sidebar-layout-width` and `--sidebar-layout-bg` custom properties to override defaults.
|
|
19
|
+
*/
|
|
20
|
+
export function SidebarLayout({ sidebar, children, right = false }: SidebarLayoutProps): ReactElement {
|
|
21
|
+
const sidebarEl = (
|
|
22
|
+
<aside key="sidebar" className={SIDEBAR_LAYOUT_CSS.sidebar}>
|
|
23
|
+
{sidebar}
|
|
24
|
+
</aside>
|
|
25
|
+
);
|
|
26
|
+
const contentEl = (
|
|
27
|
+
<div key="content" className={SIDEBAR_LAYOUT_CSS.content}>
|
|
28
|
+
<div className={SIDEBAR_LAYOUT_CSS.contentInner}>{children}</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
return (
|
|
32
|
+
<main className={getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout)}>{right ? [contentEl, sidebarEl] : [sidebarEl, contentEl]}</main>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { SIDEBAR_LAYOUT_CSS };
|
package/ui/layout/index.d.ts
CHANGED
package/ui/layout/index.js
CHANGED
package/ui/layout/index.tsx
CHANGED
package/ui/misc/Meta.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import { type MetaData, type
|
|
3
|
-
export interface MetaProps extends
|
|
2
|
+
import { type MetaData, type PossibleMeta } from "../util/meta.js";
|
|
3
|
+
export interface MetaProps extends PossibleMeta {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
}
|
|
6
6
|
/** Create or update the current meta context. */
|
package/ui/misc/Meta.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createContext, type ReactNode, use } from "react";
|
|
2
|
-
import { type MetaData, mergeMeta, type
|
|
2
|
+
import { type MetaData, mergeMeta, type PossibleMeta } from "../util/meta.js";
|
|
3
3
|
|
|
4
4
|
/** Context to store the `Config` object. */
|
|
5
5
|
const _MetaContext = createContext<MetaData>({});
|
|
6
6
|
_MetaContext.displayName = "MetaContext";
|
|
7
7
|
|
|
8
|
-
export interface MetaProps extends
|
|
8
|
+
export interface MetaProps extends PossibleMeta {
|
|
9
9
|
children: ReactNode;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
export interface HTMLProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Output a `<html>` element wrapping `<body id="root">`.
|
|
7
|
+
* - No `<head>` element is rendered. Head tags (`<title>`, `<meta>`, `<link>`, `<script>`) are emitted inline by `<Page>` / `<Head>` lower in the tree, and React 19 hoists them automatically — to the document `<head>` on the client, and to a generated `<head>` element during `renderToString` SSR.
|
|
8
|
+
* - This means the same component tree works for both modes without any shell-aware logic.
|
|
9
|
+
*/
|
|
10
|
+
export declare function HTML({ children }: HTMLProps): ReactElement;
|
package/ui/page/HTML.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { requireMeta } from "../misc/Meta.js";
|
|
3
|
+
/**
|
|
4
|
+
* Output a `<html>` element wrapping `<body id="root">`.
|
|
5
|
+
* - No `<head>` element is rendered. Head tags (`<title>`, `<meta>`, `<link>`, `<script>`) are emitted inline by `<Page>` / `<Head>` lower in the tree, and React 19 hoists them automatically — to the document `<head>` on the client, and to a generated `<head>` element during `renderToString` SSR.
|
|
6
|
+
* - This means the same component tree works for both modes without any shell-aware logic.
|
|
7
|
+
*/
|
|
8
|
+
export function HTML({ children }) {
|
|
9
|
+
const { language } = requireMeta();
|
|
10
|
+
return (_jsx("html", { lang: language, children: _jsx("body", { id: "root", children: children }) }));
|
|
11
|
+
}
|
package/ui/page/HTML.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { requireMeta } from "../misc/Meta.js";
|
|
3
|
+
|
|
4
|
+
export interface HTMLProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Output a `<html>` element wrapping `<body id="root">`.
|
|
10
|
+
* - No `<head>` element is rendered. Head tags (`<title>`, `<meta>`, `<link>`, `<script>`) are emitted inline by `<Page>` / `<Head>` lower in the tree, and React 19 hoists them automatically — to the document `<head>` on the client, and to a generated `<head>` element during `renderToString` SSR.
|
|
11
|
+
* - This means the same component tree works for both modes without any shell-aware logic.
|
|
12
|
+
*/
|
|
13
|
+
export function HTML({ children }: HTMLProps): ReactElement {
|
|
14
|
+
const { language } = requireMeta();
|
|
15
|
+
return (
|
|
16
|
+
<html lang={language}>
|
|
17
|
+
<body id="root">{children}</body>
|
|
18
|
+
</html>
|
|
19
|
+
);
|
|
20
|
+
}
|
package/ui/page/Head.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
|
-
declare const _componentProps: unique symbol;
|
|
3
|
-
export interface HeadProps {
|
|
4
|
-
readonly [_componentProps]?: never;
|
|
5
|
-
}
|
|
6
2
|
/** Use the details from the current page data context to set the document `<title>`, meta tags, and history state. */
|
|
7
3
|
export declare function Head(): ReactElement;
|
|
8
|
-
export {};
|
package/ui/page/Head.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
3
|
import { notNullish } from "../../util/null.js";
|
|
4
4
|
import { getProps } from "../../util/object.js";
|
|
@@ -11,10 +11,12 @@ const R_HTTP_EQUIV = /^[A-Z][a-zA-Z0-9]*(-[A-Z][a-zA-Z0-9]*)*$/;
|
|
|
11
11
|
export function Head() {
|
|
12
12
|
const { url, title, base, app, links, tags } = requireMeta();
|
|
13
13
|
useEffect(() => {
|
|
14
|
+
if (typeof window === "undefined")
|
|
15
|
+
return;
|
|
14
16
|
if (url)
|
|
15
17
|
window.history.replaceState(null, "", url);
|
|
16
18
|
}, [url]);
|
|
17
|
-
return (_jsxs(
|
|
19
|
+
return (_jsxs("head", { children: [_jsx("title", { children: joinTitles(title, app) }), base && _jsx("base", { href: base.href }), tags &&
|
|
18
20
|
getProps(tags)
|
|
19
21
|
.map(([k, x]) => {
|
|
20
22
|
if (notNullish(x)) {
|
package/ui/page/Head.tsx
CHANGED
|
@@ -8,22 +8,17 @@ import { joinTitles } from "../util/meta.js";
|
|
|
8
8
|
/** Meta tags with a capital first letter and hyphens, e.g. `Content-Security-Policy` or `Accept`, are `http-equiv=""` tags. */
|
|
9
9
|
const R_HTTP_EQUIV = /^[A-Z][a-zA-Z0-9]*(-[A-Z][a-zA-Z0-9]*)*$/;
|
|
10
10
|
|
|
11
|
-
declare const _componentProps: unique symbol;
|
|
12
|
-
|
|
13
|
-
export interface HeadProps {
|
|
14
|
-
readonly [_componentProps]?: never;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
/** Use the details from the current page data context to set the document `<title>`, meta tags, and history state. */
|
|
18
12
|
export function Head(): ReactElement {
|
|
19
13
|
const { url, title, base, app, links, tags } = requireMeta();
|
|
20
14
|
|
|
21
15
|
useEffect(() => {
|
|
16
|
+
if (typeof window === "undefined") return;
|
|
22
17
|
if (url) window.history.replaceState(null, "", url);
|
|
23
18
|
}, [url]);
|
|
24
19
|
|
|
25
20
|
return (
|
|
26
|
-
|
|
21
|
+
<head>
|
|
27
22
|
<title>{joinTitles(title, app)}</title>
|
|
28
23
|
{base && <base href={base.href} />}
|
|
29
24
|
{tags &&
|
|
@@ -44,6 +39,6 @@ export function Head(): ReactElement {
|
|
|
44
39
|
<link key={k} rel={k} href={v} type={v.endsWith(".png") ? "image/png" : v.endsWith(".ico") ? "image/x-icon" : undefined} />
|
|
45
40
|
) : null,
|
|
46
41
|
)}
|
|
47
|
-
|
|
42
|
+
</head>
|
|
48
43
|
);
|
|
49
44
|
}
|
package/ui/page/Page.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from "react";
|
|
2
|
-
import type {
|
|
3
|
-
export interface PageProps extends
|
|
2
|
+
import type { PossibleMeta } from "../util/meta.js";
|
|
3
|
+
export interface PageProps extends PossibleMeta {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Component for a single page (or screen) within an app.
|
|
8
|
-
* -
|
|
8
|
+
* - Sets the document title and other head metadata.
|
|
9
|
+
* - `<Head />` renders `<title>` / `<meta>` / `<link>` tags inline; React 19 hoists them automatically to the document `<head>` (or to `document.head` on the client). Works for both client-mounted SPAs and `renderToString` SSR.
|
|
9
10
|
*/
|
|
10
11
|
export declare function Page({ children, ...metadata }: PageProps): ReactElement;
|
package/ui/page/Page.js
CHANGED
|
@@ -3,7 +3,8 @@ import { Meta } from "../misc/Meta.js";
|
|
|
3
3
|
import { Head } from "./Head.js";
|
|
4
4
|
/**
|
|
5
5
|
* Component for a single page (or screen) within an app.
|
|
6
|
-
* -
|
|
6
|
+
* - Sets the document title and other head metadata.
|
|
7
|
+
* - `<Head />` renders `<title>` / `<meta>` / `<link>` tags inline; React 19 hoists them automatically to the document `<head>` (or to `document.head` on the client). Works for both client-mounted SPAs and `renderToString` SSR.
|
|
7
8
|
*/
|
|
8
9
|
export function Page({ children, ...metadata }) {
|
|
9
10
|
return (_jsxs(Meta, { ...metadata, children: [_jsx(Head, {}), children] }));
|
package/ui/page/Page.tsx
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { ReactElement, ReactNode } from "react";
|
|
2
2
|
import { Meta } from "../misc/Meta.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { PossibleMeta } from "../util/meta.js";
|
|
4
4
|
import { Head } from "./Head.js";
|
|
5
5
|
|
|
6
|
-
export interface PageProps extends
|
|
6
|
+
export interface PageProps extends PossibleMeta {
|
|
7
7
|
children: ReactNode;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Component for a single page (or screen) within an app.
|
|
12
|
-
* -
|
|
12
|
+
* - Sets the document title and other head metadata.
|
|
13
|
+
* - `<Head />` renders `<title>` / `<meta>` / `<link>` tags inline; React 19 hoists them automatically to the document `<head>` (or to `document.head` on the client). Works for both client-mounted SPAs and `renderToString` SSR.
|
|
13
14
|
*/
|
|
14
15
|
export function Page({ children, ...metadata }: PageProps): ReactElement {
|
|
15
16
|
return (
|
package/ui/page/index.d.ts
CHANGED
package/ui/page/index.js
CHANGED
package/ui/page/index.ts
CHANGED
package/ui/router/Router.d.ts
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import { type ReactElement, type ReactNode } from "react";
|
|
2
2
|
import type { Routes } from "./Routes.js";
|
|
3
3
|
export interface RouterProps {
|
|
4
|
-
routes: Routes;
|
|
5
|
-
children?: ReactNode;
|
|
6
|
-
}
|
|
7
|
-
export interface RouterIsolateProps {
|
|
8
|
-
children: ReactNode;
|
|
9
|
-
}
|
|
10
|
-
declare const _routerOutputProps: unique symbol;
|
|
11
|
-
export interface RouterOutputProps {
|
|
12
|
-
readonly [_routerOutputProps]?: never;
|
|
4
|
+
readonly routes: Routes;
|
|
5
|
+
readonly children?: ReactNode | undefined;
|
|
13
6
|
}
|
|
14
7
|
/**
|
|
15
8
|
* Provides Router based on the browser URL and renders the active route.
|
|
9
|
+
* - Creates a new `RouterStore`
|
|
10
|
+
* - Exposes it via `requireRouter()` to descendants.
|
|
11
|
+
* - Renders the route matched for the current path.
|
|
12
|
+
* - Intercepts same-origin anchor clicks (excluding anchors with a `download` attribute) to perform client-side routing.
|
|
13
|
+
* - Synchronizes store state with browser back/forward (`popstate`) events.
|
|
16
14
|
*
|
|
17
|
-
*
|
|
15
|
+
* Integrated with meta via the current `Meta`
|
|
16
|
+
* - Set `url` in a wrapper component to set the initial URL for this router, e.g. `<App url="/a/b/c">`
|
|
17
|
+
* - Set `base` , e.g. `<App base="https://p.com/subdir" />`
|
|
18
18
|
*
|
|
19
19
|
* @param routes - Route definitions used to select and render the active route
|
|
20
20
|
* @param children - Content to render inside the Router Meta; defaults to the routing output component
|
|
21
21
|
* @returns The Router provider element rendering the active route and supplying Router context
|
|
22
22
|
*/
|
|
23
23
|
export declare function Router({ routes, children }: RouterProps): ReactElement;
|
|
24
|
+
export interface RouterIsolateProps {
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}
|
|
24
27
|
/**
|
|
25
28
|
* Isolate a set of children so they remount on Router.
|
|
26
29
|
* - Use this when you want to ensure an entire React tree is remounted when the Router route changes.
|
|
@@ -28,4 +31,3 @@ export declare function Router({ routes, children }: RouterProps): ReactElement;
|
|
|
28
31
|
export declare function RouterIsolate({ children }: RouterIsolateProps): ReactElement;
|
|
29
32
|
/** Show the content active for the current router route. */
|
|
30
33
|
export declare function RouterOutput(): ReactElement | null;
|
|
31
|
-
export {};
|
package/ui/router/Router.js
CHANGED
|
@@ -3,23 +3,33 @@ import { Fragment, useEffect } from "react";
|
|
|
3
3
|
import { useInstance } from "../../react/useInstance.js";
|
|
4
4
|
import { useStore } from "../../react/useStore.js";
|
|
5
5
|
import { getURIParams } from "../../util/uri.js";
|
|
6
|
-
import { Meta } from "../misc/Meta.js";
|
|
6
|
+
import { Meta, requireMeta } from "../misc/Meta.js";
|
|
7
7
|
import { RouterContext, requireRouter } from "./RouterContext.js";
|
|
8
8
|
import { RouterStore } from "./RouterStore.js";
|
|
9
9
|
/**
|
|
10
10
|
* Provides Router based on the browser URL and renders the active route.
|
|
11
|
+
* - Creates a new `RouterStore`
|
|
12
|
+
* - Exposes it via `requireRouter()` to descendants.
|
|
13
|
+
* - Renders the route matched for the current path.
|
|
14
|
+
* - Intercepts same-origin anchor clicks (excluding anchors with a `download` attribute) to perform client-side routing.
|
|
15
|
+
* - Synchronizes store state with browser back/forward (`popstate`) events.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
17
|
+
* Integrated with meta via the current `Meta`
|
|
18
|
+
* - Set `url` in a wrapper component to set the initial URL for this router, e.g. `<App url="/a/b/c">`
|
|
19
|
+
* - Set `base` , e.g. `<App base="https://p.com/subdir" />`
|
|
13
20
|
*
|
|
14
21
|
* @param routes - Route definitions used to select and render the active route
|
|
15
22
|
* @param children - Content to render inside the Router Meta; defaults to the routing output component
|
|
16
23
|
* @returns The Router provider element rendering the active route and supplying Router context
|
|
17
24
|
*/
|
|
18
25
|
export function Router({ routes, children = _jsx(RouterOutput, {}) }) {
|
|
19
|
-
const
|
|
26
|
+
const { url, base } = requireMeta();
|
|
27
|
+
const nav = useInstance(RouterStore, routes, url, base);
|
|
20
28
|
useStore(nav);
|
|
21
29
|
// Effect to attach listeners when this router is active.
|
|
22
30
|
useEffect(() => {
|
|
31
|
+
if (typeof document === "undefined" || typeof window === "undefined")
|
|
32
|
+
return;
|
|
23
33
|
// Listen for `click` events on `<a href="">` anchors and fire the forward event instead.
|
|
24
34
|
const onClick = (e) => {
|
|
25
35
|
// Look for clicks on `<a href="">` elements or `.targeted` elements containing `<a href="" class="target">`
|
package/ui/router/Router.tsx
CHANGED
|
@@ -2,40 +2,41 @@ import { Fragment, type ReactElement, type ReactNode, useEffect } from "react";
|
|
|
2
2
|
import { useInstance } from "../../react/useInstance.js";
|
|
3
3
|
import { useStore } from "../../react/useStore.js";
|
|
4
4
|
import { getURIParams } from "../../util/uri.js";
|
|
5
|
-
import { Meta } from "../misc/Meta.js";
|
|
5
|
+
import { Meta, requireMeta } from "../misc/Meta.js";
|
|
6
6
|
import { RouterContext, requireRouter } from "./RouterContext.js";
|
|
7
7
|
import { RouterStore } from "./RouterStore.js";
|
|
8
8
|
import type { Routes } from "./Routes.js";
|
|
9
9
|
|
|
10
10
|
export interface RouterProps {
|
|
11
|
-
routes: Routes;
|
|
12
|
-
children?: ReactNode;
|
|
11
|
+
readonly routes: Routes;
|
|
12
|
+
readonly children?: ReactNode | undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export interface RouterIsolateProps {
|
|
16
|
-
children: ReactNode;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
declare const _routerOutputProps: unique symbol;
|
|
20
|
-
|
|
21
|
-
export interface RouterOutputProps {
|
|
22
|
-
readonly [_routerOutputProps]?: never;
|
|
23
|
-
}
|
|
24
15
|
/**
|
|
25
16
|
* Provides Router based on the browser URL and renders the active route.
|
|
17
|
+
* - Creates a new `RouterStore`
|
|
18
|
+
* - Exposes it via `requireRouter()` to descendants.
|
|
19
|
+
* - Renders the route matched for the current path.
|
|
20
|
+
* - Intercepts same-origin anchor clicks (excluding anchors with a `download` attribute) to perform client-side routing.
|
|
21
|
+
* - Synchronizes store state with browser back/forward (`popstate`) events.
|
|
26
22
|
*
|
|
27
|
-
*
|
|
23
|
+
* Integrated with meta via the current `Meta`
|
|
24
|
+
* - Set `url` in a wrapper component to set the initial URL for this router, e.g. `<App url="/a/b/c">`
|
|
25
|
+
* - Set `base` , e.g. `<App base="https://p.com/subdir" />`
|
|
28
26
|
*
|
|
29
27
|
* @param routes - Route definitions used to select and render the active route
|
|
30
28
|
* @param children - Content to render inside the Router Meta; defaults to the routing output component
|
|
31
29
|
* @returns The Router provider element rendering the active route and supplying Router context
|
|
32
30
|
*/
|
|
33
31
|
export function Router({ routes, children = <RouterOutput /> }: RouterProps): ReactElement {
|
|
34
|
-
const
|
|
32
|
+
const { url, base } = requireMeta();
|
|
33
|
+
const nav = useInstance(RouterStore, routes, url, base);
|
|
35
34
|
useStore(nav);
|
|
36
35
|
|
|
37
36
|
// Effect to attach listeners when this router is active.
|
|
38
37
|
useEffect(() => {
|
|
38
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
39
|
+
|
|
39
40
|
// Listen for `click` events on `<a href="">` anchors and fire the forward event instead.
|
|
40
41
|
const onClick = (e: MouseEvent) => {
|
|
41
42
|
// Look for clicks on `<a href="">` elements or `.targeted` elements containing `<a href="" class="target">`
|
|
@@ -71,6 +72,10 @@ export function Router({ routes, children = <RouterOutput /> }: RouterProps): Re
|
|
|
71
72
|
);
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
export interface RouterIsolateProps {
|
|
76
|
+
children: ReactNode;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
/**
|
|
75
80
|
* Isolate a set of children so they remount on Router.
|
|
76
81
|
* - Use this when you want to ensure an entire React tree is remounted when the Router route changes.
|
|
@@ -5,7 +5,7 @@ import { type Routes } from "./Routes.js";
|
|
|
5
5
|
/** Store the current router state. */
|
|
6
6
|
export declare class RouterStore extends URLStore {
|
|
7
7
|
readonly routes: Routes;
|
|
8
|
-
constructor(routes: Routes);
|
|
8
|
+
constructor(routes: Routes, url?: PossibleURL, base?: PossibleURL);
|
|
9
9
|
forward(possible: PossibleURL): void;
|
|
10
10
|
redirect(possible: PossibleURL): void;
|
|
11
11
|
/** Match a route against this router's routes and return the matched component. */
|
package/ui/router/RouterStore.js
CHANGED
|
@@ -4,8 +4,8 @@ import { matchRoute } from "./Routes.js";
|
|
|
4
4
|
/** Store the current router state. */
|
|
5
5
|
export class RouterStore extends URLStore {
|
|
6
6
|
routes;
|
|
7
|
-
constructor(routes) {
|
|
8
|
-
super(
|
|
7
|
+
constructor(routes, url = "/", base) {
|
|
8
|
+
super(url, base);
|
|
9
9
|
this.routes = routes;
|
|
10
10
|
}
|
|
11
11
|
forward(possible) {
|
|
@@ -7,8 +7,8 @@ import { matchRoute, type Routes } from "./Routes.js";
|
|
|
7
7
|
export class RouterStore extends URLStore {
|
|
8
8
|
readonly routes: Routes;
|
|
9
9
|
|
|
10
|
-
constructor(routes: Routes) {
|
|
11
|
-
super(
|
|
10
|
+
constructor(routes: Routes, url: PossibleURL = "/", base?: PossibleURL) {
|
|
11
|
+
super(url, base);
|
|
12
12
|
this.routes = routes;
|
|
13
13
|
}
|
|
14
14
|
|
package/ui/util/meta.d.ts
CHANGED
|
@@ -14,11 +14,11 @@ export type MetaAssets = ImmutableArray<Nullish<string>>;
|
|
|
14
14
|
export type MetaCSP = {
|
|
15
15
|
readonly [resource: string]: string[];
|
|
16
16
|
};
|
|
17
|
-
/** Combined meta
|
|
17
|
+
/** Combined meta data for a website page. */
|
|
18
18
|
export interface MetaData {
|
|
19
19
|
/** Base URL for the app (used to resolve `url` and set as `<base>` tag in `<Head>`). */
|
|
20
20
|
readonly base?: ImmutableURL | undefined;
|
|
21
|
-
/** URL of the current page (used to update history API
|
|
21
|
+
/** URL of the current page (used to update history API and as the initial URL for routing). */
|
|
22
22
|
readonly url?: ImmutableURL | undefined;
|
|
23
23
|
/** Title of the entire application. */
|
|
24
24
|
readonly app?: string | undefined;
|
|
@@ -27,7 +27,7 @@ export interface MetaData {
|
|
|
27
27
|
/** Description of the current page. */
|
|
28
28
|
readonly description?: string | undefined;
|
|
29
29
|
readonly image?: string | undefined;
|
|
30
|
-
/** Language code (used for
|
|
30
|
+
/** Language code (used for `lang` tag in HTML). */
|
|
31
31
|
readonly language?: string | undefined;
|
|
32
32
|
readonly csp?: MetaCSP | undefined;
|
|
33
33
|
readonly tags?: MetaTags | undefined;
|
|
@@ -37,7 +37,7 @@ export interface MetaData {
|
|
|
37
37
|
readonly styles?: MetaAssets | undefined;
|
|
38
38
|
}
|
|
39
39
|
/** Input metadata that can be parsed and converted to proper metadata. */
|
|
40
|
-
export interface
|
|
40
|
+
export interface PossibleMeta extends Omit<MetaData, "url"> {
|
|
41
41
|
/**
|
|
42
42
|
* New URL for the page.
|
|
43
43
|
* - Resolved using `requireURL()` if set relative to `base`
|
|
@@ -51,17 +51,17 @@ export interface PossibleMetaData extends Omit<MetaData, "url"> {
|
|
|
51
51
|
readonly params?: PossibleURIParams | undefined;
|
|
52
52
|
}
|
|
53
53
|
/** Turn a deconstructed CSP into a string. */
|
|
54
|
-
export declare function
|
|
54
|
+
export declare function joinMetaCSP(csp: Nullish<MetaCSP>): string | undefined;
|
|
55
55
|
/** Merge two page or site titles together, e.g. `Manchester Runners` + `Messages` becomes `Messages - Manchester Runners` */
|
|
56
56
|
export declare function joinTitles(...titles: (string | undefined)[]): string;
|
|
57
57
|
/**
|
|
58
|
-
* Merge two
|
|
58
|
+
* Merge two `MetaData` objects.
|
|
59
59
|
* - `title` is merged.
|
|
60
60
|
* - `URL` is resolved to an absolute URL, e.g. `./d/e/f` + `/a/b/c` becomes `https://d.com/a/b/c/d/e/f`
|
|
61
61
|
*/
|
|
62
|
-
export declare function mergeMeta(meta1: MetaData, meta2:
|
|
62
|
+
export declare function mergeMeta(meta1: MetaData, meta2: PossibleMeta, caller?: AnyCaller): MetaData;
|
|
63
63
|
/**
|
|
64
64
|
* Merge two metadata URLs.
|
|
65
65
|
* - New URL is resolved relative to: current URL, new base URL, current base URL
|
|
66
66
|
*/
|
|
67
|
-
export declare function
|
|
67
|
+
export declare function mergeMetaURL(base: ImmutableURL | undefined, current: ImmutableURL | undefined, next: PossibleURL | undefined, params: PossibleURIParams | undefined, caller?: AnyCaller): ImmutableURL | undefined;
|
package/ui/util/meta.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { withURIParams } from "../../util/uri.js";
|
|
2
2
|
import { requireURL } from "../../util/url.js";
|
|
3
3
|
/** Turn a deconstructed CSP into a string. */
|
|
4
|
-
export function
|
|
4
|
+
export function joinMetaCSP(csp) {
|
|
5
5
|
if (typeof csp === "string")
|
|
6
6
|
return csp;
|
|
7
7
|
if (csp !== null && csp !== undefined)
|
|
@@ -13,21 +13,21 @@ export function joinTitles(...titles) {
|
|
|
13
13
|
return titles.filter(Boolean).join(" - ");
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Merge two
|
|
16
|
+
* Merge two `MetaData` objects.
|
|
17
17
|
* - `title` is merged.
|
|
18
18
|
* - `URL` is resolved to an absolute URL, e.g. `./d/e/f` + `/a/b/c` becomes `https://d.com/a/b/c/d/e/f`
|
|
19
19
|
*/
|
|
20
20
|
export function mergeMeta(meta1, meta2, caller = mergeMeta) {
|
|
21
21
|
const title = joinTitles(meta2.title, meta1.title);
|
|
22
|
-
const base =
|
|
23
|
-
const url =
|
|
22
|
+
const base = mergeMetaURL(undefined, meta1.base, meta2.base, undefined, caller);
|
|
23
|
+
const url = mergeMetaURL(base, meta1.url, meta2.url, meta2.params, caller);
|
|
24
24
|
return { ...meta1, ...meta2, base, url, title };
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Merge two metadata URLs.
|
|
28
28
|
* - New URL is resolved relative to: current URL, new base URL, current base URL
|
|
29
29
|
*/
|
|
30
|
-
export function
|
|
30
|
+
export function mergeMetaURL(base, current, next, params, caller = mergeMetaURL) {
|
|
31
31
|
const url = next ? requireURL(next, base, caller) : current;
|
|
32
32
|
return url && params ? withURIParams(url, params, caller) : url;
|
|
33
33
|
}
|
package/ui/util/meta.ts
CHANGED
|
@@ -17,11 +17,11 @@ export type MetaAssets = ImmutableArray<Nullish<string>>;
|
|
|
17
17
|
/** Type for a meta `Content-Security-Policy` tag in `{ resource: string[] }` format. */
|
|
18
18
|
export type MetaCSP = { readonly [resource: string]: string[] };
|
|
19
19
|
|
|
20
|
-
/** Combined meta
|
|
20
|
+
/** Combined meta data for a website page. */
|
|
21
21
|
export interface MetaData {
|
|
22
22
|
/** Base URL for the app (used to resolve `url` and set as `<base>` tag in `<Head>`). */
|
|
23
23
|
readonly base?: ImmutableURL | undefined;
|
|
24
|
-
/** URL of the current page (used to update history API
|
|
24
|
+
/** URL of the current page (used to update history API and as the initial URL for routing). */
|
|
25
25
|
readonly url?: ImmutableURL | undefined;
|
|
26
26
|
/** Title of the entire application. */
|
|
27
27
|
readonly app?: string | undefined;
|
|
@@ -30,7 +30,7 @@ export interface MetaData {
|
|
|
30
30
|
/** Description of the current page. */
|
|
31
31
|
readonly description?: string | undefined;
|
|
32
32
|
readonly image?: string | undefined;
|
|
33
|
-
/** Language code (used for
|
|
33
|
+
/** Language code (used for `lang` tag in HTML). */
|
|
34
34
|
readonly language?: string | undefined;
|
|
35
35
|
readonly csp?: MetaCSP | undefined;
|
|
36
36
|
readonly tags?: MetaTags | undefined;
|
|
@@ -41,7 +41,7 @@ export interface MetaData {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Input metadata that can be parsed and converted to proper metadata. */
|
|
44
|
-
export interface
|
|
44
|
+
export interface PossibleMeta extends Omit<MetaData, "url"> {
|
|
45
45
|
/**
|
|
46
46
|
* New URL for the page.
|
|
47
47
|
* - Resolved using `requireURL()` if set relative to `base`
|
|
@@ -56,7 +56,7 @@ export interface PossibleMetaData extends Omit<MetaData, "url"> {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** Turn a deconstructed CSP into a string. */
|
|
59
|
-
export function
|
|
59
|
+
export function joinMetaCSP(csp: Nullish<MetaCSP>): string | undefined {
|
|
60
60
|
if (typeof csp === "string") return csp;
|
|
61
61
|
if (csp !== null && csp !== undefined) return Object.entries(csp).map(_mapCSP).join("; ");
|
|
62
62
|
}
|
|
@@ -68,15 +68,15 @@ export function joinTitles(...titles: (string | undefined)[]): string {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Merge two
|
|
71
|
+
* Merge two `MetaData` objects.
|
|
72
72
|
* - `title` is merged.
|
|
73
73
|
* - `URL` is resolved to an absolute URL, e.g. `./d/e/f` + `/a/b/c` becomes `https://d.com/a/b/c/d/e/f`
|
|
74
74
|
*/
|
|
75
|
-
export function mergeMeta(meta1: MetaData, meta2:
|
|
75
|
+
export function mergeMeta(meta1: MetaData, meta2: PossibleMeta, caller: AnyCaller = mergeMeta): MetaData {
|
|
76
76
|
const title = joinTitles(meta2.title, meta1.title);
|
|
77
77
|
|
|
78
|
-
const base =
|
|
79
|
-
const url =
|
|
78
|
+
const base = mergeMetaURL(undefined, meta1.base, meta2.base, undefined, caller);
|
|
79
|
+
const url = mergeMetaURL(base, meta1.url, meta2.url, meta2.params, caller);
|
|
80
80
|
|
|
81
81
|
return { ...meta1, ...meta2, base, url, title };
|
|
82
82
|
}
|
|
@@ -85,12 +85,12 @@ export function mergeMeta(meta1: MetaData, meta2: PossibleMetaData, caller: AnyC
|
|
|
85
85
|
* Merge two metadata URLs.
|
|
86
86
|
* - New URL is resolved relative to: current URL, new base URL, current base URL
|
|
87
87
|
*/
|
|
88
|
-
export function
|
|
88
|
+
export function mergeMetaURL(
|
|
89
89
|
base: ImmutableURL | undefined,
|
|
90
90
|
current: ImmutableURL | undefined,
|
|
91
91
|
next: PossibleURL | undefined,
|
|
92
92
|
params: PossibleURIParams | undefined,
|
|
93
|
-
caller: AnyCaller =
|
|
93
|
+
caller: AnyCaller = mergeMetaURL,
|
|
94
94
|
): ImmutableURL | undefined {
|
|
95
95
|
const url = next ? requireURL(next, base, caller) : current;
|
|
96
96
|
return url && params ? withURIParams(url, params, caller) : url;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Set of valid props for an element. */
|
|
2
|
+
export interface ElementProps {
|
|
3
|
+
readonly [key: string]: unknown;
|
|
4
|
+
readonly children?: Elements;
|
|
5
|
+
}
|
|
6
|
+
/** Element with a type, props, and optional key (compatible with `React.ReactElement`). */
|
|
7
|
+
export interface Element<P extends ElementProps = ElementProps> {
|
|
8
|
+
readonly type: string | ((props: P) => Elements | null);
|
|
9
|
+
readonly props: P;
|
|
10
|
+
readonly key: string | null;
|
|
11
|
+
readonly $$typeof?: symbol;
|
|
12
|
+
}
|
|
13
|
+
/** Collection of elements (compatible with `React.ReactNode`). */
|
|
14
|
+
export type Elements = undefined | null | string | Element | readonly Elements[];
|
|
15
|
+
/** Is an unknown value an element? */
|
|
16
|
+
export declare function isElement(value: unknown): value is Element;
|
|
17
|
+
/** Is an unknown value a collection of elements? */
|
|
18
|
+
export declare function isElements(value: unknown): value is Elements;
|
|
19
|
+
/**
|
|
20
|
+
* Strip all tags from elements to produce a plain text string.
|
|
21
|
+
*
|
|
22
|
+
* @param elements An element, a plain string, or null/undefined (or an array of those things).
|
|
23
|
+
* @returns The combined string made from the elements.
|
|
24
|
+
*
|
|
25
|
+
* @example `- Item with *strong*\n- Item with _em_` becomes `Item with strong Item with em`
|
|
26
|
+
*/
|
|
27
|
+
export declare function getElementText(elements?: Elements): string;
|
|
28
|
+
/**
|
|
29
|
+
* Iterate through all elements in a collection.
|
|
30
|
+
* - This is useful if you, e.g. want to apply a `className` to all `<h1>` elements, or make a list of all URLs found in a collection.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getElements(elements: Elements): Iterable<Element>;
|
package/util/element.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isArray } from "./array.js";
|
|
2
|
+
/** Is an unknown value an element? */
|
|
3
|
+
export function isElement(value) {
|
|
4
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
5
|
+
}
|
|
6
|
+
/** Is an unknown value a collection of elements? */
|
|
7
|
+
export function isElements(value) {
|
|
8
|
+
return value === null || typeof value === "string" || isElement(value) || isArray(value);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Strip all tags from elements to produce a plain text string.
|
|
12
|
+
*
|
|
13
|
+
* @param elements An element, a plain string, or null/undefined (or an array of those things).
|
|
14
|
+
* @returns The combined string made from the elements.
|
|
15
|
+
*
|
|
16
|
+
* @example `- Item with *strong*\n- Item with _em_` becomes `Item with strong Item with em`
|
|
17
|
+
*/
|
|
18
|
+
export function getElementText(elements) {
|
|
19
|
+
if (typeof elements === "string")
|
|
20
|
+
return elements;
|
|
21
|
+
if (isArray(elements))
|
|
22
|
+
return elements.map(getElementText).filter(Boolean).join(" ");
|
|
23
|
+
if (isElement(elements))
|
|
24
|
+
return getElementText(elements.props.children);
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Iterate through all elements in a collection.
|
|
29
|
+
* - This is useful if you, e.g. want to apply a `className` to all `<h1>` elements, or make a list of all URLs found in a collection.
|
|
30
|
+
*/
|
|
31
|
+
export function* getElements(elements) {
|
|
32
|
+
if (isArray(elements)) {
|
|
33
|
+
for (const n of elements)
|
|
34
|
+
yield* getElements(n);
|
|
35
|
+
}
|
|
36
|
+
else if (isElement(elements)) {
|
|
37
|
+
yield elements;
|
|
38
|
+
if (elements.props.children)
|
|
39
|
+
yield* getElements(elements.props.children);
|
|
40
|
+
}
|
|
41
|
+
}
|
package/util/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./dictionary.js";
|
|
|
17
17
|
export * from "./diff.js";
|
|
18
18
|
export * from "./dispose.js";
|
|
19
19
|
export * from "./duration.js";
|
|
20
|
+
export * from "./element.js";
|
|
20
21
|
export * from "./entity.js";
|
|
21
22
|
export * from "./entry.js";
|
|
22
23
|
export * from "./env.js";
|
|
@@ -33,7 +34,6 @@ export * from "./http.js";
|
|
|
33
34
|
export * from "./hydrate.js";
|
|
34
35
|
export * from "./item.js";
|
|
35
36
|
export * from "./iterate.js";
|
|
36
|
-
export * from "./jsx.js";
|
|
37
37
|
export * from "./jwt.js";
|
|
38
38
|
export * from "./lazy.js";
|
|
39
39
|
export * from "./map.js";
|
package/util/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./dictionary.js";
|
|
|
17
17
|
export * from "./diff.js";
|
|
18
18
|
export * from "./dispose.js";
|
|
19
19
|
export * from "./duration.js";
|
|
20
|
+
export * from "./element.js";
|
|
20
21
|
export * from "./entity.js";
|
|
21
22
|
export * from "./entry.js";
|
|
22
23
|
export * from "./env.js";
|
|
@@ -33,7 +34,6 @@ export * from "./http.js";
|
|
|
33
34
|
export * from "./hydrate.js";
|
|
34
35
|
export * from "./item.js";
|
|
35
36
|
export * from "./iterate.js";
|
|
36
|
-
export * from "./jsx.js";
|
|
37
37
|
export * from "./jwt.js";
|
|
38
38
|
export * from "./lazy.js";
|
|
39
39
|
export * from "./map.js";
|
package/util/jsx.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/** Set of valid props for a JSX element. */
|
|
2
|
-
export interface JSXProps {
|
|
3
|
-
readonly [key: string]: unknown;
|
|
4
|
-
readonly children?: JSXNode;
|
|
5
|
-
}
|
|
6
|
-
/** JSX element (similar to `React.ReactElement`) */
|
|
7
|
-
export interface JSXElement<P extends JSXProps = JSXProps> {
|
|
8
|
-
readonly type: string | ((props: P) => JSXNode | null);
|
|
9
|
-
readonly props: P;
|
|
10
|
-
readonly key: string | null;
|
|
11
|
-
readonly $$typeof?: symbol;
|
|
12
|
-
}
|
|
13
|
-
/** JSX node (similar to `React.ReactNode`) */
|
|
14
|
-
export type JSXNode = undefined | null | string | JSXElement | readonly JSXNode[];
|
|
15
|
-
/** Is an unknown value a JSX element? */
|
|
16
|
-
export declare function isJSXElement(value: unknown): value is JSXElement;
|
|
17
|
-
/** Is an unknown value a JSX node? */
|
|
18
|
-
export declare function isJSXNode(value: unknown): value is JSXNode;
|
|
19
|
-
/**
|
|
20
|
-
* Take a Markup JSX node and strip all tags from it to produce a plain text string.
|
|
21
|
-
*
|
|
22
|
-
* @param node A JsxNode, e.g. either a JSX element, a plain string, or null/undefined (or an array of those things).
|
|
23
|
-
* @returns The combined string made from the JSX node.
|
|
24
|
-
*
|
|
25
|
-
* @example `- Item with *strong*\n- Item with _em_` becomes `Item with strong Item with em`
|
|
26
|
-
*/
|
|
27
|
-
export declare function getJSXNodeText(node?: JSXNode): string;
|
|
28
|
-
/**
|
|
29
|
-
* Iterate through all elements in a node.
|
|
30
|
-
* - This is useful if you, e.g. want to apply a `className` to all `<h1>` elements, or make a list of all URLs found in a Node.
|
|
31
|
-
*/
|
|
32
|
-
export declare function getJSXNodeElements(node: JSXNode): Iterable<JSXElement>;
|
package/util/jsx.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { isArray } from "./array.js";
|
|
2
|
-
/** Is an unknown value a JSX element? */
|
|
3
|
-
export function isJSXElement(value) {
|
|
4
|
-
return typeof value === "object" && value !== null && "type" in value;
|
|
5
|
-
}
|
|
6
|
-
/** Is an unknown value a JSX node? */
|
|
7
|
-
export function isJSXNode(value) {
|
|
8
|
-
return value === null || typeof value === "string" || isJSXElement(value) || isArray(value);
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Take a Markup JSX node and strip all tags from it to produce a plain text string.
|
|
12
|
-
*
|
|
13
|
-
* @param node A JsxNode, e.g. either a JSX element, a plain string, or null/undefined (or an array of those things).
|
|
14
|
-
* @returns The combined string made from the JSX node.
|
|
15
|
-
*
|
|
16
|
-
* @example `- Item with *strong*\n- Item with _em_` becomes `Item with strong Item with em`
|
|
17
|
-
*/
|
|
18
|
-
export function getJSXNodeText(node) {
|
|
19
|
-
if (typeof node === "string")
|
|
20
|
-
return node;
|
|
21
|
-
if (isArray(node))
|
|
22
|
-
return node.map(getJSXNodeText).filter(Boolean).join(" ");
|
|
23
|
-
if (isJSXElement(node))
|
|
24
|
-
return getJSXNodeText(node.props.children);
|
|
25
|
-
return "";
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Iterate through all elements in a node.
|
|
29
|
-
* - This is useful if you, e.g. want to apply a `className` to all `<h1>` elements, or make a list of all URLs found in a Node.
|
|
30
|
-
*/
|
|
31
|
-
export function* getJSXNodeElements(node) {
|
|
32
|
-
if (isArray(node)) {
|
|
33
|
-
for (const n of node)
|
|
34
|
-
yield* getJSXNodeElements(n);
|
|
35
|
-
}
|
|
36
|
-
else if (isJSXElement(node)) {
|
|
37
|
-
yield node;
|
|
38
|
-
if (node.props.children)
|
|
39
|
-
yield* getJSXNodeElements(node.props.children);
|
|
40
|
-
}
|
|
41
|
-
}
|