@fusion.dev/helix-headless 0.215.3 → 0.216.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 (113) hide show
  1. package/api.json +1 -1
  2. package/custom-elements.json +1 -1
  3. package/dist/area-chart/root/area-chart-root.d.ts +1 -0
  4. package/dist/area-chart/root/area-chart-root.js +1 -1
  5. package/dist/bar-chart/root/bar-chart-root.d.ts +1 -0
  6. package/dist/bar-chart/root/bar-chart-root.js +1 -1
  7. package/dist/button/root/button-root.d.ts +3 -1
  8. package/dist/button/root/button-root.js +2 -1
  9. package/dist/chat/composer/chat-composer.d.ts +18 -1
  10. package/dist/chat/composer/chat-composer.js +8 -3
  11. package/dist/checkbox/root/checkbox-root.d.ts +3 -1
  12. package/dist/checkbox/root/checkbox-root.js +2 -1
  13. package/dist/code-block/highlight.d.ts +9 -0
  14. package/dist/code-block/highlight.js +39 -0
  15. package/dist/code-block/highlighter.d.ts +44 -0
  16. package/dist/code-block/highlighter.js +44 -0
  17. package/dist/code-block/index.d.ts +1 -0
  18. package/dist/code-block/index.js +1 -0
  19. package/dist/code-block/keyed-lines.d.ts +23 -0
  20. package/dist/code-block/keyed-lines.js +75 -0
  21. package/dist/code-block/root/code-block-root.d.ts +6 -23
  22. package/dist/code-block/root/code-block-root.js +8 -25
  23. package/dist/code-editor/index.d.ts +1 -0
  24. package/dist/code-editor/index.js +1 -0
  25. package/dist/code-editor/root/code-editor-root.d.ts +367 -0
  26. package/dist/code-editor/root/code-editor-root.js +746 -0
  27. package/dist/code-input/index.d.ts +1 -0
  28. package/dist/code-input/index.js +1 -0
  29. package/dist/code-input/root/code-input-root.d.ts +119 -0
  30. package/dist/code-input/root/code-input-root.js +142 -0
  31. package/dist/combobox/root/combobox-root.d.ts +6 -1
  32. package/dist/combobox/root/combobox-root.js +4 -2
  33. package/dist/command/root/command-root.d.ts +61 -11
  34. package/dist/command/root/command-root.js +88 -3
  35. package/dist/donut-chart/root/donut-chart-root.d.ts +1 -0
  36. package/dist/donut-chart/root/donut-chart-root.js +1 -1
  37. package/dist/element-events.generated.js +36 -3
  38. package/dist/filter-input/root/filter-input-root.d.ts +9 -1
  39. package/dist/filter-input/root/filter-input-root.js +7 -3
  40. package/dist/heatmap/root/heatmap-root.d.ts +1 -0
  41. package/dist/heatmap/root/heatmap-root.js +1 -1
  42. package/dist/index.d.ts +4 -0
  43. package/dist/index.js +4 -0
  44. package/dist/input/root/input-root.d.ts +14 -2
  45. package/dist/input/root/input-root.js +11 -3
  46. package/dist/kbd/root/kbd-root.js +9 -2
  47. package/dist/line-chart/root/line-chart-root.d.ts +1 -0
  48. package/dist/line-chart/root/line-chart-root.js +1 -1
  49. package/dist/listbox/root/listbox-root.d.ts +8 -1
  50. package/dist/listbox/root/listbox-root.js +7 -4
  51. package/dist/markdown/blocks.d.ts +66 -0
  52. package/dist/markdown/blocks.js +326 -0
  53. package/dist/markdown/index.d.ts +6 -0
  54. package/dist/markdown/index.js +5 -0
  55. package/dist/markdown/inline.d.ts +57 -0
  56. package/dist/markdown/inline.js +483 -0
  57. package/dist/markdown/root/markdown-root.d.ts +99 -0
  58. package/dist/markdown/root/markdown-root.js +93 -0
  59. package/dist/markdown/stream.d.ts +47 -0
  60. package/dist/markdown/stream.js +62 -0
  61. package/dist/markdown/tree.d.ts +57 -0
  62. package/dist/markdown/tree.js +144 -0
  63. package/dist/markdown/writer.d.ts +33 -0
  64. package/dist/markdown/writer.js +139 -0
  65. package/dist/menu/root/menu-root.d.ts +53 -0
  66. package/dist/menu/root/menu-root.js +92 -15
  67. package/dist/pagination/size/pagination-size.d.ts +3 -0
  68. package/dist/pagination/size/pagination-size.js +2 -1
  69. package/dist/scatter-chart/root/scatter-chart-root.d.ts +1 -0
  70. package/dist/scatter-chart/root/scatter-chart-root.js +1 -1
  71. package/dist/select/root/select-root.d.ts +6 -1
  72. package/dist/select/root/select-root.js +4 -2
  73. package/dist/shortcut-overlay/index.d.ts +1 -0
  74. package/dist/shortcut-overlay/index.js +1 -0
  75. package/dist/shortcut-overlay/root/shortcut-overlay-root.d.ts +128 -0
  76. package/dist/shortcut-overlay/root/shortcut-overlay-root.js +275 -0
  77. package/dist/slider/root/slider-root.d.ts +3 -1
  78. package/dist/slider/root/slider-root.js +2 -1
  79. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.d.ts +1 -0
  80. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.js +1 -1
  81. package/dist/spatial-heatmap/root/spatial-heatmap-root.d.ts +1 -0
  82. package/dist/spatial-heatmap/root/spatial-heatmap-root.js +1 -1
  83. package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.d.ts +1 -0
  84. package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.js +1 -1
  85. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.d.ts +1 -0
  86. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.js +1 -1
  87. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.d.ts +1 -0
  88. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.js +1 -1
  89. package/dist/spatial-treemap/root/spatial-treemap-root.d.ts +1 -0
  90. package/dist/spatial-treemap/root/spatial-treemap-root.js +1 -1
  91. package/dist/switch/root/switch-root.d.ts +3 -1
  92. package/dist/switch/root/switch-root.js +2 -1
  93. package/dist/tabs/tab/tab.d.ts +3 -1
  94. package/dist/tabs/tab/tab.js +2 -1
  95. package/dist/textarea/root/textarea-root.d.ts +20 -4
  96. package/dist/textarea/root/textarea-root.js +31 -23
  97. package/dist/time-input/root/time-input-root.d.ts +6 -0
  98. package/dist/time-input/root/time-input-root.js +4 -2
  99. package/dist/time-picker/root/time-picker-root.d.ts +6 -0
  100. package/dist/time-picker/root/time-picker-root.js +4 -2
  101. package/dist/toast/root/toast-root.d.ts +64 -0
  102. package/dist/toast/root/toast-root.js +128 -3
  103. package/dist/toggle/root/toggle-root.d.ts +3 -1
  104. package/dist/toggle/root/toggle-root.js +2 -1
  105. package/dist/virtual-list/root/virtual-list-root.d.ts +18 -2
  106. package/dist/virtual-list/root/virtual-list-root.js +103 -5
  107. package/examples.json +1 -1
  108. package/lint.json +1 -1
  109. package/package.json +6 -6
  110. package/types/jsx.d.ts +5315 -596
  111. package/types/svelte.d.ts +4 -0
  112. package/types/vue.d.ts +1085 -4
  113. package/types/words.d.ts +491 -0
@@ -0,0 +1,47 @@
1
+ import { type MarkdownBlock } from './blocks.js';
2
+ /**
3
+ * The part of a reply that can be drawn now, and the part held back until the
4
+ * next characters decide what it is.
5
+ *
6
+ * Only the end of the text is held, and only for as long as it is ambiguous:
7
+ *
8
+ * - a last line that is only a marker so far (`#`, `-`, `1.`, three backticks,
9
+ * `>` or `> [!NO`), until it is clear what block it starts. A fence's opening line
10
+ * is held whole, since its language arrives with it;
11
+ * - a line starting with a pipe after a line that did not, with whatever has
12
+ * arrived of the next line, until that next line shows whether the first was
13
+ * a table's header.
14
+ *
15
+ * Holding costs a few characters of latency and nothing else, so it is safe
16
+ * anywhere, a code block included: what is held is drawn, unchanged, a moment
17
+ * later.
18
+ */
19
+ export declare const visibleText: (text: string) => string;
20
+ /** What one read of a stream gives: its blocks, and how many of them are final. */
21
+ export interface MarkdownRead {
22
+ blocks: MarkdownBlock[];
23
+ /** The leading blocks no later text can change. */
24
+ closed: number;
25
+ }
26
+ /**
27
+ * Reads a growing text into blocks, reparsing only from the block that is
28
+ * still open.
29
+ *
30
+ * A top-level block is final once another has started after it, since the line
31
+ * that started the next one cannot be read differently by anything after it.
32
+ * So each read keeps those, and parses again from the first line of the last
33
+ * one. A text that does not extend the last one starts over.
34
+ */
35
+ export declare class MarkdownStream {
36
+ private text;
37
+ private blocks;
38
+ private starts;
39
+ private lines;
40
+ /**
41
+ * Reads the text so far. With `final`, the whole text is read, the held end
42
+ * included, as the finished reply.
43
+ */
44
+ read(text: string, final: boolean): MarkdownRead;
45
+ /** Forgets everything read, as a new reply does. */
46
+ reset(): void;
47
+ }
@@ -0,0 +1,62 @@
1
+ import { parseBlocks } from './blocks.js';
2
+ export const visibleText = (text) => {
3
+ const breakAt = text.lastIndexOf('\n');
4
+ const partial = text.slice(breakAt + 1);
5
+ const complete = breakAt < 0 ? '' : text.slice(0, breakAt);
6
+ const bare = partial.replace(/^[ \t>]*/, '');
7
+ if (/^(?:#{1,6}|[-*_+](?:[ \t]*[-*_])*|\d{1,9}[.)]?|`+|~+)$/.test(bare) ||
8
+ /^(?:`{3,}|~{3,})/.test(bare) ||
9
+ (/^[ \t]*>/.test(partial) && /^(?:\[(?:!(?:[A-Za-z]{0,9}\]?)?)?)?$/.test(bare))) {
10
+ return breakAt < 0 ? '' : `${complete}\n`;
11
+ }
12
+ const lines = breakAt < 0 ? [] : complete.split('\n');
13
+ const last = lines.at(-1);
14
+ const piped = (line) => line !== undefined && /^[ \t>]*\|/.test(line);
15
+ if (piped(partial) && !piped(last)) {
16
+ return breakAt < 0 ? '' : `${complete}\n`;
17
+ }
18
+ if (piped(last) && !piped(lines.at(-2)) && /^[ \t>]*[|:\- \t]*$/.test(partial)) {
19
+ return lines.length > 1 ? `${lines.slice(0, -1).join('\n')}\n` : '';
20
+ }
21
+ return text;
22
+ };
23
+ export class MarkdownStream {
24
+ constructor() {
25
+ this.text = '';
26
+ this.blocks = [];
27
+ this.starts = [];
28
+ this.lines = [];
29
+ }
30
+ read(text, final) {
31
+ const drawn = final ? text : visibleText(text);
32
+ if (!drawn.startsWith(this.text)) {
33
+ this.blocks = [];
34
+ this.starts = [];
35
+ this.lines = [];
36
+ }
37
+ this.text = drawn;
38
+ const lines = drawn === '' ? [] : drawn.replace(/\r\n?/g, '\n').split('\n');
39
+ if (lines.at(-1) === '') {
40
+ lines.pop();
41
+ }
42
+ const keep = Math.max(0, this.blocks.length - 1);
43
+ const from = this.starts[keep] ?? 0;
44
+ const same = this.lines.length >= from && lines.length >= from;
45
+ const restart = same ? from : 0;
46
+ const kept = same ? keep : 0;
47
+ const read = parseBlocks(lines.slice(restart));
48
+ this.blocks = [...this.blocks.slice(0, kept), ...read.blocks];
49
+ this.starts = [...this.starts.slice(0, kept), ...read.starts.map((start) => start + restart)];
50
+ this.lines = lines;
51
+ return {
52
+ blocks: this.blocks,
53
+ closed: final ? this.blocks.length : Math.max(0, this.blocks.length - 1),
54
+ };
55
+ }
56
+ reset() {
57
+ this.text = '';
58
+ this.blocks = [];
59
+ this.starts = [];
60
+ this.lines = [];
61
+ }
62
+ }
@@ -0,0 +1,57 @@
1
+ import type { MarkdownAlertKind, MarkdownBlock } from './blocks.js';
2
+ /** A node of what a document draws: an element, or the text of a text node. */
3
+ export type MarkdownNode = string | MarkdownElement;
4
+ /** An element of what a document draws. */
5
+ export interface MarkdownElement {
6
+ attributes: Record<string, string>;
7
+ children: MarkdownNode[];
8
+ tag: string;
9
+ }
10
+ /** How a document is drawn. */
11
+ export interface MarkdownOptions {
12
+ /** Each alert's heading by its kind. English by default. */
13
+ headings?: Partial<Record<MarkdownAlertKind, string>> | undefined;
14
+ /**
15
+ * The tags for a fence and an alert, which name the tier they are drawn in.
16
+ * `helix-code-block` and `helix-callout` by default.
17
+ */
18
+ tags?: Partial<MarkdownTags> | undefined;
19
+ }
20
+ /** The options with every default filled in. */
21
+ export interface MarkdownFilled {
22
+ headings: Record<MarkdownAlertKind, string>;
23
+ tags: MarkdownTags;
24
+ }
25
+ /** The elements a document draws from another family. */
26
+ export interface MarkdownTags {
27
+ callout: string;
28
+ codeBlock: string;
29
+ }
30
+ /**
31
+ * The address, where it is one a link may follow: a relative address, or one
32
+ * on `http`, `https`, `mailto` or `tel`. Anything else, `javascript:` and
33
+ * `data:` among them, is dropped, and the link is drawn as its text.
34
+ */
35
+ export declare const safeUrl: (url: string) => string | undefined;
36
+ /**
37
+ * Each column's share of a table's width, from its heading's length, between
38
+ * 4 and 24 characters. Set once, from the rows that arrive first, so the
39
+ * columns never move as the rest arrives.
40
+ */
41
+ export declare const columnShares: (head: string[]) => number[];
42
+ /** A block as the node it draws. `open` reads its end as still arriving. */
43
+ export declare const blockNode: (block: MarkdownBlock, open: boolean, options: MarkdownFilled) => MarkdownElement;
44
+ /** The options with every default filled in. */
45
+ export declare const withDefaults: (options?: MarkdownOptions) => MarkdownFilled;
46
+ /** A finished document's nodes. */
47
+ export declare const markdownNodes: (text: string, options?: MarkdownOptions) => MarkdownElement[];
48
+ /** A node as HTML, the way a browser serializes the element it draws. */
49
+ export declare const nodeHtml: (node: MarkdownNode) => string;
50
+ /**
51
+ * A finished document as HTML, for a server to send inside `helix-markdown`.
52
+ *
53
+ * The element takes over what it is sent where the markup is exactly what it
54
+ * would draw for its `text`, so a page served this way draws the reply before
55
+ * any script runs and does not draw it again.
56
+ */
57
+ export declare const markdownToHtml: (text: string, options?: MarkdownOptions) => string;
@@ -0,0 +1,144 @@
1
+ import { parseInline } from './inline.js';
2
+ import { MarkdownStream } from './stream.js';
3
+ const HEADINGS = {
4
+ caution: 'Caution',
5
+ important: 'Important',
6
+ note: 'Note',
7
+ tip: 'Tip',
8
+ warning: 'Warning',
9
+ };
10
+ const TONES = {
11
+ caution: 'danger',
12
+ important: undefined,
13
+ note: 'info',
14
+ tip: 'success',
15
+ warning: 'warning',
16
+ };
17
+ const element = (tag, children = [], attributes = {}) => ({ attributes, children, tag });
18
+ export const safeUrl = (url) => {
19
+ const trimmed = url.trim();
20
+ const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(trimmed)?.[1]?.toLowerCase();
21
+ return scheme === undefined || ['http', 'https', 'mailto', 'tel'].includes(scheme)
22
+ ? trimmed
23
+ : undefined;
24
+ };
25
+ const inlineNodes = (nodes) => nodes.flatMap((node) => {
26
+ switch (node.kind) {
27
+ case 'break':
28
+ return [element('br')];
29
+ case 'code':
30
+ return [element('code', [node.text])];
31
+ case 'del':
32
+ case 'em':
33
+ case 'strong':
34
+ return [element(node.kind, inlineNodes(node.children))];
35
+ case 'image': {
36
+ const href = safeUrl(node.src);
37
+ const words = node.alt || node.src;
38
+ return href === undefined
39
+ ? [words]
40
+ : [
41
+ element('a', [words], node.title === undefined ? { href } : { href, title: node.title }),
42
+ ];
43
+ }
44
+ case 'link': {
45
+ const children = inlineNodes(node.children);
46
+ if (node.href === undefined) {
47
+ return [element('a', children)];
48
+ }
49
+ const href = safeUrl(node.href);
50
+ if (href === undefined) {
51
+ return children;
52
+ }
53
+ return [
54
+ element('a', children, node.title === undefined ? { href } : { href, title: node.title }),
55
+ ];
56
+ }
57
+ default:
58
+ return node.text === '' ? [] : [node.text];
59
+ }
60
+ });
61
+ const inline = (text, open) => inlineNodes(parseInline(text, { open }));
62
+ const aligned = (align) => align === undefined ? {} : { 'data-align': align };
63
+ export const columnShares = (head) => {
64
+ const weights = head.map((cell) => Math.min(24, Math.max(4, cell.length)));
65
+ const total = weights.reduce((sum, weight) => sum + weight, 0);
66
+ return weights.map((weight) => Math.round((weight / total) * 10000) / 100);
67
+ };
68
+ const itemNode = (item, open, options) => {
69
+ const [first, ...rest] = item.children;
70
+ const lead = first?.kind === 'paragraph'
71
+ ? inline(first.text, open && rest.length === 0)
72
+ : first
73
+ ? [blockNode(first, open && rest.length === 0, options)]
74
+ : [];
75
+ const tail = rest.map((block, index) => blockNode(block, open && index === rest.length - 1, options));
76
+ if (item.checked === undefined) {
77
+ return element('li', [...lead, ...tail]);
78
+ }
79
+ const box = element('input', [], item.checked
80
+ ? { checked: '', disabled: '', type: 'checkbox' }
81
+ : { disabled: '', type: 'checkbox' });
82
+ return element('li', [box, ' ', ...lead, ...tail], { 'data-task': '' });
83
+ };
84
+ export const blockNode = (block, open, options) => {
85
+ switch (block.kind) {
86
+ case 'code': {
87
+ const language = block.info.split(/\s+/)[0] ?? '';
88
+ return element(options.tags.codeBlock, [block.text], language === '' ? {} : { label: language, language });
89
+ }
90
+ case 'heading':
91
+ return element(`h${block.level}`, inline(block.text, open));
92
+ case 'list':
93
+ return element(block.ordered ? 'ol' : 'ul', block.items.map((item, index) => itemNode(item, open && index === block.items.length - 1, options)), block.ordered && block.start !== 1 ? { start: String(block.start) } : {});
94
+ case 'paragraph':
95
+ return element('p', inline(block.text, open));
96
+ case 'quote': {
97
+ const children = block.children.map((child, index) => blockNode(child, open && index === block.children.length - 1, options));
98
+ if (block.alert === undefined) {
99
+ return element('blockquote', children);
100
+ }
101
+ const tone = TONES[block.alert];
102
+ return element(options.tags.callout, [element('span', [options.headings[block.alert]], { slot: 'heading' }), ...children], tone === undefined ? {} : { tone });
103
+ }
104
+ case 'rule':
105
+ return element('hr');
106
+ case 'table': {
107
+ const shares = columnShares(block.head);
108
+ const table = element('table', [
109
+ element('colgroup', shares.map((share) => element('col', [], { width: `${share}%` }))),
110
+ element('thead', [
111
+ element('tr', block.head.map((cell, column) => element('th', inline(cell, false), aligned(block.align[column])))),
112
+ ]),
113
+ element('tbody', block.rows.map((row, at) => element('tr', row.map((cell, column) => element('td', inline(cell, open && at === block.rows.length - 1 && column === row.length - 1), aligned(block.align[column])))))),
114
+ ], { 'data-columns': String(block.head.length) });
115
+ return element('div', [table], { 'data-table': '' });
116
+ }
117
+ }
118
+ };
119
+ export const withDefaults = (options = {}) => ({
120
+ headings: { ...HEADINGS, ...options.headings },
121
+ tags: { callout: 'helix-callout', codeBlock: 'helix-code-block', ...options.tags },
122
+ });
123
+ export const markdownNodes = (text, options = {}) => {
124
+ const filled = withDefaults(options);
125
+ return new MarkdownStream()
126
+ .read(text, true)
127
+ .blocks.map((block) => blockNode(block, false, filled));
128
+ };
129
+ const VOID = new Set(['br', 'col', 'hr', 'input']);
130
+ const escapeText = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
131
+ const escapeAttribute = (text) => escapeText(text).replace(/"/g, '&quot;');
132
+ export const nodeHtml = (node) => {
133
+ if (typeof node === 'string') {
134
+ return escapeText(node);
135
+ }
136
+ const attributes = Object.entries(node.attributes)
137
+ .map(([name, value]) => (value === '' ? ` ${name}=""` : ` ${name}="${escapeAttribute(value)}"`))
138
+ .join('');
139
+ if (VOID.has(node.tag)) {
140
+ return `<${node.tag}${attributes}>`;
141
+ }
142
+ return `<${node.tag}${attributes}>${node.children.map(nodeHtml).join('')}</${node.tag}>`;
143
+ };
144
+ export const markdownToHtml = (text, options = {}) => markdownNodes(text, options).map(nodeHtml).join('');
@@ -0,0 +1,33 @@
1
+ import { type MarkdownOptions } from './tree.js';
2
+ /**
3
+ * Draws a Markdown document into an element as its text grows.
4
+ *
5
+ * Each top-level block is its own subtree. A block that has closed is drawn
6
+ * once and never touched again, and the open one is changed only where it
7
+ * differs from what is drawn: a word arriving appends to the text node that
8
+ * holds the line, a row arriving adds a row, and nothing above it is
9
+ * rebuilt.
10
+ */
11
+ export declare class MarkdownWriter {
12
+ private readonly host;
13
+ private readonly stream;
14
+ private drawn;
15
+ private closed;
16
+ private text;
17
+ private options;
18
+ constructor(host: Element, options?: MarkdownOptions);
19
+ /** Changes how the document is drawn, and draws it again. */
20
+ configure(options: MarkdownOptions): void;
21
+ /**
22
+ * Draws the text so far. With `final`, the text is the whole document and
23
+ * every block is closed.
24
+ */
25
+ write(text: string, final: boolean): void;
26
+ /** Forgets the document, and empties the element. */
27
+ clear(): void;
28
+ /**
29
+ * Takes over what a server drew, where it is exactly these nodes, and
30
+ * otherwise clears it to draw it again.
31
+ */
32
+ private take;
33
+ }
@@ -0,0 +1,139 @@
1
+ import { MarkdownStream } from './stream.js';
2
+ import { blockNode, nodeHtml, withDefaults, } from './tree.js';
3
+ const create = (node, document) => {
4
+ if (typeof node === 'string') {
5
+ return { children: [], dom: document.createTextNode(node), node };
6
+ }
7
+ const dom = document.createElement(node.tag);
8
+ for (const [name, value] of Object.entries(node.attributes)) {
9
+ dom.setAttribute(name, value);
10
+ }
11
+ const children = node.children.map((child) => create(child, document));
12
+ for (const child of children) {
13
+ dom.appendChild(child.dom);
14
+ }
15
+ return { children, dom, node };
16
+ };
17
+ const patch = (parent, drawn, next) => {
18
+ const previous = drawn.node;
19
+ if (previous === next) {
20
+ return drawn;
21
+ }
22
+ if (typeof next === 'string') {
23
+ if (typeof previous === 'string') {
24
+ const text = drawn.dom;
25
+ if (next.startsWith(previous)) {
26
+ text.appendData(next.slice(previous.length));
27
+ }
28
+ else {
29
+ text.data = next;
30
+ }
31
+ return { children: [], dom: text, node: next };
32
+ }
33
+ }
34
+ else if (typeof previous !== 'string' && previous.tag === next.tag) {
35
+ const dom = drawn.dom;
36
+ for (const name of Object.keys(previous.attributes)) {
37
+ if (!(name in next.attributes)) {
38
+ dom.removeAttribute(name);
39
+ }
40
+ }
41
+ for (const [name, value] of Object.entries(next.attributes)) {
42
+ if (previous.attributes[name] !== value) {
43
+ dom.setAttribute(name, value);
44
+ }
45
+ }
46
+ return { children: patchChildren(dom, drawn.children, next.children), dom, node: next };
47
+ }
48
+ const replacement = create(next, parent.ownerDocument ?? parent);
49
+ parent.replaceChild(replacement.dom, drawn.dom);
50
+ return replacement;
51
+ };
52
+ const patchChildren = (parent, drawn, next) => {
53
+ const kept = next.map((node, index) => {
54
+ const before = drawn[index];
55
+ if (before) {
56
+ return patch(parent, before, node);
57
+ }
58
+ const made = create(node, parent.ownerDocument ?? parent);
59
+ parent.appendChild(made.dom);
60
+ return made;
61
+ });
62
+ for (const extra of drawn.slice(next.length)) {
63
+ extra.dom.parentNode?.removeChild(extra.dom);
64
+ }
65
+ return kept;
66
+ };
67
+ const adopt = (dom, node) => ({
68
+ children: typeof node === 'string'
69
+ ? []
70
+ : node.children.map((child, index) => adopt(dom.childNodes[index], child)),
71
+ dom,
72
+ node,
73
+ });
74
+ export class MarkdownWriter {
75
+ constructor(host, options = {}) {
76
+ this.host = host;
77
+ this.stream = new MarkdownStream();
78
+ this.drawn = [];
79
+ this.closed = 0;
80
+ this.text = '';
81
+ this.options = withDefaults(options);
82
+ }
83
+ configure(options) {
84
+ this.options = withDefaults(options);
85
+ this.closed = 0;
86
+ this.write(this.text, true);
87
+ }
88
+ write(text, final) {
89
+ if (!text.startsWith(this.text)) {
90
+ this.closed = 0;
91
+ }
92
+ this.text = text;
93
+ const read = this.stream.read(text, final);
94
+ if (this.drawn.length === 0 && this.host.childNodes.length > 0) {
95
+ this.take(read.blocks.map((block) => blockNode(block, false, this.options)));
96
+ }
97
+ const blocks = read.blocks;
98
+ const next = [];
99
+ for (const [index, block] of blocks.entries()) {
100
+ const before = this.drawn[index];
101
+ if (before && index < this.closed) {
102
+ next.push(before);
103
+ continue;
104
+ }
105
+ const node = blockNode(block, !final && index === blocks.length - 1, this.options);
106
+ if (before) {
107
+ next.push(patch(this.host, before, node));
108
+ }
109
+ else {
110
+ const made = create(node, this.host.ownerDocument);
111
+ this.host.appendChild(made.dom);
112
+ next.push(made);
113
+ }
114
+ }
115
+ for (const extra of this.drawn.slice(blocks.length)) {
116
+ extra.dom.parentNode?.removeChild(extra.dom);
117
+ }
118
+ this.drawn = next;
119
+ this.closed = read.closed;
120
+ }
121
+ clear() {
122
+ this.stream.reset();
123
+ this.drawn = [];
124
+ this.closed = 0;
125
+ this.text = '';
126
+ this.host.replaceChildren();
127
+ }
128
+ take(nodes) {
129
+ const children = [...this.host.childNodes];
130
+ const served = children
131
+ .map((child) => child.outerHTML ?? child.textContent)
132
+ .join('');
133
+ if (children.length === nodes.length && served === nodes.map(nodeHtml).join('')) {
134
+ this.drawn = nodes.map((node, index) => adopt(children[index], node));
135
+ return;
136
+ }
137
+ this.host.replaceChildren();
138
+ }
139
+ }
@@ -66,6 +66,15 @@ declare const HelixHeadlessMenu_base: (new (...args: any[]) => import("@fusion.d
66
66
  * to where it was. A touch or a pen held still on the area opens it too, on
67
67
  * every browser, since iOS sends no `contextmenu` for one.
68
68
  *
69
+ * **One menu for many things.** `for` names an area outside the menu, such as
70
+ * a table, and `targets` says which things in it the menu is about, such as
71
+ * `tr`. A request over one of them opens the menu with it as
72
+ * `contextTarget`, which a `helix-before-open` listener reads to change the
73
+ * rows for it; a request over none of them is left to the page. `showAt()`
74
+ * opens the menu at a point, for an area that finds what was pressed itself,
75
+ * such as a canvas. Shift with a right-click leaves the browser's own menu to
76
+ * the reader.
77
+ *
69
78
  * **A trigger with its own action.** With `open-on="long-press"`, a press on
70
79
  * the trigger is the trigger's own, and a press held for half a second opens
71
80
  * the menu, as React Aria's `trigger="longPress"` does. Alt+Down or Alt+Up
@@ -193,6 +202,14 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
193
202
  * property only, there being no way to write a function in markup.
194
203
  */
195
204
  filterMatch?: ((text: string, filter: string) => boolean) | undefined;
205
+ /**
206
+ * With `context`, the id of an area outside the menu whose context menu
207
+ * this is, for one menu that serves a region the page draws elsewhere, such
208
+ * as a table. A right-click, a long press, Shift+F10 or the menu key there
209
+ * opens it, as on the `trigger` slot's area. Names an element in the same
210
+ * tree, the way `Tooltip.for` does.
211
+ */
212
+ for: string;
196
213
  /**
197
214
  * Accessible name for the surface.
198
215
  *
@@ -263,6 +280,12 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
263
280
  * a way back. `flyout` opens it in a panel of its own beside its row.
264
281
  */
265
282
  submenus: 'drill' | 'flyout';
283
+ /**
284
+ * With `context`, a selector for the things in the area a menu is about,
285
+ * such as `tr` for a table's rows. A request over one opens the menu with it
286
+ * as `contextTarget`, and a request over none of them is left to the page.
287
+ */
288
+ targets: string;
266
289
  /**
267
290
  * Which page is showing, and what is on it.
268
291
  *
@@ -357,6 +380,29 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
357
380
  private openAt;
358
381
  /** Closes the menu and returns it to its first page. */
359
382
  hide(): void;
383
+ /**
384
+ * The thing the open context menu is about: the element of `targets` the
385
+ * reader asked over, the area where there are no `targets`, or what
386
+ * `showAt` was given. Set before `helix-before-open`, so a listener can
387
+ * change the rows for it, and kept until the menu next opens.
388
+ */
389
+ contextTarget: Element | undefined;
390
+ /**
391
+ * Opens the menu at a point in the viewport, as a context menu opens, for
392
+ * an area that finds what was pressed itself, such as a canvas or a map.
393
+ * `target` becomes `contextTarget`. An open menu moves there.
394
+ */
395
+ showAt(point: {
396
+ x: number;
397
+ y: number;
398
+ }, target?: Element): void;
399
+ /** The areas a context menu answers in: the `trigger` slot's, and the one `for` names. */
400
+ private get contextAreas();
401
+ /**
402
+ * The element of `targets` a request came from, the area where there are
403
+ * no `targets`, or nothing where a request over the area missed every one.
404
+ */
405
+ private targetOf;
360
406
  /**
361
407
  * Opens a context menu where the reader asked for it: at the pointer, or at
362
408
  * the area's corner where the keyboard asked and the event has no position.
@@ -459,6 +505,13 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
459
505
  private inGrace;
460
506
  connectedCallback(): void;
461
507
  disconnectedCallback(): void;
508
+ /** The area `for` named, which carries this menu's listeners while it is the context area. */
509
+ private watched;
510
+ /** The keys an area outside the menu passes on: only Shift+F10 and the menu key, while it is closed. */
511
+ private readonly handleAreaKeydown;
512
+ /** Moves the menu's listeners onto the area `for` names, and off the one it named before. */
513
+ private watchArea;
514
+ private unwatchArea;
462
515
  protected updated(changed: PropertyValues): void;
463
516
  /** Whether the surface is named by a submenu's page, which `nameSurface` undoes at the top. */
464
517
  private namedByPage;