@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
@@ -4,13 +4,13 @@ import { begin, done, made } from "../made.mjs";
4
4
  import { selectsBeside } from "../beside.mjs";
5
5
  import { caretAt, nearestOnLine, placeCaret, selectAt } from "../caret.mjs";
6
6
  import { sectionOps } from "../page-objects.mjs";
7
- import { endOf, hoverAt, innerBox, measure } from "../geometry.mjs";
7
+ import { contentOf, endOf, hoverAt, innerBox, measure } from "../geometry.mjs";
8
8
  import { BOXED, HOLDS_ONE, accepts } from "../../components/registry.mjs";
9
9
  import { DIALOG } from "../../components/kit.mjs";
10
10
 
11
11
  // Where new blocks come from (docs/editor.md, "New blocks and conversions" and "Selecting
12
- // blocks"): a click on the empty space below the last block of a box (a new paragraph there, or the
13
- // caret into the empty one already at its end), Enter at the very start of a block (a paragraph
12
+ // blocks"): a click on the empty space below the last block of a box (not one whose end is its
13
+ // padding; a new paragraph there, or the caret into the empty one already at its end), Enter at the very start of a block (a paragraph
14
14
  // before it), the handle's "+" (an empty block beside the block: addNear), and the block menu's new section after a block (components/section/page.mjs). A
15
15
  // paragraph made here is written once it has text. "在下方新建一块" in a block's menu, like the "/"
16
16
  // menu's blocks in a block with text, is the editor's insertBelow; blocks also come from conversions
@@ -49,16 +49,20 @@ export function createInsert({ struct, blocks }) {
49
49
  }
50
50
 
51
51
  // A click on empty space (selection/marquee.mjs), below the last block of the box clicked (the
52
- // page, a Sheet panel, a section, a highlight block, a disclosure's content; below the report, the
53
- // page): a new paragraph at its end, or the caret into the empty paragraph already there
54
- // (geometry.mjs endOf). To the right of a boxed block (a code block, a callout, a file…: not text)
55
- // or of a row of columns, that block selected, so Delete takes it and Enter puts a paragraph after
56
- // it. Elsewhere beside text, the caret goes into the text block on that line (the nearest of those
52
+ // page, a Sheet panel, a section, a column, a detail card's content, a flowchart's node; below the
53
+ // report, the page): a new paragraph at its end, or the caret into the empty paragraph already there
54
+ // (geometry.mjs endOf). Under the last block of a box whose end is its own padding (endIsPadding: a
55
+ // highlight block, a callout, a disclosure's content; the user's, 2026-11-06, as notion-lite's click-blank.js
56
+ // makes a block only below the page and in a column) it is a gap like any other, below. Right of
57
+ // the text area a boxed block (a code block, a callout, a file…: not text) or a row of columns
58
+ // stands in, at its height (boxedBeside), that block selected, so Delete takes it and Enter puts a
59
+ // paragraph after it; between a narrower block's right edge and its column's, as beside any block
60
+ // of no text (below). Elsewhere beside text, the caret goes into the text block on that line (the nearest of those
57
61
  // side by side), where the line is nearest the click (Notion's way); in the gap between two
58
62
  // blocks, or beside a block of no text, into the nearer of the blocks around it that is text, in the
59
63
  // innermost box at that height (boxBeside: the press may be on the page's margin, the block in a
60
64
  // Sheet's panel or a section), else in the boxes around that one, at the place nearest the click on
61
- // its nearest line (always somewhere: a click there never does nothing). The page stays where it
65
+ // its nearest line (somewhere in the box clicked or around it, when any is text). The page stays where it
62
66
  // is, that text out of sight too (caretNear; notion-lite's click-blank.js scrolls no page either).
63
67
  function clickBlank(x, y, target) {
64
68
  // In an open detail card, its content is the page: never a block behind it (marquee.mjs topOf).
@@ -77,7 +81,7 @@ export function createInsert({ struct, blocks }) {
77
81
  if (end?.focus) return caretAt(end.focus, "end");
78
82
  if (end) return newParagraph(end.box, end.after);
79
83
  const top = y + scrollY;
80
- const beside = boxedBeside(main, x, y);
84
+ const beside = boxedBeside(main, x, y, target);
81
85
  if (beside) return blocks().select(beside);
82
86
  const host = hoverAt(measure(main, { top, bottom: top }), x + scrollX, top, { reach: Infinity, skip: (unit) => !unit.text });
83
87
  if (host) return caretNear(host, x, y);
@@ -144,8 +148,8 @@ export function createInsert({ struct, blocks }) {
144
148
  return Math.max(rect.top - y, y - rect.bottom, 0);
145
149
  };
146
150
  // A block that is no text of its own (its category: a card, a container of its own or a whole
147
- // block; a code block, a callout, a formula block, a divider), as opposed to text blocks and the
148
- // boxes text flows in (a list, a quote, a section, columns).
151
+ // block, a highlight block, a callout; a code block, a formula block, a divider), as opposed to text
152
+ // blocks and the boxes text flows in (a list, a quote, a section, columns).
149
153
  const boxed = (unit) => BOXED.has(unit.getAttribute("data-ge-unit-kind"));
150
154
  // The text block of `unit` at its bottom (`last`) or its top: itself, or the last (first) of those it holds; none in a boxed block.
151
155
  function textEdge(unit, last) {
@@ -153,14 +157,23 @@ export function createInsert({ struct, blocks }) {
153
157
  const texts = [unit, ...unit.querySelectorAll("[data-ge-block], [data-ge-new]")].filter((node) => node.isContentEditable && node.getClientRects().length > 0 && !node.closest("details:not([open]) > :not(summary)"));
154
158
  return (last ? texts.at(-1) : texts[0]) ?? null;
155
159
  }
156
- // A boxed block or a row of columns at height `y` with (x, y) to its right (in the page's margin,
157
- // or beside a narrower block), or null: the innermost, but never a block inside a row of columns
158
- // the click is to the right of too (the row is selected whole).
159
- function boxedBeside(main, x, y) {
160
+ // A boxed block or a row of columns at height `y` with (x, y) right of the text area it stands in
161
+ // (the content box of its box: the page's column, a column of 分栏, a section's, a quote's, a
162
+ // highlight block's or a callout's, a disclosure's content) and right of the block itself (one
163
+ // wider than that area), or null: in the page's margin, or in the padding of the box it stands in;
164
+ // never between a narrower block's right edge and its area's (a table, a picture: the user's,
165
+ // 2026-11-07, a click there selected it), nor in another text area (the blank box `target` the
166
+ // press was on holds the block: a column further right is no place beside one in the column left
167
+ // of it). The innermost, but never a block inside a row of columns the click is to the right of too
168
+ // (the row is selected whole): past a column's edge is the gap to the next column, whose press is
169
+ // the gap's (columns/: it drags, a click alone changes nothing), or past the row, which is selected.
170
+ function boxedBeside(main, x, y, target) {
160
171
  let found = null;
161
172
  const right = (unit) => {
162
173
  const rect = unit.getBoundingClientRect();
163
- return y >= rect.top && y <= rect.bottom && x > rect.right;
174
+ if (y < rect.top || y > rect.bottom || (target?.contains && !target.contains(unit))) return false;
175
+ const box = unit.parentElement;
176
+ return x > Math.max(rect.right, contentOf(box.getBoundingClientRect(), getComputedStyle(box)).right);
164
177
  };
165
178
  for (const unit of main.querySelectorAll(`[data-ge-unit-kind]`)) {
166
179
  if (!selectsBeside(unit) || !unit.parentElement?.hasAttribute("data-ge-box") || unit.parentElement.closest("details:not([open])")) continue;
@@ -0,0 +1,75 @@
1
+ import { holdsLongRun } from "../../shared/long-runs.mjs";
2
+ import { textFilters } from "../components/registry.mjs";
3
+ import { addStyle } from "./ui.mjs";
4
+
5
+ // A long run in the text the page editor edits (src/shared/long-runs.mjs: a path, a URL, a long
6
+ // identifier, more than 20 characters with no place to break inside it) breaks where its line ends,
7
+ // as the kit breaks it in the page a reader sees (runtime/spacing.js, a span that breaks anywhere,
8
+ // .doc-long-run). No span goes into text the editor keeps equal to the file, so a text block whose
9
+ // own text holds such a run breaks anywhere as a whole (data-ge-long-run: word-break: break-all),
10
+ // and an ordinary word in that block may then be parted at a line's end too, which no other block's
11
+ // is (the compromise docs/editor.md names). The rule's weight is none: a block's own (a table's
12
+ // header keep-all, a short field normal, a code block's lines) wins; what the block holds that is a
13
+ // block or an atom of its own ([contenteditable="false"]: a nested list, a formula) breaks as it would
14
+ // without it, and an item of that list is judged by its own text. Never a block of plain text (a code
15
+ // block's lines, a tab's name: textFilters false), which keeps its own rule; a text part of an
16
+ // instance of a component package is judged as any text block (its component's own word-break, where
17
+ // it says one, wins), though the kit holds no run in an instance's own region.
18
+ //
19
+ // Judged as editing starts, for every text block, and again for the blocks whose text changed, as it
20
+ // changes, whatever changed it (typing, a paste, undo, a rebuild from the file, a block made or made
21
+ // editable): the page's mutations are watched (childList, characterData, contenteditable), and only
22
+ // the blocks they stand in are judged, each by its own text (a regular expression over a block's
23
+ // words: no scan of the page per keystroke). The marker is no text of the file's: a data-ge-* attribute
24
+ // on the block itself (core.mjs), set or taken off only where it changes.
25
+
26
+ const MARK = "data-ge-long-run";
27
+ const HOST = '[contenteditable="true"]';
28
+
29
+ const STYLE = `
30
+ /* A text block holding a long run breaks anywhere (long-runs.mjs), of no weight: a block's own rule wins. */
31
+ :where([data-ge-editing] [data-ge-long-run]) { word-break: break-all; }
32
+ /* What it holds of its own (a nested list, a formula) breaks as the page's text does. */
33
+ :where([data-ge-editing] [data-ge-long-run] [contenteditable="false"]) { word-break: normal; }
34
+ `;
35
+
36
+ // The text of `host` its own: not a block or an atom it holds (contenteditable="false"), a line break a space.
37
+ function ownText(host) {
38
+ const walker = host.ownerDocument.createTreeWalker(host, 5, { // NodeFilter.SHOW_ELEMENT | SHOW_TEXT
39
+ acceptNode: (node) => (node.nodeType === 1 && node !== host && node.hasAttribute("contenteditable") ? 2 : 1), // FILTER_REJECT, FILTER_ACCEPT
40
+ });
41
+ let text = "";
42
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
43
+ if (node.nodeType === 3) text += node.data;
44
+ else if (node.localName === "br") text += " ";
45
+ }
46
+ return text;
47
+ }
48
+
49
+ /** Marks `element` as a text block holding a long run, or takes the mark off: whether it holds one. */
50
+ export function judgeLongRuns(element) {
51
+ const long = element.isConnected && element.matches(HOST) && textFilters(element) && holdsLongRun(ownText(element));
52
+ if (long !== element.hasAttribute(MARK)) element.toggleAttribute(MARK, long);
53
+ return long;
54
+ }
55
+
56
+ /** Judges every text block under `root` now, and each again as its text changes. */
57
+ export function followLongRuns(root) {
58
+ addStyle(STYLE);
59
+ for (const host of root.querySelectorAll(`${HOST}, [${MARK}]`)) judgeLongRuns(host);
60
+ new MutationObserver((records) => {
61
+ const judged = new Set();
62
+ for (const record of records) {
63
+ if (record.type === "attributes") judged.add(record.target); // made editable, or no longer
64
+ const at = record.target.nodeType === 1 ? record.target : record.target.parentElement;
65
+ const host = at?.closest(HOST);
66
+ if (host) judged.add(host);
67
+ for (const node of record.addedNodes) {
68
+ if (node.nodeType !== 1) continue;
69
+ if (node.matches(HOST)) judged.add(node);
70
+ for (const inner of node.querySelectorAll(HOST)) judged.add(inner);
71
+ }
72
+ }
73
+ judged.forEach(judgeLongRuns);
74
+ }).observe(root, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ["contenteditable"] });
75
+ }
@@ -11,15 +11,17 @@
11
11
  //
12
12
  // Two shadows, because Notion has two surfaces: --ge-shadow, a card standing over the text (the
13
13
  // format bar, the formula and link cards, the conflicts, a notice), and --ge-shadow-pop, a menu
14
- // (Notion's large outer shadow). The emoji picker (emoji-picker/support.mjs) carries notion-lite's
15
- // own --c-* variables, taken over as they are.
14
+ // (Notion's large outer shadow). The icon picker reads this palette too (emoji-picker/styles.mjs);
15
+ // emoji-picker/support.mjs keeps notion-lite's --c-* variables only for the media menu and the image's
16
+ // alignment bar.
16
17
 
17
18
  export const TOKENS = `
18
19
  :root {
19
20
  --ge-tx: light-dark(#2c2c2b, #f0efed);
20
21
  --ge-tx2: light-dark(#7d7a75, #ada9a3);
21
22
  --ge-tx3: light-dark(#a19e99, #7d7a75);
22
- --ge-ico: light-dark(#383836, #e6e5e3);
23
+ /* The workbench's --icoPri (FinHarness-Demo's --ink-2). */
24
+ --ge-ico: light-dark(#5c5a55, #a9a69e);
23
25
  --ge-ico2: light-dark(#8e8b86, #ada9a3);
24
26
  /* The quietest icon gray (notion-lite's --c-icoTer): the block handle's dots, its bar and its +. */
25
27
  --ge-ico3: light-dark(#ada9a3, #7d7a75);
@@ -61,5 +63,15 @@ export const TOKENS = `
61
63
  --ge-note-hot-line: light-dark(rgb(222 160 0 / 90%), rgb(255 204 64 / 80%));
62
64
  /* A card of 批注 in the margin under the pointer (notion-lite's --c-hovMarDisBac): a shade of the page. */
63
65
  --ge-note-shade: light-dark(#f9f9f8, #202020);
66
+ /* The Agent's changes (changes/, 设置 › 显示变更): Notion's green and red backgrounds, the conflict card's two versions (conflicts.mjs);
67
+ words added get a 批注's look (a faint fill under a 2px underline) in green, blocks a fainter fill; words
68
+ taken out stand in red struck through on the red, deleted blocks' ghost on a fainter red. */
69
+ --ge-diff-add: light-dark(rgb(0 100 45 / 9%), rgb(108 255 172 / 15.7%));
70
+ --ge-diff-add-line: light-dark(rgb(68 131 97 / 60%), rgb(111 197 150 / 55%));
71
+ --ge-diff-add-fill: light-dark(rgb(0 100 45 / 5.5%), rgb(108 255 172 / 9%));
72
+ --ge-diff-add-text: light-dark(#448361, #6fc596);
73
+ --ge-diff-del: light-dark(rgb(223 22 0 / 9.4%), rgb(255 104 92 / 24%));
74
+ --ge-diff-del-fill: light-dark(rgb(223 22 0 / 4.5%), rgb(255 104 92 / 10%));
75
+ --ge-diff-del-text: light-dark(#cf5148, #ff8f85);
64
76
  }
65
77
  `;
@@ -1,14 +1,18 @@
1
1
  import { OLD_OUTLINE_OPEN_KEY, OLD_PREFERENCES_KEY, OUTLINE_OPEN_KEY, PREFERENCES_KEY } from "../../shared/preferences.mjs";
2
+ import { LOOK_ATTRIBUTES } from "../../shared/document-look.mjs";
2
3
 
3
4
  // The right side of the page while it is edited (docs/editor.md, "The outline and the right side"): the
4
5
  // outline (目录, outline/) docked at the window's right edge, the cards of 批注 (annotate/) between it and
5
6
  // the column, and where the body column goes beside them. The outline is a panel at the window's right
6
7
  // edge, in full (its list, 200px unless the reader dragged its guide wider or narrower: `outlineWidth`;
7
- // the guide 8px in) or as its strip (36px: a short line a heading, its card shown while the pointer is
8
- // on it), as the room allows.
8
+ // the list 8px in, its guide a little further in, close to its top level's titles: outline/list.mjs
9
+ // LIST) or as its strip (36px: a short line a heading, its card shown while the pointer is
10
+ // on it), as the room allows; or as its strip wherever the reader collapsed it (a click on its guide:
11
+ // `collapsed`, the preference outlineCollapsed, until a click on the guide in the strip's card takes it
12
+ // back, which gives the list only where the room allows it).
9
13
  //
10
14
  // One rule places the column: in the middle of what the outline leaves of the page, as far from the
11
- // window's left edge as from the outline (its guide, or its strip). With the outline the rooms at the
15
+ // window's left edge as from the outline (its list's edge, or its strip). With the outline the rooms at the
12
16
  // column's two sides are the same, and they stay the same as the guide is dragged (each gives or takes
13
17
  // half of what the list takes or gives) and as the window is resized; without it the column is
14
18
  // centered in the page as ever. Each room is the edge room at least (64: the block handle at the left,
@@ -53,11 +57,13 @@ import { OLD_OUTLINE_OPEN_KEY, OLD_PREFERENCES_KEY, OUTLINE_OPEN_KEY, PREFERENCE
53
57
  * The numbers: the edge room (the least room either side of the column keeps), the least it gives up
54
58
  * to (the block handle's: its 38px, its 12px gap, 2px), the column, the least a full-width column
55
59
  * keeps, the outline in full (its default width, the least and the most it is dragged to), its
56
- * guide's inset in its panel (the column's right room ends there), how far left of the guide its grip
57
- * reaches, its strip, the hysteresis; how far right of the column a card of 批注 stands (the button beside
60
+ * list's inset in its panel (`guide`: the column's right room ends there, at the list's edge, its
61
+ * guide's hairline LIST.guide further in, outline/list.mjs; the guide stood there
62
+ * until 2026-11-07, the numbers kept so the forms switch at the same widths), how far left of the guide's
63
+ * hairline its grip reaches (and how far short of the list what stands in the column's right room stays), its strip, the hysteresis; how far right of the column a card of 批注 stands (the button beside
58
64
  * a block, past a section's strip, ends 52px out, and a card under the pointer comes 5px left: 3px
59
65
  * clear; one focused comes 20px left, and no button shows then), a card's width, the least it gives up
60
- * to, and the room past it (clear of the outline's guide and grip).
66
+ * to, and the room past it (clear of the outline's list: its guide and grip).
61
67
  */
62
68
  export const MARGIN = Object.freeze({ edge: 64, start: 52, column: 712, least: 560, full: 200, fullMin: 160, fullMax: 360, guide: 8, grip: 4, strip: 36, hysteresis: 16, lane: 60, card: 308, cardLeast: 240, cardEnd: 20 });
63
69
 
@@ -71,19 +77,20 @@ export function outlineWidth(chosen, { full = 200, fullMin = 160, fullMax = 360
71
77
 
72
78
  /**
73
79
  * Where the right side's parts go on a page `width` px wide (its window less its scrollbar): `outline`,
74
- * whether the outline is on, `notes`, whether the page has 批注, `full`, its width in full
75
- * (outlineWidth), `wide`, a report read full width, `was`, the layout given last for the same request
76
- * (a form it laid out is kept for `hysteresis` px under the width it needs).
80
+ * whether the outline is on, `collapsed`, whether the reader collapsed it to its strip, `notes`,
81
+ * whether the page has 批注, `full`, its width in full (outlineWidth), `wide`, a report read full
82
+ * width, `was`, the layout given last for the same request (a form it laid out is kept for
83
+ * `hysteresis` px under the width it needs).
77
84
  * Returns { dock: the outline's width at the window's right edge (0, its strip's or its list's),
78
85
  * outline: its form, "full" | "strip" | null, room: what the outline takes of the page's width beside
79
- * the column's rooms (the list's less its guide's inset, the strip's, 0), side: the least room either
86
+ * the column's rooms (the list's less its inset, the strip's, 0), side: the least room either
80
87
  * side of the column keeps, left: the room at the column's left on this page, right: the room right of
81
88
  * it to the window's edge (its own and the outline's), column: its width, cards: the width of a card
82
89
  * of 批注 standing right of it (0: they fold into markers), cardRoom: what the page's rule keeps right of
83
90
  * the column for them (0: none), floor: the least room at its left }. Written with no outside name, so
84
91
  * the page's head can carry it as it is (EARLY_MARGIN).
85
92
  */
86
- export function marginLayout({ width, outline = false, notes = false, wide = false, was = null, edge = 64, start = 52, column = 712, least = 560, full = 200, guide = 8, strip = 36, hysteresis = 16, lane = 60, card = 308, cardLeast = 240, cardEnd = 20 }) {
93
+ export function marginLayout({ width, outline = false, collapsed = false, notes = false, wide = false, was = null, edge = 64, start = 52, column = 712, least = 560, full = 200, guide = 8, strip = 36, hysteresis = 16, lane = 60, card = 308, cardLeast = 240, cardEnd = 20 }) {
87
94
  const body = wide ? least : column;
88
95
  // The column in the middle of what the outline leaves, its rooms `side` at least (narrower only below
89
96
  // that), and left of there as far as a card's room asks, down to `start`, the card giving what is short.
@@ -99,10 +106,11 @@ export function marginLayout({ width, outline = false, notes = false, wide = fal
99
106
  if (!outline) return placed(null, 0, 0, edge);
100
107
  // In full while the list, as wide as the reader gave it, leaves the column the handle's room at both
101
108
  // sides (its rooms the edge room, less down to the handle's where the page is short); a form kept
102
- // under the width it needs (the hysteresis) narrows the column by what is short. Else its strip.
103
- const spare = width + guide - body; // the page beside the column, the list's room counted from its guide
109
+ // under the width it needs (the hysteresis) narrows the column by what is short. Else, or collapsed by
110
+ // the reader, its strip.
111
+ const spare = width + guide - body; // the page beside the column, the list's room counted from its edge (its inset `guide`)
104
112
  const kept = was?.outline === "full" ? hysteresis : 0;
105
- if (spare + kept >= 2 * start + full) return placed("full", full, full - guide, Math.max(start, Math.min(edge, (spare - full) / 2)));
113
+ if (!collapsed && spare + kept >= 2 * start + full) return placed("full", full, full - guide, Math.max(start, Math.min(edge, (spare - full) / 2)));
106
114
  return placed("strip", strip, strip, edge);
107
115
  }
108
116
 
@@ -143,8 +151,9 @@ const roomCss = ({ room, side, cardRoom = 0, floor = side }) => `:root[data-ge-m
143
151
 
144
152
  // The head's script (EARLY_MARGIN): the outline's room before the page shows, when the report has it
145
153
  // on (`keys.open`: the reports that have it on, shared/preferences.mjs outlineOpen; `slug`: this page's), as
146
- // wide as the reader's preferences say (`keys.preferences`: outlineWidth, the same for every report),
147
- // and the cards' room, when it has 批注 (`notes`, the edit service's word).
154
+ // wide as the reader's preferences say and in the form they chose (`keys.preferences`: outlineWidth,
155
+ // outlineCollapsed, the same for every report), and the cards' room, when it has 批注 (`notes`, the
156
+ // edit service's word).
148
157
  // Never on a page opened on its own in a frame (?solo, the 写作偏好 dialog), which has none.
149
158
  // In the head the page has no content yet, so no scrollbar: its width is read again at
150
159
  // DOMContentLoaded, when the content is in and the kit has not shown it yet (a frame later), unless the
@@ -157,13 +166,17 @@ function earlyMargin(layout, widthOf, numbers, keys, slug, notes, room, early, c
157
166
  const outline = Array.isArray(open) && open.includes(slug);
158
167
  if (!outline && !notes) return;
159
168
  const prefs = JSON.parse(read(keys.preferences, keys.oldPreferences) || "{}") || {};
160
- const asked = { outline, notes, outlineWidth: Number(prefs.outlineWidth) || 0 };
169
+ const asked = { outline, notes, outlineWidth: Number(prefs.outlineWidth) || 0, collapsed: prefs.outlineCollapsed === true };
161
170
  const root = document.documentElement;
162
171
  const sheet = document.createElement("style");
163
172
  sheet.setAttribute("data-doc-dev", "");
164
173
  sheet.setAttribute(room, "");
165
174
  const lay = () => {
166
- const given = layout({ ...numbers, width: root.clientWidth, outline, notes, full: widthOf(asked.outlineWidth, numbers), wide: root.getAttribute("data-doc-width") === "full" });
175
+ // The column the kit's stylesheet gives (a document's look, its 正文宽度 alone, makes it another
176
+ // than 712px: src/shared/document-look.mjs), read as the editor reads it (numbers() below); the kit's
177
+ // stylesheet stands before this script in the head, so it is in.
178
+ const column = parseFloat(getComputedStyle(root).getPropertyValue("--doc-content-max-width")) || numbers.column;
179
+ const given = layout({ ...numbers, column, width: root.clientWidth, outline, collapsed: asked.collapsed, notes, full: widthOf(asked.outlineWidth, numbers), wide: root.getAttribute("data-doc-width") === "full" });
167
180
  sheet.textContent = css(given);
168
181
  sheet.setAttribute(early, JSON.stringify({ given, asked }));
169
182
  };
@@ -186,7 +199,7 @@ export const earlyMarginScript = (slug, { notes = false } = {}) => {
186
199
 
187
200
  // ---------- on the page ----------
188
201
 
189
- const asked = { outline: false, notes: false, outlineWidth: 0 }; // what the outline and 批注 ask for
202
+ const asked = { outline: false, notes: false, outlineWidth: 0, collapsed: false }; // what the outline and 批注 ask for
190
203
  const listeners = new Set();
191
204
  let now = null; // the layout given last
192
205
  let request = null; // what it was given for: { outline, wide } (the outline's width keeps the form laid out)
@@ -311,14 +324,16 @@ function start() {
311
324
  addEventListener("resize", resized);
312
325
  // The page's width changes without the window's too: its scrollbar comes or goes.
313
326
  new ResizeObserver(resized).observe(document.documentElement);
314
- // A report read full width, or no longer.
315
- new MutationObserver(() => apply()).observe(document.documentElement, { attributes: true, attributeFilter: ["data-doc-width"] });
327
+ // A report read full width, or no longer; the document's look changed (its column's width with its
328
+ // 正文宽度, never its 字体大小: the kit's ui.js puts it on the root, src/shared/document-look.mjs).
329
+ new MutationObserver(() => apply()).observe(document.documentElement, { attributes: true, attributeFilter: ["data-doc-width", ...Object.values(LOOK_ATTRIBUTES)] });
316
330
  }
317
331
 
318
332
  /**
319
333
  * Ask for room: { outline } (whether it shows), { outlineWidth } (its width in full as the reader
320
- * chose it, px; 0: the default), { notes } (whether the page has 批注: the column slides when that
321
- * changes it). The layout is given at once and again whenever the page's width changes it.
334
+ * chose it, px; 0: the default), { collapsed } (the reader collapsed it to its strip), { notes }
335
+ * (whether the page has 批注: the column slides when that changes it). The layout is given at once and
336
+ * again whenever the page's width changes it.
322
337
  */
323
338
  export function setMargin(part) {
324
339
  start();
@@ -331,7 +346,13 @@ export function setMargin(part) {
331
346
  /** The layout given last ({ dock, outline, room, side, left, right, column, cards, cardRoom, floor }; see marginLayout). */
332
347
  export const margin = () => now ?? layoutFor(isWide(), null);
333
348
 
334
- /** How far right of the column what stands in its right room may reach, by `layout` (marginLayout): short of the outline's guide and its grip, or of its strip, or of the window's edge. */
349
+ /**
350
+ * Whether the page's room gives the outline its list in full now, the reader's collapse aside: what a
351
+ * click on the guide in the strip's card would bring (the whole width needed, as for any strip going up).
352
+ */
353
+ export const listFits = () => marginLayout({ ...MARGIN, width: pageWidth(), ...numbers(), ...asked, outline: true, collapsed: false, full: outlineWidth(asked.outlineWidth, MARGIN), wide: isWide(), was: null }).outline === "full";
354
+
355
+ /** How far right of the column what stands in its right room may reach, by `layout` (marginLayout): MARGIN.grip short of the outline's list (its guide and grip further in), or of its strip, or of the window's edge. */
335
356
  export const rightRoom = ({ right, room } = margin()) => right - room - MARGIN.grip;
336
357
 
337
358
  /** Call `listener(layout)` whenever the layout changes (the outline's form or width, the column's least rooms, the cards' width). */
@@ -1,7 +1,7 @@
1
1
  import { Z } from "../../shared/layers.mjs";
2
2
  import { addStyle, element, mount } from "./ui.mjs";
3
3
  import { hideTooltip, tooltip } from "./tooltip.mjs";
4
- import { svg } from "../components/icons.mjs";
4
+ import { SECTION_ICON, svg } from "../components/icons.mjs";
5
5
  import { t } from "../../shared/i18n/index.mjs";
6
6
 
7
7
  // The editor's menu card, used by the "/" menu (slash-menu/: block types, filtered as the reader
@@ -12,16 +12,31 @@ import { t } from "../../shared/i18n/index.mjs";
12
12
  // the page with a block selected). A press on a row never moves the caret. Colors follow the
13
13
  // report's light or dark theme (light-dark()). A row may open a submenu (Notion's 颜色 ›): a second
14
14
  // card right of it, opened by resting on the row, a click, Enter or →, and closed by ←, by resting
15
- // on another row, or with the menu; while it is open from the keys they move in it.
15
+ // on another row, or with the menu; while it is open from the keys they move in it. A row that both
16
+ // runs and opens a submenu (a split row: a table's 一键优化 ›) is two parts side by side, each with
17
+ // its own grey under the pointer: its words, which a click or Enter runs, and its › at its right,
18
+ // which opens the submenu (resting on it, a click, or →); resting on its words opens nothing.
19
+ //
20
+ // A card is as wide as its rows (Notion's menus: 180px at least), up to FIT.most, past which a row's
21
+ // words are cut by an ellipsis, so a short submenu (一键优化 ›: 自动调整列宽, 识别数字列; 对齐 ›) is no
22
+ // wider than its words; it once took 265px whatever it held. A card whose rows change as it is used
23
+ // keeps the one width its caller gives (`width`: the block menu's grid, the "/" menu, a code block's
24
+ // languages), or SEARCHING when it has a search field and gives none, so it never jumps as rows come
25
+ // and go.
26
+
27
+ const FIT = { least: 180, most: 320 }; // a card with no width given: as wide as its rows, between these
28
+ const SEARCHING = 240; // a card with a search field and no width given: its rows filtered as typed
16
29
 
17
30
  const STYLE = `
18
31
  .ge-pop {
19
- position: fixed; left: 0; top: 0; z-index: ${Z.menu}; width: 265px; max-width: calc(100vw - 16px); max-height: calc(100vh - 24px);
32
+ position: fixed; left: 0; top: 0; z-index: ${Z.menu}; width: max-content; min-width: ${FIT.least}px; max-width: min(${FIT.most}px, calc(100vw - 16px)); max-height: calc(100vh - 24px);
20
33
  overflow-x: hidden; overflow-y: auto; padding: 4px 0; border-radius: 10px; background: var(--ge-bg); color: var(--ge-tx);
21
34
  box-shadow: var(--ge-shadow-pop);
22
35
  font: 14px/1.2 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI Variable Display", "Segoe UI", Helvetica, "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
23
36
  user-select: none; scrollbar-width: thin; scrollbar-color: var(--ge-line-strong) transparent; animation: ge-pop-in .12s ease-out;
24
37
  }
38
+ /* A width given (data-sized: opts.width, placed by place()): that width, whatever the rows hold. */
39
+ .ge-pop[data-sized] { min-width: 0; max-width: calc(100vw - 16px); }
25
40
  @keyframes ge-pop-in { from { opacity: 0; transform: scale(.98); } }
26
41
  @media (prefers-reduced-motion: reduce) { .ge-pop { animation: none; } }
27
42
  .ge-pop-title { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: var(--ge-tx2); font-size: 12px; font-weight: 500; line-height: 14px; }
@@ -52,9 +67,21 @@ const STYLE = `
52
67
  .ge-pop-arrow { color: var(--ge-tx3); }
53
68
  .ge-pop-arrow svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
54
69
  .ge-pop-row[aria-expanded="true"] { background: var(--ge-hover); }
70
+ /* A split row (run and submenu both): its words (.ge-pop-main) and its › (.ge-pop-more, 32px wide:
71
+ the › where another submenu row has its own) side by side, 2px apart, each grey on its own: the words
72
+ when the row is highlighted and the pointer is not on the ›, the › under the pointer and while its
73
+ submenu is open. The row itself takes no grey. */
74
+ .ge-pop-row[data-split] { gap: 2px; padding: 0; }
75
+ .ge-pop-row[data-split][data-active], .ge-pop-row[data-split][aria-expanded="true"] { background: transparent; }
76
+ .ge-pop-main { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; align-self: stretch; padding: 0 8px; border-radius: 6px; }
77
+ .ge-pop-more { flex: none; display: flex; align-items: center; justify-content: center; width: 32px; align-self: stretch; border-radius: 6px; }
78
+ .ge-pop-row[data-split][data-active]:not([data-more]) > .ge-pop-main,
79
+ .ge-pop-row[data-split][data-more] > .ge-pop-more,
80
+ .ge-pop-row[data-split][aria-expanded="true"] > .ge-pop-more { background: var(--ge-hover); }
55
81
  .ge-pop-check svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
56
82
  .ge-pop-sep { height: 1px; margin: 3px 12px 4px; background: var(--ge-line); }
57
- .ge-pop-note { padding: 0 12px 6px 40px; color: var(--ge-tx3); font-size: 12px; line-height: 1.5; }
83
+ /* A note wraps at the card's width and never widens it (no width of its own: contain). */
84
+ .ge-pop-note { contain: inline-size; padding: 0 12px 6px 40px; color: var(--ge-tx3); font-size: 12px; line-height: 1.5; }
58
85
  .ge-pop-empty { padding: 8px 12px; color: var(--ge-tx3); }
59
86
  /* What a row makes, shown beside it under the pointer (a component package's preview.svg). */
60
87
  .ge-pop-preview { display: block; width: 240px; max-height: 180px; border-radius: 4px; object-fit: contain; background: var(--ge-bg); }
@@ -63,7 +90,7 @@ const STYLE = `
63
90
  .ge-pop-search input:focus { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ge-tx3) 60%, transparent); }
64
91
  .ge-pop-search input::placeholder { color: var(--ge-tx3); }
65
92
  /* Rows in a card that is there already (inside: a link card's pages): under what stands in it, as wide as it, no card of their own. */
66
- .ge-pop[data-inside] { position: static; flex: 0 0 100%; width: auto; max-width: none; padding: 4px 0 0; border-radius: 0; background: none; box-shadow: none; animation: none; }
93
+ .ge-pop[data-inside] { position: static; flex: 0 0 100%; width: auto; min-width: 0; max-width: none; padding: 4px 0 0; border-radius: 0; background: none; box-shadow: none; animation: none; }
67
94
  .ge-pop[data-inside] .ge-pop-row { width: 100%; margin-inline: 0; }
68
95
  `;
69
96
 
@@ -78,8 +105,7 @@ export const ICONS = {
78
105
  outer: svg('<rect x="3.25" y="3.25" width="13.5" height="13.5" rx="2.5" stroke-dasharray="2.2 2.2"/><rect x="6.5" y="6.5" width="7" height="7" rx="1.25"/>'),
79
106
  section: svg(`${SECTION_LINES}<path d="M14.5 12v5M12 14.5h5"/>`),
80
107
  dissolve: svg(`${SECTION_LINES}<path d="m12.75 12.75 3.5 3.5M16.25 12.75l-3.5 3.5"/>`), // section's plus, turned 45°
81
- // Blocks gathered into a new section: their lines between brackets.
82
- group: svg('<path d="M6 3.75h-.75a2 2 0 0 0-2 2v8.5a2 2 0 0 0 2 2H6M14 3.75h.75a2 2 0 0 1 2 2v8.5a2 2 0 0 1-2 2H14"/><path d="M7.25 7.75h5.5M7.25 12.25h5.5"/>'),
108
+ group: SECTION_ICON, // blocks gathered into a new section: their lines between brackets (components/icons.mjs)
83
109
  center: svg('<rect x="5.25" y="4.25" width="9.5" height="11.5" rx="1.75"/><path d="M2.75 4v12M17.25 4v12"/>'),
84
110
  full: svg('<rect x="3.25" y="4.25" width="13.5" height="11.5" rx="1.75"/><path d="m6.5 10h7M6.5 10l1.75-1.75M6.5 10l1.75 1.75M13.5 10l-1.75-1.75M13.5 10l-1.75 1.75"/>'),
85
111
  reset: svg('<path d="M4.75 6.75A6 6 0 1 1 4 10.5"/><path d="M4.5 3.5v3.5H8"/>'),
@@ -105,11 +131,13 @@ export const menuOpen = () => opened.size > 0;
105
131
  * it off; preview: { src, says }, a picture of what the row makes, where it has one, and a line on it,
106
132
  * shown at the row's right while the pointer rests on it, notion-lite's row tooltip) (or { label, icon?, submenu:
107
133
  * () => items, width?, search? } opening a submenu, with a search field at its top when `search` is
108
- * given, as `opts.search` below, and `search.empty` the words shown when nothing is found), titles
134
+ * given, as `opts.search` below, and `search.empty` the words shown when nothing is found; a row
135
+ * with both `run` and `submenu` is a split row: its words run, its › opens the submenu), titles
109
136
  * { title, icon? }, separators { separator: true } and grids { node, cells, columns } (type-grid.mjs: the
110
137
  * node shown as it is, its cells, buttons that act on their own click, highlighted as rows are).
111
- * opts: { label, empty, highlight, onClose, around, side, width } — width: the card's, in px, instead
112
- * of 265 (the block menu: as wide as its grid of types). around: the rect of the block a handle
138
+ * opts: { label, empty, highlight, onClose, around, side, width } — width: the card's, in px, whatever
139
+ * its rows hold (the block menu: as wide as its grid of types), where without it the card is as wide
140
+ * as its rows (FIT; SEARCHING with a search field). around: the rect of the block a handle
113
141
  * (the anchor) stands for; the menu keeps clear of that block: left of it beside the handle, else
114
142
  * below or above it (scrolling inside when only part of it fits), and over it only when the block
115
143
  * leaves no room at all. side: "right": beside the anchor, right of it (left of it when there is no
@@ -139,25 +167,32 @@ export function createMenu() {
139
167
  let subKeys = false; // opened from the keys: they move in it
140
168
  let subTimer = 0;
141
169
  let pointedRow = null;
170
+ let pointedMore = false; // the pointer on a split row's › (not on its words)
142
171
 
143
172
  // The pointer over a row highlights it. In a menu that starts with nothing highlighted (the block
144
173
  // menu), leaving the row takes the highlight off again, as :hover would; a row the keys moved to
145
- // stays, and so does the "/" menu's, the row Enter runs.
146
- const point = (row) => {
174
+ // stays, and so does the "/" menu's, the row Enter runs. On a split row, `more`: the pointer is on
175
+ // its › (data-more on the row, which greys the › instead of the words), the part that opens.
176
+ const point = (row, more = false) => {
147
177
  highlight(rows.indexOf(row), false);
148
178
  pointed = true;
149
- if (row === pointedRow) return;
179
+ if (row.split) row.toggleAttribute("data-more", more);
180
+ if (row === pointedRow && more === pointedMore) return;
150
181
  pointedRow = row;
182
+ pointedMore = more;
151
183
  clearTimeout(subTimer);
152
184
  // Notion's: a submenu opens 80ms after the pointer rests on its row (200ms while another is
153
- // open), and goes 250ms after the pointer rests elsewhere.
154
- if (row.submenu && row !== subRow) subTimer = setTimeout(() => openSub(row), subRow ? 200 : 80);
155
- else if (!row.submenu && subRow) subTimer = setTimeout(closeSub, 250);
185
+ // open), and goes 250ms after the pointer rests elsewhere (a split row's words being elsewhere).
186
+ const opens = Boolean(row.submenu) && (!row.split || more);
187
+ if (opens && row !== subRow) subTimer = setTimeout(() => openSub(row), subRow ? 200 : 80);
188
+ else if (!opens && subRow) subTimer = setTimeout(closeSub, 250);
156
189
  };
157
190
  const leave = (row) => {
158
191
  if (pointed && rows[active] === row && options.highlight === false) highlight(-1);
192
+ row.removeAttribute("data-more");
159
193
  if (pointedRow !== row) return;
160
194
  pointedRow = null;
195
+ pointedMore = false;
161
196
  clearTimeout(subTimer); // passing over a row opens nothing
162
197
  if (subRow === row && !subKeys) subTimer = setTimeout(closeSub, 250); // unless the pointer goes into it
163
198
  };
@@ -180,12 +215,17 @@ export function createMenu() {
180
215
  children.push(item.node);
181
216
  } else {
182
217
  const row = element("button", { type: "button", class: "ge-pop-row", role: "menuitem" });
218
+ // A split row's words go in a part of their own, its › in another (CSS above).
219
+ row.split = Boolean(item.run && item.submenu);
220
+ const words = row.split ? element("span", { class: "ge-pop-main" }) : row;
221
+ if (row.split) row.setAttribute("data-split", "");
183
222
  if (item.long) row.setAttribute("data-long", "");
184
- if (options.icons !== false) row.append(element("span", { class: "ge-pop-icon", html: item.icon ?? "" }));
185
- row.append(element("span", { class: "ge-pop-label", text: item.label }));
186
- if (item.hint) row.append(element("span", { class: "ge-pop-hint", text: item.hint }));
223
+ if (options.icons !== false) words.append(element("span", { class: "ge-pop-icon", html: item.icon ?? "" }));
224
+ words.append(element("span", { class: "ge-pop-label", text: item.label }));
225
+ if (item.hint) words.append(element("span", { class: "ge-pop-hint", text: item.hint }));
226
+ if (row.split) row.append(words, element("span", { class: "ge-pop-more", html: `<span class="ge-pop-arrow">${ICONS.chevron}</span>` }));
227
+ else if (item.submenu) row.append(element("span", { class: "ge-pop-arrow", html: ICONS.chevron }));
187
228
  if (item.submenu) {
188
- row.append(element("span", { class: "ge-pop-arrow", html: ICONS.chevron }));
189
229
  row.setAttribute("aria-haspopup", "menu");
190
230
  row.submenu = item.submenu;
191
231
  row.subWidth = item.width;
@@ -199,9 +239,10 @@ export function createMenu() {
199
239
  row.disabled = Boolean(item.disabled || item.current);
200
240
  row.run = item.run;
201
241
  row.addEventListener("mousedown", (event) => event.preventDefault());
202
- row.addEventListener("pointermove", () => point(row));
242
+ const onMore = (event) => row.split && Boolean(event.target.closest?.(".ge-pop-more"));
243
+ row.addEventListener("pointermove", (event) => point(row, onMore(event)));
203
244
  row.addEventListener("pointerleave", () => leave(row));
204
- row.addEventListener("click", () => run(row));
245
+ row.addEventListener("click", (event) => run(row, onMore(event)));
205
246
  rows.push(row);
206
247
  children.push(row);
207
248
  if (item.note) children.push(element("div", { class: "ge-pop-note", text: item.note }));
@@ -335,9 +376,11 @@ export function createMenu() {
335
376
  sub?.close();
336
377
  }
337
378
 
338
- function run(row) {
379
+ // A row's work: its submenu opened (a split row's only from its ›: `more`), a grid's cell's own
380
+ // click, or the menu closed and its action run (Enter and a click on a split row's words).
381
+ function run(row, more = false) {
339
382
  if (!row || row.disabled) return false;
340
- if (row.submenu) {
383
+ if (row.submenu && (!row.split || more)) {
341
384
  openSub(row, true);
342
385
  return true;
343
386
  }
@@ -391,10 +434,11 @@ export function createMenu() {
391
434
  const menu = {
392
435
  open(items, rect, opts = {}) {
393
436
  close();
394
- options = opts;
437
+ options = opts.search && !opts.width ? { ...opts, width: SEARCHING } : opts;
395
438
  anchor = rect;
396
439
  opened.add(menu);
397
440
  node = element("div", { class: "ge-pop", role: "menu", "aria-label": opts.label ?? t("菜单") });
441
+ if (options.width) node.setAttribute("data-sized", "");
398
442
  if (opts.inside) {
399
443
  node.setAttribute("data-inside", "");
400
444
  opts.inside.append(node);
@@ -30,6 +30,30 @@ export function commonRun(xs, ys) {
30
30
  return [...head, ...middle, ...tail];
31
31
  }
32
32
 
33
+ // A word diff of two texts, the reader's version against the file's (the conflict card's), or an
34
+ // Agent's baseline against the page (changes/): runs of { tag, text }, tag null
35
+ // for words both have, "del" for the reader's alone and "ins" for the file's alone (an ins before
36
+ // the del it stands beside). Words are runs of letters and digits, spaces, single other characters
37
+ // (each Chinese character on its own); at most 1500 words of each (commonRun above finds what is shared).
38
+ export function wordDiff(mine, theirs) {
39
+ const tokens = (text) => (text.match(/[A-Za-z0-9]+|\s+|[^A-Za-z0-9\s]/gu) ?? []).slice(0, 1500);
40
+ const a = tokens(mine);
41
+ const b = tokens(theirs);
42
+ const runs = [];
43
+ const add = (tag, text) => {
44
+ if (runs.at(-1)?.tag === tag) runs.at(-1).text += text;
45
+ else runs.push({ tag, text });
46
+ };
47
+ let [i, j] = [0, 0];
48
+ for (const [x, y] of [...commonRun(a, b), [a.length, b.length]]) {
49
+ while (j < y) add("ins", b[j++]);
50
+ while (i < x) add("del", a[i++]);
51
+ if (x < a.length) add(null, a[i++]);
52
+ j++;
53
+ }
54
+ return runs;
55
+ }
56
+
33
57
  // ---------- merging a block both sides changed ----------
34
58
 
35
59
  // Tokens of a block's inline HTML: a tag, an entity, a word, a run of spaces, or any other single
@@ -24,7 +24,7 @@ import { routine } from "./quiet.mjs";
24
24
  // so that the box has the whole window to be as wide as.
25
25
 
26
26
  const STYLE = `
27
- .ge-toast { position: fixed; left: 0; right: 0; bottom: 24px; z-index: ${Z.toast}; display: flex; align-items: center; gap: 10px; box-sizing: border-box; width: fit-content; max-width: min(640px, calc(100vw - 32px)); margin: 0 auto; padding: 8px 14px; border-radius: 8px; background: var(--ge-toast-bg); color: #f0efed; box-shadow: var(--ge-shadow); font: 14px/1.45 var(--doc-font, system-ui, sans-serif); opacity: 0; transform: translateY(10px); pointer-events: none; transition: opacity .18s, transform .18s; }
27
+ .ge-toast { position: fixed; left: 0; right: 0; bottom: 24px; z-index: ${Z.toast}; display: flex; align-items: center; gap: 10px; box-sizing: border-box; width: fit-content; max-width: min(640px, calc(100vw - 32px)); margin: 0 auto; padding: 8px 14px; border-radius: 8px; background: var(--ge-toast-bg); color: #f0efed; box-shadow: var(--ge-shadow); font: 14px/1.45 var(--doc-sans-font, var(--doc-font, system-ui, sans-serif)); opacity: 0; transform: translateY(10px); pointer-events: none; transition: opacity .18s, transform .18s; }
28
28
  .ge-toast[data-show] { opacity: 1; transform: none; }
29
29
  .ge-toast[data-tone="error"]::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: #ff6b6b; }
30
30
  `;