@tangle-network/ui 11.32.2 → 11.34.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 (60) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/editor/document-editor-pane.d.ts +7 -1
  3. package/dist/editor/document-editor-pane.js +27 -12
  4. package/dist/editor/markdown-document-editor.js +30 -12
  5. package/dist/editor/source-markdown-editor.js +256 -0
  6. package/dist/editor/source-markdown.js +495 -0
  7. package/dist/files/file-artifact-pane.d.ts +2 -0
  8. package/dist/files/file-artifact-pane.js +1 -0
  9. package/dist/index.d.ts +3 -2
  10. package/dist/index.js +3 -2
  11. package/dist/lib/control-presentation.js +2 -1
  12. package/dist/lib/focus.d.ts +5 -1
  13. package/dist/lib/focus.js +6 -1
  14. package/dist/lib/interaction.d.ts +41 -0
  15. package/dist/lib/interaction.js +42 -0
  16. package/dist/markdown/markdown-table.js +1 -1
  17. package/dist/primitives/button.js +19 -18
  18. package/dist/primitives/card.d.ts +14 -1
  19. package/dist/primitives/card.js +15 -5
  20. package/dist/primitives/dropdown-menu.js +7 -6
  21. package/dist/primitives/segmented-control.d.ts +5 -1
  22. package/dist/primitives/segmented-control.js +12 -5
  23. package/dist/primitives/select.js +4 -3
  24. package/dist/primitives/tabs.js +4 -3
  25. package/dist/run/inline-thinking-item.d.ts +7 -1
  26. package/dist/run/inline-thinking-item.js +6 -5
  27. package/dist/utils/format.d.ts +6 -0
  28. package/dist/utils/format.js +9 -1
  29. package/dist/utils.d.ts +3 -2
  30. package/dist/utils.js +3 -2
  31. package/package.json +2 -2
  32. package/src/editor/document-editor-pane.tsx +54 -16
  33. package/src/editor/markdown-document-editor.tsx +33 -18
  34. package/src/editor/source-markdown-editor.test.tsx +156 -0
  35. package/src/editor/source-markdown-editor.ts +296 -0
  36. package/src/editor/source-markdown.fixtures.ts +90 -0
  37. package/src/editor/source-markdown.test.ts +235 -0
  38. package/src/editor/source-markdown.ts +627 -0
  39. package/src/files/file-artifact-pane.test.tsx +38 -3
  40. package/src/files/file-artifact-pane.tsx +3 -0
  41. package/src/lib/control-presentation.ts +10 -4
  42. package/src/lib/focus.ts +7 -1
  43. package/src/lib/interaction.ts +64 -0
  44. package/src/markdown/markdown-table.test.tsx +9 -0
  45. package/src/markdown/markdown-table.tsx +5 -1
  46. package/src/primitives/button.tsx +29 -10
  47. package/src/primitives/card.tsx +23 -6
  48. package/src/primitives/control-presentation.test.tsx +6 -3
  49. package/src/primitives/date-picker.test.tsx +1 -1
  50. package/src/primitives/dropdown-menu.tsx +15 -10
  51. package/src/primitives/interaction-states.test.tsx +285 -0
  52. package/src/primitives/segmented-control.test.tsx +4 -2
  53. package/src/primitives/segmented-control.tsx +13 -14
  54. package/src/primitives/select.tsx +11 -5
  55. package/src/primitives/tabs.tsx +7 -3
  56. package/src/run/inline-thinking-item.test.tsx +29 -0
  57. package/src/run/inline-thinking-item.tsx +11 -4
  58. package/src/utils/format.test.ts +16 -1
  59. package/src/utils/format.ts +17 -0
  60. package/src/utils/index.ts +10 -1
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Markdown fixtures for the source-preserving editor tests.
3
+ */
4
+
5
+ /**
6
+ * One of every block and inline construct, in the less common spellings too.
7
+ * Trailing spaces are written as \u0020 so an editor cannot strip them.
8
+ */
9
+ export const KITCHEN_SINK = `
10
+
11
+ # Title with \`code\` and **bold**
12
+
13
+ Setext heading
14
+ ==============
15
+
16
+ Paragraph with *star emphasis*, _underscore emphasis_, __double under__, snake_case_name, a\\_b escaped,
17
+ a hard break here\u0020\u0020
18
+ and a [link](https://example.com "Title"), a [ref link][ref], <https://auto.link>, and ~~strike~~.
19
+
20
+ * star bullet one
21
+ * star bullet two
22
+ * nested four-space
23
+
24
+ + plus list
25
+
26
+ 1) paren ordered
27
+ 2) second
28
+
29
+ 3. starts at three
30
+ 4. four
31
+
32
+ - [ ] task open
33
+ - [x] task done
34
+
35
+ > quote line one
36
+ > > nested quote
37
+
38
+ \`\`\`ts
39
+ const x = 1 // fenced
40
+ \`\`\`
41
+
42
+ ~~~
43
+ tilde fence
44
+ ~~~
45
+
46
+ indented code
47
+
48
+ ***
49
+
50
+ ---
51
+ ___
52
+
53
+ | Left | Center | Right |
54
+ |:-----|:------:|------:|
55
+ | a | **b** | [c](https://c.example) |
56
+
57
+ <div align="center">raw html block</div>
58
+
59
+ ![image alt](images/pic.png "pic title")
60
+
61
+ Text with inline <sup>html</sup> and an emoji 🚀 and trailing spaces\u0020\u0020\u0020
62
+
63
+ [ref]: https://ref.example "Ref Title"
64
+
65
+ Line with trailing backslash\\
66
+ next line
67
+ `;
68
+
69
+
70
+ /** Shaped like an agent-written competitor call sheet: headings, prose and GFM tables. */
71
+ export const CALL_SHEET = `# Plausible Analytics: competitor call sheet
72
+
73
+ **Updated:** October 11, 2026 · **Use:** establish what the buyer is comparing before positioning Plausible.
74
+
75
+ ## Three deal paths
76
+
77
+ | Competitor | Why a buyer may choose it | Call response |
78
+ |---|---|---|
79
+ | Google Analytics 4 | Standard GA4 is free of charge. ([GA4](https://example.com/ga4)) | **Acknowledge:** the no-cost offer is real. |
80
+ | Fathom | Closest paid, cookie-free alternative. | Ask which reports they rely on today. |
81
+ | Simple Analytics | Stricter no-identifier model. | Show the dashboard and ask what is missing. |
82
+
83
+ ## Qualifying questions
84
+
85
+ 1. Which reports do you open every week?
86
+ 2. Who signs off on analytics spend?
87
+
88
+ * Keep the call to 20 minutes
89
+ * Send the comparison after the call
90
+ `;
@@ -0,0 +1,235 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ blockHtml,
4
+ blockMarkdown,
5
+ diffText,
6
+ mergeBlockEdit,
7
+ parseSourceMarkdown,
8
+ serializeSourceMarkdown,
9
+ type SourceBlock,
10
+ type SourceMarkdown,
11
+ type SourceNode,
12
+ } from "./source-markdown";
13
+ import { KITCHEN_SINK } from "./source-markdown.fixtures";
14
+
15
+ /** The serializer's view of a document nobody edited. */
16
+ function untouched(source: SourceMarkdown): SourceNode[] {
17
+ return source.blocks.map((block) => ({
18
+ block,
19
+ unchanged: true,
20
+ sameType: true,
21
+ markdown: () => block.raw,
22
+ merge: () => block.raw,
23
+ reparses: () => true,
24
+ }));
25
+ }
26
+
27
+ /** A node whose Markdown changed to `edited`, merged the way the editor merges it. */
28
+ function editedNode(block: SourceBlock, canonical: string, edited: string): SourceNode {
29
+ return {
30
+ block,
31
+ unchanged: false,
32
+ sameType: true,
33
+ markdown: () => edited,
34
+ merge: (text) => mergeBlockEdit(block.raw, canonical, text),
35
+ reparses: () => true,
36
+ };
37
+ }
38
+
39
+ function addedNode(markdown: string): SourceNode {
40
+ return {
41
+ unchanged: false,
42
+ sameType: false,
43
+ markdown: () => markdown,
44
+ merge: () => null,
45
+ reparses: () => true,
46
+ };
47
+ }
48
+
49
+ describe("parseSourceMarkdown", () => {
50
+ it("splits a file into blocks whose bytes reassemble it exactly", () => {
51
+ const source = parseSourceMarkdown(KITCHEN_SINK);
52
+ const rebuilt = source.prefix + source.blocks.map((block) => block.raw + block.gap).join("");
53
+ expect(rebuilt).toBe(KITCHEN_SINK);
54
+ expect(source.prefix).toBe("\n\n");
55
+ });
56
+
57
+ it("keeps blocks the editor cannot represent read-only, by what they are", () => {
58
+ const kinds = parseSourceMarkdown(KITCHEN_SINK)
59
+ .blocks.filter((block) => !block.editable)
60
+ .map((block) => block.kind);
61
+ expect(kinds).toEqual(["checklist", "table", "html", "image", "html", "definition"]);
62
+ });
63
+
64
+ it("gives the editor a source node for a table, carrying its raw text", () => {
65
+ const table = "| A | B |\n|---|---|\n| 1 | 2 |";
66
+ const source = parseSourceMarkdown(`Intro\n\n${table}\n`);
67
+ expect(source.html).toContain('data-md-kind="table"');
68
+ expect(source.html).toContain('data-md-raw="| A | B |\n|---|---|\n| 1 | 2 |"');
69
+ expect(source.html).toMatch(/^<p data-md-block="m[0-9a-z]+:0">Intro<\/p>/);
70
+ });
71
+
72
+ it("keeps CRLF line endings and a missing final newline", () => {
73
+ for (const file of ["one\r\n\r\n- a\r\n- b\r\n", "no final newline", "\ttab start\n\n-\ttab bullet"]) {
74
+ const source = parseSourceMarkdown(file);
75
+ expect(serializeSourceMarkdown(source, untouched(source))).toBe(file);
76
+ }
77
+ });
78
+
79
+ it("serializes an untouched file byte for byte", () => {
80
+ const source = parseSourceMarkdown(KITCHEN_SINK);
81
+ expect(serializeSourceMarkdown(source, untouched(source))).toBe(KITCHEN_SINK);
82
+ });
83
+
84
+ it("resolves reference links defined elsewhere when it re-reads one block", () => {
85
+ const source = parseSourceMarkdown("See [docs][d].\n\n[d]: https://docs.example\n");
86
+ expect(blockHtml("See [the docs][d].", source.links)).toContain('href="https://docs.example"');
87
+ expect(blockHtml("| a |\n|---|\n| 1 |")).toBeNull();
88
+ });
89
+ });
90
+
91
+ describe("mergeBlockEdit", () => {
92
+ it("keeps escapes the re-serialization would add, outside the edit", () => {
93
+ const raw = "Rename snake_case_name and *keep* it";
94
+ const canonical = "Rename snake\\_case\\_name and *keep* it";
95
+ expect(mergeBlockEdit(raw, canonical, "Rename snake\\_case\\_name and *keep* it now")).toBe(
96
+ "Rename snake_case_name and *keep* it now",
97
+ );
98
+ });
99
+
100
+ it("keeps the source's list markers and indentation around an edited item", () => {
101
+ const raw = "* star bullet one\n* star bullet two\n * nested four-space";
102
+ const canonical = "- star bullet one\n- star bullet two\n - nested four-space";
103
+ const edited = "- star bullet uno\n- star bullet two\n - nested four-space";
104
+ expect(mergeBlockEdit(raw, canonical, edited)).toBe(
105
+ "* star bullet uno\n* star bullet two\n * nested four-space",
106
+ );
107
+ });
108
+
109
+ it("keeps a setext heading's underline and underscore emphasis", () => {
110
+ expect(mergeBlockEdit("Setext heading\n==============", "# Setext heading", "# Setext heading two")).toBe(
111
+ "Setext heading two\n==============",
112
+ );
113
+ expect(mergeBlockEdit("an _emphasised_ word", "an *emphasised* word", "an *emphasised* word, edited")).toBe(
114
+ "an _emphasised_ word, edited",
115
+ );
116
+ });
117
+
118
+ it("keeps a reference link and a backslash hard break the canonical form spells differently", () => {
119
+ expect(
120
+ mergeBlockEdit(
121
+ "A [ref link][ref] here",
122
+ 'A [ref link](https://ref.example "Ref Title") here',
123
+ 'A [ref link](https://ref.example "Ref Title") here and there',
124
+ ),
125
+ ).toBe("A [ref link][ref] here and there");
126
+ expect(mergeBlockEdit("one\\\ntwo", "one \ntwo", "one! \ntwo")).toBe("one!\\\ntwo");
127
+ });
128
+
129
+ it("deletes the source's own spelling of a deleted span", () => {
130
+ expect(mergeBlockEdit("keep snake_case drop", "keep snake\\_case drop", "keep drop")).toBe("keep drop");
131
+ });
132
+
133
+ it("returns the original bytes when the edit was undone", () => {
134
+ expect(mergeBlockEdit("* a", "- a", "- a")).toBe("* a");
135
+ });
136
+ });
137
+
138
+ describe("diffText", () => {
139
+ // Seeded, so a failure reproduces.
140
+ function random(seed: number) {
141
+ let state = seed;
142
+ return () => {
143
+ state = (state * 1103515245 + 12345) % 2147483648;
144
+ return state / 2147483648;
145
+ };
146
+ }
147
+
148
+ it("turns the first text into the second for random edits", () => {
149
+ const next = random(42);
150
+ const alphabet = "ab *_-\n#[]()`\\";
151
+ for (let round = 0; round < 300; round++) {
152
+ const length = Math.floor(next() * 60);
153
+ let a = "";
154
+ for (let i = 0; i < length; i++) a += alphabet[Math.floor(next() * alphabet.length)];
155
+ let b = a;
156
+ const edits = 1 + Math.floor(next() * 4);
157
+ for (let e = 0; e < edits; e++) {
158
+ const at = Math.floor(next() * (b.length + 1));
159
+ const cut = Math.floor(next() * 4);
160
+ const insert = alphabet[Math.floor(next() * alphabet.length)].repeat(Math.floor(next() * 3));
161
+ b = b.slice(0, at) + insert + b.slice(at + cut);
162
+ }
163
+ const ops = diffText(a, b);
164
+ expect(ops).not.toBeNull();
165
+ expect(ops!.filter(([op]) => op !== 1).map(([, text]) => text).join("")).toBe(a);
166
+ expect(ops!.filter(([op]) => op !== -1).map(([, text]) => text).join("")).toBe(b);
167
+ }
168
+ });
169
+
170
+ it("gives up past its cost bound rather than allocating without limit", () => {
171
+ expect(diffText("a".repeat(400), "b".repeat(400), 100)).toBeNull();
172
+ });
173
+ });
174
+
175
+ describe("serializeSourceMarkdown", () => {
176
+ const file = "# Title\n\nFirst paragraph.\n\n* one\n* two\n\n| A |\n|---|\n| 1 |\n";
177
+
178
+ it("changes only the edited span of an edited block", () => {
179
+ const source = parseSourceMarkdown(file);
180
+ const nodes = untouched(source);
181
+ nodes[2] = editedNode(source.blocks[2], "- one\n- two", "- one\n- two\n- three");
182
+ expect(serializeSourceMarkdown(source, nodes)).toBe(
183
+ "# Title\n\nFirst paragraph.\n\n* one\n* two\n- three\n\n| A |\n|---|\n| 1 |\n",
184
+ );
185
+ });
186
+
187
+ it("separates an added block with a blank line and keeps the file's trailer", () => {
188
+ const source = parseSourceMarkdown(file);
189
+ const nodes = untouched(source);
190
+ nodes.splice(2, 0, addedNode("A new paragraph."));
191
+ nodes.push(addedNode("Closing line."));
192
+ expect(serializeSourceMarkdown(source, nodes)).toBe(
193
+ "# Title\n\nFirst paragraph.\n\nA new paragraph.\n\n* one\n* two\n\n| A |\n|---|\n| 1 |\n\nClosing line.\n",
194
+ );
195
+ });
196
+
197
+ it("drops a deleted block and its separator only", () => {
198
+ const source = parseSourceMarkdown(file);
199
+ const nodes = untouched(source).filter((_, i) => i !== 1);
200
+ expect(serializeSourceMarkdown(source, nodes)).toBe("# Title\n\n* one\n* two\n\n| A |\n|---|\n| 1 |\n");
201
+ });
202
+
203
+ it("skips an empty added paragraph, such as the editor's trailing one", () => {
204
+ const source = parseSourceMarkdown(file);
205
+ expect(serializeSourceMarkdown(source, [...untouched(source), addedNode("")])).toBe(file);
206
+ });
207
+
208
+ it("writes added text with the file's CRLF line endings", () => {
209
+ const source = parseSourceMarkdown("one\r\n");
210
+ expect(serializeSourceMarkdown(source, [...untouched(source), addedNode("two\nlines")])).toBe(
211
+ "one\r\n\r\ntwo\r\nlines\r\n",
212
+ );
213
+ });
214
+
215
+ it("falls back to the edited block when the merge does not read back as the edit", () => {
216
+ const source = parseSourceMarkdown("* one\n* two\n");
217
+ const node = editedNode(source.blocks[0], "- one\n- two", "- one\n- two\n- three");
218
+ node.reparses = () => false;
219
+ expect(serializeSourceMarkdown(source, [node])).toBe("- one\n- two\n- three\n");
220
+ });
221
+ });
222
+
223
+ describe("blockMarkdown", () => {
224
+ it("writes lists with one space after the marker, in the source's marker style", () => {
225
+ const html = "<ul><li><p>one</p></li><li><p>two</p></li></ul>";
226
+ expect(blockMarkdown(html)).toBe("- one\n- two");
227
+ expect(blockMarkdown(html, "* x")).toBe("* one\n* two");
228
+ expect(blockMarkdown('<ol start="3"><li><p>a</p></li><li><p>b</p></li></ol>', "3) a")).toBe("3) a\n4) b");
229
+ });
230
+
231
+ it("writes strikethrough and rules as GFM", () => {
232
+ expect(blockMarkdown("<p><s>gone</s></p>")).toBe("~~gone~~");
233
+ expect(blockMarkdown("<hr>")).toBe("---");
234
+ });
235
+ });