shelving 1.211.0 → 1.213.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.
@@ -20,8 +20,6 @@ export function mergeTreeElements(primary, secondary) {
20
20
  key: primary.key,
21
21
  props: {
22
22
  ...primary.props,
23
- title: primary.props.title,
24
- source: primary.props.source,
25
23
  description: primary.props.description ?? secondary.props.description,
26
24
  content: _mergeContent(primary.props.content, secondary.props.content),
27
25
  children: mergeElements(primary.props.children, secondary.props.children),
package/index.d.ts CHANGED
@@ -6,7 +6,6 @@
6
6
  export * from "./api/index.js";
7
7
  export * from "./db/index.js";
8
8
  export * from "./error/index.js";
9
- export * from "./markup/index.js";
10
9
  export * from "./schema/index.js";
11
10
  export * from "./sequence/index.js";
12
11
  export * from "./store/index.js";
package/index.js CHANGED
@@ -9,7 +9,7 @@ export * from "./error/index.js";
9
9
  // export * from "./firestore/client/index.js"; // Not exported.
10
10
  // export * from "./firestore/lite/index.js"; // Not exported.
11
11
  // export * from "./firestore/server/index.js"; // Not exported.
12
- export * from "./markup/index.js";
12
+ // export * from "./markup/index.js"; // Not exported — `shelving/markup` now compiles JSX and imports React's JSX runtime.
13
13
  // export * from "./react/index.js"; // Not exported.
14
14
  export * from "./schema/index.js";
15
15
  // export * from "./ui/index.js"; // Not exported — shelving/ui requires a bundler (CSS Modules, JSX) and is consumed as source.
@@ -1,4 +1,4 @@
1
- import type { Elements } from "../util/element.js";
1
+ import type { ReactNode } from "react";
2
2
  import type { MarkupOptions } from "./util/options.js";
3
3
  /**
4
4
  * Parse a text string as Markdownish syntax and render it as elements.
@@ -8,6 +8,6 @@ import type { MarkupOptions } from "./util/options.js";
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 Elements, i.e. either a complete `Element`, `null`, `undefined`, `string`, or an array of zero or more of those.
11
+ * @returns A React node an element, a string, `null`, or an array of zero or more of those.
12
12
  */
13
- export declare function renderMarkup(input: string, options: MarkupOptions, context?: string): Elements;
13
+ export declare function renderMarkup(input: string, options: MarkupOptions, context?: string): ReactNode;
package/markup/render.js CHANGED
@@ -6,7 +6,7 @@
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 Elements, i.e. either a complete `Element`, `null`, `undefined`, `string`, or an array of zero or more of those.
9
+ * @returns A React node an element, a string, `null`, 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));
@@ -1,5 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { renderMarkup } from "../render.js";
2
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
3
3
  import { BLOCK_CONTENT_REGEXP, createBlockRegExp } from "../util/regexp.js";
4
4
  import { createMarkupRule } from "../util/rule.js";
5
5
  const PREFIX = ">";
@@ -11,9 +11,4 @@ export const BLOCKQUOTE_REGEXP = createBlockRegExp(`${PREFIX}${BLOCK_CONTENT_REG
11
11
  * - No spaces can appear before the `>` quote character.
12
12
  * - Quote block is only broken by `\n\n` two newline characters.
13
13
  */
14
- export const BLOCKQUOTE_RULE = createMarkupRule(BLOCKQUOTE_REGEXP, ([quote], options, key) => ({
15
- key,
16
- $$typeof: REACT_ELEMENT_TYPE,
17
- type: "blockquote",
18
- props: { children: renderMarkup(quote.replace(INDENT, ""), options, "block") },
19
- }), ["block", "list"]);
14
+ export const BLOCKQUOTE_RULE = createMarkupRule(BLOCKQUOTE_REGEXP, ([quote], options, key) => _jsx("blockquote", { children: renderMarkup(quote.replace(INDENT, ""), options, "block") }, key), ["block", "list"]);
@@ -1,5 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { getRegExp } from "../../util/regexp.js";
2
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
3
3
  import { BLOCK_CONTENT_REGEXP } from "../util/regexp.js";
4
4
  import { createMarkupRule } from "../util/rule.js";
5
5
  const CODE_REGEXP = getRegExp(`(?<fence>\`+)(?<code>${BLOCK_CONTENT_REGEXP})\\k<fence>`);
@@ -10,9 +10,4 @@ const CODE_REGEXP = getRegExp(`(?<fence>\`+)(?<code>${BLOCK_CONTENT_REGEXP})\\k<
10
10
  * - Closing characters must exactly match opening characters.
11
11
  * - Same as Markdown syntax.
12
12
  */
13
- export const CODE_RULE = createMarkupRule(CODE_REGEXP, ({ groups: { code } }, _options, key) => ({
14
- key,
15
- $$typeof: REACT_ELEMENT_TYPE,
16
- type: "code",
17
- props: { children: code },
18
- }), ["inline", "list"], 10);
13
+ export const CODE_RULE = createMarkupRule(CODE_REGEXP, ({ groups: { code } }, _options, key) => _jsx("code", { children: code }, key), ["inline", "list"], 10);
@@ -1,4 +1,4 @@
1
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { BLOCK_CONTENT_REGEXP, BLOCK_START_REGEXP, createBlockRegExp, LINE_CONTENT_REGEXP, LINE_SPACE_REGEXP } from "../util/regexp.js";
3
3
  import { createMarkupRule } from "../util/rule.js";
4
4
  const FENCE = "`{3,}|~{3,}";
@@ -16,19 +16,4 @@ const FENCED_REGEXP = createBlockRegExp(`(?<code>${BLOCK_CONTENT_REGEXP})`,
16
16
  * - If there's no closing fence the code block will run to the end of the current string.
17
17
  * - Markdown-style four-space indent syntax is not supported (only fenced code since it's less confusing and more common).
18
18
  */
19
- export const FENCED_RULE = createMarkupRule(FENCED_REGEXP, ({ groups: { title, code } }, _options, key) => ({
20
- key,
21
- $$typeof: REACT_ELEMENT_TYPE,
22
- type: "pre",
23
- props: {
24
- children: {
25
- $$typeof: REACT_ELEMENT_TYPE,
26
- type: "code",
27
- key: null,
28
- props: {
29
- title: title?.trim() || undefined,
30
- children: code.trim(),
31
- },
32
- },
33
- },
34
- }), ["block", "list"], 10);
19
+ export const FENCED_RULE = createMarkupRule(FENCED_REGEXP, ({ groups: { title, code } }, _options, key) => (_jsx("pre", { children: _jsx("code", { title: title?.trim() || undefined, children: code.trim() }) }, key)), ["block", "list"], 10);
@@ -1,5 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { renderMarkup } from "../render.js";
2
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
3
3
  import { createLineRegExp, LINE_CONTENT_REGEXP, LINE_SPACE_REGEXP } from "../util/regexp.js";
4
4
  import { createMarkupRule } from "../util/rule.js";
5
5
  const HEADING_REGEXP = createLineRegExp(`(?<prefix>#{1,6})(?:${LINE_SPACE_REGEXP}+(?<heading>${LINE_CONTENT_REGEXP}))?`);
@@ -9,9 +9,8 @@ const HEADING_REGEXP = createLineRegExp(`(?<prefix>#{1,6})(?:${LINE_SPACE_REGEXP
9
9
  * - `#` must be the first character on the line.
10
10
  * - Markdown's underline syntax is not supported (for simplification).
11
11
  */
12
- export const HEADING_RULE = createMarkupRule(HEADING_REGEXP, ({ groups: { prefix, heading = "" } }, options, key) => ({
13
- key,
14
- $$typeof: REACT_ELEMENT_TYPE,
15
- type: `h${prefix.length}`,
16
- props: { children: renderMarkup(heading.trim(), options, "inline") },
17
- }), ["block"]);
12
+ export const HEADING_RULE = createMarkupRule(HEADING_REGEXP, ({ groups: { prefix, heading = "" } }, options, key) => {
13
+ // The hash count picks the heading level; cast the dynamic tag to the known `h1`–`h6` set.
14
+ const Heading = `h${prefix.length}`;
15
+ return _jsx(Heading, { children: renderMarkup(heading.trim(), options, "inline") }, key);
16
+ }, ["block"]);
@@ -17,7 +17,6 @@ export declare const MARKUP_RULES_INLINE: MarkupRules;
17
17
  * - Hard because you have to capture the entire list including `\n\n`, so there's no obvious place to end it.
18
18
  * - If there are breaks then any sub-lines need to be indented by two or more spaces otherwise it will break the list.
19
19
  * - Make reference lists support this loose format too.
20
- * @todo [ ] Default rules support tables using `|` pipe syntax.
21
20
  * @todo [ ] Default rules support todo lists using `- [x]` syntax.
22
21
  * @todo [ ] Default rules support new reference syntax (combines reference lists/sidenotes/footnotes/reference and produces <dl> syntax).
23
22
  * - All of these can be the same because reference links and Extended Markdown footnotes are basically the same.
@@ -44,4 +43,5 @@ export * from "./link.js";
44
43
  export * from "./ordered.js";
45
44
  export * from "./paragraph.js";
46
45
  export * from "./separator.js";
46
+ export * from "./table.js";
47
47
  export * from "./unordered.js";
@@ -8,6 +8,7 @@ import { AUTOLINK_RULE, LINK_RULE } from "./link.js";
8
8
  import { ORDERED_RULE } from "./ordered.js";
9
9
  import { PARAGRAPH_RULE } from "./paragraph.js";
10
10
  import { SEPARATOR_RULE } from "./separator.js";
11
+ import { TABLE_RULE } from "./table.js";
11
12
  import { UNORDERED_RULE } from "./unordered.js";
12
13
  /** Markup rules that work in a block context. */
13
14
  export const MARKUP_RULES_BLOCK = [
@@ -17,6 +18,7 @@ export const MARKUP_RULES_BLOCK = [
17
18
  UNORDERED_RULE,
18
19
  ORDERED_RULE,
19
20
  BLOCKQUOTE_RULE,
21
+ TABLE_RULE,
20
22
  PARAGRAPH_RULE,
21
23
  ];
22
24
  /** Markup rules that work in an inline context. */
@@ -34,7 +36,6 @@ export const MARKUP_RULES_INLINE = [CODE_RULE, LINK_RULE, AUTOLINK_RULE, INLINE_
34
36
  * - Hard because you have to capture the entire list including `\n\n`, so there's no obvious place to end it.
35
37
  * - If there are breaks then any sub-lines need to be indented by two or more spaces otherwise it will break the list.
36
38
  * - Make reference lists support this loose format too.
37
- * @todo [ ] Default rules support tables using `|` pipe syntax.
38
39
  * @todo [ ] Default rules support todo lists using `- [x]` syntax.
39
40
  * @todo [ ] Default rules support new reference syntax (combines reference lists/sidenotes/footnotes/reference and produces <dl> syntax).
40
41
  * - All of these can be the same because reference links and Extended Markdown footnotes are basically the same.
@@ -61,4 +62,5 @@ export * from "./link.js";
61
62
  export * from "./ordered.js";
62
63
  export * from "./paragraph.js";
63
64
  export * from "./separator.js";
65
+ export * from "./table.js";
64
66
  export * from "./unordered.js";
@@ -1,5 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { renderMarkup } from "../render.js";
2
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
3
3
  import { createWordRegExp } from "../util/regexp.js";
4
4
  import { createMarkupRule } from "../util/rule.js";
5
5
  /** Map characters, e.g. `*`, to their coresponding HTML tag, e.g. `strong` */
@@ -18,9 +18,7 @@ const INLINE_REGEXP = createWordRegExp(`(?<wrap>(?<char>[${Object.keys(INLINE_CH
18
18
  * - Closing characters must exactly match opening characters.
19
19
  * - Different to Markdown: strong is always surrounded by `*asterisks*` and emphasis is always surrounded by `_underscores_` (strong isn't 'double emphasis').
20
20
  */
21
- export const INLINE_RULE = createMarkupRule(INLINE_REGEXP, ({ groups: { char, text } }, options, key) => ({
22
- key,
23
- $$typeof: REACT_ELEMENT_TYPE,
24
- type: INLINE_CHARS[char],
25
- props: { children: renderMarkup(text, options, "inline") },
26
- }), ["inline", "list", "link"]);
21
+ export const INLINE_RULE = createMarkupRule(INLINE_REGEXP, ({ groups: { char, text } }, options, key) => {
22
+ const Inline = INLINE_CHARS[char];
23
+ return _jsx(Inline, { children: renderMarkup(text, options, "inline") }, key);
24
+ }, ["inline", "list", "link"]);
@@ -1,4 +1,4 @@
1
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createMarkupRule } from "../util/rule.js";
3
3
  /**
4
4
  * Hard linebreak (`<br />` tag).
@@ -10,9 +10,8 @@ import { createMarkupRule } from "../util/rule.js";
10
10
  * - This is more intuitive (a linebreak becomes a linebreak is isn't silently ignored).
11
11
  * - This works better with textareas that wrap text (since manually breaking up long lines is no longer necessary).
12
12
  */
13
- export const LINEBREAK_RULE = createMarkupRule(/[^\n\S]*\n[^\n\S]*/, (_match, _options, key) => ({
14
- key,
15
- $$typeof: REACT_ELEMENT_TYPE,
16
- type: "br",
17
- props: {},
18
- }), ["inline", "list", "link"]);
13
+ export const LINEBREAK_RULE = createMarkupRule(/[^\n\S]*\n[^\n\S]*/, (_match, _options, key) => _jsx("br", {}, key), [
14
+ "inline",
15
+ "list",
16
+ "link",
17
+ ]);
@@ -1,9 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { formatURI } from "../../util/format.js";
2
3
  import { getLink } from "../../util/link.js";
3
4
  import { getRegExp } from "../../util/regexp.js";
4
5
  import { HTTP_SCHEMES } from "../../util/uri.js";
5
6
  import { renderMarkup } from "../render.js";
6
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
7
7
  import { createMarkupRule } from "../util/rule.js";
8
8
  /** Render `<a href="">` if the link is a valid one, or `<a>` (with no `href`) if it isn't. */
9
9
  function renderLinkMarkupRule({ groups: { title, href: unsafeHref } }, options, key) {
@@ -11,12 +11,7 @@ function renderLinkMarkupRule({ groups: { title, href: unsafeHref } }, options,
11
11
  const link = getLink(unsafeHref, url, root);
12
12
  const href = link && schemes.includes(link.protocol) ? link?.href : undefined;
13
13
  const children = title ? renderMarkup(title, options, "link") : link ? formatURI(link) : "";
14
- return {
15
- key,
16
- $$typeof: REACT_ELEMENT_TYPE,
17
- type: "a",
18
- props: { href, rel, children },
19
- };
14
+ return (_jsx("a", { href: href, rel: rel, children: children }, key));
20
15
  }
21
16
  export const LINK_REGEXP = getRegExp(/\[(?<title>[^\]\n]*?)\]\((?<href>[^)\n]*?)\)/);
22
17
  /**
@@ -27,8 +22,7 @@ export const LINK_REGEXP = getRegExp(/\[(?<title>[^\]\n]*?)\]\((?<href>[^)\n]*?)
27
22
  * - If link is not valid (using `new URL(url)` then unparsed text will be returned.
28
23
  * - For security only `http://` or `https://` links will work (if invalid the unparsed text will be returned).
29
24
  */
30
- export const LINK_RULE = createMarkupRule(LINK_REGEXP, //
31
- renderLinkMarkupRule, ["inline", "list"]);
25
+ export const LINK_RULE = createMarkupRule(LINK_REGEXP, renderLinkMarkupRule, ["inline", "list"]);
32
26
  export const AUTOLINK_REGEXP = getRegExp(/(?<href>[a-z]{2,}:\S+)(?: +(?:\((?<title>[^)\n]*?)\)))?/);
33
27
  /**
34
28
  * Autolinked URL starts with `scheme://` (any scheme in `options.schemes`) and matches an unlimited number of non-space characters.
@@ -37,5 +31,4 @@ export const AUTOLINK_REGEXP = getRegExp(/(?<href>[a-z]{2,}:\S+)(?: +(?:\((?<tit
37
31
  * - If link is not valid (using `new URL(url)` then unparsed text will be returned.
38
32
  * - For security only schemes that appear in `options.schemes` will match (defaults to `http:` and `https:`).
39
33
  */
40
- export const AUTOLINK_RULE = createMarkupRule(AUTOLINK_REGEXP, //
41
- renderLinkMarkupRule, ["inline", "list"]);
34
+ export const AUTOLINK_RULE = createMarkupRule(AUTOLINK_REGEXP, renderLinkMarkupRule, ["inline", "list"]);
@@ -1,5 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { renderMarkup } from "../render.js";
2
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
3
3
  import { BLOCK_CONTENT_REGEXP, BLOCK_SPACE_REGEXP, createBlockRegExp, LINE_SPACE_REGEXP } from "../util/regexp.js";
4
4
  import { createMarkupRule } from "../util/rule.js";
5
5
  const INDENT = /^\t/gm; // Nesting is recognised with tabs only.
@@ -13,27 +13,11 @@ export const ORDERED_REGEXP = createBlockRegExp(`(?<list>${NUMBER}(?:${LINE_SPAC
13
13
  * - Second-level list can be created by indenting with `\t` one tab.
14
14
  * - Sparse lists are not supported.
15
15
  */
16
- export const ORDERED_RULE = createMarkupRule(ORDERED_REGEXP, ({ groups: { list } }, options, key) => ({
17
- key,
18
- $$typeof: REACT_ELEMENT_TYPE,
19
- type: "ol",
20
- props: {
21
- children: Array.from(_getOrderedItems(list, options)),
22
- },
23
- }), ["block", "list"]);
16
+ export const ORDERED_RULE = createMarkupRule(ORDERED_REGEXP, ({ groups: { list } }, options, key) => _jsx("ol", { children: Array.from(_getOrderedItems(list, options)) }, key), ["block", "list"]);
24
17
  /** Parse a markdown list into a set of items elements. */
25
18
  function* _getOrderedItems(list, options) {
26
19
  let key = 0;
27
20
  for (const [_unused, number = "", item = ""] of list.matchAll(ITEM)) {
28
- yield {
29
- $$typeof: REACT_ELEMENT_TYPE,
30
- type: "li",
31
- props: {
32
- value: Number.parseInt(number, 10),
33
- children: renderMarkup(item.replace(INDENT, ""), options, "list"),
34
- },
35
- key: key.toString(),
36
- };
37
- key++;
21
+ yield (_jsx("li", { value: Number.parseInt(number, 10), children: renderMarkup(item.replace(INDENT, ""), options, "list") }, key++));
38
22
  }
39
23
  }
@@ -1,5 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { renderMarkup } from "../render.js";
2
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
3
3
  import { BLOCK_SPACE_REGEXP, BLOCK_START_REGEXP, createBlockRegExp } from "../util/regexp.js";
4
4
  import { createMarkupRule } from "../util/rule.js";
5
5
  export const PARAGRAPH_REGEXP = createBlockRegExp("(?<paragraph>(?:(?=\\S)[\\s\\S]*?\\S))",
@@ -11,9 +11,4 @@ export const PARAGRAPH_REGEXP = createBlockRegExp("(?<paragraph>(?:(?=\\S)[\\s\\
11
11
  * - Any run of non-whitespace.
12
12
  * - Leading and trailing whitespace is trimmed.
13
13
  */
14
- export const PARAGRAPH_RULE = createMarkupRule(PARAGRAPH_REGEXP, ({ groups: { paragraph } }, options, key) => ({
15
- key,
16
- $$typeof: REACT_ELEMENT_TYPE,
17
- type: "p",
18
- props: { children: renderMarkup(paragraph, options, "inline") },
19
- }), ["block"], -10);
14
+ export const PARAGRAPH_RULE = createMarkupRule(PARAGRAPH_REGEXP, ({ groups: { paragraph } }, options, key) => _jsx("p", { children: renderMarkup(paragraph, options, "inline") }, key), ["block"], -10);
@@ -1,4 +1,4 @@
1
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createLineRegExp } from "../util/regexp.js";
3
3
  import { createMarkupRule } from "../util/rule.js";
4
4
  const SEPARATOR_REGEXP = createLineRegExp("([-*•+_=])(?: *\\1){2,}");
@@ -9,9 +9,4 @@ const SEPARATOR_REGEXP = createLineRegExp("([-*•+_=])(?: *\\1){2,}");
9
9
  * - Character must be the same every time (can't mix)
10
10
  * - Might have infinite number of spaces between the characters.
11
11
  */
12
- export const SEPARATOR_RULE = createMarkupRule(SEPARATOR_REGEXP, (_match, _options, key) => ({
13
- key,
14
- $$typeof: REACT_ELEMENT_TYPE,
15
- type: "hr",
16
- props: {},
17
- }), ["block"]);
12
+ export const SEPARATOR_RULE = createMarkupRule(SEPARATOR_REGEXP, (_match, _options, key) => _jsx("hr", {}, key), ["block"]);
@@ -0,0 +1,13 @@
1
+ /** Regular expression matching a table block: a header row, a delimiter row, then any number of pipe rows. */
2
+ export declare const TABLE_REGEXP: import("../../index.js").NamedRegExp<{
3
+ table: string;
4
+ }>;
5
+ /**
6
+ * Table.
7
+ * - Markdown-style pipe table: a header row, a `|---|` delimiter row, then body rows.
8
+ * - Cells are pipe-separated; outer pipes are optional and whitespace around cells is trimmed.
9
+ * - Extra `|---|` delimiter rows split the table into sections: the first section becomes `<thead>`, the last becomes `<tfoot>` (only when there are three or more sections), and every section in between becomes its own `<tbody>`.
10
+ * - Column count and per-column alignment (`:--` left, `--:` right, `:-:` centered) come from the first delimiter row; ragged rows are padded or truncated to that count.
11
+ * - Cell content is rendered as inline markup; write `\|` for a literal pipe inside a cell.
12
+ */
13
+ export declare const TABLE_RULE: import("../util/rule.js").MarkupRule;
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { renderMarkup } from "../render.js";
3
+ import { createBlockRegExp, LINE_SPACE_REGEXP } from "../util/regexp.js";
4
+ import { createMarkupRule } from "../util/rule.js";
5
+ // Constants.
6
+ const _SPACE = `${LINE_SPACE_REGEXP}*`; // Run of line whitespace (never crosses a newline).
7
+ const _CELL = `${_SPACE}:?-+:?${_SPACE}`; // Delimiter-row cell: one or more dashes with optional `:` alignment markers.
8
+ const _DELIMITER_SOURCE = `${_SPACE}\\|?(?:${_CELL}\\|)+(?:${_CELL})?${_SPACE}`; // Delimiter row: pipe-separated dash cells.
9
+ const _DELIMITER = new RegExp(`^${_DELIMITER_SOURCE}$`, "u"); // Tests whether a single line is a delimiter row.
10
+ const _ROW = "[^\\n]*\\|[^\\n]*"; // Any line containing at least one pipe.
11
+ const _SPLIT = /(?<!\\)\|/; // Splits a row into cells on unescaped pipes.
12
+ /** Regular expression matching a table block: a header row, a delimiter row, then any number of pipe rows. */
13
+ export const TABLE_REGEXP = createBlockRegExp(`(?<table>${_ROW}\\n${_DELIMITER_SOURCE}(?:\\n${_ROW})*)`);
14
+ /**
15
+ * Table.
16
+ * - Markdown-style pipe table: a header row, a `|---|` delimiter row, then body rows.
17
+ * - Cells are pipe-separated; outer pipes are optional and whitespace around cells is trimmed.
18
+ * - Extra `|---|` delimiter rows split the table into sections: the first section becomes `<thead>`, the last becomes `<tfoot>` (only when there are three or more sections), and every section in between becomes its own `<tbody>`.
19
+ * - Column count and per-column alignment (`:--` left, `--:` right, `:-:` centered) come from the first delimiter row; ragged rows are padded or truncated to that count.
20
+ * - Cell content is rendered as inline markup; write `\|` for a literal pipe inside a cell.
21
+ */
22
+ export const TABLE_RULE = createMarkupRule(TABLE_REGEXP, ({ groups: { table } }, options, key) => _renderTable(table, options, key), [
23
+ "block",
24
+ ]);
25
+ /** Render a matched table block into a `<table>` element. */
26
+ function _renderTable(table, options, key) {
27
+ const lines = table.split("\n");
28
+ // Column count and alignment come from the first delimiter row — always line 1, guaranteed by `TABLE_REGEXP`.
29
+ const aligns = _splitRow(lines[1] ?? "").map(_getAlign);
30
+ // Split lines into sections at delimiter rows. Line 0 is the header and is never treated as a delimiter.
31
+ const sections = [];
32
+ let section = [lines[0] ?? ""];
33
+ for (let i = 1; i < lines.length; i++) {
34
+ const line = lines[i] ?? "";
35
+ if (_DELIMITER.test(line)) {
36
+ sections.push(section);
37
+ section = [];
38
+ }
39
+ else {
40
+ section.push(line);
41
+ }
42
+ }
43
+ sections.push(section);
44
+ // Build the table with explicit loops — markup elements are static and positional, so the loop index is the natural key.
45
+ const last = sections.length - 1;
46
+ const body = [];
47
+ for (let s = 0; s < sections.length; s++) {
48
+ // First section is `<thead>`; the last is `<tfoot>` with 3+ sections; sections in between are each a `<tbody>`.
49
+ const Section = s === 0 ? "thead" : s === last && last >= 2 ? "tfoot" : "tbody";
50
+ const Cell = s === 0 ? "th" : "td";
51
+ const rowLines = sections[s] ?? [];
52
+ const rows = [];
53
+ for (let r = 0; r < rowLines.length; r++) {
54
+ const values = _splitRow(rowLines[r] ?? "");
55
+ const cells = [];
56
+ for (let c = 0; c < aligns.length; c++) {
57
+ cells.push(_jsx(Cell, { align: aligns[c], children: renderMarkup(values[c] ?? "", options, "inline") }, c));
58
+ }
59
+ rows.push(_jsx("tr", { children: cells }, r));
60
+ }
61
+ body.push(_jsx(Section, { children: rows }, s));
62
+ }
63
+ return _jsx("table", { children: body }, key);
64
+ }
65
+ /** Split a table row into trimmed cell strings, honouring `\|` escaped pipes. */
66
+ function _splitRow(row) {
67
+ let line = row.trim();
68
+ if (line.startsWith("|"))
69
+ line = line.slice(1);
70
+ if (line.endsWith("|"))
71
+ line = line.slice(0, -1);
72
+ return line.split(_SPLIT).map(cell => cell.trim().replaceAll("\\|", "|"));
73
+ }
74
+ /** Get the alignment of a delimiter-row cell, or `undefined` for the default (left). */
75
+ function _getAlign(cell) {
76
+ const start = cell.startsWith(":");
77
+ const end = cell.endsWith(":");
78
+ if (start && end)
79
+ return "center";
80
+ if (end)
81
+ return "right";
82
+ return undefined;
83
+ }
@@ -1,4 +1,4 @@
1
- import type { Element } from "../../util/element.js";
1
+ import type { ReactElement } from "react";
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<Element>;
16
+ export declare function _getItems(list: string, options: MarkupOptions): Iterable<ReactElement>;
@@ -1,5 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { renderMarkup } from "../render.js";
2
- import { REACT_ELEMENT_TYPE } from "../util/internal.js";
3
3
  import { BLOCK_CONTENT_REGEXP, BLOCK_SPACE_REGEXP, createBlockRegExp, LINE_SPACE_REGEXP } from "../util/regexp.js";
4
4
  import { createMarkupRule } from "../util/rule.js";
5
5
  const INDENT = /^\t/gm; // Nesting is recognised with tabs only.
@@ -14,26 +14,11 @@ export const UNORDERED_REGEXP = createBlockRegExp(`(?<list>${BULLET}(?:${LINE_SP
14
14
  * - List block is ended by `\n\n` two newline characters.
15
15
  * - Sparse lists are not supported.
16
16
  */
17
- export const UNORDERED_RULE = createMarkupRule(UNORDERED_REGEXP, ({ groups: { list = "" } }, options, key) => ({
18
- key,
19
- $$typeof: REACT_ELEMENT_TYPE,
20
- type: "ul",
21
- props: {
22
- children: Array.from(_getItems(list, options)),
23
- },
24
- }), ["block", "list"]);
17
+ export const UNORDERED_RULE = createMarkupRule(UNORDERED_REGEXP, ({ groups: { list = "" } }, options, key) => _jsx("ul", { children: Array.from(_getItems(list, options)) }, key), ["block", "list"]);
25
18
  /** Parse a markdown list into a set of items elements. */
26
19
  export function* _getItems(list, options) {
27
20
  let key = 0;
28
21
  for (const [_unused, item = ""] of list.matchAll(ITEM)) {
29
- yield {
30
- $$typeof: REACT_ELEMENT_TYPE,
31
- type: "li",
32
- props: {
33
- children: renderMarkup(item.replace(INDENT, ""), options, "list"),
34
- },
35
- key: key.toString(),
36
- };
37
- key++;
22
+ yield _jsx("li", { children: renderMarkup(item.replace(INDENT, ""), options, "list") }, key++);
38
23
  }
39
24
  }
@@ -1,4 +1,4 @@
1
- import type { Element } from "../../util/element.js";
1
+ import type { ReactElement } from "react";
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[]];
@@ -6,7 +6,7 @@ export interface MarkupRule {
6
6
  /** Regular expression used for matching the rule. */
7
7
  regexp: RegExp;
8
8
  /** Use the matched data to render an element. */
9
- render(match: RegExpExecArray, options: MarkupOptions, key: string): Element;
9
+ render(match: RegExpExecArray, options: MarkupOptions, key: string): ReactElement;
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 createMarkupRule<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;
17
+ export declare function createMarkupRule<T extends NamedRegExp | RegExp>(regexp: T, render: T extends NamedRegExp<infer X> ? (match: NamedRegExpExecArray<X>, options: MarkupOptions, key: string) => ReactElement : (match: RegExpExecArray, options: MarkupOptions, key: string) => ReactElement, contexts: MarkupContexts, priority?: number): MarkupRule;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.211.0",
3
+ "version": "1.213.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,7 +15,7 @@ html:has(.layout) {
15
15
  width: 100vw;
16
16
  width: 100dvw;
17
17
  overflow: hidden;
18
- overflow-wrap: anywhere;
18
+ overflow-wrap: break-word; /* Break overlong words only as a last resort; `anywhere` would also break mid-word to fit narrow table/flex columns. */
19
19
  overscroll-behavior: none;
20
20
  }
21
21
 
@@ -11,8 +11,8 @@ export interface SidebarLayoutProps {
11
11
  /**
12
12
  * Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
13
13
  * - The sidebar is rendered as `<nav>` — it almost always contains the page's primary navigation.
14
- * - On narrow viewports the sidebar slides off the left of the screen and is toggled with a "show menu" button.
15
- * - The toggle is driven by a hidden checkbox and pure CSS, so it works even when the page ships no client-side JavaScript.
14
+ * - On narrow viewports the sidebar becomes an off-canvas drawer toggled by the "show menu" / "close" buttons.
15
+ * - Inside a `<Navigation>` the drawer closes itself whenever the route changes (e.g. tapping a sidebar link).
16
16
  * - Use the `--sidebar-layout-width` and `--sidebar-layout-bg` custom properties to override defaults.
17
17
  */
18
18
  export declare function SidebarLayout({ sidebar, children, right }: SidebarLayoutProps): ReactElement;
@@ -1,21 +1,29 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid";
3
- import { useId } from "react";
3
+ import { use, useEffect, useState } from "react";
4
+ import { useStore } from "../../react/useStore.js";
5
+ import { Button } from "../form/Button.js";
6
+ import { NavigationContext } from "../router/NavigationContext.js";
4
7
  import { getClass } from "../util/css.js";
5
8
  import { LAYOUT_CSS } from "./Layout.js";
6
9
  import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
7
10
  /**
8
11
  * Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
9
12
  * - The sidebar is rendered as `<nav>` — it almost always contains the page's primary navigation.
10
- * - On narrow viewports the sidebar slides off the left of the screen and is toggled with a "show menu" button.
11
- * - The toggle is driven by a hidden checkbox and pure CSS, so it works even when the page ships no client-side JavaScript.
13
+ * - On narrow viewports the sidebar becomes an off-canvas drawer toggled by the "show menu" / "close" buttons.
14
+ * - Inside a `<Navigation>` the drawer closes itself whenever the route changes (e.g. tapping a sidebar link).
12
15
  * - Use the `--sidebar-layout-width` and `--sidebar-layout-bg` custom properties to override defaults.
13
16
  */
14
17
  export function SidebarLayout({ sidebar, children, right = false }) {
15
- // Ties the toggle checkbox to its `<label>` buttons — `useId()` is stable during static (non-hydrated) rendering.
16
- const id = useId();
17
- const sidebarEl = (_jsxs("nav", { className: SIDEBAR_LAYOUT_CSS.sidebar, children: [_jsx("label", { htmlFor: id, title: "Close menu", className: SIDEBAR_LAYOUT_CSS.close, children: _jsx(XMarkIcon, {}) }), sidebar] }, "sidebar"));
18
- const contentEl = (_jsxs("div", { className: getClass(LAYOUT_CSS.layout, SIDEBAR_LAYOUT_CSS.content), children: [_jsx("label", { htmlFor: id, title: "Show menu", className: SIDEBAR_LAYOUT_CSS.show, children: _jsx(Bars3Icon, {}) }), _jsx("div", { className: SIDEBAR_LAYOUT_CSS.contentInner, children: children })] }, "content"));
19
- return (_jsxs("main", { className: getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout), children: [_jsx("input", { type: "checkbox", id: id, className: SIDEBAR_LAYOUT_CSS.toggle, "aria-label": "Show or hide menu" }), right ? [contentEl, sidebarEl] : [sidebarEl, contentEl]] }));
18
+ const [open, setOpen] = useState(false);
19
+ // Close the drawer whenever navigation changes the URL — covers tapping a link inside the sidebar.
20
+ const href = useStore(use(NavigationContext))?.href;
21
+ useEffect(() => {
22
+ if (href)
23
+ setOpen(false);
24
+ }, [href]);
25
+ const sidebarEl = (_jsxs("nav", { className: getClass(SIDEBAR_LAYOUT_CSS.sidebar, open && SIDEBAR_LAYOUT_CSS.open), children: [_jsx("div", { className: SIDEBAR_LAYOUT_CSS.close, children: _jsx(Button, { plain: true, fit: true, title: "Close menu", onClick: () => setOpen(false), children: _jsx(XMarkIcon, {}) }) }), sidebar] }, "sidebar"));
26
+ const contentEl = (_jsxs("div", { className: getClass(LAYOUT_CSS.layout, SIDEBAR_LAYOUT_CSS.content), children: [_jsx("div", { className: SIDEBAR_LAYOUT_CSS.show, children: _jsx(Button, { plain: true, fit: true, title: "Show menu", onClick: () => setOpen(true), children: _jsx(Bars3Icon, {}) }) }), _jsx("div", { className: SIDEBAR_LAYOUT_CSS.contentInner, children: children })] }, "content"));
27
+ return (_jsx("main", { className: getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout), children: right ? [contentEl, sidebarEl] : [sidebarEl, contentEl] }));
20
28
  }
21
29
  export { SIDEBAR_LAYOUT_CSS };
@@ -6,9 +6,8 @@
6
6
  * The inner `.content` element reuses the `.layout` class too so it picks up the standard
7
7
  * layout padding, safe-area insets, and `overflow: hidden auto` scroll behaviour.
8
8
  *
9
- * On narrow viewports the sidebar becomes an off-canvas drawer: it slides off the left edge of the
10
- * screen and is toggled by the hidden `.toggle` checkbox via its `.show` / `.close` `<label>` buttons.
11
- * Driving the drawer with a checkbox + CSS means it works even when the page ships no client-side JS.
9
+ * On narrow viewports the sidebar becomes an off-canvas drawer that slides off the left edge of the
10
+ * screen; the `.show` / `.close` buttons toggle it (both are hidden entirely on wide viewports).
12
11
  */
13
12
 
14
13
  .main {
@@ -38,38 +37,16 @@
38
37
  margin: 0 auto;
39
38
  }
40
39
 
41
- /* Toggle checkbox — hidden entirely on wide viewports (see media query for the narrow-viewport state). */
42
- .toggle {
43
- display: none;
44
- }
45
-
46
- /* Menu toggle buttons — hidden by default, only shown on narrow viewports (see media query below). */
40
+ /* Wrappers for the menu toggle buttons — hidden on wide viewports, shown on narrow ones (see media query). */
47
41
  .show,
48
42
  .close {
49
43
  display: none;
50
- width: fit-content;
51
- margin: 0;
52
- align-items: center;
53
- justify-content: center;
54
- padding: var(--space-small);
55
- border-radius: var(--radius-xsmall);
56
- color: var(--color-text);
57
- cursor: pointer;
58
-
59
- & svg {
60
- width: var(--size-icon);
61
- height: var(--size-icon);
62
- }
63
-
64
- &:hover {
65
- background: var(--color-surface);
66
- }
44
+ margin-bottom: var(--space-xsmall);
67
45
  }
68
46
 
69
47
  /* The close button sits at the top of the sidebar, aligned to its trailing edge. */
70
48
  .close {
71
- margin-inline-start: auto;
72
- margin-bottom: var(--space-xsmall);
49
+ justify-content: flex-end;
73
50
  }
74
51
 
75
52
  /* On narrow viewports the sidebar becomes an off-canvas drawer that slides in from the left. */
@@ -87,28 +64,12 @@
87
64
  transform: translateX(-100%);
88
65
  transition: transform var(--duration-normal) ease-in-out;
89
66
  }
90
- /* Checked checkbox → drawer slides on screen. */
91
- .toggle:checked ~ .sidebar {
67
+ .sidebar.open {
92
68
  transform: translateX(0);
93
69
  box-shadow: 0 0 1.5rem var(--color-overlay);
94
70
  }
95
- /* Keep the checkbox visually hidden but still focusable for keyboard users. */
96
- .toggle {
97
- display: block;
98
- position: absolute;
99
- width: 1px;
100
- height: 1px;
101
- overflow: hidden;
102
- clip-path: inset(50%);
103
- }
104
71
  .show,
105
72
  .close {
106
73
  display: flex;
107
74
  }
108
- /* Forward the checkbox's keyboard focus ring to whichever toggle button is on screen. */
109
- .toggle:focus-visible ~ .content .show,
110
- .toggle:focus-visible ~ .sidebar .close {
111
- outline: var(--stroke-focus) solid var(--color-focus);
112
- outline-offset: 2px;
113
- }
114
75
  }
@@ -1,5 +1,8 @@
1
1
  import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/solid";
2
- import { type ReactElement, type ReactNode, useId } from "react";
2
+ import { type ReactElement, type ReactNode, use, useEffect, useState } from "react";
3
+ import { useStore } from "../../react/useStore.js";
4
+ import { Button } from "../form/Button.js";
5
+ import { NavigationContext } from "../router/NavigationContext.js";
3
6
  import { getClass } from "../util/css.js";
4
7
  import { LAYOUT_CSS } from "./Layout.js";
5
8
  import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
@@ -16,35 +19,41 @@ export interface SidebarLayoutProps {
16
19
  /**
17
20
  * Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
18
21
  * - The sidebar is rendered as `<nav>` — it almost always contains the page's primary navigation.
19
- * - On narrow viewports the sidebar slides off the left of the screen and is toggled with a "show menu" button.
20
- * - The toggle is driven by a hidden checkbox and pure CSS, so it works even when the page ships no client-side JavaScript.
22
+ * - On narrow viewports the sidebar becomes an off-canvas drawer toggled by the "show menu" / "close" buttons.
23
+ * - Inside a `<Navigation>` the drawer closes itself whenever the route changes (e.g. tapping a sidebar link).
21
24
  * - Use the `--sidebar-layout-width` and `--sidebar-layout-bg` custom properties to override defaults.
22
25
  */
23
26
  export function SidebarLayout({ sidebar, children, right = false }: SidebarLayoutProps): ReactElement {
24
- // Ties the toggle checkbox to its `<label>` buttons — `useId()` is stable during static (non-hydrated) rendering.
25
- const id = useId();
27
+ const [open, setOpen] = useState(false);
28
+
29
+ // Close the drawer whenever navigation changes the URL — covers tapping a link inside the sidebar.
30
+ const href = useStore(use(NavigationContext))?.href;
31
+ useEffect(() => {
32
+ if (href) setOpen(false);
33
+ }, [href]);
26
34
 
27
35
  const sidebarEl = (
28
- <nav key="sidebar" className={SIDEBAR_LAYOUT_CSS.sidebar}>
29
- <label htmlFor={id} title="Close menu" className={SIDEBAR_LAYOUT_CSS.close}>
30
- <XMarkIcon />
31
- </label>
36
+ <nav key="sidebar" className={getClass(SIDEBAR_LAYOUT_CSS.sidebar, open && SIDEBAR_LAYOUT_CSS.open)}>
37
+ <div className={SIDEBAR_LAYOUT_CSS.close}>
38
+ <Button plain fit title="Close menu" onClick={() => setOpen(false)}>
39
+ <XMarkIcon />
40
+ </Button>
41
+ </div>
32
42
  {sidebar}
33
43
  </nav>
34
44
  );
35
45
  const contentEl = (
36
46
  <div key="content" className={getClass(LAYOUT_CSS.layout, SIDEBAR_LAYOUT_CSS.content)}>
37
- <label htmlFor={id} title="Show menu" className={SIDEBAR_LAYOUT_CSS.show}>
38
- <Bars3Icon />
39
- </label>
47
+ <div className={SIDEBAR_LAYOUT_CSS.show}>
48
+ <Button plain fit title="Show menu" onClick={() => setOpen(true)}>
49
+ <Bars3Icon />
50
+ </Button>
51
+ </div>
40
52
  <div className={SIDEBAR_LAYOUT_CSS.contentInner}>{children}</div>
41
53
  </div>
42
54
  );
43
55
  return (
44
- <main className={getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout)}>
45
- <input type="checkbox" id={id} className={SIDEBAR_LAYOUT_CSS.toggle} aria-label="Show or hide menu" />
46
- {right ? [contentEl, sidebarEl] : [sidebarEl, contentEl]}
47
- </main>
56
+ <main className={getClass(SIDEBAR_LAYOUT_CSS.main, LAYOUT_CSS.layout)}>{right ? [contentEl, sidebarEl] : [sidebarEl, contentEl]}</main>
48
57
  );
49
58
  }
50
59
 
package/ui/page/HTML.d.ts CHANGED
@@ -4,7 +4,7 @@ export interface HTMLProps extends PossibleMeta {
4
4
  children: ReactNode;
5
5
  }
6
6
  /**
7
- * Output a `<html>` element wrapping `<head>` (via `<Head>`) and `<body id="root">`.
7
+ * Output a `<html>` element wrapping `<head>` (via `<Head>`) and `<body>`.
8
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
10
  export declare function HTML({ children, ...meta }: HTMLProps): ReactElement;
package/ui/page/HTML.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { MetaContext, requireMeta } from "../misc/MetaContext.js";
3
3
  /**
4
- * Output a `<html>` element wrapping `<head>` (via `<Head>`) and `<body id="root">`.
4
+ * Output a `<html>` element wrapping `<head>` (via `<Head>`) and `<body>`.
5
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.
6
6
  */
7
7
  export function HTML({ children, ...meta }) {
8
8
  const merged = requireMeta(meta);
9
9
  const { language, root: 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 }) })] }));
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", { children: _jsx(MetaContext, { value: merged, children: children }) })] }));
11
11
  }
package/ui/page/HTML.tsx CHANGED
@@ -7,7 +7,7 @@ export interface HTMLProps extends PossibleMeta {
7
7
  }
8
8
 
9
9
  /**
10
- * Output a `<html>` element wrapping `<head>` (via `<Head>`) and `<body id="root">`.
10
+ * Output a `<html>` element wrapping `<head>` (via `<Head>`) and `<body>`.
11
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
13
  export function HTML({ children, ...meta }: HTMLProps): ReactElement {
@@ -20,7 +20,7 @@ export function HTML({ children, ...meta }: HTMLProps): ReactElement {
20
20
  {base && <base href={base.href} />}
21
21
  {app && <title>{app}</title>}
22
22
  </head>
23
- <body id="root">
23
+ <body>
24
24
  <MetaContext value={merged}>{children}</MetaContext>
25
25
  </body>
26
26
  </html>
package/ui/util/meta.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { AnyCaller } from "../../util/function.js";
4
4
  import { type PossibleLink } from "../../util/link.js";
5
5
  import type { Nullish } from "../../util/null.js";
6
6
  import { type ImmutableURI, type PossibleURI, type PossibleURIParams } from "../../util/uri.js";
7
- import { type ImmutableURL } from "../../util/url.js";
7
+ import { type ImmutableURL, type PossibleURL } from "../../util/url.js";
8
8
  /** Set of named meta `<meta />` tags in `{ name: content }` format. */
9
9
  export type MetaTags = ImmutableDictionary<string | boolean | null | undefined>;
10
10
  /** Set of named meta `<link />` tags in `{ rel: href }` format. */
@@ -42,7 +42,9 @@ export interface Meta {
42
42
  readonly stylesheets?: MetaAssets | undefined;
43
43
  }
44
44
  /** Input metadata that can be parsed and converted to proper metadata. */
45
- export interface PossibleMeta extends Omit<Meta, "url" | "links" | "scripts" | "modules" | "stylesheets"> {
45
+ export interface PossibleMeta extends Omit<Meta, "root" | "url" | "links" | "scripts" | "modules" | "stylesheets"> {
46
+ /** Base URL for the app — accepts a string or `URL`, resolved with `requireURL()`. */
47
+ readonly root?: PossibleURL | undefined;
46
48
  /**
47
49
  * New URL for the page.
48
50
  * - Resolved using `requireURL()` if set relative to `root`
@@ -70,11 +72,16 @@ export declare function joinTitles(...titles: (string | undefined)[]): string;
70
72
  * - `stylesheets` and `links` hrefs newly set in `meta2` are absolutified against the merged `url`/`base`, so they stay correct no matter where they are later rendered.
71
73
  */
72
74
  export declare function mergeMeta(meta1: Meta, meta2: PossibleMeta, caller?: AnyCaller): Meta;
75
+ /**
76
+ * Create a fully-formed `Meta` from a `PossibleMeta`.
77
+ * - Like `mergeMeta()` but with no previous `Meta` to merge into — initialises meta from scratch.
78
+ */
79
+ export declare function createMeta(meta: PossibleMeta, caller?: AnyCaller): Meta;
73
80
  /**
74
81
  * Merge two metadata URLs.
75
82
  * - New URL is resolved relative to: current URL, new base URL, current base URL
76
83
  */
77
- export declare function mergeMetaURL(base: ImmutableURL | undefined, current: ImmutableURL | undefined, next: PossibleURI | undefined, params: PossibleURIParams | undefined, caller?: AnyCaller): ImmutableURL | undefined;
84
+ export declare function mergeMetaURL(base: ImmutableURL | undefined, current: ImmutableURL | undefined, next: PossibleURL | undefined, params: PossibleURIParams | undefined, caller?: AnyCaller): ImmutableURL | undefined;
78
85
  /**
79
86
  * Merge two metadata tags.
80
87
  * - New assets are resolved relative to current URL (relative paths) and root URL (absolute paths).
package/ui/util/meta.js CHANGED
@@ -37,6 +37,13 @@ export function mergeMeta(meta1, meta2, caller = mergeMeta) {
37
37
  stylesheets: mergeMetaAssets(meta1.stylesheets, meta2.stylesheets, url, root, caller),
38
38
  };
39
39
  }
40
+ /**
41
+ * Create a fully-formed `Meta` from a `PossibleMeta`.
42
+ * - Like `mergeMeta()` but with no previous `Meta` to merge into — initialises meta from scratch.
43
+ */
44
+ export function createMeta(meta, caller = createMeta) {
45
+ return mergeMeta({}, meta, caller);
46
+ }
40
47
  /**
41
48
  * Merge two metadata URLs.
42
49
  * - New URL is resolved relative to: current URL, new base URL, current base URL
package/ui/util/meta.ts CHANGED
@@ -4,7 +4,7 @@ import type { AnyCaller } from "../../util/function.js";
4
4
  import { type PossibleLink, requireLink } from "../../util/link.js";
5
5
  import type { Nullish } from "../../util/null.js";
6
6
  import { type ImmutableURI, type PossibleURI, type PossibleURIParams, withURIParams } from "../../util/uri.js";
7
- import { type ImmutableURL, requireURL } from "../../util/url.js";
7
+ import { type ImmutableURL, type PossibleURL, requireURL } from "../../util/url.js";
8
8
 
9
9
  /** Set of named meta `<meta />` tags in `{ name: content }` format. */
10
10
  export type MetaTags = ImmutableDictionary<string | boolean | null | undefined>;
@@ -52,7 +52,10 @@ export interface Meta {
52
52
  }
53
53
 
54
54
  /** Input metadata that can be parsed and converted to proper metadata. */
55
- export interface PossibleMeta extends Omit<Meta, "url" | "links" | "scripts" | "modules" | "stylesheets"> {
55
+ export interface PossibleMeta extends Omit<Meta, "root" | "url" | "links" | "scripts" | "modules" | "stylesheets"> {
56
+ /** Base URL for the app — accepts a string or `URL`, resolved with `requireURL()`. */
57
+ readonly root?: PossibleURL | undefined;
58
+
56
59
  /**
57
60
  * New URL for the page.
58
61
  * - Resolved using `requireURL()` if set relative to `root`
@@ -111,6 +114,14 @@ export function mergeMeta(meta1: Meta, meta2: PossibleMeta, caller: AnyCaller =
111
114
  };
112
115
  }
113
116
 
117
+ /**
118
+ * Create a fully-formed `Meta` from a `PossibleMeta`.
119
+ * - Like `mergeMeta()` but with no previous `Meta` to merge into — initialises meta from scratch.
120
+ */
121
+ export function createMeta(meta: PossibleMeta, caller: AnyCaller = createMeta): Meta {
122
+ return mergeMeta({}, meta, caller);
123
+ }
124
+
114
125
  /**
115
126
  * Merge two metadata URLs.
116
127
  * - New URL is resolved relative to: current URL, new base URL, current base URL
@@ -118,7 +129,7 @@ export function mergeMeta(meta1: Meta, meta2: PossibleMeta, caller: AnyCaller =
118
129
  export function mergeMetaURL(
119
130
  base: ImmutableURL | undefined,
120
131
  current: ImmutableURL | undefined,
121
- next: PossibleURI | undefined,
132
+ next: PossibleURL | undefined,
122
133
  params: PossibleURIParams | undefined,
123
134
  caller: AnyCaller = mergeMetaURL,
124
135
  ): ImmutableURL | undefined {
package/util/element.d.ts CHANGED
@@ -2,18 +2,19 @@ import type { ImmutableArray } from "./array.js";
2
2
  import type { AbsolutePath } from "./path.js";
3
3
  import type { Query } from "./query.js";
4
4
  /** Set of valid props for an element. */
5
- export interface ElementProps {
6
- readonly [key: string]: unknown;
5
+ export type ElementProps = {
7
6
  readonly children?: Elements;
8
- }
9
- /** Element with a type, props, and optional key (compatible with `React.ReactElement`). */
10
- export interface Element<P extends ElementProps = ElementProps> {
11
- readonly [key: string]: unknown;
7
+ };
8
+ /**
9
+ * Element with a type, props, and optional key (compatible with `React.ReactElement`).
10
+ * - Declared as a `type`, not an `interface`, so its implicit index signature lets it satisfy `Data` — `queryElements()` runs elements through `queryItems<T extends Data>`.
11
+ */
12
+ export type Element<P extends ElementProps = ElementProps> = {
12
13
  readonly type: string | ((props: P) => Elements | null);
13
14
  readonly props: P;
14
15
  readonly key: string | null;
15
16
  readonly $$typeof?: symbol;
16
- }
17
+ };
17
18
  /** Collection of elements (compatible with `React.ReactNode`). */
18
19
  export type Elements<T extends Element = Element> = undefined | null | string | T | Iterable<Elements<T>>;
19
20
  /** Props for a tree element — must have a `tree-` prefixed type. */