@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,6 +2,7 @@ import { unitElement, unitsOf, withLeads } from "../core.mjs";
2
2
  import { alive, ordered } from "../block-info.mjs";
3
3
  import { entersAfter } from "../beside.mjs";
4
4
  import { createFills } from "../fills.mjs";
5
+ import { FOLDED } from "../folded.mjs";
5
6
  import { describe, note, report } from "../overlays.mjs";
6
7
  import { t } from "../../../shared/i18n/index.mjs";
7
8
 
@@ -20,6 +21,12 @@ import { t } from "../../../shared/i18n/index.mjs";
20
21
  // page, made to match the selection the moment it changes (render), never on a later frame, so no
21
22
  // fill outlives its block's selection, and none is drawn while the block tools are off. The frames
22
23
  // only keep them where their blocks are (paint); a fill that cannot be drawn goes.
24
+ //
25
+ // What a folded heading hides (fold/) is never selected: select() leaves it out, Shift+↑/↓ and ↑/↓
26
+ // pass over it; what takes a folded heading away whole takes it along when it acts (fold/ takenWith).
27
+
28
+ // Whether a block is hidden by a folded heading (data-ge-folded on it or a block around it).
29
+ const folded = (node) => node.closest(`[${FOLDED}]`) !== null;
23
30
 
24
31
  /**
25
32
  * info: block-info.mjs. active(): whether block tools may show. say(text, kind, hold): the status
@@ -37,7 +44,7 @@ export function createSelection({ info, active, say, closeMenu, schedule }) {
37
44
  /** Select a block, or several (none is clearing); `quiet`: without saying so. */
38
45
  function select(targets, { quiet = false } = {}) {
39
46
  closeMenu();
40
- const next = ordered([targets].flat().filter(alive));
47
+ const next = ordered([targets].flat().filter(alive)).filter((node) => !folded(node));
41
48
  if (next.length === 0) return clear();
42
49
  if (next.length !== picked.length || next.some((node, index) => node !== picked[index])) {
43
50
  picked = next;
@@ -79,7 +86,7 @@ export function createSelection({ info, active, say, closeMenu, schedule }) {
79
86
  // Shift+↑/↓: the selection widens (or narrows) by one block among the anchor's neighbours.
80
87
  function extend(delta) {
81
88
  const container = anchor?.parentElement;
82
- const list = container ? unitsOf(container) : [];
89
+ const list = container ? unitsOf(container).filter((unit) => !folded(unit)) : [];
83
90
  const at = list.indexOf(anchor);
84
91
  if (at < 0) return;
85
92
  const inside = picked.map((node) => list.indexOf(node)).filter((index) => index >= 0);
@@ -94,7 +101,7 @@ export function createSelection({ info, active, say, closeMenu, schedule }) {
94
101
  function sibling(node, forward) {
95
102
  for (let next = forward ? node.nextElementSibling : withLeads(node)[0].previousElementSibling; next; next = forward ? next.nextElementSibling : next.previousElementSibling) {
96
103
  const unit = unitElement(next);
97
- if (unit && unit.parentElement === node.parentElement && unit !== node) return unit;
104
+ if (unit && unit.parentElement === node.parentElement && unit !== node && !folded(unit)) return unit;
98
105
  }
99
106
  return null;
100
107
  }
@@ -1,4 +1,5 @@
1
1
  import { OLD_PROTOCOL, PROTOCOL, PROTOCOLS, VERSION, keyCombo } from "../../shared/session.mjs";
2
+ import { lookOf, lookOfElement } from "../../shared/document-look.mjs";
2
3
 
3
4
  // The report page inside a workbench (an iframe of the same origin): it answers a few commands and
4
5
  // tells the workbench its state, never the report's content. Nothing happens when the page is not
@@ -7,11 +8,13 @@ import { OLD_PROTOCOL, PROTOCOL, PROTOCOLS, VERSION, keyCombo } from "../../shar
7
8
  // A message counts only from the parent window, of this origin and protocol. A command names the
8
9
  // report it is meant for: one for another report (the iframe went to another page meanwhile) is
9
10
  // refused. Every command gets exactly one reply, once it is done. Besides the editor's commands the
10
- // session itself answers four: `theme` (show the report light or dark, or as the reader chose, without
11
- // storing it), `keys` (key combinations to hand to the workbench when the page does not use them),
11
+ // session itself answers five: `theme` (show the report light or dark, or as the reader chose, without
12
+ // storing it), `look` (show the document in a look, 字体与布局: { look }, src/shared/document-look.mjs; the
13
+ // workbench writes it into doc.json), `keys` (key combinations to hand to the workbench when the page does not use them),
12
14
  // `scroll-top` (back to the top when scrolled down) and `close-card` (the detail card on top closes as a
13
15
  // press on its backdrop closes it: the workbench asks on a press on its own bars, or Esc there, while
14
- // the page says a card is open, `card` in what it sends).
16
+ // the page says a card is open, `card` in what it sends). A card stands in the middle of the
17
+ // workbench's window, not of the frame (centreCards, below).
15
18
 
16
19
  /** A page opened in a frame on its own (?solo), not as the workbench's report: no session, its own notices. */
17
20
  export const SOLO = new URLSearchParams(location.search).has("solo");
@@ -34,6 +37,7 @@ export const SOLO = new URLSearchParams(location.search).has("solo");
34
37
  */
35
38
  export function connectWorkbench({ slug, snapshot, commands, modals }) {
36
39
  if (window.parent === window || SOLO) return null;
40
+ centreCards(modals);
37
41
  // The id the workbench speaks (src/shared/session.mjs, OLD_PROTOCOL): null until its first command,
38
42
  // and meanwhile every message goes under both, so that a workbench of either version hears it.
39
43
  let spoken = null;
@@ -66,6 +70,13 @@ export function connectWorkbench({ slug, snapshot, commands, modals }) {
66
70
  dispatchEvent(new CustomEvent("doc-theme-request", { detail: { theme } })); // the kit's ui.js
67
71
  return { ok: true };
68
72
  },
73
+ // The document's look shown at once (the kit's ui.js puts it on <html> and lays out again), never
74
+ // stored here: the workbench writes doc.json. Answers with the look the page shows then.
75
+ async look({ look = null }) {
76
+ if (look !== null && (typeof look !== "object" || Array.isArray(look))) return { ok: false, reason: "look" };
77
+ dispatchEvent(new CustomEvent("doc-look-request", { detail: { look: lookOf(look) } }));
78
+ return { ok: true, look: lookOfElement(document.documentElement) };
79
+ },
69
80
  async keys({ keys: list }) {
70
81
  if (!Array.isArray(list) || !list.every((key) => typeof key === "string")) return { ok: false, reason: "keys" };
71
82
  keys = new Set(list);
@@ -115,3 +126,43 @@ export function connectWorkbench({ slug, snapshot, commands, modals }) {
115
126
  workspace: () => send("workspace", {}),
116
127
  };
117
128
  }
129
+
130
+ // A detail card stands in the middle of the workbench's window, as the workbench's own dialogs do
131
+ // (组件列表, 写作偏好), not in the middle of this frame right of its sidebar and under its top bar;
132
+ // the workbench covers its bars while the page says a card is open (`card`), the card's backdrop
133
+ // the rest. The kit's dialog.css puts the card's middle at --doc-dialog-centre-x and -y, as far as
134
+ // the frame lets it stay clear of its sides, and makes it 80% as high as --doc-dialog-window-height.
135
+ // They are read from the frame's place in the workbench's window (of this origin, as every message
136
+ // of the session is), and said in a sheet the document adopts, which is in no markup (a copy of the
137
+ // view, the file); again as the frame changes size (the window resized, the sidebar opened, closed or
138
+ // dragged wider) and as a card opens or closes.
139
+ function centreCards(modals) {
140
+ let sheet;
141
+ try {
142
+ sheet = new CSSStyleSheet();
143
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
144
+ } catch {
145
+ return; // a browser with no constructed sheets: the card in the middle of the frame
146
+ }
147
+ let said = null;
148
+ const place = () => {
149
+ let rule = "";
150
+ try {
151
+ const frame = window.frameElement;
152
+ const outer = frame?.ownerDocument.documentElement;
153
+ if (outer) {
154
+ const box = frame.getBoundingClientRect();
155
+ const style = frame.ownerDocument.defaultView.getComputedStyle(frame);
156
+ const left = box.left + frame.clientLeft + parseFloat(style.paddingLeft);
157
+ const top = box.top + frame.clientTop + parseFloat(style.paddingTop);
158
+ rule = `:root { --doc-dialog-centre-x: ${outer.clientWidth / 2 - left}px; --doc-dialog-centre-y: ${outer.clientHeight / 2 - top}px; --doc-dialog-window-height: ${outer.clientHeight}px; }`;
159
+ }
160
+ } catch { /* a frame of another origin's page, or one taken away: in the middle of the frame */ }
161
+ if (rule === said) return;
162
+ said = rule;
163
+ sheet.replaceSync(rule);
164
+ };
165
+ place();
166
+ addEventListener("resize", place);
167
+ modals.listen(place);
168
+ }
@@ -2,7 +2,7 @@ import { belowControl, colorSection, colorSwatch, FORMAT_ICONS, formatLook, list
2
2
  import { HIGHLIGHTS, TEXT_COLORS } from "../marks.mjs";
3
3
  import { placeBox } from "../placement.mjs";
4
4
  import { hideTooltip, shortcut } from "../tooltip.mjs";
5
- import { element, fadeIn, fadeOut, mount } from "../ui.mjs";
5
+ import { addStyle, element, fadeIn, fadeOut, mount } from "../ui.mjs";
6
6
  import { alignItems } from "./aligns.mjs";
7
7
  import { TABLE_ICONS } from "./icons.mjs";
8
8
  import { t } from "../../../shared/i18n/index.mjs";
@@ -19,6 +19,8 @@ import { t } from "../../../shared/i18n/index.mjs";
19
19
  const APPEAR = 120;
20
20
  const PANEL = 120;
21
21
  const GAP = 8; // between the cells and the bar
22
+ // A cell's fill is a block's: its swatches and its "A" show --doc-fill, not the words' deeper fill the kit gives a span.
23
+ const STYLE = `.ge-cell-bar :is(.ge-ink-sample, .ge-swatch > span)[class*="doc-highlight-"] { background: var(--doc-fill); }`;
22
24
 
23
25
  /**
24
26
  * run(command, value): "color" with { kind: "text" | "background", color } (null: none), "bold", "align" with an
@@ -27,6 +29,7 @@ const GAP = 8; // between the cells and the bar
27
29
  */
28
30
  export function createCellBar(run) {
29
31
  formatLook();
32
+ addStyle(STYLE);
30
33
  const sample = element("span", { class: "ge-ink-sample", text: "A" });
31
34
  const tools = {
32
35
  color: tool("color", t("颜色"), sample, () => toggle("colors")),
@@ -15,7 +15,7 @@ import { createCellLists } from "./lists.mjs";
15
15
  import { t } from "../../../shared/i18n/index.mjs";
16
16
 
17
17
  // Selected cells of a table and the keys in a table (notion-lite's, src/blocks/table/edit/; the
18
- // rules in table-math-plan-2026-10-18.md), with the cells' bar (cell-bar.mjs) over them.
18
+ // rules in notes/plans/table-math-plan-2026-10-18.md), with the cells' bar (cell-bar.mjs) over them.
19
19
  //
20
20
  // - Selecting: a press in a cell dragged into another selects the cells between (the text selection
21
21
  // goes; back in the first, that one cell, no caret); Shift+click from the selection's anchor (or
@@ -20,7 +20,7 @@ export const TABLE_ICONS = {
20
20
  // A row and a column taken out: the table with the row (column) crossed.
21
21
  removeRow: svg('<rect x="3.25" y="4.25" width="13.5" height="11.5" rx="2"/><path d="M3.25 8.25h13.5M3.25 11.75h13.5"/><path d="m8.25 8.75 3.5 2.5M11.75 8.75l-3.5 2.5"/>'),
22
22
  removeColumn: svg('<rect x="3.25" y="4.25" width="13.5" height="11.5" rx="2"/><path d="M8.25 4.25v11.5M11.75 4.25v11.5"/><path d="m8.75 8.25 2.5 3.5M11.25 8.25l-2.5 3.5"/>'),
23
- // 均分列宽: a table of three equal columns; 一键调整列宽: two edges and the arrows of a width between them.
23
+ // 均分列宽: a table of three equal columns; 自动调整列宽: two edges and the arrows of a width between them.
24
24
  even: svg('<rect x="3.25" y="4.25" width="13.5" height="11.5" rx="2"/><path d="M7.75 4.25v11.5M12.25 4.25v11.5"/>'),
25
25
  autoWidth: svg('<path d="M3.25 4v12M16.75 4v12"/><path d="M6.25 10h7.5M8.25 8l-2 2 2 2M11.75 8l2 2-2 2"/>'),
26
26
  // 合并单元格: a cell's two halves pushed together; 拆分单元格: a cell pulled apart at its middle line.
@@ -24,7 +24,7 @@ import { createColumnWidths } from "./widths.mjs";
24
24
  import { t } from "../../../shared/i18n/index.mjs";
25
25
 
26
26
  // A table's own tools (notion-lite's simple table, src/blocks/table/; the plan's T2, its rules in
27
- // table-math-plan-2026-10-18.md), for a table written as the kit writes one (the unit the server marked GRID):
27
+ // notes/plans/table-math-plan-2026-10-18.md), for a table written as the kit writes one (the unit the server marked GRID):
28
28
  //
29
29
  // - The selection: cells (a rectangle between two of them, cells.mjs, grown so that it cuts no
30
30
  // merged cell), rows or columns, of one table; { unit, type: "cell" | "row" | "column", anchor,
@@ -535,7 +535,7 @@ export function createTableEditing({ active, struct, look, selectBlock, resizeTa
535
535
  if (type === "column") {
536
536
  // The columns' widths: shared out equally (several selected); the kit's own, or fixed as they are.
537
537
  if (to > from) items.push({ label: t("均分列宽"), icon: ICONS.even, run: () => widths.even(unit, from, to) });
538
- items.push({ label: t("一键调整列宽"), icon: ICONS.autoWidth, run: () => widths.fit(unit) });
538
+ items.push({ label: t("自动调整列宽"), icon: ICONS.autoWidth, run: () => widths.fit(unit) });
539
539
  }
540
540
  items.push(
541
541
  { label: t("创建副本"), icon: ICONS.duplicate, hint: "Ctrl+D", run: () => duplicate(unit, type, from, to) },
@@ -918,16 +918,21 @@ export function createTableEditing({ active, struct, look, selectBlock, resizeTa
918
918
 
919
919
  addEventListener("scroll", () => { if (!drag) draw(); }, { passive: true, capture: true });
920
920
 
921
- // The rows of a table the tools manage.
921
+ // The rows of a table the tools manage. 一键优化 is a split row (menu.mjs): its words run it, its ›
922
+ // opens two of its parts to run alone, 自动调整列宽 and 识别数字列 (the user's, 2026-11-07).
922
923
  function toolRows(unit) {
923
924
  const state = tableState(unit);
924
925
  return [
925
926
  { label: t("标题行"), icon: ICONS.headerRow, checked: state.headerRow, run: () => headerRow(unit, state) },
926
927
  { label: t("标题列"), icon: ICONS.headerColumn, checked: state.headerColumn, run: () => change(unit, () => setHeaderColumn(unit, !state.headerColumn), state.headerColumn ? t("已取消标题列") : t("已设为标题列")) },
927
- { label: t("一键优化"), icon: ICONS.magic, run: () => optimize(unit) },
928
- { label: t("一键调整列宽"), icon: ICONS.autoWidth, run: () => widths.fit(unit) },
928
+ {
929
+ label: t("一键优化"), icon: ICONS.magic, run: () => optimize(unit),
930
+ submenu: () => [
931
+ { label: t("自动调整列宽"), icon: ICONS.autoWidth, run: () => widths.fit(unit) },
932
+ { label: t("识别数字列"), icon: ICONS.numbers, run: () => numbers(unit) },
933
+ ],
934
+ },
929
935
  { label: t("均分列宽"), icon: ICONS.even, run: () => widths.even(unit) },
930
- { label: t("识别数字列"), icon: ICONS.numbers, run: () => numbers(unit) },
931
936
  ];
932
937
  }
933
938
 
@@ -974,8 +979,8 @@ export function createTableEditing({ active, struct, look, selectBlock, resizeTa
974
979
  /**
975
980
  * A table's rows of its block menu (notion-lite's 表格 section, under the menu's one title): a
976
981
  * table of the kind these tools manage has 标题行 and 标题列, switches (checked when on); then
977
- * actions, each applied once (no check): 一键优化, 一键调整列宽, 均分列宽, 识别数字列; any table
978
- * in its frame has its size's rows after them (layoutRows). [] for any other block.
982
+ * actions, each applied once (no check): 一键优化 › (自动调整列宽, 识别数字列 in its submenu),
983
+ * 均分列宽; any table in its frame has its size's rows after them (layoutRows). [] for any other block.
979
984
  */
980
985
  menuItems(target) {
981
986
  if (!target?.matches?.('[data-ge-unit-kind="table"]')) return [];
@@ -2,6 +2,7 @@ import { SPAN } from "../../../shared/media.mjs";
2
2
  import { COLUMN_WIDTH, MIN_COLUMN_WIDTH, widthOf } from "../../components/table/index.mjs";
3
3
  import { relayout, tableParts, tableState } from "../../components/table/page.mjs";
4
4
  import { widthHolder } from "../../../shared/table-slots.mjs";
5
+ import { GONE_WORDS } from "../core.mjs";
5
6
  import { gestureLock } from "../gesture.mjs";
6
7
  import { drawBox } from "../overlay-layer.mjs";
7
8
  import { hideTooltip, tooltip } from "../tooltip.mjs";
@@ -15,7 +16,7 @@ import { t } from "../../../shared/i18n/index.mjs";
15
16
  // index.mjs COLUMN_WIDTH; shared/table-slots.mjs widthHolder: a merged cell holds none), the kit laying the table
16
17
  // out at them (tables/layout.js manualLayout); the first
17
18
  // drag of a table the kit lays out gives every column the width it has then. A double click fits
18
- // the column to its widest cell, unwrapped. The menus' 一键调整列宽 takes the widths off (the kit's
19
+ // the column to its widest cell, unwrapped. The menus' 自动调整列宽 takes the widths off (the kit's
19
20
  // again) or gives each column its width as it is; 均分列宽 shares the columns' room out equally.
20
21
  // Sizing columns takes the table's own width off (--doc-span: the table is as wide as its
21
22
  // columns again).
@@ -114,18 +115,22 @@ export function createColumnWidths(tools) {
114
115
  relayout(unit);
115
116
  }
116
117
 
117
- // A column fitted to its widest cell of it alone, unwrapped (+1px).
118
+ // A column fitted to its widest cell of it alone, unwrapped (+1px): by its words, not the words an
119
+ // Agent took out that the page shows in it (changes/: out of the layout while it is measured).
118
120
  function fit(unit, index) {
119
121
  const { slots } = tableState(unit);
120
122
  const widest = Math.max(0, ...[...slots.places].filter(([, place]) => place.c === index && place.cols === 1).map(([cell]) => {
121
123
  const style = getComputedStyle(cell);
122
124
  const was = cell.style.whiteSpace;
125
+ const gone = [...cell.querySelectorAll(GONE_WORDS)];
126
+ for (const mark of gone) mark.hidden = true;
123
127
  cell.style.whiteSpace = "nowrap";
124
128
  const range = document.createRange();
125
129
  range.selectNodeContents(cell);
126
130
  const width = range.getBoundingClientRect().width + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
127
131
  cell.style.whiteSpace = was;
128
132
  if (cell.getAttribute("style") === "") cell.removeAttribute("style");
133
+ for (const mark of gone) mark.hidden = false;
129
134
  return width;
130
135
  }));
131
136
  const widths = shown(unit);
@@ -291,9 +296,9 @@ export function createColumnWidths(tools) {
291
296
  /** The edges plain again (the tools no longer stand by the table). */
292
297
  disarm() { armed = null; },
293
298
  get dragging() { return drag !== null; },
294
- /** 一键调整列宽: the kit's widths, from the columns' words (the reader's taken off; sizing a column fixes them again). */
299
+ /** 自动调整列宽: the kit's widths, from the columns' words (the reader's taken off; sizing a column fixes them again). */
295
300
  fit(unit) {
296
- if (set(unit, null, t("已一键调整列宽"))) return true;
301
+ if (set(unit, null, t("已自动调整列宽"))) return true;
297
302
  tools.say(t("列宽已按内容调整,无需再调。"));
298
303
  return false;
299
304
  },
@@ -4,6 +4,7 @@ import { boxRole } from "../../components/registry.mjs";
4
4
  import { UNIT, can, isHost, mergeInto, partsOf, removalTarget, takeOut, textEnd, unitElement } from "../core.mjs";
5
5
  import { atomOf, caretAt, caretAtOffset, dragPlace, editable, hosts, pointRange, select } from "../caret.mjs";
6
6
  import { edgeFollower } from "../edge-scroll.mjs";
7
+ import { FOLDED } from "../folded.mjs";
7
8
  import { gapScore } from "../geometry.mjs";
8
9
  import { endsGesture, gestureLock } from "../gesture.mjs";
9
10
  import { joinCode } from "../marks.mjs";
@@ -21,8 +22,8 @@ import { t } from "../../../shared/i18n/index.mjs";
21
22
  // the window's edges), drawn with the CSS Custom Highlight API in the selection's own colors, the
22
23
  // browser's selecting held off meanwhile; back in that block, the browser's again. Shift+click in
23
24
  // another block selects from the caret to there.
24
- // Its text is that of body text, headings and list items (in quotes, highlight blocks, columns…).
25
- // A block holding none (a code block, a table, a file, an image, a formula block, a callout, a
25
+ // Its text is that of body text, headings and list items (in quotes, highlight blocks, callouts, columns…).
26
+ // A block holding none (a code block, a table, a file, an image, a formula block, a
26
27
  // divider…) is taken whole as it passes it, filled as a selected block is (selection/ mark), its
27
28
  // own text not marked: over one, an end of the selection stands before it or after it, as the drag
28
29
  // goes, and a press begun in one (its code, a cell) takes it whole once it has left it.
@@ -250,6 +251,20 @@ export function createTextSelection({ isEditing, struct, say, keepText, selectUn
250
251
  if (host === selection.end.host) part.setEnd(selection.end.node, selection.end.offset);
251
252
  return { host, range: part };
252
253
  });
254
+ // What a folded heading hides (fold/) between the ends goes with the selection, wherever in the
255
+ // heading's words it begins: its text blocks' text in full and its other blocks whole (below), as
256
+ // a deletion takes the blocks between the ends whole (unitsBetween), so a copy or a cut carries
257
+ // what the deletion takes. An end is never in what is hidden (hosts() are those shown), so what is
258
+ // hidden and in the range lies between the ends.
259
+ const hidden = [...document.querySelectorAll(`main [${FOLDED}] :is([data-ge-block], [data-ge-new]), main :is([data-ge-block], [data-ge-new])[${FOLDED}]`)]
260
+ .filter((host) => isHost(host) && isText(host) && range.intersectsNode(host) && !folded(host));
261
+ for (const host of hidden) {
262
+ const part = document.createRange();
263
+ part.setStart(host, 0);
264
+ part.setEnd(host, textEnd(host));
265
+ texts.push({ host, range: part });
266
+ }
267
+ if (hidden.length > 0) texts.sort((a, b) => (a.host.compareDocumentPosition(b.host) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));
253
268
  const wholes = [...wholeClosed];
254
269
  for (const unit of document.querySelectorAll(`main :is(${UNIT})`)) {
255
270
  if (wholes.some((other) => other === unit || other.contains(unit))) continue;
@@ -348,7 +363,7 @@ export function createTextSelection({ isEditing, struct, say, keepText, selectUn
348
363
 
349
364
  // Drawn again over its text blocks turned into another type (`texts`: where its pieces are now, in
350
365
  // page order), as notion-lite's placeCaret(keep) keeps a selection; when one of them holds no text of
351
- // the body now (a callout, a disclosure's title, code), let go instead. Returns whether it is drawn.
366
+ // the body now (a disclosure's title, code), let go instead. Returns whether it is drawn.
352
367
  function reselect(texts) {
353
368
  if (!cross) return false;
354
369
  if (texts.length === 0 || !texts.every(({ host }) => host.isConnected && isText(host))) {
@@ -22,7 +22,7 @@ const STYLE = `
22
22
  const DELAY = 150;
23
23
  const GAP = 6;
24
24
  const MAC = /Mac|iPhone|iPad/.test(globalThis.navigator?.platform ?? "");
25
- const KEYS = MAC ? { mod: "⌘", shift: "⇧", alt: "⌥" } : { mod: "Ctrl", shift: "⇧", alt: "Alt" };
25
+ const KEYS = MAC ? { mod: "⌘", shift: "⇧", alt: "⌥", enter: "↩" } : { mod: "Ctrl", shift: "⇧", alt: "Alt", enter: "Enter" };
26
26
  let shown = null; // { node, tip }
27
27
  let timer = 0;
28
28
 
@@ -6,8 +6,9 @@ import { t } from "../../shared/i18n/index.mjs";
6
6
 
7
7
  // The types a block can be turned into, as icons side by side (as Feishu's block menu), the block's
8
8
  // own type marked: the top of the block menu (block-menu/) and the format bar's "turn into"
9
- // (format-bar/). The buttons are the format bar's (32×28px, 4px apart, its blue for the current
10
- // one), so both read alike. Each icon names its type, and its Markdown shortcut, in a tooltip.
9
+ // (format-bar/). The buttons are the format bar's (32×28px, 4px apart; the current one marked as
10
+ // a pressed tool of the bar is, its icon's lines in the bar's blue, --ge-accent, and no fill of its
11
+ // own, so the pointer's gray fills it as any other), so both read alike. Each icon names its type, and its Markdown shortcut, in a tooltip.
11
12
  // A type that says `corner` has no cell of its own but the gray corner of that type's (a split
12
13
  // button): 小字's of 文本's, so 文本 and the headings fill one line. The corner shows only with the
13
14
  // pointer (or the keys) on the cell, or, blue, when the block is of that type. It is a click target
@@ -26,7 +27,7 @@ const STYLE = `
26
27
  .ge-type { display: flex; align-items: center; justify-content: center; width: ${CELL}px; height: 28px; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--ge-ico); cursor: pointer; transition: background 20ms ease-in; }
27
28
  .ge-type:hover, .ge-type[data-active] { background: var(--ge-hover); }
28
29
  .ge-type:active { background: var(--ge-press); }
29
- .ge-type[aria-current="true"] { background: var(--ge-fill); color: var(--ge-accent); }
30
+ .ge-type[aria-current="true"] { color: var(--ge-accent); }
30
31
  .ge-type-pair { position: relative; border-radius: 6px; overflow: hidden; }
31
32
  .ge-type-corner { position: absolute; right: 0; bottom: 0; width: ${CORNER}px; height: ${CORNER}px; padding: 0; border: 0; background: transparent; cursor: pointer; }
32
33
  .ge-type-corner::before { content: ""; position: absolute; inset: 0; background: light-dark(rgb(55 53 47 / 12%), rgb(255 255 255 / 13%)); clip-path: path("${TRIANGLE}"); opacity: 0; transition: background 20ms ease-in, opacity 120ms ease-out; }
@@ -12,7 +12,11 @@ import { ENTRIES } from "../../components/registry.mjs";
12
12
  // the caret in a pad (pads.mjs). Code blocks are text blocks of their own (components/code/) and
13
13
  // have none of this. A component's entry in the text (`entry`: a chip as padded as code) has the
14
14
  // same stops, and Backspace at the stop after it (Delete at the one before it) takes the whole
15
- // entry away (beforeDelete says what the page says of that; editor.mjs records the step).
15
+ // entry away (takeEntry, on the key's keydown through beforeDelete and on its beforeinput where an
16
+ // input method's keydown says 229; it says what the page says of that, editor.mjs records the step).
17
+ // A Backspace that takes the last character between inline code or an entry and the caret after it
18
+ // leaves the caret outside it, at the stop after it (deletingBack, deletedBack): the browser puts it
19
+ // in at the end (an entry's: left of its icon), its end and the text after it one place to it.
16
20
 
17
21
  const EDGED = ["code", ...ENTRIES.map(({ at }) => at)].join(", ");
18
22
  const isEdged = (node) => node?.nodeType === 1 && node.matches(EDGED);
@@ -187,28 +191,48 @@ export function codePress(host, x, y) {
187
191
  }
188
192
 
189
193
  /**
190
- * Backspace or Delete with the caret in a pad at an edge: the pad goes first, the caret where the
191
- * key then takes the character beside it, as it would without the pad (keydown, before the browser
192
- * deletes or the editor merges blocks). At the stop beside a component's entry the whole entry goes,
193
- * and what the page says of that is returned; nothing otherwise.
194
+ * Backspace (`back`) at the stop after a component's entry, or Delete at the one before it: the whole
195
+ * entry goes, as a chip of the text does, the caret where it stood, and what the page says of that is
196
+ * returned; nothing otherwise. The keydown's (beforeDelete) and the beforeinput's, for a key whose
197
+ * keydown the editor passed by (an input method's, keyCode 229: editor.mjs). The pads beside it go
198
+ * with it, before the text either side of it is joined, so that no zero-width space is left in that
199
+ * text (a pad an undo step put back is part of the text beside it by then).
194
200
  */
195
- export function beforeDelete(host, key) {
201
+ export function takeEntry(host, back) {
196
202
  const selection = getSelection();
197
203
  if (!selection.rangeCount || !selection.isCollapsed) return;
198
- const back = key === "Backspace";
199
- // At the stop after an entry (or before it, for Delete): the whole entry goes.
200
204
  const outside = caretAtEdge(host);
201
205
  const entry = outside && !outside.inside ? entryOf(outside.code) : undefined;
202
- if (entry && (back ? outside.side === "end" : outside.side === "start")) {
203
- const caret = host.ownerDocument.createRange();
204
- if (isPad(selection.anchorNode)) selection.anchorNode.remove();
205
- caret.setStartBefore(outside.code);
206
- caret.collapse(true);
207
- outside.code.remove();
208
- host.normalize();
209
- select(caret);
210
- return entry.removed;
206
+ if (!entry || (back ? outside.side !== "end" : outside.side !== "start")) return;
207
+ const { code } = outside;
208
+ for (const [next, trim] of [["previousSibling", new RegExp(`${ZWSP}+$`)], ["nextSibling", new RegExp(`^${ZWSP}+`)]]) {
209
+ for (let node = code[next]; node?.nodeType === 3; node = code[next]) {
210
+ node.data = node.data.replace(trim, "");
211
+ if (node.data !== "") break;
212
+ node.remove();
213
+ }
211
214
  }
215
+ const caret = host.ownerDocument.createRange();
216
+ caret.setStartBefore(code);
217
+ caret.collapse(true);
218
+ code.remove();
219
+ host.normalize();
220
+ select(caret);
221
+ return entry.removed;
222
+ }
223
+
224
+ /**
225
+ * Backspace or Delete with the caret in a pad at an edge: the pad goes first, the caret where the
226
+ * key then takes the character beside it, as it would without the pad (keydown, before the browser
227
+ * deletes or the editor merges blocks). At the stop beside a component's entry the whole entry goes
228
+ * (takeEntry), and what the page says of that is returned; nothing otherwise.
229
+ */
230
+ export function beforeDelete(host, key) {
231
+ const back = key === "Backspace";
232
+ const removed = takeEntry(host, back);
233
+ if (removed) return removed;
234
+ const selection = getSelection();
235
+ if (!selection.rangeCount || !selection.isCollapsed) return;
212
236
  const pad = selection.anchorNode;
213
237
  if (!isPad(pad) || !host.contains(pad)) return;
214
238
  const edge = caretAtEdge(host);
@@ -226,3 +250,25 @@ export function beforeDelete(host, key) {
226
250
  caret.collapse(true);
227
251
  select(caret);
228
252
  }
253
+
254
+ // The host whose caret a deletion backward found in text outside any inline code or entry (no pad),
255
+ // for the input after it (deletedBack).
256
+ let backFrom = null;
257
+
258
+ /** A deletion backward about to happen (its beforeinput): where the caret stands, for deletedBack. */
259
+ export function deletingBack(host) {
260
+ const selection = getSelection();
261
+ const node = selection.rangeCount && selection.isCollapsed ? selection.anchorNode : null;
262
+ backFrom = node?.nodeType === 3 && !isPad(node) && !node.parentElement.closest(EDGED) ? host : null;
263
+ }
264
+
265
+ /**
266
+ * The deletion done (its input): a caret it took from the text after inline code or an entry into
267
+ * it, at its end, goes back out to the stop after it, where the reader deleting back stays.
268
+ */
269
+ export function deletedBack(host) {
270
+ const was = backFrom === host;
271
+ backFrom = null;
272
+ const edge = was && caretAtEdge(host);
273
+ if (edge && edge.inside && edge.side === "end") padCaret(edge.code, "after");
274
+ }
@@ -47,13 +47,12 @@ function writtenTogether(host, node, start, end) {
47
47
  /**
48
48
  * struct(parents, touched, mutate): the editor's undoable structural change (history.mjs). say: the
49
49
  * status line. keepText(unit): why a block may not go (block-info.mjs). paragraphBefore(host): an
50
- * empty paragraph before the block, the caret staying (insert/). paragraphAfter(unit): one after a
51
- * block, the caret in it, where its box takes one (insert/). removeBlocks(units): the blocks
50
+ * empty paragraph before the block, the caret staying (insert/). removeBlocks(units): the blocks
52
51
  * deleted as the selected ones are (selection/delete.mjs). adding(): the block the "/" menu the
53
52
  * handle's "+" opened is open in (slash-menu/), or null. leave: where Enter and Backspace leave a
54
- * container (leave.mjs).
53
+ * container (leave.mjs). showHidden(node): the folds hiding a block opened (fold/ showAround).
55
54
  */
56
- export function createTyping({ struct, say, keepText, paragraphBefore, paragraphAfter, removeBlocks, selectBlock = () => {}, adding = () => null, leave }) {
55
+ export function createTyping({ struct, say, keepText, paragraphBefore, removeBlocks, selectBlock = () => {}, adding = () => null, leave, showHidden = () => {} }) {
57
56
  // A key, asked of the component of the text block `host` and then of what stands around it, nearest
58
57
  // first (docs/editor.md, "The order of keys"): whether it was theirs. `ctx` is notion-lite's, as
59
58
  // much of it as a key reads, the block being an element here.
@@ -75,8 +74,7 @@ export function createTyping({ struct, say, keepText, paragraphBefore, paragraph
75
74
  // in any editor, and so does the empty last paragraph of a panel), the rule of every block.
76
75
  if (asked((keys) => keys.enter, host) || leave.enter(host)) return;
77
76
  if (!can(host, "split")) {
78
- // At the very end of a block that says so, a paragraph follows it, as one that splits would.
79
- if (!HOSTS[host.getAttribute("data-ge-kind")]?.opensAfter || !atEdge(host, true) || !paragraphAfter(host)) say(limitText(host, "split"));
77
+ say(limitText(host, "split"));
80
78
  return;
81
79
  }
82
80
  const selection = getSelection();
@@ -159,8 +157,15 @@ export function createTyping({ struct, say, keepText, paragraphBefore, paragraph
159
157
  return true;
160
158
  }
161
159
 
160
+ // A block on the line above or below that a folded heading hides (fold/: data-ge-folded on it or
161
+ // around it): its fold opens first, so a join never takes in words out of sight.
162
+ const opened = (line) => {
163
+ if (line?.closest?.("[data-ge-folded]")) showHidden(line);
164
+ };
165
+
162
166
  // Backspace at the start of a block. Returns whether it was handled.
163
167
  function backspace(host) {
168
+ opened(previousLine(host));
164
169
  // The block's component (a list item's: a nested item moves out a level, the first or last item of
165
170
  // a top-level list leaves it as body text, an emptied item holding lists stays), then what stands
166
171
  // around it (the item a block stands under; a node, at the start of its first line, holding
@@ -192,6 +197,7 @@ export function createTyping({ struct, say, keepText, paragraphBefore, paragraph
192
197
 
193
198
  // Delete at the end of a block. Returns whether it was handled.
194
199
  function forwardDelete(host) {
200
+ opened(nextLine(host));
195
201
  // Its component (an empty item holding blocks stays), then what stands around it (at the end of a
196
202
  // node's last block nothing is taken in).
197
203
  if (asked((keys) => keys.delete, host)) return true;
@@ -15,7 +15,7 @@ import { BLOCK_TYPES, LEAVES_BY_ENTER, accepts } from "../../components/registry
15
15
  // - A block of a type that says `resettable` (a quote's paragraph, a list item) turns into a
16
16
  // paragraph where it stands, its container splitting around it (convert.mjs).
17
17
  // Enter in a title that goes into its content (a disclosure, a card, a code block) is another rule
18
- // (their components'), and so is Enter at the end of a block that says `opensAfter` (typing/).
18
+ // (their components').
19
19
 
20
20
  /**
21
21
  * The empty last block of a box that says `leaveByEnter`, moved out after the box (the box goes if
@@ -2,8 +2,8 @@ import { attr, hasClass } from "./html-tree.mjs";
2
2
  import { BLOCK_ID, randomId } from "./ids.mjs";
3
3
 
4
4
  // 批注 (docs/workbench.md "Annotations", docs/editor.md "Annotations"): the reader's suggestions for a
5
- // local Agent, each about a piece of a report: words of its text, or blocks taken whole. One with no
6
- // text is a plain mark of that place. Kept in reports/<slug>/annotations.json, never in report.html:
5
+ // local Agent, each about a piece of a report: words of its text, or blocks taken whole. One has
6
+ // words: an item with none is none (cleanItem). Kept in reports/<slug>/annotations.json, never in report.html:
7
7
  //
8
8
  // { v: 1, items: [{ id, created, updated, text, anchor }] }
9
9
  // anchor: { kind: "text", blocks: [data-block-id…], quote, prefix, suffix }
@@ -389,12 +389,13 @@ export function cleanAnchor(anchor) {
389
389
  export const sameWords = (a, b) => Boolean(a && b) && a.kind === b.kind && (a.blocks ?? []).join() === (b.blocks ?? []).join() && squash(a.quote) === squash(b.quote)
390
390
  && squash(a.prefix).slice(-CONTEXT_KEPT) === squash(b.prefix).slice(-CONTEXT_KEPT) && squash(a.suffix).slice(0, CONTEXT_KEPT) === squash(b.suffix).slice(0, CONTEXT_KEPT);
391
391
 
392
- /** An annotation as the file keeps it, from what was sent (`now`: when it is saved), or null when it is none. */
392
+ /** An annotation as the file keeps it, from what was sent (`now`: when it is saved), or null when it is none (no id, no anchor, no words). */
393
393
  export function cleanItem(item, now = new Date().toISOString()) {
394
394
  if (!item || typeof item !== "object" || typeof item.id !== "string" || !ID.test(item.id) || typeof item.text !== "string") return null;
395
395
  const anchor = cleanAnchor(item.anchor);
396
396
  if (!anchor) return null;
397
397
  const text = item.text.replace(/\r\n?/g, "\n").slice(0, TEXT_MOST);
398
+ if (!text.trim()) return null;
398
399
  return { id: item.id, created: typeof item.created === "string" ? item.created : now, updated: now, text, anchor };
399
400
  }
400
401
 
@@ -421,7 +422,6 @@ export const itemsText = (items) => (items.length === 0 ? null : `${JSON.stringi
421
422
  export const MARKDOWN_WORDS = {
422
423
  head: "以下是我对这份文档的批注,共 {0} 条。请逐条按“建议”修改底稿;每条给出它在文档里的位置、块的 data-block-id 和批注的原文。",
423
424
  headOne: "以下是我对这份文档的一条批注。请按“建议”修改底稿;它给出在文档里的位置、块的 data-block-id 和批注的原文。",
424
- marks: "没有“建议”的批注只是标记了一个位置,不用改。",
425
425
  report: "文档",
426
426
  source: "底稿",
427
427
  block: "块",
@@ -441,22 +441,20 @@ const fill = (text, ...values) => text.replace(/\{(\d+)\}/g, (whole, index) => (
441
441
  * then each annotation in page order: where it is (`where`: the tab, the headings above it, the
442
442
  * disclosure it is in; `top`: before the report's first heading, said 页首, which no tab or heading
443
443
  * named 正文 is taken for), its blocks' ids (as the file has them), its quote and the suggestion, each
444
- * of its lines a paragraph (blank lines in it make no more of them). One with no suggestion marks a
445
- * place: it has no such line, and a line under the first says what such a one is.
444
+ * of its lines a paragraph (blank lines in it make no more of them).
446
445
  * data: { title, path, items: [{ where: [text], top, blocks: [id], whole, quote, text }] }.
447
446
  */
448
447
  export function annotationsMarkdown({ title, path, items }, words = MARKDOWN_WORDS) {
449
448
  const w = { ...MARKDOWN_WORDS, ...words };
450
449
  const head = items.length === 1 && w.headOne ? w.headOne : fill(w.head, items.length);
451
- const suggestions = items.map((item) => String(item.text ?? "").replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean));
452
- const lines = [head, ...(suggestions.some((said) => said.length === 0) ? [w.marks] : []), "", `- ${w.report}${w.colon}${title}`, `- ${w.source}${w.colon}${path}`];
450
+ const lines = [head, "", `- ${w.report}${w.colon}${title}`, `- ${w.source}${w.colon}${path}`];
453
451
  items.forEach((item, index) => {
454
452
  const where = item.where?.length ? item.where.join(" › ") : item.top ? w.top : w.body;
455
453
  const ids = item.blocks?.length ? item.blocks.map((id) => `\`data-block-id="${id}"\``).join(w.comma) + (item.whole ? w.whole : "") : w.noId;
456
454
  lines.push("", `## ${index + 1}. ${where}`, "", `${w.block}${w.colon}${ids}`, "");
457
455
  for (const line of String(item.quote ?? "").split("\n")) lines.push(`> ${line}`);
458
- const said = suggestions[index];
459
- if (said.length > 0) lines.push("", `${w.suggestion}${w.colon}${said[0]}`);
456
+ const said = String(item.text ?? "").replace(/\r\n?/g, "\n").split("\n").map((line) => line.trim()).filter(Boolean);
457
+ lines.push("", `${w.suggestion}${w.colon}${said[0] ?? ""}`);
460
458
  for (const line of said.slice(1)) lines.push("", line);
461
459
  });
462
460
  return `${lines.join("\n")}\n`;
@@ -3,7 +3,11 @@
3
3
  // after a line that is folded (the kit's styles/code.css asks for it as `@fold-dots`, which
4
4
  // kit-build.mjs fills in; the editor's styles interpolate it). Its colors are the kit's tokens.
5
5
 
6
- /** A folding line's arrow (markup; it takes the color of its button). */
6
+ /**
7
+ * A folding line's arrow (markup; it takes the color of its button), pointing down; folded, it is
8
+ * turned to point at what it folds: a code block's and its lines' -90° (right: they stand left of
9
+ * the code), a heading's 90° (left: it stands right of the words, page/fold/).
10
+ */
7
11
  export const FOLD_ARROW = '<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3.25 4.5h5.5L6 8z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>';
8
12
 
9
13
  /** The three dots of a folded line's "…" (a CSS image). */