shelving 1.201.0 → 1.203.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.
Files changed (98) hide show
  1. package/extract/DirectoryExtractor.d.ts +29 -0
  2. package/extract/DirectoryExtractor.js +48 -0
  3. package/extract/Extractor.d.ts +11 -0
  4. package/extract/Extractor.js +6 -0
  5. package/extract/FileExtractor.d.ts +17 -0
  6. package/extract/FileExtractor.js +25 -0
  7. package/extract/MarkdownExtractor.d.ts +13 -0
  8. package/extract/MarkdownExtractor.js +32 -0
  9. package/extract/TypescriptExtractor.d.ts +11 -0
  10. package/extract/TypescriptExtractor.js +244 -0
  11. package/extract/index.d.ts +5 -0
  12. package/extract/index.js +5 -0
  13. package/markup/render.d.ts +4 -4
  14. package/markup/render.js +4 -4
  15. package/markup/rule/unordered.d.ts +2 -2
  16. package/markup/util/rule.d.ts +4 -4
  17. package/package.json +8 -6
  18. package/ui/app/App.d.ts +2 -2
  19. package/ui/app/App.js +4 -6
  20. package/ui/app/App.tsx +6 -8
  21. package/ui/block/Video.js +1 -1
  22. package/ui/block/Video.tsx +1 -1
  23. package/ui/form/ButtonInput.d.ts +1 -1
  24. package/ui/form/CheckboxInput.d.ts +1 -1
  25. package/ui/form/DateInput.d.ts +1 -1
  26. package/ui/form/FileInput.d.ts +1 -1
  27. package/ui/form/Input.d.ts +1 -1
  28. package/ui/form/NumberInput.d.ts +1 -1
  29. package/ui/form/Popover.d.ts +1 -1
  30. package/ui/form/RadioInput.d.ts +1 -1
  31. package/ui/form/TextInput.d.ts +1 -1
  32. package/ui/index.d.ts +1 -0
  33. package/ui/index.js +1 -0
  34. package/ui/index.ts +1 -0
  35. package/ui/layout/SidebarLayout.d.ts +17 -0
  36. package/ui/layout/SidebarLayout.js +15 -0
  37. package/ui/layout/SidebarLayout.module.css +47 -0
  38. package/ui/layout/SidebarLayout.tsx +36 -0
  39. package/ui/layout/index.d.ts +1 -0
  40. package/ui/layout/index.js +1 -0
  41. package/ui/layout/index.tsx +1 -0
  42. package/ui/misc/Mapper.d.ts +32 -0
  43. package/ui/misc/Mapper.js +46 -0
  44. package/ui/misc/Mapper.tsx +71 -0
  45. package/ui/misc/Meta.d.ts +2 -2
  46. package/ui/misc/Meta.tsx +2 -2
  47. package/ui/misc/index.d.ts +1 -0
  48. package/ui/misc/index.js +1 -0
  49. package/ui/misc/index.tsx +1 -0
  50. package/ui/page/HTML.d.ts +10 -0
  51. package/ui/page/HTML.js +11 -0
  52. package/ui/page/HTML.tsx +20 -0
  53. package/ui/page/Head.d.ts +0 -5
  54. package/ui/page/Head.js +4 -2
  55. package/ui/page/Head.tsx +3 -8
  56. package/ui/page/Page.d.ts +4 -3
  57. package/ui/page/Page.js +2 -1
  58. package/ui/page/Page.tsx +4 -3
  59. package/ui/page/index.d.ts +1 -0
  60. package/ui/page/index.js +1 -0
  61. package/ui/page/index.ts +1 -0
  62. package/ui/router/Router.d.ts +13 -11
  63. package/ui/router/Router.js +13 -3
  64. package/ui/router/Router.tsx +19 -14
  65. package/ui/router/RouterStore.d.ts +1 -1
  66. package/ui/router/RouterStore.js +2 -2
  67. package/ui/router/RouterStore.tsx +2 -2
  68. package/ui/tree/TreeApp.d.ts +24 -0
  69. package/ui/tree/TreeApp.js +22 -0
  70. package/ui/tree/TreeApp.tsx +64 -0
  71. package/ui/tree/TreeCards.d.ts +9 -0
  72. package/ui/tree/TreeCards.js +8 -0
  73. package/ui/tree/TreeCards.module.css +31 -0
  74. package/ui/tree/TreeCards.tsx +20 -0
  75. package/ui/tree/TreeMenu.d.ts +9 -0
  76. package/ui/tree/TreeMenu.js +8 -0
  77. package/ui/tree/TreeMenu.module.css +29 -0
  78. package/ui/tree/TreeMenu.tsx +22 -0
  79. package/ui/tree/TreePage.d.ts +15 -0
  80. package/ui/tree/TreePage.js +17 -0
  81. package/ui/tree/TreePage.tsx +24 -0
  82. package/ui/tree/index.d.ts +4 -0
  83. package/ui/tree/index.js +4 -0
  84. package/ui/tree/index.ts +4 -0
  85. package/ui/util/meta.d.ts +8 -8
  86. package/ui/util/meta.js +5 -5
  87. package/ui/util/meta.ts +11 -11
  88. package/util/data.d.ts +11 -1
  89. package/util/data.js +9 -2
  90. package/util/element.d.ts +218 -0
  91. package/util/element.js +136 -0
  92. package/util/file.d.ts +10 -0
  93. package/util/file.js +15 -0
  94. package/util/index.d.ts +1 -1
  95. package/util/index.js +1 -1
  96. package/util/iterate.d.ts +1 -1
  97. package/util/jsx.d.ts +0 -32
  98. package/util/jsx.js +0 -41
@@ -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);
@@ -4,4 +4,4 @@ import { type InputProps } from "./Input.js";
4
4
  export interface ButtonInputProps extends InputProps, ClickableProps {
5
5
  }
6
6
  /** Return either a `<button>` or an `<a href="">` styled as an input, based on whether an `onClick` or `href` prop is provided. */
7
- export declare function ButtonInput({ title, placeholder, disabled, href, onClick, target, download, children }: ButtonInputProps): ReactElement;
7
+ export declare function ButtonInput({ title, placeholder, disabled, href, onClick, target, download, children, }: ButtonInputProps): ReactElement;
@@ -4,4 +4,4 @@ export interface CheckboxProps extends ValueInputProps<boolean> {
4
4
  children?: ReactNode | undefined;
5
5
  }
6
6
  /** Checkbox element. */
7
- export declare function CheckboxInput({ name, title, placeholder, required, disabled, message, value, onValue, children }: CheckboxProps): ReactElement;
7
+ export declare function CheckboxInput({ name, title, placeholder, required, disabled, message, value, onValue, children, }: CheckboxProps): ReactElement;
@@ -9,4 +9,4 @@ export interface DateInputProps extends ValueInputProps<string, PossibleDate> {
9
9
  step?: number | undefined;
10
10
  }
11
11
  export declare function DateInput({ name, title, placeholder, // Placeholder must be defined or `:placeholder-shown` CSS rules won't show.
12
- required, disabled, message, value, onValue, min, max, input, step }: DateInputProps): ReactElement;
12
+ required, disabled, message, value, onValue, min, max, input, step, }: DateInputProps): ReactElement;
@@ -4,4 +4,4 @@ import { type ValueInputProps } from "./Input.js";
4
4
  export interface FileInputProps extends ValueInputProps<string | File> {
5
5
  types?: FileTypes | undefined;
6
6
  }
7
- export declare function FileInput({ name, title, placeholder, required, disabled, message, onValue, types }: FileInputProps): ReactElement;
7
+ export declare function FileInput({ name, title, placeholder, required, disabled, message, onValue, types, }: FileInputProps): ReactElement;
@@ -38,7 +38,7 @@ export interface ValueInputProps<O, I = never> extends InputProps {
38
38
  onValue(value: O | undefined): void;
39
39
  }
40
40
  /** Return either a `<button>` or an `<a href="">` styled as an input, based on whether an `onClick` or `href` prop is provided. */
41
- export declare function Input({ title, placeholder, disabled, href, onClick, target, download, children }: InputProps & ClickableProps): ReactElement;
41
+ export declare function Input({ title, placeholder, disabled, href, onClick, target, download, children, }: InputProps & ClickableProps): ReactElement;
42
42
  /** Input that is loading. */
43
43
  export declare const LOADING_INPUT: import("react/jsx-runtime").JSX.Element;
44
44
  /** Wraps an input with support for absolutely-positioned `data-slot` icon elements on either side. */
@@ -8,5 +8,5 @@ export interface NumberInputProps extends ValueInputProps<number> {
8
8
  formatter?: NumberFormatter | undefined;
9
9
  }
10
10
  export declare function NumberInput({ name, title, placeholder, // Placeholder must be defined or `:placeholder-shown` CSS rules won't show.
11
- required, disabled, message, value, onValue, formatter }: NumberInputProps): ReactElement;
11
+ required, disabled, message, value, onValue, formatter, }: NumberInputProps): ReactElement;
12
12
  export {};
@@ -28,4 +28,4 @@ export interface PopoverProps {
28
28
  * @todo DH: Would love to use new HTML `popover="auto"` functionality for this but the anchor positioning it needs is not supported everywhere yet.
29
29
  */
30
30
  export declare function Popover({ children: [trigger, ...popover], //
31
- onClose, open }: PopoverProps): ReactElement;
31
+ onClose, open, }: PopoverProps): ReactElement;
@@ -4,4 +4,4 @@ export interface RadioInputProps extends ValueInputProps<boolean> {
4
4
  children?: ReactNode | undefined;
5
5
  }
6
6
  /** A single `<input type="radio">` in a `<label>` wrapper styled as an `<Input>` */
7
- export declare function RadioInput({ name, title, placeholder, required, disabled, message, value, onValue, children }: RadioInputProps): ReactElement;
7
+ export declare function RadioInput({ name, title, placeholder, required, disabled, message, value, onValue, children, }: RadioInputProps): ReactElement;
@@ -12,5 +12,5 @@ export interface TextInputProps extends ValueInputProps<string> {
12
12
  formatter?: TextFormatter | undefined;
13
13
  }
14
14
  export declare function TextInput({ name, title, placeholder, // Placeholder must be defined or `:placeholder-shown` CSS rules won't show.
15
- required, disabled, message, value, onValue, input, min, max, rows, formatter }: TextInputProps): ReactElement;
15
+ required, disabled, message, value, onValue, input, min, max, rows, formatter, }: TextInputProps): ReactElement;
16
16
  export {};
package/ui/index.d.ts CHANGED
@@ -9,4 +9,5 @@ export * from "./notice/index.js";
9
9
  export * from "./page/index.js";
10
10
  export * from "./router/index.js";
11
11
  export * from "./transition/index.js";
12
+ export * from "./tree/index.js";
12
13
  export * from "./util/index.js";
package/ui/index.js CHANGED
@@ -9,4 +9,5 @@ export * from "./notice/index.js";
9
9
  export * from "./page/index.js";
10
10
  export * from "./router/index.js";
11
11
  export * from "./transition/index.js";
12
+ export * from "./tree/index.js";
12
13
  export * from "./util/index.js";
package/ui/index.ts CHANGED
@@ -9,4 +9,5 @@ export * from "./notice/index.js";
9
9
  export * from "./page/index.js";
10
10
  export * from "./router/index.js";
11
11
  export * from "./transition/index.js";
12
+ export * from "./tree/index.js";
12
13
  export * from "./util/index.js";
@@ -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 };
@@ -1,2 +1,3 @@
1
1
  export * from "./CenteredLayout.js";
2
2
  export * from "./Layout.js";
3
+ export * from "./SidebarLayout.js";
@@ -1,2 +1,3 @@
1
1
  export * from "./CenteredLayout.js";
2
2
  export * from "./Layout.js";
3
+ export * from "./SidebarLayout.js";
@@ -1,2 +1,3 @@
1
1
  export * from "./CenteredLayout.js";
2
2
  export * from "./Layout.js";
3
+ export * from "./SidebarLayout.js";
@@ -0,0 +1,32 @@
1
+ import { type ComponentType, type JSX, type ReactNode } from "react";
2
+ /**
3
+ * Element mapping — maps intrinsic element type strings to React components.
4
+ * - Keys are element type names from `JSX.IntrinsicElements` (e.g. `"tree-file"`, `"tree-directory"`).
5
+ * - Each component must accept the props declared in `JSX.IntrinsicElements` for that element type.
6
+ */
7
+ export type Mapping = {
8
+ [K in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[K]>;
9
+ };
10
+ /** Props for the `Mapping` component returned by `createElementMapper()`. */
11
+ export interface MappingProps {
12
+ /** Mapping entries that override the defaults (and any parent `Mapping` entries). */
13
+ mapping: Mapping;
14
+ children: ReactNode;
15
+ }
16
+ /** Props for the `Mapper` component returned by `createElementMapper()`. */
17
+ export interface MapperProps {
18
+ children?: ReactNode;
19
+ }
20
+ /**
21
+ * Create an element mapper — a `[Mapping, Mapper]` pair of React components.
22
+ *
23
+ * - `Mapping` — wraps a subtree to override or extend the element mapping for this mapper.
24
+ * - `Mapper` — maps children, replacing element types with registered components.
25
+ *
26
+ * Each mapper has its own React context, so different mappers (e.g. `TreeMenu`, `TreePage`)
27
+ * can have independent mappings without interfering with each other.
28
+ *
29
+ * @param defaults Default mapping entries (lowest priority — overridden by any `Mapping` wrapper).
30
+ * @returns A `[Mapping, Mapper]` tuple of React components.
31
+ */
32
+ export declare function createMapper(defaults?: Mapping): [Mapping: React.FunctionComponent<MappingProps>, Mapper: React.FunctionComponent<MapperProps>];
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, use } from "react";
3
+ import { isElement } from "../../util/element.js";
4
+ import { isProp } from "../../util/index.js";
5
+ import { isIterable } from "../../util/iterate.js";
6
+ /**
7
+ * Create an element mapper — a `[Mapping, Mapper]` pair of React components.
8
+ *
9
+ * - `Mapping` — wraps a subtree to override or extend the element mapping for this mapper.
10
+ * - `Mapper` — maps children, replacing element types with registered components.
11
+ *
12
+ * Each mapper has its own React context, so different mappers (e.g. `TreeMenu`, `TreePage`)
13
+ * can have independent mappings without interfering with each other.
14
+ *
15
+ * @param defaults Default mapping entries (lowest priority — overridden by any `Mapping` wrapper).
16
+ * @returns A `[Mapping, Mapper]` tuple of React components.
17
+ */
18
+ export function createMapper(defaults = {}) {
19
+ const Context = createContext(defaults);
20
+ /** Override or extend the element mapping for this mapper's context. */
21
+ function Mapping({ mapping, children }) {
22
+ const existing = use(Context);
23
+ return _jsx(Context, { value: { ...existing, ...mapping }, children: children });
24
+ }
25
+ /** Map children, replacing element types with components from this mapper's context. */
26
+ function Mapper({ children }) {
27
+ const entries = use(Context);
28
+ return _mapNode(children, entries);
29
+ }
30
+ return [Mapping, Mapper];
31
+ }
32
+ /** Recursively map a ReactNode, replacing element types with registered components. */
33
+ function _mapNode(node, entries) {
34
+ if (!node)
35
+ return node;
36
+ if (isElement(node))
37
+ return _mapElement(node, entries);
38
+ if (isIterable(node))
39
+ return Array.from(node, el => _mapNode(el, entries));
40
+ return node;
41
+ }
42
+ /** Map a single element, replacing its type with a registered component if found. */
43
+ function _mapElement(element, entries) {
44
+ const found = typeof element.type === "string" && isProp(entries, element.type) ? entries[element.type] : undefined;
45
+ return found ? { ...element, type: found } : element;
46
+ }
@@ -0,0 +1,71 @@
1
+ import { type ComponentType, createContext, type JSX, type ReactElement, type ReactNode, use } from "react";
2
+ import { isElement } from "../../util/element.js";
3
+ import { isProp } from "../../util/index.js";
4
+ import { isIterable } from "../../util/iterate.js";
5
+
6
+ /**
7
+ * Element mapping — maps intrinsic element type strings to React components.
8
+ * - Keys are element type names from `JSX.IntrinsicElements` (e.g. `"tree-file"`, `"tree-directory"`).
9
+ * - Each component must accept the props declared in `JSX.IntrinsicElements` for that element type.
10
+ */
11
+ export type Mapping = {
12
+ [K in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[K]>;
13
+ };
14
+
15
+ /** Props for the `Mapping` component returned by `createElementMapper()`. */
16
+ export interface MappingProps {
17
+ /** Mapping entries that override the defaults (and any parent `Mapping` entries). */
18
+ mapping: Mapping;
19
+ children: ReactNode;
20
+ }
21
+
22
+ /** Props for the `Mapper` component returned by `createElementMapper()`. */
23
+ export interface MapperProps {
24
+ children?: ReactNode;
25
+ }
26
+
27
+ /**
28
+ * Create an element mapper — a `[Mapping, Mapper]` pair of React components.
29
+ *
30
+ * - `Mapping` — wraps a subtree to override or extend the element mapping for this mapper.
31
+ * - `Mapper` — maps children, replacing element types with registered components.
32
+ *
33
+ * Each mapper has its own React context, so different mappers (e.g. `TreeMenu`, `TreePage`)
34
+ * can have independent mappings without interfering with each other.
35
+ *
36
+ * @param defaults Default mapping entries (lowest priority — overridden by any `Mapping` wrapper).
37
+ * @returns A `[Mapping, Mapper]` tuple of React components.
38
+ */
39
+ export function createMapper(
40
+ defaults: Mapping = {},
41
+ ): [Mapping: React.FunctionComponent<MappingProps>, Mapper: React.FunctionComponent<MapperProps>] {
42
+ const Context = createContext<Mapping>(defaults);
43
+
44
+ /** Override or extend the element mapping for this mapper's context. */
45
+ function Mapping({ mapping, children }: MappingProps): ReactNode {
46
+ const existing = use(Context);
47
+ return <Context value={{ ...existing, ...mapping }}>{children}</Context>;
48
+ }
49
+
50
+ /** Map children, replacing element types with components from this mapper's context. */
51
+ function Mapper({ children }: MapperProps): ReactNode {
52
+ const entries = use(Context);
53
+ return _mapNode(children, entries);
54
+ }
55
+
56
+ return [Mapping, Mapper];
57
+ }
58
+
59
+ /** Recursively map a ReactNode, replacing element types with registered components. */
60
+ function _mapNode(node: ReactNode, entries: Mapping): ReactNode {
61
+ if (!node) return node;
62
+ if (isElement(node)) return _mapElement(node, entries);
63
+ if (isIterable(node)) return Array.from(node, el => _mapNode(el, entries));
64
+ return node;
65
+ }
66
+
67
+ /** Map a single element, replacing its type with a registered component if found. */
68
+ function _mapElement(element: ReactElement, entries: Mapping): ReactElement {
69
+ const found = typeof element.type === "string" && isProp(entries, element.type) ? entries[element.type] : undefined;
70
+ return found ? { ...element, type: found } : element;
71
+ }
package/ui/misc/Meta.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
- import { type MetaData, type PossibleMetaData } from "../util/meta.js";
3
- export interface MetaProps extends PossibleMetaData {
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 PossibleMetaData } from "../util/meta.js";
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 PossibleMetaData {
8
+ export interface MetaProps extends PossibleMeta {
9
9
  children: ReactNode;
10
10
  }
11
11
 
@@ -1,3 +1,4 @@
1
1
  export * from "./Catcher.js";
2
2
  export * from "./Loading.js";
3
+ export * from "./Mapper.js";
3
4
  export * from "./Meta.js";
package/ui/misc/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./Catcher.js";
2
2
  export * from "./Loading.js";
3
+ export * from "./Mapper.js";
3
4
  export * from "./Meta.js";
package/ui/misc/index.tsx CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./Catcher.js";
2
2
  export * from "./Loading.js";
3
+ export * from "./Mapper.js";
3
4
  export * from "./Meta.js";
@@ -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;
@@ -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
+ }
@@ -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 { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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(_Fragment, { children: [_jsx("title", { children: joinTitles(title, app) }), base && _jsx("base", { href: base.href }), tags &&
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 { PossibleMetaData } from "../util/meta.js";
3
- export interface PageProps extends PossibleMetaData {
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
- * - Changes the document title (that appears in the page tab) on render.
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
- * - Changes the document title (that appears in the page tab) on render.
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 { PossibleMetaData } from "../util/meta.js";
3
+ import type { PossibleMeta } from "../util/meta.js";
4
4
  import { Head } from "./Head.js";
5
5
 
6
- export interface PageProps extends PossibleMetaData {
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
- * - Changes the document title (that appears in the page tab) on render.
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 (
@@ -1,2 +1,3 @@
1
1
  export * from "./Head.js";
2
+ export * from "./HTML.js";
2
3
  export * from "./Page.js";
package/ui/page/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./Head.js";
2
+ export * from "./HTML.js";
2
3
  export * from "./Page.js";
package/ui/page/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from "./Head.js";
2
+ export * from "./HTML.js";
2
3
  export * from "./Page.js";