@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
@@ -5,6 +5,7 @@ import { caretAt, editable, select } from "../caret.mjs";
5
5
  import { ordered } from "../block-info.mjs";
6
6
  import { firstHost, isHost, unitElement } from "../core.mjs";
7
7
  import { createFills } from "../fills.mjs";
8
+ import { reveal } from "../folded.mjs";
8
9
  import { FOLLOW, task } from "../frames.mjs";
9
10
  import { BAR } from "../handle-sizes.mjs";
10
11
  import { MARGIN, margin, onMargin, rightRoom, setMargin } from "../margin.mjs";
@@ -44,17 +45,17 @@ import { HOT, MARK, STYLE, barNode, cardNode, fitField, layerNode, markerNode }
44
45
  // mark (blocks, the fill selected blocks have, in a warm colour: fills.mjs): words have it where the
45
46
  // format bar stood (below their last line, by their last character), blocks at the last one's bottom
46
47
  // right (at the window's bottom where that is below it), always inside the column. It exists once
47
- // it is left or something is typed in it, words or none: one with no words is a plain mark of its place
48
- // (Esc on a new one that never held words takes it back); typing saves it (500ms after the last key, at
49
- // once when it closes); a press elsewhere, Esc, Enter, Tab or Ctrl+Alt+M close it into a marker
48
+ // something is typed in it (white space is nothing): a card left with none leaves nothing, and a saved
49
+ // one emptied goes as its card closes, offered back as its delete button's is (an annotation has words:
50
+ // the user's, 2026-11-06); typing saves it (500ms after the last key, at once when it closes; a text
51
+ // with no words never: the file keeps its last words while the card is open); a press elsewhere, Esc, Enter, Tab or Ctrl+Alt+M close it into a marker
50
52
  // in the column's right gutter, at its first line or the middle of its several blocks (right of a
51
53
  // section's strip for what is in that section; the place the button beside a block stands on:
52
54
  // markerSpot(), which says no where a marker stands already). Closed
53
55
  // by a key the caret goes back to its words, or to its blocks where the keys opened it; a card of
54
56
  // blocks the pointer opened places none. Resting on a marker (or clicking it) shows its card again
55
57
  // where it was written, by its words, which light up, and it stays while the pointer goes over to it.
56
- // The button at a card's right deletes it, and that alone does, or its words or blocks going: a card
57
- // emptied stays, with no words. One card shows at a time, and starting on the words of an annotation already there shows its
58
+ // The button at a card's right deletes it, and so does emptying it, or its words or blocks going. One card shows at a time, and starting on the words of an annotation already there shows its
58
59
  // card rather than a second one. The workbench's list asks for the page's annotations (rows()) and
59
60
  // goes to one (goTo()): what hides it opened, the page scrolled to it, its card shown as resting on its
60
61
  // marker shows it, until a press elsewhere or the words leave the window. Markers stack without
@@ -109,6 +110,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
109
110
  let reading = null; // the first read from the server, under way or done (taken back when it failed)
110
111
  let counted = false; // that read is in and what it brought found: the page can say how many it has
111
112
  let items = new Map(); // id -> { id, created, updated, text, anchor } in page order, as the server last said, with the page's own changes
113
+ let filed = new Map(); // id -> its text as the file last held it (the server's last answer): what 撤销 brings back of one emptied in its card
112
114
  let version = null; // the file's, as last read or written
113
115
  const lost = new Map(); // id -> an annotation a check deleted this visit (its words gone): back when an undo brings them back
114
116
  const timers = new Map(); // id -> the timer of its save (typed, not saved yet)
@@ -132,9 +134,9 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
132
134
  let remeasure = true; // all of them to measure again (what may move them: the page's size, its structure), else those in `stale`
133
135
  const stale = new Set(); // ids found again since they were measured
134
136
  // The annotation whose card shows, or stands in front: { id, focus, hover, keys, located, side, begun,
135
- // held, opening, within }: `keys`, opened by the keys, which give the caret back; `located`, shown by
137
+ // opening, within }: `keys`, opened by the keys, which give the caret back; `located`, shown by
136
138
  // the workbench's list; `side`, by its words, "below" or "above" them, kept while it is written;
137
- // `begun`, new, and `held`, having held words; `opening`, opened for writing and not laid out yet in
139
+ // `begun`, new; `opening`, opened for writing and not laid out yet in
138
140
  // the margin; `within`, what it is laid out by there instead of its anchor, which is out of the window.
139
141
  let shown = null;
140
142
  let hot = null; // the id whose words are under the pointer
@@ -198,6 +200,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
198
200
  heard = true;
199
201
  version = answer.version;
200
202
  const dropped = new Set(answer.dropped ?? []);
203
+ filed = new Map((answer.items ?? []).map((item) => [item.id, item.text]));
201
204
  const next = new Map();
202
205
  for (const item of answer.items ?? []) {
203
206
  const mine = items.get(item.id);
@@ -298,12 +301,13 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
298
301
  return answer;
299
302
  }
300
303
 
301
- // Save it now: made (its first save) or changed, with words or with none (a plain mark of its place).
304
+ // Save it now: made (its first save) or changed; never with no words (left so, it goes: close).
302
305
  async function save(id) {
303
306
  clearTimeout(timers.get(id));
304
307
  timers.delete(id);
305
308
  const item = items.get(id);
306
309
  if (!item) return;
310
+ if (!item.text.trim()) return; // no words: nothing to save, the file keeps its last words
307
311
  if (!item.anchor) {
308
312
  if (!made.has(id)) return;
309
313
  item.anchor = anchorOf(id);
@@ -366,14 +370,16 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
366
370
 
367
371
  /** Save what is typed and not saved yet, now (Ctrl+S, the workbench's save, the page closing: `keepalive`). */
368
372
  function flush({ keepalive = false } = {}) {
369
- const ids = [...new Set([...timers.keys(), ...[...made.keys()].filter((id) => !sending.has(id))])]; // one begun and not saved yet counts, typed in or not
373
+ // What was typed, and one begun and not saved yet; one with no words saves nothing (save), and one
374
+ // the file has keeps its last words (the page going, no 撤销 could be offered for its going).
375
+ const ids = [...new Set([...timers.keys(), ...[...made.keys()].filter((id) => !sending.has(id))])];
370
376
  if (keepalive) {
371
377
  sync({ keepalive: true });
372
378
  for (const id of ids) {
373
379
  clearTimeout(timers.get(id));
374
380
  timers.delete(id);
375
381
  const item = items.get(id);
376
- if (!item) continue;
382
+ if (!item?.text.trim()) continue;
377
383
  if (!item.anchor && made.has(id)) item.anchor = anchorOf(id);
378
384
  postJson(ROUTE, token, { slug, put: wire(item) }, { keepalive: true }).catch(() => {});
379
385
  }
@@ -460,9 +466,9 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
460
466
  // A card's field has the focus (a card being written); `shown.focus` follows it.
461
467
  const focused = () => Boolean(document.activeElement?.closest?.(".ge-note-card"));
462
468
  const writing = () => Boolean(shown) && focused();
463
- // A new one that has held no words yet (`begun`, not `held`): no annotation until it is left, Esc
464
- // taking it back.
465
- const drafted = (id) => shown?.id === id && shown.begun && !shown.held;
469
+ // A new one with no words (made on the page, not saved yet, nothing typed but white space): no
470
+ // annotation yet: no marker, no count, no row; left so, it leaves nothing (close).
471
+ const drafted = (id) => made.has(id) && !(items.get(id)?.text ?? "").trim();
466
472
 
467
473
  // The annotation already on `selection` (the same words or blocks), or undefined.
468
474
  function sameAs(selection) {
@@ -533,7 +539,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
533
539
  if (closing) {
534
540
  event.preventDefault();
535
541
  event.stopPropagation();
536
- backToText({ drop: event.key === "Escape" && drafted(id) });
542
+ backToText();
537
543
  }
538
544
  });
539
545
  // A press in a card edits it, the caret where pressed (at the end of its text for a press beside the
@@ -604,7 +610,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
604
610
  if (!focus) return true;
605
611
  card.field.focus({ preventScroll: true });
606
612
  if (document.activeElement !== card.field) {
607
- if (made.has(id) && !item.text) {
613
+ if (drafted(id)) {
608
614
  items.delete(id);
609
615
  made.delete(id);
610
616
  dropView(id);
@@ -627,6 +633,15 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
627
633
  wake();
628
634
  }
629
635
 
636
+ // What 撤销 puts back of one going at the reader's word (its delete button, emptied and closed): as
637
+ // it is, or, emptied in its card, with the words the file last held; null when the file never had it.
638
+ function backOf(id) {
639
+ const item = items.get(id);
640
+ if (!item?.anchor) return null;
641
+ const text = item.text.trim() ? item.text : filed.get(id);
642
+ return text ? { ...wire(item), text } : null;
643
+ }
644
+
630
645
  // An annotation gone at the reader's word, from the page and the file (one never saved leaves nothing).
631
646
  function forget(id) {
632
647
  const item = items.get(id);
@@ -640,26 +655,30 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
640
655
  dropView(id);
641
656
  }
642
657
 
643
- // The card closes into its marker: what was typed saved at once, one with no words saved as it is
644
- // (a plain mark of its place). `drop`: it goes instead (its delete button, Esc on a new one). With
645
- // room it goes back among the others.
658
+ // The card closes into its marker: what was typed saved at once. One left with no words is none: it
659
+ // goes, from the page and the file, and one the file had is offered back as its delete button's is
660
+ // (`removed`, with its last words). `drop`: it goes whatever it says (its delete button, which offers
661
+ // it back itself). With room it goes back among the others.
646
662
  function close({ drop = false } = {}) {
647
663
  if (!shown) return;
648
664
  const { id } = shown;
649
665
  shown = null;
650
666
  const item = items.get(id);
651
- if (drop) forget(id);
652
- else if (item && (timers.has(id) || !item.anchor)) save(id);
667
+ if (item && (drop || !item.text.trim())) {
668
+ const back = drop ? null : backOf(id);
669
+ forget(id);
670
+ if (back) removed([back]);
671
+ } else if (item && (timers.has(id) || !item.anchor)) save(id);
653
672
  changed();
654
673
  wake();
655
674
  }
656
675
 
657
- // A card's delete button: its annotation goes at once, and the workbench offers it back. One never
658
- // saved leaves nothing to bring back.
676
+ // A card's delete button: its annotation goes at once, and the workbench offers it back (emptied in
677
+ // its card, with the words the file last held). One never saved leaves nothing to bring back.
659
678
  function erase(id) {
660
679
  const item = items.get(id);
661
680
  if (!item) return;
662
- const was = item.anchor ? wire(item) : null;
681
+ const was = backOf(id);
663
682
  if (shown?.id !== id) {
664
683
  forget(id);
665
684
  changed();
@@ -699,7 +718,6 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
699
718
  const was = drafted(id);
700
719
  item.text = field.value;
701
720
  item.updated = new Date().toISOString();
702
- if (item.text.trim() && shown?.id === id) shown.held = true;
703
721
  schedule(id);
704
722
  changed();
705
723
  if (!standing || was !== drafted(id)) wake();
@@ -824,7 +842,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
824
842
  const view = views.get(id);
825
843
  if (!view || drafted(id)) return false;
826
844
  const start = view.range?.startContainer ?? view.units[0];
827
- globalThis.DocKit.reveal(start.nodeType === 1 ? start : start.parentElement);
845
+ reveal(start.nodeType === 1 ? start : start.parentElement); // a folded heading's section too (fold/)
828
846
  remeasure = true; // what was hidden stands now, before the observer says so
829
847
  const line = lineOf(view);
830
848
  if (!line) return false;
@@ -896,7 +914,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
896
914
  const view = known ?? {};
897
915
  if (!found || (found.units?.length ?? 0) === 0) {
898
916
  // A new one with no words, not saved yet, is about nothing now: it goes with them, for no save to keep.
899
- if (made.has(id) && !item.anchor && !item.text) {
917
+ if (drafted(id) && !item.anchor) {
900
918
  forget(id);
901
919
  if (shown?.id === id) shown = null;
902
920
  continue;
@@ -946,16 +964,25 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
946
964
  }
947
965
 
948
966
  // The outermost closed disclosure `node` is hidden in (in its content, not its title, nor `node`
949
- // itself: it shows, as its title), or null.
967
+ // itself: it shows, as its title), or the folded heading whose section hides it (fold/: the
968
+ // outermost, the heading its own title), whichever holds the other; null where neither hides it.
950
969
  function closedAround(node) {
951
970
  const element = node?.nodeType === 1 ? node : node?.parentElement;
952
971
  let found = null;
953
972
  for (let details = element?.closest("details:not([open])"); details; details = details.parentElement?.closest("details:not([open])")) {
954
973
  if (details !== node && !details.querySelector(":scope > summary")?.contains(element)) found = details;
955
974
  }
956
- return found;
975
+ let marked = null;
976
+ for (let at = element?.closest("[data-ge-folded]"); at; at = at.parentElement?.closest("[data-ge-folded]")) marked = at;
977
+ const heading = marked && document.querySelector(`[data-ge-id="${CSS.escape(marked.getAttribute("data-ge-folded"))}"]`);
978
+ if (!heading) return found;
979
+ return found && found.contains(heading) ? found : heading;
957
980
  }
958
981
 
982
+ // What stands for a closed disclosure or a folded section (closedAround): the disclosure's title, or
983
+ // the heading itself, which has none.
984
+ const titleOf = (closed) => closed.querySelector(":scope > summary") ?? closed;
985
+
959
986
  // The first line of `node` (window coordinates): { top, height, left }: of its first words; where a
960
987
  // picture standing as a block comes before them, the picture's top (as the handle's first line,
961
988
  // handle/: not its caption, far from there); its box's top where it holds neither.
@@ -987,13 +1014,13 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
987
1014
  // holds), box (what it stands for as shown) }, or null when it does not show: its first line, or all
988
1015
  // of them for several blocks, from the first's top to the last's bottom, the marker at their middle.
989
1016
  // In a closed disclosure (laid out though hidden), its title's line: checked first, as the hidden
990
- // text still has boxes.
1017
+ // text still has boxes; in a folded heading's section (fold/), the heading's.
991
1018
  function lineOf(view) {
992
1019
  const start = view.range?.startContainer ?? view.units?.find((unit) => unit.isConnected);
993
1020
  if (!start?.isConnected) return null;
994
1021
  const closed = closedAround(start);
995
1022
  if (closed) {
996
- const summary = closed.querySelector(":scope > summary");
1023
+ const summary = titleOf(closed);
997
1024
  if (!summary?.getClientRects().length) return null;
998
1025
  const box = summary.getBoundingClientRect();
999
1026
  return { ...firstLine(summary), folded: true, box, bottom: box.bottom };
@@ -1074,13 +1101,13 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
1074
1101
  // What a card stands by (window coordinates), or null when it does not show: words, their box and
1075
1102
  // where their last line ends ({ words, top, left, right: the last character's, bottom: the last
1076
1103
  // line's }); blocks, theirs ({ top, bottom and right: the last one's }); in a closed disclosure (laid
1077
- // out though hidden), its title as a block.
1104
+ // out though hidden), its title as a block; in a folded heading's section, the heading.
1078
1105
  function anchorBox(target) {
1079
1106
  const start = target.range?.startContainer ?? target.units?.find((unit) => unit.isConnected);
1080
1107
  if (!start?.isConnected) return null;
1081
1108
  const closed = closedAround(start);
1082
1109
  if (closed) {
1083
- const summary = closed.querySelector(":scope > summary");
1110
+ const summary = titleOf(closed);
1084
1111
  if (!summary?.getClientRects().length) return null;
1085
1112
  const box = summary.getBoundingClientRect();
1086
1113
  return { top: box.top, bottom: box.bottom, right: box.right };
@@ -1379,7 +1406,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
1379
1406
  quiet = false;
1380
1407
 
1381
1408
  // A block anchor's blocks filled while it is shown, or its card in the margin is under the pointer;
1382
- // those in a closed disclosure, the disclosure.
1409
+ // those in a closed disclosure, the disclosure; in a folded section, its heading.
1383
1410
  const filled = views.get(shown?.id ?? (standing ? hot : null));
1384
1411
  fills.paint(filled && !filled.range && !filled.hidden ? ordered(filled.units.filter((unit) => unit.isConnected).map((unit) => closedAround(unit) ?? unit)) : []);
1385
1412
  paintMarks();
@@ -1562,7 +1589,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
1562
1589
  },
1563
1590
  };
1564
1591
 
1565
- /** How many annotations the page has (their words or blocks still there; a new one once left, or once it has words). */
1592
+ /** How many annotations the page has (their words or blocks still there; a new one once it has words). */
1566
1593
  const count = () => [...items.keys()].filter((id) => views.has(id) && !drafted(id)).length;
1567
1594
 
1568
1595
  provide("annotations", () => ({
@@ -42,6 +42,10 @@ const QUIET = 1500; // how long the reader has changed nothing before the page r
42
42
  const STORE_EVERY = 1000; // the crash buffer while typing, at most this often
43
43
  const JOURNAL_FOR = 60 * 60 * 1000; // how long a committed text is checked for being put back
44
44
  const OWN_FOR = 5000; // how long the live-reload channel may still announce a version this page wrote
45
+ // A page of the Agent's own (its address's ?agent: the Agent 自用 one gallery open prints, gallery
46
+ // component try's): its commits say so (`agent`), and are no reader's edit to the review of the
47
+ // Agent's changes, so none ends a document's first writing (docs/editor.md "Agent's changes").
48
+ const AGENT_PAGE = typeof location !== "undefined" && new URLSearchParams(location.search).has("agent");
45
49
 
46
50
  // A committed text as the revert check names it, whichever load named it: the report's title, a block of
47
51
  // a unit by the unit's id (or what named the unit, with none) and its place in it, any other by what
@@ -347,6 +351,8 @@ export function createAutosave({ map, commits, blocked, changedAt, holding = ()
347
351
  */
348
352
  function heard(version, anyway = false, at = Date.now()) {
349
353
  if (closed) return;
354
+ // A write the reader asked for is on its way (showOwn): what is heard meanwhile is its own, or heard again once it failed.
355
+ if (owning) return void (missed = true);
350
356
  // It may be the commit on its way, or one whose answer was lost: known once that one is answered
351
357
  // (as of when it was announced).
352
358
  if ((flight || parked) && !anyway) return void events.push([version, at]);
@@ -396,10 +402,13 @@ export function createAutosave({ map, commits, blocked, changedAt, holding = ()
396
402
  } catch {}
397
403
  checking = false;
398
404
  if (closed) return false;
405
+ // Of the blocks it made that the write took out, those inside another such go with it, one card.
406
+ const goneUnits = (answer?.units ?? []).filter(({ gone }) => gone).map(({ key }) => key);
399
407
  const back = answer?.ok ? [
400
408
  ...answer.reverted.map(({ key, theirs }) => ({ ...texts.find((entry) => entry.key === key), theirs })).filter((entry) => entry.key),
401
- // A block put back: by its unit, with what it holds (the reader's words, to copy).
402
- ...(answer.units ?? []).filter(({ key }) => units.some((unit) => unit.key === key)).map(({ key, gone }) => ({ key: `p${key}`, unit: key, gone: Boolean(gone), mine: commits.unitText(key) })),
409
+ // A block put back: by its unit, with what it holds (the reader's words, to copy) and, taken out, the
410
+ // blocks taken out inside it (`within`, decided with it; what it holds that the file still has is not).
411
+ ...(answer.units ?? []).filter(({ key, gone }) => units.some((unit) => unit.key === key) && !(gone && commits.heldWithin?.(key, goneUnits))).map(({ key, gone }) => ({ key: `p${key}`, unit: key, gone: Boolean(gone), mine: commits.unitText(key), ...(gone ? { within: goneUnits.filter((inner) => commits.heldWithin?.(inner, [key])) } : {}) })),
403
412
  // Parts a component wrote put back: by its unit, with the hashes the file holds of them (`written`)
404
413
  // and what the page holds of them (`data`, the reader's, kept with the crash buffer); none the page
405
414
  // holds as the file does, however the file spells it (heldOtherwise).
@@ -499,7 +508,7 @@ export function createAutosave({ map, commits, blocked, changedAt, holding = ()
499
508
  // A block put back: kept (made again, or moved again where the page has it), or the file's. Parts a
500
509
  // component wrote: written again over the file's (its hashes of them), or the file's.
501
510
  if (entry.written) commits.rewrite(entry.unit, entry.written, mine);
502
- else commits.putBack(entry.unit, { gone: entry.gone, mine });
511
+ else commits.putBack(entry.unit, { gone: entry.gone, mine, within: entry.within });
503
512
  reverts.delete(key);
504
513
  storedFrom = null;
505
514
  if (mine) changed(true);
@@ -523,6 +532,42 @@ export function createAutosave({ map, commits, blocked, changedAt, holding = ()
523
532
  return true;
524
533
  }
525
534
 
535
+ /**
536
+ * A write the reader asked for that changes the file (撤回 of the Agent's changes and its 撤销:
537
+ * changes/): `run()` makes it, resolving with whether it did; then the page shows the file as after a
538
+ * write of its own, at once, asking nothing of what it put back (the revert check: the reader asked
539
+ * for it), and what is heard meanwhile shows nothing first. The caller saved what waited first
540
+ * (flush) and made sure nothing waits for the reader. Resolves with whether the page goes.
541
+ */
542
+ let owning = false; // showOwn's write is on its way
543
+ let missed = false; // and the live-reload channel spoke meanwhile
544
+ async function showOwn(run) {
545
+ if (closed || reloading) return false;
546
+ reloading = owning = true;
547
+ missed = false;
548
+ clearTimeout(reloadTimer);
549
+ let done = false;
550
+ try {
551
+ done = Boolean(await run());
552
+ } catch {}
553
+ owning = false;
554
+ if (closed) return done;
555
+ if (!done) {
556
+ reloading = false;
557
+ refresh();
558
+ if (missed) heard(null, true);
559
+ else if (foreign) maybeReload();
560
+ return false;
561
+ }
562
+ storage.remove(bufferKey());
563
+ storage.remove(keptKey());
564
+ storedFrom = null;
565
+ keepJournal();
566
+ refresh();
567
+ reloadView(map.slug, null, null);
568
+ return true;
569
+ }
570
+
526
571
  /** Show the file as it is once nothing waits (the reader gave a change up for the file's, or what the page is made of changed). */
527
572
  function reloadSoon() {
528
573
  foreign = true;
@@ -556,6 +601,7 @@ export function createAutosave({ map, commits, blocked, changedAt, holding = ()
556
601
  store,
557
602
  decide,
558
603
  reloadSoon,
604
+ showOwn,
559
605
  close,
560
606
  get state() {
561
607
  return { waiting: waiting(), syncing: Boolean(flight), offline, stopped, foreign, reloading, held: commits.issues.size + reverts.size, drafts, unstored };
@@ -566,7 +612,7 @@ export function createAutosave({ map, commits, blocked, changedAt, holding = ()
566
612
  // A commit sent: the server's answer, or { ok: false, reason: "server" } when it failed or answered
567
613
  // no JSON (a lost connection rejects: the caller reads that as offline).
568
614
  async function postCommit(map, request, { keepalive = false } = {}) {
569
- const response = await postJson("/__gallery/edit/commit", map.token, { slug: map.slug, ...request }, { keepalive });
615
+ const response = await postJson("/__gallery/edit/commit", map.token, { slug: map.slug, ...request, ...(AGENT_PAGE ? { agent: true } : {}) }, { keepalive });
570
616
  if (response.status >= 500) return { ok: false, reason: "server" };
571
617
  return response.json().catch(() => ({ ok: false, reason: "server" }));
572
618
  }
@@ -6,16 +6,16 @@ import { isHost } from "./core.mjs";
6
6
  // (selection/).
7
7
 
8
8
  /**
9
- * Whether a click to the right of a block (outside it) selects it: a boxed block (a code block, a
10
- * highlight block, a disclosure, a card, a table, a divider…) or a row of columns, as opposed to text
11
- * and the boxes text flows in (a list, a quote, a section).
9
+ * Whether a click to the right of a block, past the text area it stands in (insert/ boxedBeside),
10
+ * selects it: a boxed block (a code block, a highlight block, a disclosure, a card, a table, a
11
+ * divider…) or a row of columns, as opposed to text and the boxes text flows in (a list, a quote, a
12
+ * section).
12
13
  */
13
14
  export const selectsBeside = (unit) => BESIDE.has(unit?.getAttribute?.("data-ge-unit-kind"));
14
15
 
15
16
  /**
16
17
  * Whether Enter on a block selected (the first of those selected) puts a new paragraph after them
17
18
  * (blocks.mjs, insert.paragraphAfter): a block of no text of its own, those a click to their right
18
- * selects and a 选项卡区块; not a text block among them (a callout, whose text Enter edits, a formula
19
- * block, whose editor it opens).
19
+ * selects and a 选项卡区块; not a text block among them (a formula block, whose editor it opens).
20
20
  */
21
21
  export const entersAfter = (unit) => Boolean(unit) && !isHost(unit) && ENTERS_AFTER.has(unit.getAttribute("data-ge-unit-kind"));
@@ -12,7 +12,7 @@ import { t } from "../../../shared/i18n/index.mjs";
12
12
  // show one of the nine with none of a family (a callout's background is blue: `defaults`), that
13
13
  // family has no 默认: that color is ringed for none, and picking it takes the class away. Each
14
14
  // swatch names its color in a tooltip. The swatches are drawn with the kit's own classes, so they
15
- // match the text in either theme. A pick gives every target that color of its family, the other family's staying (one
15
+ // match the text in either theme, a background's as the block's fill (`--doc-fill`). A pick gives every target that color of its family, the other family's staying (one
16
16
  // undo step: look, history.mjs; a table cell's one color: table/). Only the families the targets take show.
17
17
 
18
18
  const STYLE = `
@@ -21,6 +21,8 @@ const STYLE = `
21
21
  .ge-pop .ge-color-cell > span { display: flex; align-items: center; justify-content: center; box-sizing: border-box; width: 26px; height: 26px; border-radius: 6px;
22
22
  font-size: 16px; font-weight: 500; line-height: 1; box-shadow: inset 0 0 0 1px var(--ge-ring); }
23
23
  .ge-pop .ge-color-cell > span:not([class*="doc-text-"]) { color: var(--ge-tx); }
24
+ /* A block's background swatch shows the block's fill, not the words' deeper one the kit gives a span. */
25
+ .ge-pop .ge-color-cell > span[class*="doc-highlight-"] { background: var(--doc-fill); }
24
26
  .ge-pop .ge-color-cell:hover > span, .ge-pop .ge-color-cell[data-active] > span { box-shadow: inset 0 0 0 2px var(--ge-ring); }
25
27
  .ge-pop .ge-color-cell[aria-current="true"] > span { box-shadow: inset 0 0 0 2px var(--ge-ring-strong); }
26
28
  ${Object.entries(RINGS).map(([name, [light, dark, strong, strongDark]]) => `.ge-pop .ge-color-cell[data-ring="${name}"] { --ge-ring: light-dark(${light}, ${dark}); --ge-ring-strong: light-dark(${strong}, ${strongDark}); }`).join("\n ")}
@@ -54,7 +54,7 @@ const WIDTH = MENU_WIDTH; // the menu, as wide as its grid (no room left beside
54
54
  * color rows). copyLink(target): the block's address on the clipboard, in the click on its row; null
55
55
  * on a page no address leads to; linkAhead(): the menu opens with that row, what the click will want is read now.
56
56
  */
57
- export function createBlockMenu({ info, menu, selection, remove, turnInto, convert, turnIntoAll = () => null, convertAll = () => {}, below, belowTypes = () => [], add, look, copyLink = () => {}, linkAhead = () => {} }) {
57
+ export function createBlockMenu({ info, menu, selection, remove, takenWith = (units) => units, turnInto, convert, turnIntoAll = () => null, convertAll = () => {}, below, belowTypes = () => [], add, look, copyLink = () => {}, linkAhead = () => {} }) {
58
58
  const chapters = sectionOps();
59
59
  const columns = columnOps();
60
60
  const { kindOf, nameOf, keepText, boxKindOf } = info;
@@ -95,12 +95,13 @@ export function createBlockMenu({ info, menu, selection, remove, turnInto, conve
95
95
  // where it is. What it holds comes out as it stands, so a block in a column keeps its column, the row
96
96
  // of columns coming out whole, and one in a section in a Sheet panel ends in the panel. None for a
97
97
  // block that has no 分区与分栏 › of its own (no columns row, in a section: a list item, a flowchart's
98
- // node, a quote's later paragraph, a block an item holds).
98
+ // node, a quote's later paragraph, a block an item holds): asked of the block alone (a folded heading
99
+ // whose section a column cannot take keeps it).
99
100
  const dissolveRow = (units) => {
100
101
  const section = chapters?.dissolvesFrom(units[0]);
101
102
  if (!section || units.some((unit) => chapters.dissolvesFrom(unit) !== section)) return [];
102
103
  const own = units[0] === section;
103
- if (!own && units.some((unit) => (columns?.columnsCheck([unit]) ?? null) === null)) return [];
104
+ if (!own && units.some((unit) => (columns?.columnsCheck([unit], (alone) => [alone]) ?? null) === null)) return [];
104
105
  const reason = chapters.unwrapCheck(section);
105
106
  return [{ label: own ? t("解散分区") : t("解散所在分区"), icon: ICONS.dissolve, disabled: Boolean(reason), note: reason || undefined, run: () => chapters.unwrap(section, own ? null : units) }];
106
107
  };
@@ -149,8 +150,9 @@ export function createBlockMenu({ info, menu, selection, remove, turnInto, conve
149
150
  const colors = colorRow(group);
150
151
  items.push(...colors, ...(colors.length > 0 ? [{ separator: true }] : []));
151
152
  // Blocks next to each other in the page or a Sheet panel become a section; a selection with gaps does not.
152
- const wrap = chapters?.wrapCheck(group) ?? null;
153
- const sections = [...(wrap !== null ? [{ label: t("组成新分区"), icon: ICONS.group, disabled: Boolean(wrap), note: wrap || undefined, run: () => chapters.wrap(group) }] : []), ...dissolveRow(group)];
153
+ // A folded heading among them goes in with what it hides (fold/).
154
+ const wrap = chapters?.wrapCheck(takenWith(group)) ?? null;
155
+ const sections = [...(wrap !== null ? [{ label: t("组成新分区"), icon: ICONS.group, disabled: Boolean(wrap), note: wrap || undefined, run: () => chapters.wrap(takenWith(group)) }] : []), ...dissolveRow(group)];
154
156
  // Neighbours in one container go side by side, a column each.
155
157
  items.push(...groupRow(sections, splitRows(group)));
156
158
  items.push({ label: t("删除"), icon: ICONS.trash, hint: "Del", disabled: Boolean(refused), note, run: () => remove(group) });
@@ -193,7 +195,7 @@ export function createBlockMenu({ info, menu, selection, remove, turnInto, conve
193
195
  }
194
196
  const sections = [];
195
197
  if (chapter(target)) sections.push({ label: t("在下方新建分区"), icon: ICONS.section, run: () => add(target, "section") });
196
- if (chapters?.wrapCheck([target]) === "") sections.push({ label: t("组成新分区"), icon: ICONS.group, run: () => chapters.wrap([target]) });
198
+ if (chapters?.wrapCheck(takenWith([target])) === "") sections.push({ label: t("组成新分区"), icon: ICONS.group, run: () => chapters.wrap(takenWith([target])) });
197
199
  sections.push(...dissolveRow([target]));
198
200
  const splits = splitRows([target]);
199
201
  // 选中外层 joins 分区与分栏 › where the block has that submenu; a block with none gets none for it.
@@ -1,6 +1,7 @@
1
1
  import { createBlockInfo } from "./block-info.mjs";
2
2
  import { createBlockMenu } from "./block-menu/index.mjs";
3
3
  import { createDrag } from "./drag/index.mjs";
4
+ import { createFolds } from "./fold/index.mjs";
4
5
  import { createHandle } from "./handle/index.mjs";
5
6
  import { createMenu } from "./menu.mjs";
6
7
  import { createDelete } from "./selection/delete.mjs";
@@ -19,6 +20,7 @@ import { pageObjects } from "./page-objects.mjs";
19
20
  // block-menu/ the menu a handle opens: turning a block into another type, layout, a new block below, columns, sections, deleting
20
21
  // selection/ the selected blocks and their fills; box selection (marquee.mjs); deleting (delete.mjs)
21
22
  // drag/ moving blocks: dragging a handle, where they would go (drop.mjs), Ctrl+Shift+arrows
23
+ // fold/ a heading's section folded: the triangle right of its words, what is hidden, the section going with its heading
22
24
  //
23
25
  // What they share: what the page knows of a block (block-info.mjs), the menu card (menu.mjs), one
24
26
  // task of the editor's frames (frames.mjs) that keeps the handle, the bar and the fills where their
@@ -49,8 +51,9 @@ import { pageObjects } from "./page-objects.mjs";
49
51
  * notes.write(units) the annotation begun.
50
52
  * copyLink(unit): 复制块链接, the block menu's (the editor's copyBlockLink), or null for no such row.
51
53
  * linkAhead(): a block's menu opens with that row in it: what the click will want is read now.
54
+ * slug: the report's (what is folded is kept by report: fold/).
52
55
  */
53
- export function createBlocks({ units, boxes, active, struct, look, say, edit, turnInto, convert, turnIntoAll = () => null, convertAll = () => null, below, belowTypes = () => [], add, after = () => false, blank, nest, keepsHandle = () => false, addNear = () => {}, copyLink = () => {}, linkAhead = () => {}, notes }) {
56
+ export function createBlocks({ slug = "", units, boxes, active, struct, look, say, edit, turnInto, convert, turnIntoAll = () => null, convertAll = () => null, below, belowTypes = () => [], add, after = () => false, blank, nest, keepsHandle = () => false, addNear = () => {}, copyLink = () => {}, linkAhead = () => {}, notes }) {
54
57
  const info = createBlockInfo({ units, boxes });
55
58
  const menu = createMenu();
56
59
  const closeMenu = () => menu.close();
@@ -59,10 +62,11 @@ export function createBlocks({ units, boxes, active, struct, look, say, edit, tu
59
62
  const schedule = () => frames.wake();
60
63
 
61
64
  const selection = createSelection({ info, active, say, closeMenu, schedule });
62
- const drag = createDrag({ info, selection, struct, say, closeMenu, schedule });
63
- const blockMenu = createBlockMenu({ info, menu, selection, remove: (targets) => remove(targets), turnInto, convert, turnIntoAll, convertAll, below, belowTypes, add, look, copyLink, linkAhead });
64
- const handle = createHandle({ info, active, selection, drag, menu, openMenu: blockMenu.open, drawing: () => marquee.drawing, schedule, keeps: keepsHandle, add: (target, where) => { selection.clear(); addNear(target, where); }, notes });
65
- const remove = createDelete({ info, selection, struct, say, closeMenu, forget: () => handle.forget() });
65
+ const folds = createFolds({ slug, selection, active, schedule });
66
+ const drag = createDrag({ info, selection, struct, say, closeMenu, schedule, folds });
67
+ const blockMenu = createBlockMenu({ info, menu, selection, remove: (targets) => remove(targets), takenWith: folds.takenWith, turnInto, convert, turnIntoAll, convertAll, below, belowTypes, add, look, copyLink, linkAhead });
68
+ const handle = createHandle({ info, active, selection, drag, menu, openMenu: blockMenu.open, drawing: () => marquee.drawing, schedule, keeps: (node) => keepsHandle(node) || folds.holds(node), aside: folds.holds, add: (target, where) => { selection.clear(); addNear(target, where); }, notes });
69
+ const remove = createDelete({ info, selection, struct, say, closeMenu, forget: () => handle.forget(), folds });
66
70
  // What a component makes with these tools (`blockTools`): a Sheet's tabs as Notion's pills, a click, a drag, their menu, + (components/sheets/pills.mjs).
67
71
  const parts = pageObjects().flatMap((page) => page.blockTools?.({ active, drag, menu, openMenu: blockMenu.open, struct, say }) ?? []);
68
72
  // A press on empty space: a box selection, or a click there (marquee.mjs clears the selection).
@@ -86,8 +90,9 @@ export function createBlocks({ units, boxes, active, struct, look, say, edit, tu
86
90
  step("drag", () => drag.step(), () => drag.end(false));
87
91
  step("selection", () => selection.paint(drag.active), () => selection.unpaint());
88
92
  step("handle", () => handle.place(), () => handle.hide());
93
+ step("fold", () => folds.place(handle.pointed), () => folds.hide());
89
94
  if (drag.active) return true;
90
- return handle.showing || selection.picked.length > 0 ? FOLLOW : false;
95
+ return handle.showing || folds.showing || selection.picked.length > 0 ? FOLLOW : false;
91
96
  }
92
97
 
93
98
  // One tool's part of a frame; when it throws, what that tool draws goes, and it is said.
@@ -105,7 +110,7 @@ export function createBlocks({ units, boxes, active, struct, look, say, edit, tu
105
110
  // A press anywhere but on the block tools closes the menu; on a divider line it selects the line,
106
111
  // on empty space it may start a box selection, elsewhere it clears the selection.
107
112
  document.addEventListener("pointerdown", (event) => {
108
- if (handle.holds(event.target) || menu.contains(event.target) || parts.some((part) => part.holds(event.target))) return;
113
+ if (handle.holds(event.target) || folds.holds(event.target) || menu.contains(event.target) || parts.some((part) => part.holds(event.target))) return;
109
114
  closeMenu();
110
115
  const line = event.button === 0 && active() ? handle.lineAt(event.target, event.clientX, event.clientY) : null;
111
116
  if (line) {
@@ -199,6 +204,7 @@ export function createBlocks({ units, boxes, active, struct, look, say, edit, tu
199
204
  drag.end(false);
200
205
  marquee.cancel();
201
206
  handle.hide();
207
+ folds.hide();
202
208
  selection.unpaint();
203
209
  }
204
210
 
@@ -206,6 +212,7 @@ export function createBlocks({ units, boxes, active, struct, look, say, edit, tu
206
212
  if (drag.active) drag.end(false);
207
213
  marquee.cancel();
208
214
  handle.hide();
215
+ folds.hide();
209
216
  parts.forEach((part) => part.hide());
210
217
  selection.hide();
211
218
  closeMenu();
@@ -225,6 +232,8 @@ export function createBlocks({ units, boxes, active, struct, look, say, edit, tu
225
232
  mark: selection.mark,
226
233
  keepText: info.keepText,
227
234
  moveBy: drag.moveBy,
235
+ /** Headings' sections folded (fold/). */
236
+ folds,
228
237
  remove,
229
238
  closeMenu,
230
239
  /** A block selected and its menu open by `rect` (a component's own button: an image's 更多). */
@@ -507,7 +507,10 @@ export function reveal(host, line, margin = 24) {
507
507
  const style = getComputedStyle(scroller ?? document.documentElement);
508
508
  const above = kept(style.scrollPaddingTop, margin);
509
509
  const below = kept(style.scrollPaddingBottom, margin);
510
- const delta = line.bottom > view.bottom - below ? line.bottom - view.bottom + below : line.top < view.top + above ? line.top - view.top - above : 0;
510
+ const exact = line.bottom > view.bottom - below ? line.bottom - view.bottom + below : line.top < view.top + above ? line.top - view.top - above : 0;
511
+ // Whole pixels, away from the line: a scroll by a fraction is rounded by the browser, and rounded
512
+ // down it left a line at the window's bottom a fraction of a pixel past it.
513
+ const delta = exact > 0 ? Math.ceil(exact) : Math.floor(exact);
511
514
  if (!delta) return;
512
515
  if (scroller) scroller.scrollTop += delta;
513
516
  else scrollBy(0, delta);