@heycys/gallery 0.1.0 → 0.1.1

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 (237) hide show
  1. package/THIRD-PARTY-NOTICES.md +51 -1
  2. package/browser/editor/components/callout/index.mjs +40 -15
  3. package/browser/editor/components/columns/page.mjs +41 -28
  4. package/browser/editor/components/details/index.mjs +2 -1
  5. package/browser/editor/components/dialog/index.mjs +2 -1
  6. package/browser/editor/components/dialog/page.mjs +0 -18
  7. package/browser/editor/components/divider/index.mjs +1 -1
  8. package/browser/editor/components/figure/page.mjs +10 -1
  9. package/browser/editor/components/file/page.mjs +2 -2
  10. package/browser/editor/components/flowchart/add.mjs +2 -2
  11. package/browser/editor/components/heading/index.mjs +2 -0
  12. package/browser/editor/components/heading/page.mjs +6 -0
  13. package/browser/editor/components/highlight/index.mjs +4 -20
  14. package/browser/editor/components/icons.mjs +5 -2
  15. package/browser/editor/components/image/page.mjs +2 -2
  16. package/browser/editor/components/installed/index.mjs +71 -25
  17. package/browser/editor/components/installed/page.mjs +122 -21
  18. package/browser/editor/components/kit.mjs +3 -0
  19. package/browser/editor/components/media/page.mjs +44 -14
  20. package/browser/editor/components/pages.mjs +2 -2
  21. package/browser/editor/components/registry.mjs +56 -30
  22. package/browser/editor/components/section/index.mjs +2 -1
  23. package/browser/editor/components/set-apart.mjs +24 -0
  24. package/browser/editor/components/table/index.mjs +5 -3
  25. package/browser/editor/components/table/page.mjs +29 -18
  26. package/browser/editor/page/annotate/anchors.mjs +4 -3
  27. package/browser/editor/page/annotate/index.mjs +61 -34
  28. package/browser/editor/page/autosave.mjs +50 -4
  29. package/browser/editor/page/beside.mjs +5 -5
  30. package/browser/editor/page/block-menu/colors.mjs +3 -1
  31. package/browser/editor/page/block-menu/index.mjs +8 -6
  32. package/browser/editor/page/blocks.mjs +16 -7
  33. package/browser/editor/page/caret.mjs +4 -1
  34. package/browser/editor/page/changes/bar.mjs +102 -0
  35. package/browser/editor/page/changes/ghost.mjs +206 -0
  36. package/browser/editor/page/changes/gone-words.mjs +94 -0
  37. package/browser/editor/page/changes/index.mjs +447 -0
  38. package/browser/editor/page/changes/words.mjs +27 -0
  39. package/browser/editor/page/clipboard/import/colors.mjs +3 -3
  40. package/browser/editor/page/clipboard/index.mjs +32 -13
  41. package/browser/editor/page/clipboard/own/blocks.mjs +14 -5
  42. package/browser/editor/page/clipboard/own/describe.mjs +16 -7
  43. package/browser/editor/page/clipboard/own/export.mjs +6 -6
  44. package/browser/editor/page/clipboard/own/selection.mjs +2 -2
  45. package/browser/editor/page/clipboard/own/writers.mjs +0 -11
  46. package/browser/editor/page/columns/index.mjs +1 -1
  47. package/browser/editor/page/commit.mjs +19 -4
  48. package/browser/editor/page/conflicts.mjs +6 -26
  49. package/browser/editor/page/convert.mjs +8 -7
  50. package/browser/editor/page/core.mjs +55 -17
  51. package/browser/editor/page/drag/index.mjs +40 -24
  52. package/browser/editor/page/drag/preview.mjs +9 -4
  53. package/browser/editor/page/editor.mjs +70 -15
  54. package/browser/editor/page/emoji-picker/emoji-tab.mjs +22 -13
  55. package/browser/editor/page/emoji-picker/grid.mjs +7 -5
  56. package/browser/editor/page/emoji-picker/icon-tab.mjs +4 -4
  57. package/browser/editor/page/emoji-picker/icons.mjs +0 -1
  58. package/browser/editor/page/emoji-picker/index.mjs +11 -9
  59. package/browser/editor/page/emoji-picker/styles.mjs +161 -2
  60. package/browser/editor/page/emoji-picker/support.mjs +6 -4
  61. package/browser/editor/page/emoji-picker/upload.mjs +12 -11
  62. package/browser/editor/page/fills.mjs +5 -3
  63. package/browser/editor/page/fold/index.mjs +348 -0
  64. package/browser/editor/page/fold/rule.mjs +125 -0
  65. package/browser/editor/page/folded.mjs +23 -0
  66. package/browser/editor/page/format-bar/bar.mjs +2 -1
  67. package/browser/editor/page/format-bar/index.mjs +1 -1
  68. package/browser/editor/page/geometry.mjs +8 -6
  69. package/browser/editor/page/handle/index.mjs +17 -6
  70. package/browser/editor/page/hints/index.mjs +1 -1
  71. package/browser/editor/page/insert/index.mjs +30 -17
  72. package/browser/editor/page/long-runs.mjs +75 -0
  73. package/browser/editor/page/look.mjs +15 -3
  74. package/browser/editor/page/margin.mjs +45 -24
  75. package/browser/editor/page/menu.mjs +69 -25
  76. package/browser/editor/page/merge.mjs +24 -0
  77. package/browser/editor/page/notices.mjs +1 -1
  78. package/browser/editor/page/offered.mjs +1 -1
  79. package/browser/editor/page/outline/folds.mjs +11 -10
  80. package/browser/editor/page/outline/index.mjs +131 -45
  81. package/browser/editor/page/outline/list.mjs +23 -11
  82. package/browser/editor/page/outline/read.mjs +1 -1
  83. package/browser/editor/page/outline/strip.mjs +7 -1
  84. package/browser/editor/page/panel.mjs +5 -4
  85. package/browser/editor/page/resize/index.mjs +1 -1
  86. package/browser/editor/page/selection/delete.mjs +8 -6
  87. package/browser/editor/page/selection/index.mjs +10 -3
  88. package/browser/editor/page/session.mjs +54 -3
  89. package/browser/editor/page/table/cell-bar.mjs +4 -1
  90. package/browser/editor/page/table/cells.mjs +1 -1
  91. package/browser/editor/page/table/icons.mjs +1 -1
  92. package/browser/editor/page/table/index.mjs +13 -8
  93. package/browser/editor/page/table/widths.mjs +9 -4
  94. package/browser/editor/page/text-select/index.mjs +18 -3
  95. package/browser/editor/page/tooltip.mjs +1 -1
  96. package/browser/editor/page/type-grid.mjs +4 -3
  97. package/browser/editor/page/typing/code-edges.mjs +63 -17
  98. package/browser/editor/page/typing/index.mjs +12 -6
  99. package/browser/editor/page/typing/leave.mjs +1 -1
  100. package/browser/shared/annotations.mjs +8 -10
  101. package/browser/shared/code-folds.mjs +5 -1
  102. package/browser/shared/colors.mjs +15 -13
  103. package/browser/shared/component-catalog.mjs +24 -10
  104. package/browser/shared/components/callout.mjs +4 -2
  105. package/browser/shared/components/index.mjs +61 -7
  106. package/browser/shared/components/installed.mjs +75 -5
  107. package/browser/shared/components/page.mjs +10 -1
  108. package/browser/shared/components/table.mjs +1 -1
  109. package/browser/shared/components/text.mjs +2 -2
  110. package/browser/shared/document-look.mjs +178 -0
  111. package/browser/shared/emoji/data/LICENSE-lucide.txt +43 -0
  112. package/browser/shared/emoji/data/README.md +4 -2
  113. package/browser/shared/emoji/data/categories.json +1 -1
  114. package/browser/shared/emoji/data/keywords-en.json +1 -1
  115. package/browser/shared/emoji/data/keywords-zh.json +1 -1
  116. package/browser/shared/emoji/data/names.json +1 -1
  117. package/browser/shared/emoji/data/positions.json +1 -1
  118. package/browser/shared/emoji/data/twemoji-index.json +1 -1
  119. package/browser/shared/emoji/data/twemoji-svg.bin +0 -0
  120. package/browser/shared/emoji/data.mjs +10 -12
  121. package/browser/shared/emoji/emoji.mjs +8 -7
  122. package/browser/shared/emoji/icon-value.mjs +4 -3
  123. package/browser/shared/html-tree.mjs +11 -5
  124. package/browser/shared/i18n/en.mjs +67 -8
  125. package/browser/shared/long-runs.mjs +39 -0
  126. package/browser/shared/new-names.mjs +5 -2
  127. package/browser/shared/outline.mjs +194 -0
  128. package/browser/shared/preferences.mjs +10 -4
  129. package/browser/shared/session.mjs +5 -4
  130. package/browser/shared/workspace-look.mjs +91 -0
  131. package/browser/shared/zoom.mjs +5 -5
  132. package/dist/chunk-3HIRPVLU.mjs +2 -0
  133. package/dist/chunk-4FS7Z7WR.mjs +2 -0
  134. package/dist/{chunk-LHTFLRB6.mjs → chunk-5MHMDLZD.mjs} +2 -2
  135. package/dist/{chunk-YXAUQPVK.mjs → chunk-5ZKJ3H4P.mjs} +3 -3
  136. package/dist/{chunk-ZBIM6ARY.mjs → chunk-64K7IPLO.mjs} +55 -16
  137. package/dist/{chunk-QYKCJ7FG.mjs → chunk-6QXGZM36.mjs} +1 -1
  138. package/dist/chunk-6RGJOMJP.mjs +16 -0
  139. package/dist/{chunk-EUX7IC5J.mjs → chunk-7DTINW6U.mjs} +2 -2
  140. package/dist/{chunk-6CYG3Z4V.mjs → chunk-AJ73XJF3.mjs} +1 -1
  141. package/dist/chunk-AUZGSZJH.mjs +13 -0
  142. package/dist/{chunk-7QQBR7PB.mjs → chunk-BFE45AKL.mjs} +1 -1
  143. package/dist/chunk-BGGDRNYQ.mjs +10 -0
  144. package/dist/{chunk-L4DVONOG.mjs → chunk-BXO2ZY5P.mjs} +1 -1
  145. package/dist/chunk-CEH4MRCC.mjs +2 -0
  146. package/dist/chunk-CY2FNNAB.mjs +16 -0
  147. package/dist/{chunk-LC7YNQFG.mjs → chunk-DFUDV4LX.mjs} +1 -1
  148. package/dist/chunk-DKXG4XDG.mjs +2 -0
  149. package/dist/chunk-E7LWZRXY.mjs +4 -0
  150. package/dist/chunk-EZQ34M3F.mjs +124 -0
  151. package/dist/chunk-FDLXYYG4.mjs +4 -0
  152. package/dist/chunk-FUKQNOQS.mjs +9 -0
  153. package/dist/{chunk-DB4WKGYF.mjs → chunk-GB5SOYQJ.mjs} +2 -2
  154. package/dist/{chunk-ACOPDCNB.mjs → chunk-IVDLMMBD.mjs} +1 -1
  155. package/dist/chunk-JPOSLGOC.mjs +6 -0
  156. package/dist/chunk-JVHHHGEA.mjs +3 -0
  157. package/dist/chunk-L3QKH4OK.mjs +26 -0
  158. package/dist/{chunk-B3VTYBYR.mjs → chunk-LCTHTZIL.mjs} +2 -2
  159. package/dist/chunk-LEF6DRCK.mjs +59 -0
  160. package/dist/chunk-LS4F7JUM.mjs +2 -0
  161. package/dist/chunk-NO5NJCHO.mjs +2 -0
  162. package/dist/{chunk-UNUEWH4S.mjs → chunk-NQPVAU63.mjs} +1 -1
  163. package/dist/chunk-O35FIXRB.mjs +2 -0
  164. package/dist/chunk-PL6GHLGD.mjs +15 -0
  165. package/dist/{chunk-G7TOXONE.mjs → chunk-QJIR2BLP.mjs} +1 -1
  166. package/dist/chunk-QUPLVPB2.mjs +7 -0
  167. package/dist/chunk-R7BNENLY.mjs +4 -0
  168. package/dist/chunk-S6O4UHLC.mjs +2 -0
  169. package/dist/{chunk-HMHK3EQ6.mjs → chunk-SGJWQKEI.mjs} +1 -1
  170. package/dist/chunk-SYGN7GLU.mjs +2 -0
  171. package/dist/chunk-TYK6VNAB.mjs +10 -0
  172. package/dist/{chunk-EVJ3BX3M.mjs → chunk-U6KNPJPR.mjs} +1 -1
  173. package/dist/{chunk-YZ5Z6H4W.mjs → chunk-UALRNVSE.mjs} +1 -1
  174. package/dist/chunk-UMHUSOPJ.mjs +2 -0
  175. package/dist/chunk-V3HXVQEM.mjs +2 -0
  176. package/dist/{chunk-UXUK4PMC.mjs → chunk-V5JTW2LH.mjs} +1 -1
  177. package/dist/chunk-V5VXUJKG.mjs +2 -0
  178. package/dist/chunk-VBXMADMP.mjs +2 -0
  179. package/dist/chunk-VNV55RUA.mjs +2 -0
  180. package/dist/chunk-VPD2PS35.mjs +2 -0
  181. package/dist/chunk-W3BGBJQW.mjs +2 -0
  182. package/dist/chunk-W4VKN3GF.mjs +7 -0
  183. package/dist/chunk-W5BXXPDJ.mjs +3 -0
  184. package/dist/{chunk-Z52HTUW7.mjs → chunk-XNROSH2T.mjs} +2 -2
  185. package/dist/chunk-XUNHRCOG.mjs +6 -0
  186. package/dist/{chunk-6U2SLSDN.mjs → chunk-Y6RLRPG3.mjs} +1 -1
  187. package/dist/cli.mjs +1 -1
  188. package/dist/server.mjs +1 -1
  189. package/dist/worker.mjs +1 -1
  190. package/kit/math.js +18 -2
  191. package/kit/report.css +248 -137
  192. package/kit/runtime.js +126 -51
  193. package/kit/tables.js +51 -1
  194. package/kit/ui.js +97 -0
  195. package/package.json +1 -1
  196. package/release.json +8 -8
  197. package/sdk/doc-kit.d.ts +9 -3
  198. package/skill/SKILL.md +22 -10
  199. package/skill/references/building-components.md +46 -6
  200. package/skill/references/example-report.html +1 -1
  201. package/skill/references/writing-html.md +4 -4
  202. package/skill/sdk/doc-kit.d.ts +9 -3
  203. package/web/assets/index-Bh74DfsV.css +1 -0
  204. package/web/assets/index-UW0RyFFt.js +13 -0
  205. package/web/index.html +2 -2
  206. package/browser/editor/components/callout/page.mjs +0 -13
  207. package/dist/chunk-2PBALTR3.mjs +0 -57
  208. package/dist/chunk-3LA3EMDE.mjs +0 -15
  209. package/dist/chunk-5FRMCDPY.mjs +0 -16
  210. package/dist/chunk-5LEK2CBS.mjs +0 -2
  211. package/dist/chunk-5OFN5AHC.mjs +0 -2
  212. package/dist/chunk-5SIYS5HN.mjs +0 -2
  213. package/dist/chunk-5TEK3RAD.mjs +0 -2
  214. package/dist/chunk-7GHT2UCA.mjs +0 -15
  215. package/dist/chunk-ATCJYAR4.mjs +0 -2
  216. package/dist/chunk-C7HWOBIF.mjs +0 -2
  217. package/dist/chunk-E7VYDG2X.mjs +0 -2
  218. package/dist/chunk-FQGQH4KM.mjs +0 -2
  219. package/dist/chunk-GPF4YWWB.mjs +0 -9
  220. package/dist/chunk-GYQV7Y4J.mjs +0 -120
  221. package/dist/chunk-HQQMJCQJ.mjs +0 -10
  222. package/dist/chunk-IERUOGC3.mjs +0 -6
  223. package/dist/chunk-ITNLYXXV.mjs +0 -3
  224. package/dist/chunk-NFIBI4IP.mjs +0 -26
  225. package/dist/chunk-O52JMOCZ.mjs +0 -2
  226. package/dist/chunk-QMQUXTU4.mjs +0 -12
  227. package/dist/chunk-QVWVFC5K.mjs +0 -6
  228. package/dist/chunk-QZY6Q64C.mjs +0 -2
  229. package/dist/chunk-TDQP3DKQ.mjs +0 -10
  230. package/dist/chunk-V4CPWKMZ.mjs +0 -2
  231. package/dist/chunk-VACRB2NE.mjs +0 -3
  232. package/dist/chunk-W63L7367.mjs +0 -7
  233. package/dist/chunk-W7CXPCRV.mjs +0 -6
  234. package/dist/chunk-XINNG4F2.mjs +0 -4
  235. package/dist/chunk-YL2DPWVS.mjs +0 -4
  236. package/web/assets/index-B6Ai3wsa.js +0 -13
  237. package/web/assets/index-SYPC3EZy.css +0 -1
@@ -0,0 +1,447 @@
1
+ import { t } from "../../../shared/i18n/index.mjs";
2
+ import { onPreferences, preference } from "../../../shared/preferences.mjs";
3
+ import { readText, rangeOf } from "../annotate/anchors.mjs";
4
+ import { GONE_WORDS, nestedBlocks } from "../core.mjs";
5
+ import { createFills } from "../fills.mjs";
6
+ import { FOLLOW, task } from "../frames.mjs";
7
+ import { onSettle, own, provide } from "../overlays.mjs";
8
+ import { failure, postAnswer } from "../post.mjs";
9
+ import { addStyle } from "../ui.mjs";
10
+ import { UNDO_SHOWN, createBar } from "./bar.mjs";
11
+ import { GHOST_STYLE, createGhosts } from "./ghost.mjs";
12
+ import { editsBy, goneMark, placeOf, putMark, takeMark } from "./gone-words.mjs";
13
+ import { wordMarks } from "./words.mjs";
14
+
15
+ // The Agent's changes on the page (docs/editor.md "Agent's changes"), as Cursor reviews an Agent's:
16
+ // what an Agent wrote to the report since the reader last kept or reverted it (the server's review,
17
+ // editor/changes.mjs, read at the editor's start and never holding it back, drawn once the page is
18
+ // ready), marked without a change to the file: a block added (or changed with no words to tell, or
19
+ // whose words cannot be compared) a green fill (fills.mjs), words added the 批注's look in green (a
20
+ // CSS highlight under the 批注's own), the words taken out shown where they were, in red struck
21
+ // through (gone-words.mjs: an empty inline mark of the editor's own, its words drawn by CSS, which
22
+ // nothing that reads or writes the block's content takes), the blocks deleted shown where they were,
23
+ // a ghost of them in red struck through in room made for it (ghost.mjs, drawn in the overlay layer:
24
+ // nothing of it in the document). Nothing to rest the pointer on: all of it shows at once. The reader
25
+ // editing a block takes its words taken out away first (the keys, input, an input method, a paste,
26
+ // cut or drop in it, the format bar over it), and they are not drawn in it again until the caret has
27
+ // left it (the review read again then). The bar at the window's bottom (bar.mjs) says how many blocks
28
+ // and offers 撤回 (the server writes the baseline back: the reader's own saved edits stay; undone within
29
+ // UNDO_SHOWN from the bar of the page shown again) and 确认 (no more marks). 显示变更 off (设置),
30
+ // nothing is drawn and a review read is kept at once, quietly. The page reads the review again when
31
+ // its server says it changed (another tab kept it, an Agent wrote more) and 1.5s after a commit of its
32
+ // own while the bar shows. Not on a page opened on its own in a frame (?solo), printed, or presented
33
+ // (the preview's ?present has no editor).
34
+
35
+ const ROUTE = "/__gallery/edit/changes";
36
+ const REREAD_AFTER = 1500; // after a commit of the page's own, the review read again this long after (the reader paused)
37
+ const LEFT_AFTER = 150; // the caret left a block the reader edited: the review read again this long after (once saved)
38
+ const KEEP_TRIES = 3; // a quiet keep refused (the review grew meanwhile) is tried again this often
39
+ // What may edit the block it happens in: its words taken out go first (gone-words.mjs editsBy for a key).
40
+ const EDITS = ["keydown", "beforeinput", "compositionstart", "paste", "cut", "drop", "dragstart"];
41
+
42
+ const STYLE = `
43
+ ::highlight(ge-diff-add) { background-color: var(--ge-diff-add); text-decoration: underline 2px var(--ge-diff-add-line); }
44
+ .ge-diff-del { user-select: none; pointer-events: none; }
45
+ .ge-diff-del::before { content: attr(data-ge-gone-words); border-radius: 3px; background: var(--ge-diff-del); color: var(--ge-diff-del-text); text-decoration: line-through; text-decoration-color: var(--ge-diff-del-text); -webkit-box-decoration-break: clone; box-decoration-break: clone; }
46
+ .ge-diff-del[data-ge-gone-lead]::before { margin-left: .28em; }
47
+ .ge-diff-del[data-ge-gone-tail]::after { content: " "; }
48
+ ${GHOST_STYLE}
49
+ @media print { ::highlight(ge-diff-add) { background-color: transparent; text-decoration: none; } .ge-diff-del, .ge-diff-halo { display: none; } }
50
+ `;
51
+
52
+ const highlights = () => Boolean(globalThis.CSS?.highlights && globalThis.Highlight);
53
+ const holdsSelection = (block) => {
54
+ const selection = getSelection();
55
+ return selection.rangeCount > 0 && (block.contains(selection.anchorNode) || block.contains(selection.focusNode));
56
+ };
57
+
58
+ /**
59
+ * slug, token: the map's; commits(), autosave(): the page's (made later than this); flush(): what
60
+ * waits saved now (as Ctrl+S); say(text, kind); session: sessionStorage (storage.mjs storageOf), where
61
+ * a revert's undo is kept over the page shown again; across: the selection across blocks (text-select/:
62
+ * texts(), redraw(texts)), kept on its words while marks go and come. Returns { start(),
63
+ * committed(receipt), leave() }.
64
+ */
65
+ export function createChanges({ slug, token, commits, autosave, flush, say, session, across = null }) {
66
+ const undoKey = `gallery-edit:${slug}:changes-undo`;
67
+ const shows = () => preference("showChanges");
68
+ let review = null; // the review as last read and drawn: { stamp, counts, added, changed, removed }
69
+ let started = false;
70
+ let left = false;
71
+ let busy = false; // 撤回, 确认 or 撤销 under way
72
+ let reading = read(); // at once: the page's start does not wait for it
73
+ let rereadTimer = 0;
74
+ let keepTries = 0;
75
+ let offering = 0; // until when the bar offers 撤销 of this tab's revert
76
+ const filled = new Set(); // block elements filled green
77
+ const changedBlocks = new Map(); // a text block -> its words in the baseline
78
+ const added = new Map(); // a text block -> the ranges of its words added (the green highlight's)
79
+ let dels = []; // the words taken out shown: { node, block }
80
+ const edited = new Set(); // text blocks the reader edited since their words taken out were drawn
81
+ const ghosts = createGhosts();
82
+ let fills = null;
83
+ const bar = createBar({ revert: () => revert(), accept: () => keep() });
84
+
85
+ function read() {
86
+ return postAnswer(ROUTE, token, { slug, action: "read" });
87
+ }
88
+
89
+ // ---------- finding the blocks it names (in the file now) ----------
90
+ const unitOf = (id) => document.querySelector(`[data-ge-unit="${id}"]`);
91
+ const body = () => document.querySelector("main");
92
+ function find(ref) {
93
+ if (!ref) return null;
94
+ const main = body() ?? document;
95
+ if (ref.title) return main.querySelector('[data-ge-kind="title"]'); // the page's own title, as the editor made it a text block
96
+ if (ref.id) return main.querySelector(`[data-block-id="${CSS.escape(ref.id)}"]`);
97
+ if (typeof ref.hash !== "string") return null;
98
+ const list = commits();
99
+ if (!list) return null;
100
+ let nth = 0;
101
+ for (const node of main.querySelectorAll("[data-ge-unit]")) {
102
+ if (list.fileHash(Number(node.getAttribute("data-ge-unit"))) !== ref.hash) continue;
103
+ if (nth++ === ref.nth) return node;
104
+ }
105
+ return null;
106
+ }
107
+ // A unit's own text blocks in the page's order (the server's: its scope line's first, if a table's).
108
+ function ownBlocks(unit, ref) {
109
+ if (ref.title) return [unit];
110
+ const owner = (block) => {
111
+ const at = block.closest("[data-ge-unit], [data-ge-lead]");
112
+ return at?.hasAttribute("data-ge-lead") && !at.hasAttribute("data-ge-unit") ? unitOf(at.getAttribute("data-ge-lead")) : at;
113
+ };
114
+ const lead = document.querySelector(`[data-ge-lead="${unit.getAttribute("data-ge-unit")}"]`);
115
+ const within = (root) => [root, ...root.querySelectorAll("[data-ge-block]")].filter((block) => block.hasAttribute("data-ge-block") && owner(block) === unit);
116
+ return [...(lead && lead !== unit ? within(lead) : []), ...within(unit)];
117
+ }
118
+ // A text block's words as the server reads them: a list item's own, not the blocks it holds.
119
+ const readBlock = (block) => {
120
+ const held = nestedBlocks(block);
121
+ return readText([block], held.length ? { skip: (child) => held.includes(child) } : undefined);
122
+ };
123
+
124
+ // ---------- drawing ----------
125
+ const frames = task("changes", { run: place, fail: () => keepingAcross(takeDown) });
126
+ // A selection across blocks (text-select/) kept on its words while marks go and come: its ends are
127
+ // points of its own, which a text node joined again (takeMark) would leave off the page, and so its
128
+ // pieces' live ranges are taken first (a text node split, putMark, they follow; one joined, takeMark
129
+ // sets them) and it is drawn again from them.
130
+ function keepingAcross(change) {
131
+ const texts = across?.texts() ?? null;
132
+ change(texts ? texts.map(({ range }) => range) : []);
133
+ if (texts) across.redraw(texts);
134
+ }
135
+ function takeDown(ranges = []) {
136
+ fills?.clear();
137
+ for (const { node } of dels) if (node.isConnected) takeMark(node, ranges);
138
+ dels = [];
139
+ ghosts.clear();
140
+ filled.clear();
141
+ changedBlocks.clear();
142
+ added.clear();
143
+ if (highlights()) CSS.highlights.delete("ge-diff-add");
144
+ }
145
+
146
+ function draw() {
147
+ keepingAcross((ranges) => drawNow(ranges));
148
+ }
149
+ function drawNow(ranges) {
150
+ takeDown(ranges);
151
+ if (!review || !shows()) {
152
+ edited.clear();
153
+ return;
154
+ }
155
+ addStyle(STYLE);
156
+ fills ??= createFills({ kind: "changes", className: "ge-diff-halo", stands: (block) => filled.has(block) });
157
+ for (const { ref } of review.added) {
158
+ const unit = find(ref);
159
+ if (unit) filled.add(unit);
160
+ }
161
+ for (const change of review.changed) {
162
+ const unit = find(change.ref);
163
+ if (!unit) continue;
164
+ if (change.whole) {
165
+ filled.add(unit);
166
+ continue;
167
+ }
168
+ const blocks = ownBlocks(unit, change.ref);
169
+ for (const { nth, base } of change.texts ?? []) {
170
+ if (blocks[nth]) changedBlocks.set(blocks[nth], base);
171
+ }
172
+ }
173
+ // A block the reader edited keeps its words taken out away while the caret is in it.
174
+ for (const block of [...edited]) if (!changedBlocks.has(block) || !holdsSelection(block)) edited.delete(block);
175
+ for (const [block, base] of changedBlocks) markWords(block, base, !edited.has(block));
176
+ paintAdded();
177
+ for (const group of review.removed) {
178
+ const anchor = group.before ? find(group.before) : group.after ? find(group.after) : body();
179
+ if (anchor) ghosts.add(group, anchor, group.before ? "before" : group.after ? "after" : "into");
180
+ }
181
+ if (filled.size || added.size || dels.length || ghosts.count) frames.wake();
182
+ }
183
+
184
+ // A changed text block's marks: the ranges of its words added (live: they follow the reader's
185
+ // typing), and, `taken`, its words taken out put in where they were (from the last, so that each
186
+ // place read before is still where it was: a text node is split only after it).
187
+ function markWords(block, base, taken) {
188
+ const text = readBlock(block);
189
+ const { ins, cuts } = wordMarks(base, text.text);
190
+ added.set(block, ins.map(([from, to]) => rangeOf(text, from, to)).filter(Boolean));
191
+ if (!taken) return;
192
+ for (const { at, text: words } of [...cuts].reverse()) {
193
+ const where = placeOf(text, at);
194
+ if (!where) continue;
195
+ const node = goneMark(words, document, { before: text.text[at - 1], after: text.text[at] });
196
+ const entry = { node, block };
197
+ own(node, "changes", { owner: block, alive: () => block.contains(node) && dels.includes(entry) });
198
+ putMark(node, where);
199
+ dels.push(entry);
200
+ }
201
+ }
202
+
203
+ function paintAdded() {
204
+ if (!highlights()) return;
205
+ const ranges = [...added.values()].flat();
206
+ if (ranges.length === 0) return void CSS.highlights.delete("ge-diff-add");
207
+ const highlight = new Highlight(...ranges);
208
+ highlight.priority = -1; // a 批注's marks over these
209
+ CSS.highlights.set("ge-diff-add", highlight);
210
+ }
211
+
212
+ // Each frame something may have moved: the fills where their blocks are, the ghosts where their rooms are.
213
+ function place() {
214
+ if (!review) return false;
215
+ fills?.paint([...filled].filter((block) => block.isConnected));
216
+ ghosts.place();
217
+ return filled.size || ghosts.count ? FOLLOW : false;
218
+ }
219
+
220
+ // What the page's edits took off the page goes with it (a block deleted, its marks): the words taken
221
+ // out shown in a block deleted or turned into another are taken out of it too, so that undo, which
222
+ // puts the very same element back, puts back none of them.
223
+ // A copy of a mark that a change made (a cell turned into a header cell or back, cells merged or
224
+ // filled, a disclosure's title made a paragraph, a block duplicated: whatever clones a block's
225
+ // content) stands for nothing and goes at once, before the overlays' audit would find it a stray:
226
+ // the review read again after the change's commit draws the words taken out where they are then.
227
+ onSettle(() => {
228
+ const drawn = dels.length > 0;
229
+ for (const block of [...filled]) if (!block.isConnected) filled.delete(block);
230
+ dels = dels.filter((entry) => {
231
+ if (entry.block.isConnected && entry.node.isConnected) return true;
232
+ if (entry.node.parentNode) takeMark(entry.node);
233
+ return false;
234
+ });
235
+ if (drawn) {
236
+ const ours = new Set(dels.map((entry) => entry.node));
237
+ for (const node of document.querySelectorAll(`main ${GONE_WORDS}`)) if (!ours.has(node)) takeMark(node);
238
+ }
239
+ ghosts.settle();
240
+ });
241
+
242
+ // ---------- the reader edits a block holding words taken out ----------
243
+ // They go first, before the edit touches the block (window, capture: before the editor's own
244
+ // listeners): the text either side of each one text node again, the caret where it was, and the
245
+ // block's words added marked anew (a range that ended in the half that went would cover the wrong
246
+ // words). The block is `edited`: drawn without them until the caret leaves it.
247
+ // The blocks an edit may touch: the one it happens in, those the selection reaches, the browser's
248
+ // or the page's across blocks (text-select/, which the browser's then holds none of).
249
+ function blocksOf(event) {
250
+ const selection = getSelection();
251
+ const range = selection.rangeCount ? selection.getRangeAt(0) : null;
252
+ const target = event?.target instanceof Node ? event.target : null;
253
+ const reached = across?.texts()?.map(({ host }) => host) ?? [];
254
+ const hit = new Set();
255
+ for (const { block } of dels) {
256
+ if (hit.has(block)) continue;
257
+ if ((target && block.contains(target)) || (range && range.intersectsNode(block)) || reached.some((host) => host === block || host.contains(block) || block.contains(host))) hit.add(block);
258
+ }
259
+ return hit;
260
+ }
261
+ function takeOut(blocks) {
262
+ if (blocks.size === 0) return;
263
+ keepingAcross((ranges) => {
264
+ for (const entry of dels) if (blocks.has(entry.block) && entry.node.isConnected) takeMark(entry.node, ranges);
265
+ });
266
+ dels = dels.filter((entry) => !blocks.has(entry.block));
267
+ for (const block of blocks) {
268
+ edited.add(block);
269
+ const base = changedBlocks.get(block);
270
+ if (base !== undefined) markWords(block, base, false);
271
+ }
272
+ paintAdded();
273
+ }
274
+ function editing(event) {
275
+ if (dels.length === 0 || (event.type === "keydown" && !editsBy(event))) return;
276
+ takeOut(blocksOf(event));
277
+ }
278
+ // The format bar over words of such a block (its buttons keep the selection where it is).
279
+ function pressed(event) {
280
+ if (dels.length > 0 && event.target instanceof Element && event.target.closest(".ge-format")) takeOut(blocksOf(null));
281
+ }
282
+ // The caret left a block the reader edited: the review read again once what waits is saved, and drawn.
283
+ function moved() {
284
+ if (edited.size === 0) return;
285
+ let leaving = false;
286
+ for (const block of [...edited]) {
287
+ if (block.isConnected && holdsSelection(block)) continue;
288
+ edited.delete(block);
289
+ leaving = true;
290
+ }
291
+ if (leaving) rereadSaved(LEFT_AFTER);
292
+ }
293
+ for (const type of EDITS) addEventListener(type, editing, true);
294
+ addEventListener("pointerdown", pressed, true);
295
+ document.addEventListener("selectionchange", moved);
296
+
297
+ // ---------- what the server says ----------
298
+ function take(answer) {
299
+ if (left) return;
300
+ review = answer?.ok && answer.pending ? answer : null;
301
+ if (review && !shows()) return void keep({ quiet: true });
302
+ if (busy) return;
303
+ if (review) bar.counts(review.counts);
304
+ else if (Date.now() >= offering) bar.hide();
305
+ draw();
306
+ }
307
+ async function reread() {
308
+ clearTimeout(rereadTimer);
309
+ if (left || !started) return;
310
+ reading = read();
311
+ take(await reading);
312
+ }
313
+ // Read again `after` ms from now, once nothing waits to be saved (the reader paused).
314
+ function rereadSaved(after) {
315
+ clearTimeout(rereadTimer);
316
+ const again = () => {
317
+ if (autosave()?.state.waiting > 0) rereadTimer = setTimeout(again, REREAD_AFTER);
318
+ else reread();
319
+ };
320
+ rereadTimer = setTimeout(again, after);
321
+ }
322
+
323
+ // 确认: the review ended; `quiet`, as the setting being off does it (nothing said).
324
+ async function keep({ quiet = false } = {}) {
325
+ if (!review || busy) return;
326
+ const stamp = review.stamp;
327
+ busy = true;
328
+ review = null;
329
+ draw();
330
+ bar.hide();
331
+ const answer = await postAnswer(ROUTE, token, { slug, action: "accept", stamp });
332
+ busy = false;
333
+ if (answer.ok) {
334
+ keepTries = 0;
335
+ return;
336
+ }
337
+ // Refused (the review grew meanwhile, the file was held): read again, and kept again when quiet, a few times at most.
338
+ if (quiet) {
339
+ if (++keepTries > KEEP_TRIES) return;
340
+ } else if (answer.reason === "changed") say(failure(answer, t("Agent 刚刚又改了文档,再看一眼")), "note");
341
+ else say(t("没能确认:{0}", failure(answer, t("预览服务器出错"))), "error");
342
+ reread();
343
+ }
344
+
345
+ // 撤回: what waits is saved first, and nothing may wait for the reader (a conflict); then the server
346
+ // writes the baseline, and the page is shown again as after a write of its own (autosave showOwn: no
347
+ // question of what that write put back of the reader's, which the reader asked for).
348
+ async function revert() {
349
+ if (!review || busy) return;
350
+ const stamp = review.stamp;
351
+ busy = true;
352
+ await flush();
353
+ const state = autosave()?.state;
354
+ if (!state || state.waiting > 0 || state.syncing || state.held > 0 || state.offline) {
355
+ busy = false;
356
+ say(t("还有修改没保存或有冲突待处理,处理完再撤回"), "error");
357
+ return;
358
+ }
359
+ let answer = null;
360
+ const shown = await autosave().showOwn(async () => {
361
+ answer = await postAnswer(ROUTE, token, { slug, action: "revert", stamp });
362
+ if (!answer.ok || answer.none) return false;
363
+ session?.set(undoKey, JSON.stringify({ undo: answer.undo, at: Date.now() }));
364
+ return true;
365
+ });
366
+ if (shown) return; // the page goes: the one shown again offers 撤销
367
+ busy = false;
368
+ if (answer?.ok) return void reread(); // nothing was under review any more
369
+ say(answer?.reason === "changed" ? failure(answer, t("Agent 刚刚又改了文档,再看一眼")) : t("没能撤回:{0}", failure(answer ?? {}, t("预览服务器出错"))), answer?.reason === "changed" ? "note" : "error");
370
+ reread();
371
+ }
372
+
373
+ // 撤销 of a revert: the server writes back what the revert wrote over, while the file is still the revert's.
374
+ async function undo(id) {
375
+ if (busy) return;
376
+ busy = true;
377
+ offering = 0;
378
+ session?.remove(undoKey);
379
+ bar.hide();
380
+ await flush();
381
+ let answer = null;
382
+ const shown = await autosave().showOwn(async () => {
383
+ answer = await postAnswer(ROUTE, token, { slug, action: "undo", undo: id });
384
+ return Boolean(answer.ok);
385
+ });
386
+ if (shown) return;
387
+ busy = false;
388
+ say(t("没能撤销:{0}", failure(answer ?? {}, t("预览服务器出错"))), "error");
389
+ }
390
+
391
+ // A revert of this tab's, the page shown again since: 撤销 on the bar for what is left of UNDO_SHOWN.
392
+ function undoShown() {
393
+ let kept = null;
394
+ try { kept = JSON.parse(session?.get(undoKey) ?? "null"); } catch {}
395
+ const remaining = kept && typeof kept.undo === "string" ? UNDO_SHOWN - (Date.now() - kept.at) : 0;
396
+ if (remaining <= 0) {
397
+ session?.remove(undoKey);
398
+ return false;
399
+ }
400
+ offering = Date.now() + remaining;
401
+ bar.reverted(remaining, () => undo(kept.undo));
402
+ setTimeout(() => session?.remove(undoKey), remaining);
403
+ return true;
404
+ }
405
+
406
+ // The server's word that the review changed (another tab kept or reverted it, the Agent wrote more).
407
+ const heard = () => {
408
+ clearTimeout(rereadTimer);
409
+ rereadTimer = setTimeout(reread, 100);
410
+ };
411
+ addEventListener("doc-changes-changed", heard);
412
+ // 显示变更 switched (设置, another page of the origin): off, what is under review is kept and nothing drawn; on, read again.
413
+ onPreferences(() => {
414
+ if (!started || left) return;
415
+ if (!shows()) {
416
+ if (review) keep({ quiet: true });
417
+ return;
418
+ }
419
+ reread();
420
+ });
421
+
422
+ provide("changes", () => ({ pending: Boolean(review), counts: review?.counts ?? null, filled: filled.size, words: dels.length, ghosts: ghosts.count, edited: edited.size }));
423
+
424
+ return {
425
+ /** The page is ready: what was read is drawn (the bar, or 撤销 after a revert of this tab's). */
426
+ async start() {
427
+ if (started) return;
428
+ started = true;
429
+ undoShown();
430
+ take(await reading);
431
+ },
432
+ /** A commit of the page's was taken in: while the bar shows, the review is read again once the reader pauses. */
433
+ committed(receipt) {
434
+ if (!receipt?.wrote || !review || !started) return;
435
+ rereadSaved(REREAD_AFTER);
436
+ },
437
+ /** The page goes. */
438
+ leave() {
439
+ left = true;
440
+ clearTimeout(rereadTimer);
441
+ removeEventListener("doc-changes-changed", heard);
442
+ for (const type of EDITS) removeEventListener(type, editing, true);
443
+ removeEventListener("pointerdown", pressed, true);
444
+ document.removeEventListener("selectionchange", moved);
445
+ },
446
+ };
447
+ }
@@ -0,0 +1,27 @@
1
+ import { wordDiff } from "../merge.mjs";
2
+
3
+ // The words an Agent changed in a text block, as the page marks them (docs/editor.md "Agent's
4
+ // changes"): the word diff of the block's text in the baseline against its text on the page
5
+ // (merge.mjs wordDiff), as places in the page's text.
6
+
7
+ /**
8
+ * What `now` (the block's text on the page) has that `base` did not and where `base`'s words were
9
+ * taken out of it: { ins: [[from, to]] (ranges of `now`), cuts: [{ at, text }] (a place in `now`,
10
+ * what was taken out there; one where several runs meet) }.
11
+ */
12
+ export function wordMarks(base, now) {
13
+ const ins = [];
14
+ const cuts = [];
15
+ let at = 0;
16
+ for (const { tag, text } of wordDiff(base, now)) {
17
+ if (tag === "del") {
18
+ const last = cuts.at(-1);
19
+ if (last?.at === at) last.text += text;
20
+ else cuts.push({ at, text });
21
+ continue;
22
+ }
23
+ if (tag === "ins") ins.push([at, at + text.length]);
24
+ at += text.length;
25
+ }
26
+ return { ins, cuts };
27
+ }
@@ -1,8 +1,8 @@
1
1
  // Pasted colors onto the page's nine (feishu-lite model/color-match.ts and css-color.ts, after Feishu
2
2
  // 855470 · bn / mG / WW, 99121 · FB, 768176 CIEDE2000; Notion keeps only its own colors). Pure, no
3
3
  // DOM. A color is given as the CSS text of a style (named, #hex, rgb[a](), hsl[a]()).
4
- // exact a color equal to one of the page's (the kit's light values, doc-text-* and
5
- // doc-highlight-*) is that one: what a page whose styles are not meant (any web page)
4
+ // exact a color equal to one of the page's (the kit's light values: doc-text-*, and
5
+ // doc-highlight-* as a block's fill or a run of words') is that one: what a page whose styles are not meant (any web page)
6
6
  // keeps
7
7
  // nearest for sources whose styles are meant (Word, Google Docs, Notion): text — too dark (HSV
8
8
  // v ≤ 0.3) none, unsaturated (s < 0.08) gray, a pure CSS color by a fixed table, else the
@@ -14,7 +14,7 @@ import { BODY, COLORS } from "../../../../shared/colors.mjs";
14
14
 
15
15
  // The page's light values (src/shared/colors.mjs).
16
16
  const TEXT = COLORS.map(({ name, text }) => [name, text[0]]);
17
- const BACKGROUND = COLORS.map(({ name, fill }) => [name, fill[0]]);
17
+ const BACKGROUND = COLORS.flatMap(({ name, fill, mark }) => [[name, fill[0]], [name, mark[0]]]);
18
18
  const BODY_TEXT = BODY.text[0];
19
19
  const BODY_BACKGROUND = BODY.background[0];
20
20
 
@@ -32,7 +32,7 @@ import { t } from "../../../shared/i18n/index.mjs";
32
32
  // Ctrl+V reads the clipboard in Notion's order: the page's own blocks (or text), else another
33
33
  // program's content (import/read.mjs) as blocks, one formatted line, or plain text; picture
34
34
  // files (a screenshot) become images, unless they come with Office's or a table's HTML (Excel
35
- // puts a picture of the cells beside them).
35
+ // puts a picture of the cells beside them), right after the block, where blocks pasted there go.
36
36
  // With the caret in a text block: one line goes in at the caret, keeping the format bar's marks
37
37
  // where the block takes them, its own alone (not the caret's: notion-lite's text pasted) (text
38
38
  // copied in a block of the page with its line breaks); blocks
@@ -75,7 +75,7 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
75
75
  copyText(event, selection.getRangeAt(0));
76
76
  return;
77
77
  }
78
- const picked = editing() ? blocks().picked : [];
78
+ const picked = editing() ? blocks().folds.takenWith(blocks().picked) : []; // a folded heading with what it hides (fold/)
79
79
  if (picked.length === 0) return;
80
80
  event.preventDefault();
81
81
  // A list's item selected on its own carries its list's kind (notion-lite's item is a block of its
@@ -86,7 +86,7 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
86
86
  });
87
87
  const refused = picked.find((_, index) => !described[index]);
88
88
  if (refused) {
89
- say(refusalOf(refused), "note", 6000);
89
+ say(refusalOf(unmade(refused)), "note", 6000); // the block in it the page cannot make, as a selection across blocks says it
90
90
  return;
91
91
  }
92
92
  // A cut takes the blocks off the page, where a card's copy is of its unit: what is made only of its unit is refused, not lost.
@@ -261,13 +261,30 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
261
261
  }
262
262
  };
263
263
 
264
- // Files pasted by `at`: a block below it for the first of them the first component takes, in the
265
- // order of the page objects (`acceptsFile`, `fromFiles`: notion-lite's fileBlockDef; a picture an
266
- // image, any other file a file block). Returns whether one did.
267
- const fromFiles = (at, files) => pageObjects().some((page) => {
268
- const file = page.acceptsFile && [...files].find(page.acceptsFile);
269
- return Boolean(file) && page.fromFiles(at, [file]);
270
- });
264
+ // Files pasted by `at`: a block for the first of them the first component takes, in the order of the
265
+ // page objects (`acceptsFile`, `fromFiles`: notion-lite's fileBlockDef; a picture an image, any other
266
+ // file a file block). Where it goes is the paste's (notion-lite's pasteFiles, moveBlocks(ids, { parent:
267
+ // anchor.parent, after })): right after the block of `at` (unitElement: a new code block's code stands
268
+ // for the block), whole, where a block pasted there goes (placeFor: the first box out that takes it, a
269
+ // list or quote that does not splitting after the block), one undo step (`put`, handed to the
270
+ // component, which makes the block and fills it). Returns whether one did.
271
+ const fromFiles = (at, files) => {
272
+ const unit = unitElement(at);
273
+ if (!unit?.closest("main")) return false;
274
+ const put = (block) => {
275
+ const place = placeFor(unit, [block.getAttribute("data-ge-unit-kind")]);
276
+ if (!place) {
277
+ say(t("这里放不下这种块。"));
278
+ return false;
279
+ }
280
+ struct(place.parents, [], () => ({ touched: [], focus: null, created: placeAfter(place, [block]) }));
281
+ return true;
282
+ };
283
+ return pageObjects().some((page) => {
284
+ const file = page.acceptsFile && [...files].find(page.acceptsFile);
285
+ return Boolean(file) && page.fromFiles(put, [file]);
286
+ });
287
+ };
271
288
 
272
289
  // ---------- blocks into the page ----------
273
290
 
@@ -284,7 +301,9 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
284
301
  let stays = false;
285
302
  let box = anchor.parentElement;
286
303
  while (!takes(box)) {
287
- const outer = box?.closest(OUTER);
304
+ // A container made on the page and not written yet (a list just made of "- ", whose
305
+ // contenteditable OUTER passes over) is the block around what it holds, as the file's is.
306
+ const outer = box?.closest(`${OUTER}, [data-ge-new][data-ge-box]`);
288
307
  if (!outer?.closest("main")) return null;
289
308
  parents.push(box);
290
309
  // A list or a quote whose box does not take them splits after the anchor.
@@ -632,7 +651,7 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
632
651
  if (!data || (!host && picked.length === 0)) return;
633
652
  event.preventDefault();
634
653
  const anchor = host ?? picked.at(-1);
635
- // Picture or other files (a screenshot): an image or a file block below the block, unless they
654
+ // Picture or other files (a screenshot): an image or a file block after the block, unless they
636
655
  // come with the page's own blocks, or Office's or a table's HTML (Excel's picture of its cells).
637
656
  const files = data.files;
638
657
  if (files?.length > 0 && !ownData(data)) {
@@ -681,7 +700,7 @@ export function createClipboard({ editing, blocks, struct, recordText, say, load
681
700
  return;
682
701
  }
683
702
  if (!host.isConnected || !host.contains(range.startContainer)) return;
684
- // An address alone: a link wherever the block takes them (a cell, a callout, a caption too).
703
+ // An address alone: a link wherever the block takes them (a cell, a caption too).
685
704
  if (href && can(host, "links")) {
686
705
  const link = document.createElement("a");
687
706
  link.setAttribute("href", page?.href ?? href);
@@ -1,7 +1,7 @@
1
1
  import { escapeText } from "../../../../shared/escape.mjs";
2
- import { newContainer, newHost, pastedLines } from "../../core.mjs";
2
+ import { heldIn, newContainer, newHost, pastedLines } from "../../core.mjs";
3
3
  import { pageBuild } from "../../../components/builders.mjs";
4
- import { BLOCK_TYPES, COPIED_AS_ITSELF, TEXT_TYPES, TOGGLES, UNSAID, accepts, clipboardHooks, clipboardOf, isLead, sizeValue } from "../../../components/registry.mjs";
4
+ import { BLOCK_TYPES, COPIED_AS_ITSELF, HOLDS_ONE, TEXT_TYPES, TOGGLES, UNSAID, accepts, clipboardHooks, clipboardOf, isLead, sizeValue } from "../../../components/registry.mjs";
5
5
  import { BLOCK_COLORS } from "../../../components/colors.mjs";
6
6
  import { MAX_CELL_HTML } from "../../../components/table/index.mjs";
7
7
  import { copied, copyOf, reportOf } from "./describe.mjs";
@@ -62,7 +62,8 @@ export function checkBlocks(blocks) {
62
62
  */
63
63
  export function unmadeCopy(doc, blocks) {
64
64
  for (const block of blocks) {
65
- const found = block.type === "copy" ? ((doc && copied(doc, block)) || block.data ? null : block) : unmadeCopy(doc, heldBy(block));
65
+ // A copy made of its data is made of what that holds too (an instance's slots: a card in one).
66
+ const found = block.type === "copy" ? ((doc && copied(doc, block)) ? null : block.data ? unmadeCopy(doc, heldBy(block.data)) : block) : unmadeCopy(doc, heldBy(block));
66
67
  if (found) return found;
67
68
  }
68
69
  return null;
@@ -165,8 +166,16 @@ export function build(doc, block, options = {}) {
165
166
  };
166
167
  const made = clipboardOf(block.type).build ?? pageBuild(block.type);
167
168
  if (made) return made(what);
168
- const element = BLOCK_TYPES[block.type]?.box ? what.box() : what.host();
169
- element.append(...(BLOCK_TYPES[block.type]?.box ? what.inner(block.blocks) : what.inline(block.html)));
169
+ const { box, leaf } = BLOCK_TYPES[block.type] ?? {};
170
+ const element = box ? what.box() : what.host();
171
+ element.append(...(box ? what.inner(block.blocks) : what.inline(block.html)));
172
+ // A box that always holds a block (holdsOne: a highlight block, a callout) described with none (one
173
+ // the file has empty, copied or cut) holds an empty block of its `leaf` (a paragraph, as a new one
174
+ // made from "/" holds), after its lead (a highlight block's icon), as one a change empties does
175
+ // (core.mjs tidyEmptied) and a disclosure's content made again (details/page.mjs): a block to write
176
+ // in, and one the commit writes, where a box of no blocks is written by none (core.mjs nodeOf) and
177
+ // was gone at the next reload.
178
+ if (leaf && HOLDS_ONE.has(box) && heldIn(element).length === 0) element.append(what.host(leaf));
170
179
  return what.colored(element);
171
180
  }
172
181