@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
@@ -2,7 +2,7 @@ import { isHost, nestedBlocks, serializeInner, unitsOf } from "../../core.mjs";
2
2
  import { BLOCK_TYPES, COPIED_AS_ITSELF, COPY_KEEPS, TEXT_TYPES, askClipboard, clipboardOf, sizeValue, unitClipboard } from "../../../components/registry.mjs";
3
3
  import { BLOCK_COLORS } from "../../../components/colors.mjs";
4
4
  import { containerType, typeOf } from "../../convert.mjs";
5
- import { pageOfType } from "../../page-objects.mjs";
5
+ import { pageOfType, pageOfUnit } from "../../page-objects.mjs";
6
6
  import { referencedIds } from "../../../../shared/copy-ids.mjs";
7
7
 
8
8
  // Selected blocks as the clipboard's own description (docs/editor.md, "Copying blocks"): each
@@ -64,6 +64,10 @@ export function copied(doc, block) {
64
64
  if (block.report !== reportOf(doc)) return null;
65
65
  const unit = doc.querySelector(`main [data-ge-unit="${block.unit}"]`);
66
66
  if (!unit || unit.getAttribute("data-ge-unit-kind") !== block.kind) return null;
67
+ // One whose unit's copy cannot stand for it (`copiesAsItself`: an instance with slots) is made of
68
+ // what it carries, here too: that copy would hold blocks the file writes whole, with ids no answer
69
+ // of a commit gives the page (source/apply.mjs restoredText names the block alone).
70
+ if (pageOfUnit(unit)?.copiesAsItself?.(unit) === false) return null;
67
71
  const copy = copyOf(unit);
68
72
  return (typeof block.data?.html === "string" ? copy.outerHTML === block.html : copy.textContent === block.text) ? unit : null;
69
73
  }
@@ -84,12 +88,6 @@ const plain = (node) => (node?.textContent ?? "").replace(/\s+/g, " ").trim();
84
88
  /** What copies of a block the page could make: { type, html, text, blocks, lists }, or null. */
85
89
  export function describe(node) {
86
90
  const kind = node.getAttribute("data-ge-unit-kind");
87
- if (COPIES.has(kind)) {
88
- if (!node.hasAttribute("data-ge-unit") || node.hasAttribute("data-ge-new")) return null; // not in the file yet
89
- const copy = copyOf(node);
90
- const data = unitClipboard(node).data?.(node, { serializeInner, copyOf, clipboardOf, copiedType, page: pageOfType });
91
- return { type: "copy", kind, unit: Number(node.getAttribute("data-ge-unit")), report: reportOf(node.ownerDocument), text: copy.textContent, html: copy.outerHTML, ...(data ? { data } : {}) };
92
- }
93
91
  // What an element holds: its units. One that is no box has none (a list of a class of its own:
94
92
  // source/units.mjs boxKind), and what stands in it is read as it stands.
95
93
  const holding = (box) => {
@@ -98,6 +96,17 @@ export function describe(node) {
98
96
  const blocks = (units.length > 0 || box.hasAttribute("data-ge-box") ? units : [...box.children]).map(describe);
99
97
  return blocks.every(Boolean) ? blocks : null;
100
98
  };
99
+ // Copied as itself. One whose unit's copy cannot stand for it (`copiesAsItself` of its page object:
100
+ // an instance with slots, whose blocks that copy would write whole) is copied by what it carries
101
+ // alone (`data`, those blocks described apart), and is no copy where it carries nothing (a block in a
102
+ // slot the page cannot make).
103
+ if (COPIES.has(kind)) {
104
+ if (!node.hasAttribute("data-ge-unit") || node.hasAttribute("data-ge-new")) return null; // not in the file yet
105
+ const copy = copyOf(node);
106
+ const data = unitClipboard(node).data?.(node, { serializeInner, copyOf, clipboardOf, copiedType, page: pageOfType, holding });
107
+ if (!data && pageOfUnit(node)?.copiesAsItself?.(node) === false) return null;
108
+ return { type: "copy", kind, unit: Number(node.getAttribute("data-ge-unit")), report: reportOf(node.ownerDocument), text: copy.textContent, html: copy.outerHTML, ...(data ? { data } : {}) };
109
+ }
101
110
  const what = { page: pageOfType, holding, describe, copiedType, isHost, nestedBlocks, serializeInner, plain, colorOf, sizeValue };
102
111
  const block = askClipboard("describe", node, what);
103
112
  if (block !== undefined) return block;
@@ -10,16 +10,16 @@ import { WRITERS, heldToWords } from "./writers.mjs";
10
10
  // Copied blocks for other programs (notion-lite serialize.js, after Notion 210528 / 389867: the
11
11
  // Markdown its export writes, and HTML that reads as the same), each block by the writer of its kind
12
12
  // (writers.mjs). Pasted back here without the clipboard's own data, both read as the same blocks
13
- // (import/), but for a callout: its Markdown a highlight block, its HTML a paragraph.
13
+ // (import/), but for a callout: its Markdown a highlight block, its HTML a group of its blocks.
14
14
  // text/plain Markdown: # … #### headings, "- " / "1. " / "- [ ] " items (an item's lists 4 spaces
15
- // in), "> " quotes, <aside> callouts (a highlight block: its emoji and text, its blocks;
16
- // a callout: its text),
15
+ // in), "> " quotes, <aside> callouts (a highlight block and a callout: its emoji, a highlight
16
+ // block's, and the text of its first block, its other blocks),
17
17
  // ``` code (its language), $$ formula blocks (an empty one: none), | tables |, ---, a disclosure as an item
18
18
  // holding its blocks, a tabs area as each tab's name and blocks (a row of columns, copied
19
19
  // as itself, as its text); **bold** *italic* ~~strike~~ `code` $formula$, links as their text
20
20
  // (Notion's plain text); a list's items one line apart, other blocks an empty line; a
21
21
  // line that would read as a tag or a fence escaped (writers.mjs heldToWords).
22
- // text/html plain markup: p (a callout's too), h1–h4, ul / ol / li ("[x] " / "[ ] " before a to-do's text), blockquote,
22
+ // text/html plain markup: p, a div of a highlight block's or a callout's blocks, h1–h4, ul / ol / li ("[x] " / "[ ] " before a to-do's text), blockquote,
23
23
  // details, pre > code.language-x, hr, table, a formula block as $$ … $$ (an empty one:
24
24
  // nothing, as in Markdown) and an inline one
25
25
  // as $ … $ (Notion's export), colors as the kit's classes with their color in a style.
@@ -27,8 +27,8 @@ import { WRITERS, heldToWords } from "./writers.mjs";
27
27
  // both. Text selected in one block (textFormats) is its inline HTML for other programs, and as plain
28
28
  // text its words alone (notion-lite's inlinePlain: no list's marker, no Markdown).
29
29
 
30
- // The kit's light colors, for programs without its styles (src/shared/colors.mjs).
31
- const INK = Object.fromEntries(COLORS.flatMap(({ name, text, fill }) => [[`doc-text-${name}`, `color:${text[0]}`], [`doc-highlight-${name}`, `background-color:${fill[0]}`]]));
30
+ // The kit's light colors (a background on words its words' fill), for programs without its styles (src/shared/colors.mjs).
31
+ const INK = Object.fromEntries(COLORS.flatMap(({ name, text, mark }) => [[`doc-text-${name}`, `color:${text[0]}`], [`doc-highlight-${name}`, `background-color:${mark[0]}`]]));
32
32
  const FORMULA = /^\s*\\\(([\s\S]*)\\\)\s*$/;
33
33
 
34
34
  // Inline HTML (the format bar's marks: serializeInner's) as nodes to walk, a link shown with its icon
@@ -14,12 +14,12 @@ import { groupItems } from "./rules.mjs";
14
14
  // starts, those between whole, the last up to where it ends; a list's items each an
15
15
  // item of its kind (`list`, as an item selected on its own) with its checked state
16
16
  // and what of the blocks it holds the selection reaches, a quote's paragraphs in a
17
- // quote, and a highlight block, a disclosure or a section the selection ends in
17
+ // quote, and a highlight block, a callout, a disclosure or a section the selection ends in
18
18
  // with what of it comes before the end (its icon, its title), a tabs area with the
19
19
  // tab shown and what of its panel comes before the end; a flowchart's nodes a
20
20
  // flowchart with its direction (never loose paragraphs: its steps are no items),
21
21
  // the node it ends in a node with what of it comes before the end. Around where it
22
- // starts, a highlight block, a disclosure, a section, a flowchart's node, an item
22
+ // starts, a highlight block, a callout, a disclosure, a section, a flowchart's node, an item
23
23
  // whose own text comes before it, a row of columns, a column or a tabs area are not
24
24
  // copied, only what of them it holds (notion-lite copies no block its start is in:
25
25
  // a callout, a toggle, a list item's parent; a row of columns it flattens wherever).
@@ -180,8 +180,6 @@ const paragraph = {
180
180
  html: (block, how) => `<p>${how.portable(block.html ?? "")}</p>`,
181
181
  };
182
182
 
183
- const inAside = (text) => ["<aside>", ...text.split("\n"), "</aside>"];
184
-
185
183
  const todo = list("ul", (_, checked, gap) => `- [${checked ? "x" : " "}]${gap}`);
186
184
 
187
185
  export const WRITERS = {
@@ -218,15 +216,6 @@ export const WRITERS = {
218
216
  },
219
217
  html: (block, how) => `<div>${block.blocks.map(how.html).join("")}</div>`,
220
218
  },
221
- // A text block in one <aside>: its text alone, no blocks to set apart from it by an empty line. Its
222
- // HTML is a paragraph of its text, as notion-lite's is no block of HTML: an <aside> would be rich
223
- // formatting, for which the import reads the HTML of all that is copied with it, and has no block
224
- // of its own. An empty one has none: an empty element would be read for it.
225
- callout: {
226
- copied: (block, how) => inAside(how.inline(block.html ?? "")),
227
- exported: (unit, how) => inAside(how.own(unit)),
228
- html: (block, how) => (block.html?.trim() ? `<p>${how.portable(block.html)}</p>` : ""),
229
- },
230
219
  // A disclosure: "- " and its title, its blocks 4 spaces in (Notion writes a toggle as an item), but
231
220
  // no list: an empty line stands between it and a list beside it.
232
221
  toggle: {
@@ -50,7 +50,7 @@ const STYLE = `
50
50
  .ge-gap-pin > .ge-gap-add { left: -3px; top: -8px; opacity: 0; transform: scale(0); transform-origin: 50% 90%; transition: opacity .15s, transform .15s; }
51
51
  .ge-gap-pin:hover > .ge-gap-dot { opacity: 0; }
52
52
  .ge-gap-pin:hover > .ge-gap-add { opacity: 1; transform: scale(1); }
53
- .ge-gap-badge { padding: 0 4px; border-radius: 4px; color: #fff; background: rgb(0 0 0 / 30%); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); font: 12px/18px var(--doc-font, system-ui, sans-serif); pointer-events: none; white-space: nowrap; }
53
+ .ge-gap-badge { padding: 0 4px; border-radius: 4px; color: #fff; background: rgb(0 0 0 / 30%); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); font: 12px/18px var(--doc-sans-font, var(--doc-font, system-ui, sans-serif)); pointer-events: none; white-space: nowrap; }
54
54
  /* The arrow under a flowchart's gap line (the node's ::after, the kit's) dimmed while the line shows. */
55
55
  ol.doc-flow > li::after { transition: opacity .12s; }
56
56
  ol.doc-flow > li[data-ge-gap-dim]::after { opacity: .15; }
@@ -1598,21 +1598,34 @@ export function createCommits({ root, map, baselines, units, boxes, getLayout, s
1598
1598
  * Someone else's write put back a unit this load made (`gone`) or moved (`moved`), and the reader
1599
1599
  * keeps theirs (`mine`): one it made is a block made on the page again, written anew (around its
1600
1600
  * blocks from the source it was put back with, if it was); one it moved goes where the page has it
1601
- * again. Otherwise it is no longer checked.
1601
+ * again. Otherwise it is no longer checked. `within`: the units inside one made that the write took out
1602
+ * with it (the revert check's, heldWithin), decided with it.
1602
1603
  */
1603
- function putBack(unit, { gone, mine }) {
1604
+ function putBack(unit, { gone, mine, within = [] }) {
1604
1605
  const { around } = placed.get(unit) ?? {};
1605
- placed.delete(unit);
1606
+ // What the write took out with it: kept or given up with it, made on the page again inside it (the
1607
+ // blocks of an instance's slots, of a disclosure made this load). What it holds that the file still
1608
+ // has (a block moved in, kept where the Agent unwrapped it) stays the file's, a unit the commit
1609
+ // making it again takes along, as one it was put back around, and is decided by its own card if any.
1610
+ const inner = gone ? within : [];
1611
+ for (const id of [unit, ...inner]) placed.delete(id);
1606
1612
  if (!mine) return;
1607
1613
  touched(); // the layout changes in place
1608
1614
  if (gone) {
1609
1615
  const key = map.units[unit].cid;
1610
1616
  drop(unit);
1617
+ inner.forEach(drop);
1611
1618
  if (around && dropped.has(key)) dropped.get(key).around = around;
1612
1619
  } else for (const [box, order] of getLayout()?.orders ?? []) getLayout().orders.set(box, order.filter((id) => id !== unit));
1613
1620
  marks();
1614
1621
  }
1615
1622
 
1623
+ // The units the page shows inside unit `unit`'s element, at any depth.
1624
+ const unitsWithin = (unit) => [...(unitElement(unit)?.querySelectorAll("[data-ge-unit]") ?? [])].map(idOf).filter((id) => id !== null);
1625
+
1626
+ /** Whether unit `unit` stands inside one of `units` on the page: what someone else's write took out with it is decided with it. */
1627
+ const heldWithin = (unit, units) => units.some((other) => other !== unit && unitsWithin(other).includes(unit));
1628
+
1616
1629
  /** The text of the blocks in a unit the page shows (the reader's words), or "". */
1617
1630
  const unitText = (unit) => {
1618
1631
  const element = unitElement(unit);
@@ -1728,9 +1741,11 @@ export function createCommits({ root, map, baselines, units, boxes, getLayout, s
1728
1741
  }
1729
1742
 
1730
1743
  return {
1731
- build, apply, collect, current, marks, counts, buffer, texts, resolve, committed, unjournal, placedUnits, putBack, unitText, written, carry, dataOfUnit, heldOtherwise, rewrite, issues, removed, pageIds, idFor,
1744
+ build, apply, collect, current, marks, counts, buffer, texts, resolve, committed, unjournal, placedUnits, putBack, heldWithin, unitText, written, carry, dataOfUnit, heldOtherwise, rewrite, issues, removed, pageIds, idFor,
1732
1745
  /** The id a unit of the page has in the file, or the one it proposes for it; null for none. */
1733
1746
  fileId: (unit) => map.units[unit]?.blockId ?? map.units[unit]?.proposed ?? null,
1747
+ /** The hash a unit of the page has in the file now (its map's, kept by every answer); null for none (changes/ finds the Agent's blocks by it). */
1748
+ fileHash: (unit) => map.units[unit]?.hash ?? null,
1734
1749
  /** save(pending) as JSON, with the page's body (sync-state.mjs pack). */
1735
1750
  keep: (pending) => pack(root, save(pending)),
1736
1751
  /** Identities more than one element on the page carries (none, ever). */
@@ -1,5 +1,6 @@
1
1
  import { blocksText, partOwner, plainText } from "./core.mjs";
2
- import { commonRun } from "./merge.mjs";
2
+ import { reveal } from "./folded.mjs";
3
+ import { wordDiff } from "./merge.mjs";
3
4
  import { PANEL_STYLE, panelGone, panelShown } from "./panel.mjs";
4
5
  import { addStyle, button, element, mount } from "./ui.mjs";
5
6
  import { t } from "../../shared/i18n/index.mjs";
@@ -26,7 +27,7 @@ import { t } from "../../shared/i18n/index.mjs";
26
27
  // a popover like the workbench's, no colored frame, the flagged block is what stands out; the
27
28
  // reader's words and the file's are told apart by one pair of colors.
28
29
  const STYLE = `
29
- .ge-conflicts { --ge-mine: light-dark(rgba(223,22,0,.094), rgba(255,104,92,.24)); --ge-mine-text: light-dark(#cf5148, #ff8f85); --ge-theirs: light-dark(rgba(0,100,45,.09), rgba(108,255,172,.157)); --ge-theirs-text: light-dark(#448361, #6fc596); }
30
+ .ge-conflicts { --ge-mine: var(--ge-diff-del); --ge-mine-text: var(--ge-diff-del-text); --ge-theirs: var(--ge-diff-add); --ge-theirs-text: var(--ge-diff-add-text); }
30
31
  .ge-conflict[data-ge-current] { background: var(--ge-hover); }
31
32
  .ge-conflict-label { display: flex; gap: 6px; margin: 10px 0 3px; color: var(--ge-tx2); font-size: 12px; }
32
33
  .ge-conflict :is(ins, del, .ge-chip) { border-radius: 3px; text-decoration: none; }
@@ -84,29 +85,8 @@ function describe(entry, restorable) {
84
85
  return { text: t("你新建(或放回)的这一块没法放进文件(周围的块已被 AI 改动或删除)。"), actions: [[t("放到附近"), "nearby", true], [t("撤销这一块"), "undo"], copy] };
85
86
  }
86
87
 
87
- // A word diff of two texts, the reader's version against the file's: runs of { tag, text }, tag null
88
- // for words both have, "del" for the reader's alone and "ins" for the file's alone (an ins before
89
- // the del it stands beside). Words are runs of letters and digits, spaces, single other characters
90
- // (each Chinese character on its own); at most 1500 words of each (merge.mjs commonRun finds what is shared).
91
- export function wordDiff(mine, theirs) {
92
- const tokens = (text) => (text.match(/[A-Za-z0-9]+|\s+|[^A-Za-z0-9\s]/gu) ?? []).slice(0, 1500);
93
- const a = tokens(mine);
94
- const b = tokens(theirs);
95
- const runs = [];
96
- const add = (tag, text) => {
97
- if (runs.at(-1)?.tag === tag) runs.at(-1).text += text;
98
- else runs.push({ tag, text });
99
- };
100
- let [i, j] = [0, 0];
101
- for (const [x, y] of [...commonRun(a, b), [a.length, b.length]]) {
102
- while (j < y) add("ins", b[j++]);
103
- while (i < x) add("del", a[i++]);
104
- if (x < a.length) add(null, a[i++]);
105
- j++;
106
- }
107
- return runs;
108
- }
109
-
88
+ // The word diff (merge.mjs wordDiff): its runs as the card's nodes.
89
+ export { wordDiff };
110
90
  const diffNodes = (mine, theirs) => wordDiff(mine, theirs).map(({ tag, text }) => tag ? element(tag, { text }) : document.createTextNode(text));
111
91
 
112
92
  /**
@@ -240,7 +220,7 @@ export function createConflicts({ act, nodeOf, restorable, copyBlock, say }) {
240
220
  function locate(key) {
241
221
  const found = cards.get(key);
242
222
  const node = found && nodeOf(found.entry);
243
- if (node) globalThis.DocKit?.reveal(node);
223
+ if (node) reveal(node); // a folded heading's section too (fold/)
244
224
  node?.scrollIntoView({ block: "center", behavior: "smooth" });
245
225
  for (const other of cards.values()) other.node.toggleAttribute("data-ge-current", other === found);
246
226
  }
@@ -12,9 +12,9 @@ import { asTitle, flowchartFromList, flowchartFromPanel, itemFromStep, listFromF
12
12
 
13
13
  // Turning blocks into other types on the page (DOM only; tested with jsdom): text, small text
14
14
  // (小字, the kit's .doc-note), four heading levels, bulleted, numbered and to-do lists and flow charts (流程图), disclosures
15
- // (折叠列表), callouts (提示块), highlight panels (高亮块: a container with the opening summary's
16
- // look, whose blocks keep their own types, lists and headings among them; a block turned into one, or
17
- // into a disclosure, goes into it as it is), code blocks, quotes and
15
+ // (折叠列表), highlight panels (高亮块: a container with the opening summary's look, whose blocks keep
16
+ // their own types, lists and headings among them) and callouts (提示块: the same kind of container, its
17
+ // look its own; a block turned into either, or into a disclosure, goes into it as it is), code blocks, quotes and
18
18
  // dividers; the types are the components' (src/editor/components/, registry.mjs), and so is how a
19
19
  // type that is no text block is made (its `create`). The "/" menu,
20
20
  // Markdown shortcuts, the block menu and its "在下方新建一块" all come here (editor.mjs:
@@ -48,7 +48,8 @@ const ownAttribute = (host, name) => Boolean(ATTRIBUTES[host.getAttribute("data-
48
48
 
49
49
  // The types a block, or a group of blocks, goes into as it is (each keeping its type: `takesAsIs`): a
50
50
  // highlight block (notion-lite's callout, whose body is its children: transformBlocks moves the blocks
51
- // into a new one) and, as the reader asked, a disclosure (into its content; its title left empty to type).
51
+ // into a new one), a callout (the same kind of block) and, as the reader asked, a disclosure (into its
52
+ // content; its title left empty to type).
52
53
  const TAKES_AS_IS = new Set(Object.keys(typesWith("takesAsIs")));
53
54
 
54
55
  // Whether a box takes the block of a type: what its role accepts, and none of a type that does not
@@ -181,7 +182,7 @@ export function containerType(unit) {
181
182
  const contentOf = (unit) => BLOCK_TYPES[containerType(unit)]?.content?.(unit) ?? unit;
182
183
 
183
184
  // The containers that turn into one another, what they hold carried along (Notion's: HOLDERS): a
184
- // disclosure, a quote, a highlight block. Each is the kind of box it is, as well as of block.
185
+ // disclosure, a quote, a highlight block, a callout. Each is the kind of box it is, as well as of block.
185
186
 
186
187
  // The blocks a disclosure, quote or highlight block holds (a highlight block's icon not among them).
187
188
  const heldBy = (unit) => heldIn(contentOf(unit));
@@ -272,8 +273,8 @@ function standIn(host, type) {
272
273
  return standsFor(next, host);
273
274
  }
274
275
 
275
- // Whether a block already is the text block of a type: its tag, and the class that tells a
276
- // callout from a div; of the paragraph types, the one its class makes it.
276
+ // Whether a block already is the text block of a type: its tag, and its class where its type has
277
+ // one; of the paragraph types, the one its class makes it.
277
278
  function isOfType(host, type) {
278
279
  const spec = BLOCK_TYPES[type];
279
280
  if (host.localName !== spec.tag) return false;
@@ -344,13 +344,13 @@ export function serializeInner(element) {
344
344
  if (inner) return inner(element);
345
345
  // A table cell's lists are its text (单元格里的列表); any other block's are blocks of their own.
346
346
  const lists = element.getAttribute?.("data-ge-kind") === "cell";
347
- return ownNodes(element).map((node) => serializeNode(node, lists)).join("").replace(/<br>\s*$/, "");
347
+ return serializeNodes(ownNodes(element), lists).replace(/<br>\s*$/, "");
348
348
  }
349
349
 
350
350
  // A list in a table cell, its items' inline content (a line break holding an empty item's caret left out).
351
351
  function serializeList(list) {
352
352
  const tag = list.tagName.toLowerCase();
353
- const items = [...list.children].filter((item) => item.tagName === "LI").map((item) => `<li>${[...item.childNodes].map((child) => serializeNode(child)).join("").replace(/<br>\s*$/, "")}</li>`);
353
+ const items = [...list.children].filter((item) => item.tagName === "LI").map((item) => `<li>${serializeNodes([...item.childNodes]).replace(/<br>\s*$/, "")}</li>`);
354
354
  return items.length > 0 ? `<${tag}>${items.join("")}</${tag}>` : "";
355
355
  }
356
356
 
@@ -383,11 +383,16 @@ export function textEnd(element) {
383
383
 
384
384
  const ownNodes = (element) => [...element.childNodes].slice(0, textEnd(element));
385
385
 
386
- /** A block's own content as HTML (innerHTML, without the blocks a list item holds). */
386
+ /**
387
+ * A block's own content as HTML (innerHTML, without the blocks a list item holds, nor the words an
388
+ * Agent took out that the page shows in it: GONE_WORDS), what undo and redo record of it.
389
+ */
387
390
  export function ownHTML(element) {
388
- if (nestedBlocks(element).length === 0) return element.innerHTML;
391
+ const shown = element.querySelector(GONE_WORDS);
392
+ if (nestedBlocks(element).length === 0 && !shown) return element.innerHTML;
389
393
  const copy = element.ownerDocument.createElement("div");
390
394
  copy.append(...ownNodes(element).map((node) => node.cloneNode(true)));
395
+ if (shown) copy.querySelectorAll(GONE_WORDS).forEach((node) => node.remove());
391
396
  return copy.innerHTML;
392
397
  }
393
398
 
@@ -429,6 +434,33 @@ export const parentItem = (item) => item?.tagName === "LI" && item.parentElement
429
434
  // edits becomes a space too.
430
435
  const spaced = (text) => escapeTyped(text).replace(/ *\u00a0[ \u00a0]*/g, " ");
431
436
 
437
+ // The editor's own nodes in a text block (data-doc-dev: the words an Agent took out, which the page
438
+ // shows where they were, changes/) are no content: left out, the text on either side of one is
439
+ // written as one text, as it was before the node split it (a space either side of it the one space
440
+ // it was, never two).
441
+ function serializeNodes(nodes, lists = false) {
442
+ let html = "";
443
+ let text = null; // a text node's, not written yet: the text after an editor's node joins it
444
+ let across = false; // an editor's node stands between it and what comes next
445
+ for (const node of nodes) {
446
+ if (node.nodeType === 1 && node.hasAttribute("data-doc-dev")) {
447
+ across = text !== null;
448
+ continue;
449
+ }
450
+ if (node.nodeType === 3 && node.data !== ZWSP) {
451
+ if (text !== null && !across) html += spaced(text);
452
+ text = text !== null && across ? text + node.data : node.data;
453
+ across = false;
454
+ continue;
455
+ }
456
+ if (text !== null) html += spaced(text);
457
+ text = null;
458
+ across = false;
459
+ html += serializeNode(node, lists);
460
+ }
461
+ return text === null ? html : html + spaced(text);
462
+ }
463
+
432
464
  function serializeNode(node, lists = false) {
433
465
  // A text node holding nothing but a zero-width space keeps the caret after a mark the editor
434
466
  // made (markdown/, typing shortcuts); it is never content.
@@ -443,7 +475,7 @@ function serializeNode(node, lists = false) {
443
475
  // and their text kept.
444
476
  const fromSource = node.hasAttribute("data-ge-src") && node.tagName in TAGS;
445
477
  const tag = fromSource ? node.tagName.toLowerCase() : TAGS[node.tagName];
446
- const inner = [...node.childNodes].map((child) => serializeNode(child)).join("");
478
+ const inner = serializeNodes([...node.childNodes]);
447
479
  const ink = node.tagName === "SPAN" ? [...node.classList].filter(isInkClass) : [];
448
480
  if (ink.length > 0) return inner ? `<span class="${ink.join(" ")}">${inner}</span>` : "";
449
481
  // Small text's end part (its Tab: components/paragraph/page.mjs), at the line's right end.
@@ -498,8 +530,9 @@ export function isEmpty(element) {
498
530
  const siblingTag = (element) => element.tagName === "LI" ? "li" : "p";
499
531
 
500
532
  /**
501
- * A text block made on the page: data-ge-new, with its type and unit kind. Inside a component it
502
- * leaves the frame to the component, as `from` (the block it was split off) does.
533
+ * A text block made on the page: data-ge-new, with its type and unit kind, a text block of the flow
534
+ * (every type's that is one: a callout's words, its own kind until 2026-11-07, are a paragraph of it
535
+ * now). Inside a component it leaves the frame to the component, as `from` (the block it was split off) does.
503
536
  */
504
537
  export function newHost(doc, type, from = null) {
505
538
  const spec = BLOCK_TYPES[type];
@@ -509,9 +542,8 @@ export function newHost(doc, type, from = null) {
509
542
  block.setAttribute("data-ge-new", "");
510
543
  block.setAttribute("data-ge-type", type);
511
544
  block.setAttribute("data-ge-unit-kind", spec.kind);
512
- const kind = spec.host ?? "flow";
513
- block.setAttribute("data-ge-kind", kind);
514
- block.setAttribute("data-ge-caps", capsFor(kind));
545
+ block.setAttribute("data-ge-kind", "flow");
546
+ block.setAttribute("data-ge-caps", capsFor("flow"));
515
547
  if (from?.hasAttribute("data-ge-framed")) block.setAttribute("data-ge-framed", "");
516
548
  return block;
517
549
  }
@@ -872,6 +904,9 @@ export function mergeInto(into, from, { anyKind = false } = {}) {
872
904
  // with its own.
873
905
 
874
906
  export const ELEMENT_ID = "data-ge-id";
907
+
908
+ /** The words an Agent took out of a text block, shown where they were (changes/): never its content. */
909
+ export const GONE_WORDS = "[data-ge-gone-words]";
875
910
  const ELEMENT_ID_PREFIX = Math.random().toString(36).slice(2, 6); // a page's own, as formulas' keys
876
911
  let elementIds = 0;
877
912
 
@@ -927,10 +962,10 @@ export const inTable = (node) => /^(TR|TD|TH)$/.test(node?.tagName ?? "") && nod
927
962
  export function unitElement(node) {
928
963
  let found = node?.closest?.(UNIT_OR_LEAD) ?? null;
929
964
  while (inTable(found)) found = found.parentElement?.closest(UNIT_OR_LEAD) ?? null;
930
- // Text that is part of a block made on the page (a new disclosure's title, a new code block's
931
- // code) stands for that block.
965
+ // Text made on the page that is part of a block (a new code block's code, a media block's new
966
+ // caption) stands for that block: the part's own block, not the one around it.
932
967
  const part = found && !found.hasAttribute("data-ge-unit") ? partOf(found) : null;
933
- if (part) found = (part.matches("pre, .doc-code") ? part : part.parentElement).closest(UNIT);
968
+ if (part) found = part.closest(UNIT);
934
969
  if (!found?.hasAttribute("data-ge-lead")) return found;
935
970
  const id = found.getAttribute("data-ge-lead");
936
971
  let next = found.nextElementSibling;
@@ -1180,12 +1215,12 @@ export function snapshotLayout(root) {
1180
1215
  }
1181
1216
 
1182
1217
  // Whether a text block may be written with no text: it is of a type whose component says so (its
1183
- // `empty`: a paragraph, a heading, a list item, a callout; the server's emptyLeaf reads the same)
1218
+ // `empty`: a paragraph, a heading, a list item; the server's emptyLeaf reads the same)
1184
1219
  // and can be emptied (can "empty"), an item holding blocks too, its line kept over them (keepLine). A
1185
1220
  // field that may be emptied (a cell, a caption) is no block written empty; a new code block is
1186
1221
  // written empty by its type (describeNode).
1187
1222
  const clearable = (element) => can(element, "empty");
1188
- /** Whether an empty text block stays, written with no text (a paragraph, a heading, a list item, a callout, a code block). */
1223
+ /** Whether an empty text block stays, written with no text (a paragraph, a heading, a list item, a code block). */
1189
1224
  const keepsWhenEmpty = (element) => BLOCK_TYPES[textTypeOf(element)]?.empty === true && clearable(element);
1190
1225
 
1191
1226
  /** Whether a text block may go: it can be removed, is not kept (data-ge-keep) and holds no blocks (it would show no line of its own). */
@@ -1208,8 +1243,11 @@ const leaves = (node, emptied) => gone(node) || emptied.has(node);
1208
1243
  // The text block of a block: the block itself, or where its component says (a code block's code).
1209
1244
  const textOf = (node) => BLOCK_TYPES[blockTypeOf(node)]?.textHost?.(node) ?? node;
1210
1245
 
1211
- // What a component's description is given to say what goes with a block (`extras`).
1246
+ // What a component's description is given to say what goes with a block (`extras`). `nodes(box)`: the
1247
+ // blocks of a box of its own as the commit writes them (nodeOf), a block written whole holding those of
1248
+ // boxes of its own apart from its markup (an instance's slots: installed/index.mjs).
1212
1249
  const HELPERS = { sizeValue, serializeInner, unitsOf };
1250
+ const helpersFor = (emptied) => ({ ...HELPERS, nodes: (box) => unitsOf(box).map((child) => nodeOf(child, emptied)).filter(Boolean) });
1213
1251
 
1214
1252
  /**
1215
1253
  * The first element in `root` (in document order, `root` itself left out) carrying each value of
@@ -1351,7 +1389,7 @@ function describeNode(element, emptied) {
1351
1389
  // (`content`: a disclosure's content region), its title (`title`), its id (`keyed`: a detail
1352
1390
  // card's), when it is written at all (`written`: a row once each of its columns has something, a
1353
1391
  // detail card while a link opens it; else once it holds something).
1354
- const extras = spec.extras?.(element, HELPERS) ?? {};
1392
+ const extras = spec.extras?.(element, helpersFor(emptied)) ?? {};
1355
1393
  if (spec.bare) return spec.written?.(element, []) === false ? null : { type, ...extras };
1356
1394
  if (!spec.box) {
1357
1395
  const text = textOf(element);
@@ -28,6 +28,8 @@ import { t } from "../../../shared/i18n/index.mjs";
28
28
  // cancels, and so does anything that ends the press another way (the handle losing the pointer
29
29
  // capture, the window losing the focus, the page hidden): never a drop. Ending is idempotent, and
30
30
  // the line, tint and copy belong to their drag (overlays.mjs). Ctrl+Shift+arrows move selected blocks standing next to each other by one place (moveBy).
31
+ // A folded heading goes with what it hides, or as the section it covers whole (fold/ takenWith); its
32
+ // copy shows what is in sight, and Ctrl+Shift+↑/↓ take it a section of its level at a time.
31
33
 
32
34
  const STYLE = `
33
35
  ${gestureLock("data-ge-drag", { cursor: "grabbing", select: true })}
@@ -43,9 +45,9 @@ const STYLE = `
43
45
  * info: block-info.mjs. selection: selection/index.mjs. struct(parents, touched, mutate): the
44
46
  * editor's undoable structural change (history.mjs). say: the status line. closeMenu(): the block
45
47
  * menu closes. schedule(): the block tools' next frame (blocks.mjs), which calls step() while a
46
- * drag lasts.
48
+ * drag lasts. folds: the headings' folds (fold/).
47
49
  */
48
- export function createDrag({ info, selection, struct, say, closeMenu, schedule }) {
50
+ export function createDrag({ info, selection, struct, say, closeMenu, schedule, folds }) {
49
51
  addStyle(STYLE);
50
52
  const { addColumn, columnsOf, rowAround } = columnOps() ?? {};
51
53
  const { kindOf, nameOf, named, fixedText, boxKindOf } = info;
@@ -105,7 +107,8 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule }
105
107
  }
106
108
 
107
109
  // Put blocks beside a block (drop.mjs `aside`): { target, side }, the two side by side in a new
108
- // row of columns; { row, index, whole }, a new column of that row at `index` holding them, or,
110
+ // row of columns (a folded heading's column taking what it hides: fold/ withHidden); { row, index,
111
+ // whole }, a new column of that row at `index` holding them, or,
109
112
  // when they are all of one column of it (`whole`), that column moved there. One undo step that
110
113
  // keeps them selected; a container they leave empty goes, as when they move (and a row left
111
114
  // with one column gives its blocks back: history.mjs).
@@ -122,7 +125,7 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule }
122
125
  const leftovers = leftoversOf(targets, aside.row ?? aside.target);
123
126
  const parents = [...new Set([...targets.map((target) => target.parentNode), aside.row ?? aside.target.parentElement, ...leftovers.map((node) => node.parentNode)].filter(Boolean))];
124
127
  struct(parents, [], () => {
125
- const { created } = aside.row ? addColumn(aside.row, aside.index, parts) : rowAround(aside.target, [parts], aside.side);
128
+ const { created } = aside.row ? addColumn(aside.row, aside.index, parts) : rowAround(folds.withHidden(aside.target), [parts], aside.side);
126
129
  leftovers.forEach((node) => node.remove());
127
130
  return { touched: [], focus: null, select: targets, reselect: true, created };
128
131
  });
@@ -134,30 +137,40 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule }
134
137
  */
135
138
  function moveBy(targets, delta, { keepFocus = false } = {}) {
136
139
  closeMenu();
137
- targets = ordered([targets].flat().filter(alive));
140
+ const picked = ordered([targets].flat().filter(alive));
141
+ targets = folds.takenWith(picked);
138
142
  if (targets.length === 0) return false;
139
143
  const reason = targets.map(fixedText).find(Boolean);
140
144
  if (reason) {
141
145
  say(reason);
142
146
  return false;
143
147
  }
144
- // The blocks to move among: not a box's lead (a highlight block's icon stays first in it).
148
+ const name = folds.namedWith(picked, targets, named);
149
+ // The blocks to move among: not a box's lead (a highlight block's icon stays first in it), as the
150
+ // reader sees them: a folded heading and what it hides one (fold/ groupsOf).
145
151
  const boxKind = boxKindOf(targets[0].parentElement);
146
152
  const list = unitsOf(targets[0].parentElement).filter((unit) => !isLead(boxKind, kindOf(unit)));
147
- const first = list.indexOf(targets[0]);
148
- if (!targets.every((target, index) => list[first + index] === target)) {
153
+ const groups = folds.groupsOf(list);
154
+ const first = groups.findIndex((group) => group[0] === targets[0]);
155
+ let last = first;
156
+ let count = first < 0 ? 0 : groups[first].length;
157
+ while (first >= 0 && count < targets.length && last + 1 < groups.length) count += groups[++last].length;
158
+ if (first < 0 || !groups.slice(first, last + 1).flat().every((unit, index) => targets[index] === unit) || count !== targets.length) {
149
159
  say(t("选中的块不相邻,不能一起上下移动;可以拖动其中一块的手柄,把它们一起移到别处。"));
150
160
  return false;
151
161
  }
152
- const neighbour = list[delta < 0 ? first - 1 : first + targets.length];
153
- if (!neighbour) {
154
- say(list.length === targets.length ? t("这里只有{0};可以拖动手柄移到别处。", targets.length === 1 ? t("这一个{0}", nameOf(targets[0])) : t("这几块")) : t("{0}已经在{1}。", named(targets), delta < 0 ? t("最前面") : t("最后面")));
162
+ // A folded heading alone goes a section of its level at a time (fold/ stepOver); others a block in sight at a time.
163
+ const alone = first === last && folds.isFolded(targets[0]);
164
+ const step = alone ? folds.stepOver(targets[0], delta, list) : null;
165
+ const over = alone ? step && [step.first, step.last] : groups[delta < 0 ? first - 1 : last + 1];
166
+ if (!over) {
167
+ say(groups.length === last - first + 1 ? t("这里只有{0};可以拖动手柄移到别处。", targets.length === 1 ? t("这一个{0}", nameOf(targets[0])) : t("这几块")) : t("{0}已经在{1}。", name, delta < 0 ? t("最前面") : t("最后面")));
155
168
  return false;
156
169
  }
157
- moveTo(targets, targets[0].parentElement, delta < 0 ? withLeads(neighbour)[0] : neighbour.nextSibling);
170
+ moveTo(targets, targets[0].parentElement, delta < 0 ? withLeads(over[0])[0] : over.at(-1).nextSibling);
158
171
  if (!keepFocus) selection.select(targets, { quiet: true });
159
172
  targets[delta < 0 ? 0 : targets.length - 1].scrollIntoView({ block: "nearest", inline: "nearest" });
160
- say(t("已移动{0} · Ctrl+Z 撤销", named(targets)));
173
+ say(t("已移动{0} · Ctrl+Z 撤销", name));
161
174
  return true;
162
175
  }
163
176
 
@@ -165,7 +178,8 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule }
165
178
  // the drag started (a block that cannot move says why).
166
179
  function start(target, x, y) {
167
180
  const picked = selection.picked;
168
- const targets = picked.length > 1 && picked.includes(target) ? [...picked] : [target];
181
+ const chosen = picked.length > 1 && picked.includes(target) ? [...picked] : [target];
182
+ const targets = folds.takenWith(chosen); // a folded heading with what it hides (fold/)
169
183
  const reason = targets.map(fixedText).find(Boolean);
170
184
  if (reason) {
171
185
  say(reason);
@@ -174,7 +188,7 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule }
174
188
  closeMenu();
175
189
  selection.select(targets, { quiet: true });
176
190
  const session = {
177
- targets, target, from: target.parentElement, x, y, drop: null, rest: null, opened: new Set(),
191
+ targets, target, from: target.parentElement, x, y, drop: null, rest: null, opened: new Set(), name: folds.namedWith(chosen, targets, named),
178
192
  line: element("div", { class: "ge-drop-line", hidden: "" }),
179
193
  tint: element("div", { class: "ge-drop-tint", hidden: "" }),
180
194
  preview: null,
@@ -184,12 +198,12 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule }
184
198
  // The line and the tint go in the layer of where the blocks would go
185
199
  // (overlay-layer.mjs), when there is one; the copy follows the pointer in the window.
186
200
  for (const kind of ["line", "tint"]) own(session[kind], `drag ${kind}`, { owner: target, alive });
187
- session.preview = createPreview(targets, x, y, { owner: target, alive });
201
+ session.preview = createPreview(targets.filter((node) => !folds.hidden(node)), x, y, { owner: target, alive }); // what is in sight
188
202
  drag = session;
189
203
  note("drag", `start ${targets.map(describe).join(", ")}`);
190
204
  targets.flatMap(withLeads).forEach((part) => part.setAttribute("data-ge-dragging", ""));
191
205
  document.documentElement.setAttribute("data-ge-drag", "");
192
- say(t("正在移动{0} · 拖到要放的位置后松开 · 停在选项卡上切换,停在折叠块标题上展开 · Esc 取消", named(targets)), "prompt");
206
+ say(t("正在移动{0} · 拖到要放的位置后松开 · 停在选项卡上切换,停在折叠块标题上展开 · Esc 取消", session.name), "prompt");
193
207
  update();
194
208
  schedule();
195
209
  return true;
@@ -219,8 +233,9 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule }
219
233
  : items ? accepts(box.kind, "item") || accepts(box.kind, "list")
220
234
  : steps ? accepts(box.kind, "step") || (accepts(box.kind, "flowchart") && box.kind !== "nest" && !box.el.closest("ol.doc-flow"))
221
235
  : [...kinds].every((kind) => accepts(box.kind, kind)));
222
- // Blocks a column takes may go beside a block, into a column (分栏).
223
- const aside = !tabs && [...kinds].every((kind) => accepts("column", kind)) && ((unit) => accepts("column", unit.kind));
236
+ // Blocks a column takes may go beside a block, into a column (分栏); beside a folded heading when a
237
+ // column takes what it hides too, which goes into its column with it (besideTo).
238
+ const aside = !tabs && [...kinds].every((kind) => accepts("column", kind)) && ((unit) => folds.withHidden(unit.el).every((node) => accepts("column", kindOf(node))));
224
239
  return resolveDrop(drag.tree, x + scrollX, y + scrollY, { takes, dragged: (unit) => targets.includes(unit.el), count: targets.length, items, aside, edge: scrollX });
225
240
  }
226
241
 
@@ -272,7 +287,7 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule }
272
287
  // Releasing (commit) or cancelling: the blocks go where the line showed, or stay.
273
288
  function end(commit) {
274
289
  if (!drag) return;
275
- const { targets, target, drop, line, tint, preview, opened } = drag;
290
+ const { targets, target, drop, line, tint, preview, opened, name } = drag;
276
291
  clearTimeout(drag.rest?.timer);
277
292
  drag = null;
278
293
  line.remove();
@@ -293,16 +308,17 @@ export function createDrag({ info, selection, struct, say, closeMenu, schedule }
293
308
  besideTo(targets, drop.aside);
294
309
  selection.select(targets, { quiet: true });
295
310
  target.scrollIntoView({ block: "nearest", inline: "nearest" });
296
- say(t("{0} · 拖动栏间的竖线调宽度 · Ctrl+Z 撤销", drop.aside.whole ? t("已移动这一栏") : drop.aside.row ? t("已把{0}放进新的一栏", named(targets)) : t("已把{0}并排分栏", named(targets))));
311
+ say(t("{0} · 拖动栏间的竖线调宽度 · Ctrl+Z 撤销", drop.aside.whole ? t("已移动这一栏") : drop.aside.row ? t("已把{0}放进新的一栏", name) : t("已把{0}并排分栏", name)));
297
312
  return;
298
313
  }
299
314
  const others = unitsOf(drop.container).filter((unit) => !targets.includes(unit) && extentOf(unit));
300
- // The place is never before a box's lead (a highlight block's icon): drop.mjs.
301
- const reference = drop.slot < others.length ? withLeads(others[drop.slot])[0] : others.length > 0 ? others.at(-1).nextSibling : null;
315
+ // The place is never before a box's lead (a highlight block's icon): drop.mjs. At the end, after
316
+ // what the last block in sight hides too (a folded heading's section: fold/), never between them.
317
+ const reference = drop.slot < others.length ? withLeads(others[drop.slot])[0] : others.length > 0 ? folds.lastOf(others.at(-1)).nextSibling : null;
302
318
  const emptied = moveTo(targets, drop.nest ?? drop.container, drop.nest ? null : reference);
303
319
  selection.select(targets, { quiet: true });
304
320
  target.scrollIntoView({ block: "nearest", inline: "nearest" });
305
- say(t("已移动{0}{1}{2} · Ctrl+Z 撤销", named(targets), drop.nest ? t("到上一项下面") : "", emptied ? t("(原来的外层已空,一并去掉)") : ""));
321
+ say(t("已移动{0}{1}{2} · Ctrl+Z 撤销", name, drop.nest ? t("到上一项下面") : "", emptied ? t("(原来的外层已空,一并去掉)") : ""));
306
322
  }
307
323
 
308
324
  // The window losing the focus, or the page going out of sight, ends a drag: cancelled.