@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
@@ -1,6 +1,6 @@
1
1
  import { TYPE_COMPONENT, blocksHidden } from "../components/registry.mjs";
2
2
 
3
- // What the menus that make a block offer of the reader's 设置 › 组件 (docs/workbench.md): the one place
3
+ // What the menus that make a block offer of the reader's 设置 › 组件列表 (docs/workbench.md): the one place
4
4
  // that hears what is hidden, gallery.json's as the page's map holds it (`hidden`), then as its event
5
5
  // stream says it (`doc-settings-changed`), which costs the page nothing. A block type goes with its
6
6
  // component. "/", 在下方新建 (both slash-menu/ typeItems) and the handle's grid (block-menu/) read it; no
@@ -1,23 +1,24 @@
1
1
  // What is folded in the outline, kept for each report (localStorage, so the page the workbench shows
2
2
  // again after an Agent's write, which is a new page, folds what the reader folded): a node's key (a
3
- // heading's block id, a tab's area and tab), the reports folded last kept and the oldest let go.
3
+ // heading's block id, a tab's area and tab), the reports folded last kept and the oldest let go. The
4
+ // headings folded on the page (fold/) are kept the same way under a key of their own (`key`).
4
5
 
5
6
  import { KEYS } from "../../../shared/renamed-storage.mjs";
6
7
 
7
8
  export const FOLDS_KEY = KEYS.outlineFolds;
8
9
  const KEEP = 100; // reports
9
10
 
10
- function read(storage) {
11
+ function read(storage, key) {
11
12
  try {
12
- const value = JSON.parse(storage.get(FOLDS_KEY) ?? "null");
13
+ const value = JSON.parse(storage.get(key) ?? "null");
13
14
  if (value && typeof value === "object" && value.folds && typeof value.folds === "object") return value;
14
15
  } catch {}
15
16
  return { folds: {} };
16
17
  }
17
18
 
18
- /** The keys folded in report `slug` (`storage`: { get, set }, page/storage.mjs). */
19
- export function foldsOf(storage, slug) {
20
- const keys = read(storage).folds[slug];
19
+ /** The keys folded in report `slug` (`storage`: { get, set }, page/storage.mjs), kept under `key`. */
20
+ export function foldsOf(storage, slug, key = FOLDS_KEY) {
21
+ const keys = read(storage, key).folds[slug];
21
22
  return new Set(Array.isArray(keys) ? keys.filter((key) => typeof key === "string") : []);
22
23
  }
23
24
 
@@ -25,14 +26,14 @@ export function foldsOf(storage, slug) {
25
26
  * Keep `folded` for report `slug`: only the keys the page still has (`present`), the report the one
26
27
  * folded last; none folded forgets the report. At most KEEP reports, the oldest going first.
27
28
  */
28
- export function keepFolds(storage, slug, folded, present = null) {
29
- const value = read(storage);
30
- const keys = [...folded].filter((key) => !present || present.has(key));
29
+ export function keepFolds(storage, slug, folded, present = null, key = FOLDS_KEY) {
30
+ const value = read(storage, key);
31
+ const keys = [...folded].filter((each) => !present || present.has(each));
31
32
  delete value.folds[slug]; // put back last: the report folded last
32
33
  if (keys.length) value.folds[slug] = keys;
33
34
  const slugs = Object.keys(value.folds);
34
35
  for (const old of slugs.slice(0, Math.max(0, slugs.length - KEEP))) delete value.folds[old];
35
- storage.set(FOLDS_KEY, JSON.stringify(value));
36
+ storage.set(key, JSON.stringify(value));
36
37
  }
37
38
 
38
39
  /**
@@ -2,15 +2,17 @@ import { IN_LAYER, Z } from "../../../shared/layers.mjs";
2
2
  import { onOutlineOpen, onPreferences, outlineOpen, preference, setPreference } from "../../../shared/preferences.mjs";
3
3
  import { t } from "../../../shared/i18n/index.mjs";
4
4
  import { SHEETS } from "../../components/kit.mjs";
5
+ import { reveal } from "../folded.mjs";
5
6
  import { task } from "../frames.mjs";
6
7
  import { cancelOnEscape, endsGesture, gestureLock } from "../gesture.mjs";
7
- import { MARGIN, onMargin, outlineMost, outlineWidth, setMargin } from "../margin.mjs";
8
+ import { MARGIN, listFits, onMargin, outlineMost, outlineWidth, setMargin } from "../margin.mjs";
8
9
  import { drawOver, shapeOf } from "../overlay-layer.mjs";
9
10
  import { onSettle, own } from "../overlays.mjs";
10
11
  import { local } from "../storage.mjs";
12
+ import { hideTooltip, tooltipAt } from "../tooltip.mjs";
11
13
  import { addStyle, element } from "../ui.mjs";
12
14
  import { carryFolds, foldsOf, keepFolds } from "./folds.mjs";
13
- import { ROW, createList } from "./list.mjs";
15
+ import { LIST, ROW, createList } from "./list.mjs";
14
16
  import { entriesOf, touchesOutline } from "./read.mjs";
15
17
  import { STRIP_TOP, createStrip } from "./strip.mjs";
16
18
  import { currentOf, followTop, glideFrom, keysOf, markedRow, nodesOf, pathTo, rowsOf, signatureOf, spyLine, treeOf } from "./tree.mjs";
@@ -27,9 +29,9 @@ import { currentOf, followTop, glideFrom, keysOf, markedRow, nodesOf, pathTo, ro
27
29
  // What it lists (read.mjs, tree.mjs): the heading blocks, nested by level, a tabs area's tabs as pills
28
30
  // with their headings under them. Its form is margin.mjs's: the list in full at the window's right
29
31
  // edge, the column in the middle of what it leaves (200px unless the reader dragged its guide: a quiet
30
- // list on the page's background, a hairline guide down its left, the current row's segment dark on
31
- // it), or, with less room, its strip (strip.mjs), which shows the list as a card while the pointer is on it, each of its
32
- // lines lighting its own row. A form that changes after the page came is faded in (the list sliding a
32
+ // list on the page's background, a hairline guide down its left close to its top level's titles, the
33
+ // current row's segment dark on it), or, with less room, its strip (strip.mjs), which shows the list
34
+ // as a card while the pointer is on it, each of its lines lighting its own row. A form that changes after the page came is faded in (the list sliding a
33
35
  // little from the right), never there all at once.
34
36
  //
35
37
  // - A click on a heading's row scrolls the page smoothly (at once with reduced motion; from more than
@@ -55,24 +57,41 @@ import { currentOf, followTop, glideFrom, keysOf, markedRow, nodesOf, pathTo, ro
55
57
  // reader scrolled it in the last 1.5s (the pointer gone after that, a row scrolled out of sight is
56
58
  // brought back). The wheel over it scrolls it, and at its ends the page.
57
59
  // - A triangle at a parent's left folds or unfolds it (a folded one always shows it; an open one while
58
- // the pointer is over the list or the keyboard is in it); what is folded is kept for the report
59
- // (folds.mjs), so the page the workbench shows again after an Agent's write folds the same, and
60
+ // the pointer is over the rows or the keyboard is in the list, the guide fading out meanwhile: each
61
+ // just short of its title, a top-level one on the guide, the titles never moving); what is folded is kept for the report (folds.mjs), so the page the workbench shows again after an Agent's write folds the same, and
60
62
  // follows a heading whose key changes (one with no block id, keyed by its text, typed in or given its
61
63
  // id as it is saved). The top level's headings are bold.
62
64
  // - Its guide line, dragged, makes the list wider or narrower (160 to 360px, as far as the room lets
63
65
  // it while the column keeps its edge room at both sides), the column staying in the middle of what
64
66
  // it leaves (margin.mjs: its two rooms giving or taking half each); let go, the width is the
65
67
  // reader's for every report (shared/preferences.mjs outlineWidth); Esc puts it back.
68
+ // - Its guide line clicked (pressed and let go before the pointer went CLICK px across, the sidebar's
69
+ // edge's rule) collapses the list to its strip, even where the room gives the list: the reader's for
70
+ // every report, as the width (outlineCollapsed, which the page's head reads too: margin.mjs). The
71
+ // guide in the strip's card clicked takes it back: the list in full where the room allows it
72
+ // (margin.mjs listFits), else the strip stays (never forced) and the list comes once there is room.
73
+ // Each guide says so in a tooltip by the pointer (a tall target), the card's why nothing opened.
66
74
  // - It follows the page as it changes (one reading a frame at most, the rows drawn again only when
67
75
  // what they show changed): headings typed, renamed, added, deleted, moved, undone, tabs renamed or
68
76
  // shown; not while an input method composes (its letters are no title yet: read once it commits).
69
77
  // It is the editor's own UI (data-doc-dev): a press on it starts no box selection, the handle
70
78
  // does not show by it, and while a block is dragged or a box drawn it lets the pointer through.
71
79
 
72
- const DOCK = { top: 52, left: MARGIN.guide, right: 8 }; // the list's room in the dock: below the page's top room, its guide 8px in (where the column's right room ends: margin.mjs), 8px short of the scrollbar
80
+ const DOCK = { top: 52, left: MARGIN.guide, right: 8 }; // the list's room in the dock: below the page's top room, from 8px in (where the column's right room ends: margin.mjs; its guide LIST.guide further in), 8px short of the scrollbar
73
81
  const CARD = 56; // the strip's card: as wide as the list chosen, and this much more
82
+ const CARD_PAD = 8; // and its list this far in from its left edge (as far in as the card's right padding)
83
+ const GUIDE_FADE = 120; // ms the guide fades out as the triangles show, and in as they go (at once with reduced motion: the kit's base.css)
84
+ // The pointer's rest after it left the rows (for the grip, or along the guide from one top-level triangle to another,
85
+ // across the grip between them) before the triangles go and the guide comes back (the grip's darker bar comes as late).
86
+ const LEAVE = 120;
87
+ // What shows the triangles and fades the guide: the pointer on a row (its triangle included), the keys in the list (the
88
+ // focus seen). The pointer on the list but on no row (its gutter, left of the grip) shows nothing: crossing it to the grip
89
+ // changes nothing.
90
+ const OVER = ":has(.ge-outline-row:hover), :focus-visible, :has(:focus-visible)";
91
+ const GRIPS = ".ge-outline-grip, .ge-outline-card-grip"; // the guide's grips (in the list's scroller, the card's)
74
92
  const ENTER = { ms: 160, slide: 8 }; // the list in full coming after the page did: faded in, sliding this far from the right
75
93
  const RESIZING = "data-ge-outline-resize"; // on the document element while the guide is dragged
94
+ const CLICK = 3; // px across the pointer goes on the guide before a press is a drag, not a click (the workbench's sidebar edge: Sidebar.tsx Resizer)
76
95
  const JUMP_TOP = 24; // a heading's top after a jump, below the window's
77
96
  const STAY = 40; // px the page moves after a jump before the heading gone to stops being the current one
78
97
  const FOLLOW_QUIET = 1500; // ms after the reader scrolled the list, it is not scrolled to the current row
@@ -105,42 +124,48 @@ const STYLE = `
105
124
  .ge-outline-scroll[data-more="up"] { -webkit-mask-image: linear-gradient(transparent, #000 24px); mask-image: linear-gradient(transparent, #000 24px); }
106
125
  .ge-outline-scroll[data-more="down"] { -webkit-mask-image: linear-gradient(#000 calc(100% - 32px), transparent); mask-image: linear-gradient(#000 calc(100% - 32px), transparent); }
107
126
  .ge-outline-scroll[data-more="up down"] { -webkit-mask-image: linear-gradient(transparent, #000 24px, #000 calc(100% - 32px), transparent); mask-image: linear-gradient(transparent, #000 24px, #000 calc(100% - 32px), transparent); }
108
- /* The guide's grip: dragged, the list wider or narrower; under the pointer (a moment after it came) and while dragged, the guide a wider, darker bar. */
109
- .ge-outline-grip { position: absolute; left: -${MARGIN.grip}px; top: 0; bottom: 24px; width: 9px; cursor: col-resize; pointer-events: auto; }
110
- .ge-outline-grip::after { content: ""; position: absolute; left: 3px; top: 4px; bottom: 4px; width: 3px; border-radius: 2px; transition: background-color 120ms ease; }
111
- .ge-outline-grip:hover::after { background: var(--ge-line-strong); transition-delay: 120ms; }
127
+ /* The guide's grip (MARGIN.grip left of its hairline, 5px right): dragged, the list wider or narrower; clicked, the list collapsed to its strip; under the pointer (a moment after it came) and while dragged, the guide a wider, darker bar. It stands in the list's scroller, placed by the frame (the card), so it lies under a top-level triangle shown (z-index): that triangle, its row's, takes its own box; the grip the rest of the guide (rows with no triangle shown, above and below one), and every one hidden (visibility: no target). */
128
+ .ge-outline-grip, .ge-outline-card-grip { position: absolute; z-index: 1; width: 9px; pointer-events: auto; }
129
+ .ge-outline-grip { left: ${LIST.guide - MARGIN.grip}px; top: 0; bottom: 24px; cursor: col-resize; }
130
+ .ge-outline-grip::after, .ge-outline-card-grip::after { content: ""; position: absolute; left: 3px; top: 4px; bottom: 4px; width: 3px; border-radius: 2px; transition: background-color 120ms ease; }
131
+ .ge-outline-grip:hover::after, .ge-outline-card-grip[data-fits]:hover::after { background: var(--ge-line-strong); transition-delay: 120ms; }
112
132
  :root[${RESIZING}] .ge-outline-grip::after { background: color-mix(in srgb, var(--ge-tx3) 70%, transparent); transition-delay: 0s; }
113
133
  :root[${RESIZING}] .ge-outline-scroll { pointer-events: none; }
114
134
  ${gestureLock(RESIZING, { cursor: "col-resize", select: true })}
115
135
 
116
- /* The list: a hairline guide at its left, the current row's segment darker on it, the lit row's blue (whole pixels: crisp at any scale). */
117
- .ge-outline-list { position: relative; padding-left: 6px; outline: none; }
118
- .ge-outline-list::before { content: ""; position: absolute; left: 0; top: 4px; bottom: 4px; width: 1px; background: var(--ge-line-strong); }
136
+ /* The list (list.mjs LIST): a hairline guide a little in from its left edge, close to the top level's titles, the current row's segment darker on it, the lit row's blue (whole pixels: crisp at any scale); the rows from right of the guide (a top-level one's fill reaching across it to take in its triangle: below). */
137
+ .ge-outline-list { position: relative; padding-left: ${LIST.row}px; outline: none; }
138
+ .ge-outline-list::before { content: ""; position: absolute; left: ${LIST.guide}px; top: 4px; bottom: 4px; width: 1px; background: var(--ge-line-strong); }
119
139
  .ge-outline-list[data-empty]::before { display: none; }
120
- .ge-outline-mark { position: absolute; left: -1px; top: 0; width: 2px; height: 16px; border-radius: 1px; background: var(--ge-tx); pointer-events: none; }
140
+ .ge-outline-mark { position: absolute; left: ${LIST.guide - 1}px; top: 0; width: 2px; height: 16px; border-radius: 1px; background: var(--ge-tx); pointer-events: none; }
121
141
  .ge-outline-mark[data-hot] { background: var(--ge-accent); }
122
142
  .ge-outline-mark[hidden] { display: none; }
123
- .ge-outline-row { position: relative; display: flex; align-items: center; gap: 2px; height: ${ROW}px; box-sizing: border-box; padding: 0 6px 0 calc(2px + var(--ge-depth, 0) * 12px); border-radius: 6px; cursor: pointer; white-space: nowrap; outline: none; }
143
+ /* While the pointer is on the rows or the keys are in the list (OVER), the triangles show and the guide (its hairline, its segments) fades out at once; the rows left, it comes back a moment later (LEAVE), so the pointer going along the guide from one top-level triangle to another, across the grip, keeps them. */
144
+ .ge-outline-list::before, .ge-outline-list > .ge-outline-mark { transition: opacity ${GUIDE_FADE}ms ease ${LEAVE}ms; }
145
+ .ge-outline-list:is(${OVER})::before, .ge-outline-list:is(${OVER}) > .ge-outline-mark { opacity: 0; transition-delay: 0s; }
146
+ .ge-outline-row { position: relative; display: flex; align-items: center; height: ${ROW}px; box-sizing: border-box; padding: 0 6px 0 calc(${LIST.text}px + var(--ge-depth, 0) * ${LIST.step}px); border-radius: 6px; cursor: pointer; white-space: nowrap; outline: none; }
124
147
  .ge-outline-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
125
- .ge-outline-row:is(:hover, :focus-visible, [data-hot]) { color: var(--ge-tx); background: var(--ge-hover); }
126
- .ge-outline-row:active { background: var(--ge-press); }
148
+ /* A row's fill (under the pointer, the keys' focus, lit from the strip, pressed) takes in its triangle's column: it starts at the row's left (2px clear of the hairline) or ${LIST.step + LIST.text - LIST.twist - LIST.gap}px left of the column, whichever is further left (that room is what a second-level row's fill leaves left of its triangle). Only a top-level row's column stands left of the row (across the guide), so only its fill reaches further, a step (LIST.step) further left, past the hairline, its triangle in the grey as a deeper one is (the user's of 2026-11-07), a leaf's as its siblings'. That part is the row's shadow, no target: the pointer there is still the list's gutter or the grip. */
149
+ .ge-outline-row { --ge-row-fill: transparent; background: var(--ge-row-fill); box-shadow: min(0px, calc((var(--ge-depth, 0) - 1) * ${LIST.step}px)) 0 var(--ge-row-fill); }
150
+ .ge-outline-row:is(:hover, :focus-visible, [data-hot]) { color: var(--ge-tx); --ge-row-fill: var(--ge-hover); }
151
+ .ge-outline-row:active { --ge-row-fill: var(--ge-press); }
127
152
  .ge-outline-row[data-current] { color: var(--ge-tx); font-weight: ${MEDIUM}; }
128
153
  /* The top level's headings bold: at the top, or right under a tab (its pill keeps its own look). */
129
154
  .ge-outline-row[data-top] { font-weight: 600; }
130
- /* The triangle at a parent's left, in a column of its own (a leaf keeps the room): a folded one always shows (pointing right), an open one (pointing down) while the pointer is over the list or the keyboard is in it (the focus seen: a fold by the pointer leaves the focus on its row, unseen). No grey under the pointer, only while pressed. */
131
- .ge-outline-twist, .ge-outline-slot { flex: none; width: 16px; height: 16px; }
132
- .ge-outline-twist { border-radius: 4px; color: var(--ge-tx3); }
133
- .ge-outline-twist svg { fill: currentColor; transition: transform 120ms ease; }
155
+ /* The triangle at a parent's left, out of the row's flow (its title where a leaf's is), in its indentation just short of its title, at every level the same: a top-level one on the guide, over its grip (above it: z-index). A folded one always shows (pointing right), an open one (pointing down) while the pointer is on the rows or the keys are in the list (the focus seen: a fold by the pointer leaves the focus on its row, unseen; the timing above). No grey under the pointer, only while pressed. */
156
+ .ge-outline-twist { position: absolute; z-index: 2; top: ${(ROW - 16) / 2}px; left: calc(var(--ge-depth, 0) * ${LIST.step}px + ${LIST.text - LIST.twist - LIST.gap}px); width: ${LIST.twist}px; height: 16px; border-radius: 4px; color: var(--ge-tx3); }
157
+ .ge-outline-twist svg { width: 16px; margin-left: ${(LIST.twist - 16) / 2}px; fill: currentColor; transition: transform 120ms ease; }
134
158
  .ge-outline-twist:hover { color: var(--ge-tx2); }
135
159
  .ge-outline-twist:active { background: var(--ge-press); }
136
160
  .ge-outline-row[aria-expanded="true"] .ge-outline-twist svg { transform: rotate(90deg); }
137
- .ge-outline-list:not(:hover, :focus-visible, :has(:focus-visible)) .ge-outline-row[aria-expanded="true"] .ge-outline-twist { visibility: hidden; }
161
+ .ge-outline-row[aria-expanded="true"] .ge-outline-twist { visibility: hidden; transition: visibility 0s linear ${LEAVE}ms; }
162
+ .ge-outline-list:is(${OVER}) .ge-outline-row[aria-expanded="true"] .ge-outline-twist { visibility: visible; transition-delay: 0s; }
138
163
  /* A tabs area's tab: its name as a pill at the rows' size (its text in line with the rows beside it), the tab shown filled as the page's pills, the others ringed by a hairline (plain, they read as less than their own headings). */
139
164
  .ge-outline-row[data-kind="tab"] .ge-outline-text { margin-left: -4px; }
140
165
  .ge-outline-tab { display: inline-block; max-width: 100%; box-sizing: border-box; height: 22px; padding: 0 6px; border-radius: 6px; overflow: hidden; text-overflow: ellipsis; vertical-align: top; font-weight: ${MEDIUM}; line-height: 22px; box-shadow: inset 0 0 0 1px var(--ge-line-strong); }
141
166
  .ge-outline-row[data-shown] .ge-outline-tab { background: var(--ge-hover); color: var(--ge-tx); box-shadow: none; }
142
167
  .ge-outline-row[data-shown]:is(:hover, :focus-visible, [data-hot]) .ge-outline-tab { background: transparent; }
143
- .ge-outline-empty { padding: 4px 0 4px 8px; color: var(--ge-tx3); white-space: normal; }
168
+ .ge-outline-empty { padding: 4px 0 4px ${LIST.text}px; color: var(--ge-tx3); white-space: normal; }
144
169
 
145
170
  /* The strip: short lines, each its own target: the one under the pointer blue, longer and thicker. */
146
171
  .ge-outline-strip { position: fixed; top: 0; right: 0; bottom: 0; z-index: ${Z.outline}; width: ${MARGIN.strip}px; pointer-events: none; }
@@ -152,10 +177,13 @@ const STYLE = `
152
177
  .ge-outline-ticks i[data-hot] { width: calc(var(--ge-tick, 16px) + 6px); background: var(--ge-accent); transform: scaleY(1.5); }
153
178
  .ge-outline-none { display: block; width: 16px; height: 16px; color: var(--ge-tx3); }
154
179
  /* Its card: the list over the page beside the strip, its top level with the strip's lines, the menus' surface; the list scrolls inside it, faded at its ends as in full. */
155
- .ge-outline-card { position: fixed; z-index: ${Z.menu}; top: ${STRIP_TOP}px; right: ${MARGIN.strip + 4}px; width: ${MARGIN.full + CARD}px; max-height: calc(100vh - ${STRIP_TOP + 16}px); box-sizing: border-box; display: flex; flex-direction: column; padding: 8px 8px 8px 12px; border-radius: 10px; background: var(--ge-bg); box-shadow: var(--ge-shadow-pop); }
180
+ .ge-outline-card { position: fixed; z-index: ${Z.menu}; top: ${STRIP_TOP}px; right: ${MARGIN.strip + 4}px; width: ${MARGIN.full + CARD}px; max-height: calc(100vh - ${STRIP_TOP + 16}px); box-sizing: border-box; display: flex; flex-direction: column; padding: 8px 8px 8px ${CARD_PAD}px; border-radius: 10px; background: var(--ge-bg); box-shadow: var(--ge-shadow-pop); }
181
+ /* Its guide's grip (about the hairline, CARD_PAD + LIST.guide in; in the card's scroller as the dock's is in its own, under a top-level triangle shown): clicked, the list in full again; a hand and the darker bar only where the room allows it (data-fits). */
182
+ .ge-outline-card-grip { left: ${CARD_PAD + LIST.guide - MARGIN.grip}px; top: 8px; bottom: 8px; }
183
+ .ge-outline-card-grip[data-fits] { cursor: pointer; }
156
184
 
157
185
  /* While a block is dragged, a box drawn, text selected across blocks or something resized, the outline lets the pointer through. */
158
- :root:is([data-ge-drag], [data-ge-marquee], [data-ge-cross], [data-ge-resize], [data-ge-gap-drag], [data-ge-tdrag], [data-ge-tresize], [data-ge-tcells]) :is(.ge-outline-scroll, .ge-outline-ticks, .ge-outline-grip) { pointer-events: none; }
186
+ :root:is([data-ge-drag], [data-ge-marquee], [data-ge-cross], [data-ge-resize], [data-ge-gap-drag], [data-ge-tdrag], [data-ge-tresize], [data-ge-tcells]) :is(.ge-outline-scroll, .ge-outline-ticks, .ge-outline-grip, .ge-outline-card-grip) { pointer-events: none; }
159
187
 
160
188
  /* A heading lit for a moment after a jump: its fill's shape, a quiet gray. */
161
189
  .ge-outline-flash { z-index: ${IN_LAYER.tint}; pointer-events: none; background: var(--ge-outline-flash); }
@@ -178,6 +206,7 @@ export function createOutline({ slug }) {
178
206
 
179
207
  let on = outlineOpen(slug); // this report's switch (off until switched on here)
180
208
  let chosen = preference("outlineWidth"); // the list's width in full as the reader dragged it (0: the default)
209
+ let collapsed = preference("outlineCollapsed"); // the reader collapsed the list to its strip (a click on its guide)
181
210
  let form = null; // the form shown: "full", "strip" or null
182
211
  let tree = [];
183
212
  let rows = [];
@@ -199,9 +228,12 @@ export function createOutline({ slug }) {
199
228
  let flashing = null; // { node, heading }: the heading lit after a jump
200
229
 
201
230
  const list = createList({ jump: go, fold: toggle, escape: giveBack, page: (direction) => page(direction) });
202
- const scroller = element("div", { class: "ge-outline-scroll" }, list.node);
231
+ // The grip in the scroller, before the list, placed by the frame (it neither scrolls nor counts in what
232
+ // scrolls): in one stacking context with the rows' triangles (the scroller's, masked at its ends), so a
233
+ // top-level one shown stands over it (STYLE).
203
234
  const grip = element("div", { class: "ge-outline-grip", role: "separator", "aria-orientation": "vertical", "aria-label": t("拖动调整目录宽度") });
204
- const dock = element("nav", { class: "ge-outline", "aria-label": t("目录"), "data-doc-dev": "", hidden: "" }, element("div", { class: "ge-outline-frame" }, scroller, grip));
235
+ const scroller = element("div", { class: "ge-outline-scroll" }, grip, list.node);
236
+ const dock = element("nav", { class: "ge-outline", "aria-label": t("目录"), "data-doc-dev": "", hidden: "" }, element("div", { class: "ge-outline-frame" }, scroller));
205
237
  const strip = createStrip({
206
238
  list,
207
239
  opened: () => frames.wake(),
@@ -318,9 +350,11 @@ export function createOutline({ slug }) {
318
350
  }
319
351
  }
320
352
 
353
+ // The grips stand in the scrollers but are not the list (placed by the frame, the card): the wheel over one scrolls the page.
354
+ const onList = (target) => holds(target) && !target.closest?.(GRIPS) ? target.closest?.(".ge-outline-scroll") ?? null : null;
321
355
  for (const box of [scroller, strip.scroll]) {
322
356
  box.addEventListener("scroll", edges, { passive: true });
323
- for (const type of ["wheel", "touchmove"]) box.addEventListener(type, () => { listScrolled = Date.now(); }, { passive: true });
357
+ for (const type of ["wheel", "touchmove"]) box.addEventListener(type, (event) => { if (onList(event.target)) listScrolled = Date.now(); }, { passive: true });
324
358
  }
325
359
  for (const node of [dock, strip.strip, strip.card]) {
326
360
  node.addEventListener("pointerenter", () => { pointerIn = true; });
@@ -454,7 +488,7 @@ export function createOutline({ slug }) {
454
488
  const area = node.ref?.closest(SHEETS.match);
455
489
  if (!area) return;
456
490
  const key = node.ref.getAttribute(SHEETS.tabKey);
457
- globalThis.DocKit.reveal([...area.children].find((child) => child.getAttribute(SHEETS.panelKey) === key) ?? area);
491
+ reveal([...area.children].find((child) => child.getAttribute(SHEETS.panelKey) === key) ?? area);
458
492
  chosenKey = null;
459
493
  landedAt = null;
460
494
  jumping = true;
@@ -462,7 +496,7 @@ export function createOutline({ slug }) {
462
496
  } else {
463
497
  const heading = node.ref;
464
498
  if (!heading?.isConnected) return;
465
- globalThis.DocKit.reveal(heading);
499
+ reveal(heading); // a fold hiding it opened, not its own (fold/)
466
500
  chosenKey = node.key;
467
501
  landedAt = null;
468
502
  jumping = true;
@@ -512,8 +546,8 @@ export function createOutline({ slug }) {
512
546
  // The wheel over the list scrolls it, and at its end in the wheel's way the page: then it is the reader's.
513
547
  const listEnd = (box, down) => (down ? box.scrollTop + box.clientHeight >= box.scrollHeight - 1 : box.scrollTop <= 0);
514
548
  addEventListener("wheel", (event) => {
515
- const box = holds(event.target) ? event.target.closest(".ge-outline-scroll") : null;
516
- if (!holds(event.target) || !box || listEnd(box, event.deltaY > 0)) takeOver();
549
+ const box = onList(event.target);
550
+ if (!box || listEnd(box, event.deltaY > 0)) takeOver();
517
551
  }, { passive: true, capture: true });
518
552
  addEventListener("touchstart", (event) => { if (!holds(event.target)) takeOver(); }, { passive: true, capture: true });
519
553
  addEventListener("keydown", (event) => {
@@ -604,13 +638,19 @@ export function createOutline({ slug }) {
604
638
  cardWidth();
605
639
 
606
640
  // Shown in the form the room gives it as the editor starts, at once (it comes with the page), then
607
- // as the room changes (the window, the list's width) and switched on or off by the
608
- // reader, faded in (switched off, gone at once). Another page of the origin dragging the list's width
609
- // (every report's): followed. This report's outline switched by another page (the workbench's top bar,
641
+ // as the room changes (the window, the list's width, the reader's collapse) and switched on or off by
642
+ // the reader, faded in (switched off, gone at once). Another page of the origin dragging the list's
643
+ // width or collapsing it (every report's): followed. This report's outline switched by another page (the workbench's top bar,
610
644
  // the report in another tab): followed; another report's switch changes nothing here.
611
- show(setMargin({ outline: on, outlineWidth: chosen }));
645
+ show(setMargin({ outline: on, outlineWidth: chosen, collapsed }));
612
646
  onMargin((layout) => show(layout, { fade: true }));
613
647
  onPreferences(() => {
648
+ const fold = preference("outlineCollapsed");
649
+ if (fold !== collapsed) {
650
+ endSizing(false);
651
+ collapsed = fold;
652
+ setMargin({ collapsed });
653
+ }
614
654
  const width = preference("outlineWidth");
615
655
  if (width === chosen || sizing) return;
616
656
  chosen = width;
@@ -624,37 +664,42 @@ export function createOutline({ slug }) {
624
664
  show(setMargin({ outline: on }), { fade: true });
625
665
  });
626
666
 
627
- // ---------- the width ----------
667
+ // ---------- the width, collapsed ----------
628
668
 
629
669
  // The guide dragged: the list wider or narrower as the pointer goes (the column staying in the middle
630
670
  // of what it leaves: margin.mjs), within 160px and what leaves the column its edge room at both sides
631
- // (outlineMost); let go, the
671
+ // (outlineMost), once the pointer has gone CLICK px across (let go short of that, it was a click: the
672
+ // list collapsed); let go, the
632
673
  // width is the reader's (every report, every page of the origin); Esc, the window losing the focus or
633
674
  // the pointer taken away put it back.
634
- let sizing = null; // { x, width, most, now, before }
675
+ let sizing = null; // { x, width, most, now, before, moved }
635
676
  grip.addEventListener("pointerdown", (event) => {
636
677
  if (event.button !== 0 || form !== "full") return;
637
678
  event.preventDefault();
638
679
  const width = Math.round(dock.getBoundingClientRect().width);
639
- sizing = { x: event.clientX, width, most: Math.max(width, outlineMost()), now: width, before: chosen };
680
+ sizing = { x: event.clientX, width, most: Math.max(width, outlineMost()), now: width, before: chosen, moved: false };
640
681
  try { document.documentElement.setPointerCapture(event.pointerId); } catch {}
641
682
  document.documentElement.setAttribute(RESIZING, "");
642
683
  });
643
684
  document.documentElement.addEventListener("pointermove", (event) => {
644
685
  if (!sizing) return;
686
+ if (!sizing.moved && Math.abs(event.clientX - sizing.x) < CLICK) return;
687
+ sizing.moved = true;
645
688
  const width = Math.round(Math.max(MARGIN.fullMin, Math.min(sizing.most, sizing.width + sizing.x - event.clientX)));
646
689
  if (width === sizing.now) return;
647
690
  sizing.now = width;
648
691
  setMargin({ outlineWidth: width });
649
692
  });
650
693
  // Let go where it began (or never moved): nothing chosen, the width chosen before laid out again (the
651
- // drag gave the layout the width shown, narrower than the one chosen where the room is short).
694
+ // drag gave the layout the width shown, narrower than the one chosen where the room is short). Let go
695
+ // before it moved: a click, the list collapsed.
652
696
  function endSizing(keep) {
653
697
  if (!sizing) return;
654
- const { now, before, width } = sizing;
698
+ const { now, before, width, moved } = sizing;
655
699
  sizing = null;
656
700
  document.documentElement.removeAttribute(RESIZING);
657
- if (!keep || now === width) setMargin({ outlineWidth: before });
701
+ if (keep && !moved) setCollapsed(true);
702
+ else if (!keep || now === width) setMargin({ outlineWidth: before });
658
703
  else {
659
704
  chosen = now;
660
705
  setPreference("outlineWidth", now);
@@ -665,6 +710,47 @@ export function createOutline({ slug }) {
665
710
  endsGesture({ active: () => Boolean(sizing), cancel: () => endSizing(false) });
666
711
  cancelOnEscape({ active: () => Boolean(sizing), cancel: () => endSizing(false) });
667
712
 
713
+ // The reader's collapse, kept for every report and every page of the origin (the page's head reads it
714
+ // before the page shows); the form is the room's to give (margin.mjs): taken back where the room is
715
+ // short, the strip stays and the list comes once there is room.
716
+ function setCollapsed(value) {
717
+ if (value === collapsed) return;
718
+ collapsed = value;
719
+ setPreference("outlineCollapsed", value);
720
+ setMargin({ collapsed: value });
721
+ }
722
+
723
+ // A tooltip by the pointer on a guide (a tall target: over where the pointer rests, as the
724
+ // workbench's sidebar edge has it), once it has rested there a moment; none while the guide is pressed.
725
+ const tipAt = (event, text, hint) => tooltipAt({ left: event.clientX, right: event.clientX, top: event.clientY, bottom: event.clientY, width: 0, height: 0 }, text, hint);
726
+ function guideTip(node, says) {
727
+ const rest = (event) => {
728
+ if (sizing || event.buttons || document.querySelector(".ge-tip")) return; // shown: it stays where it came
729
+ tipAt(event, ...says());
730
+ };
731
+ node.addEventListener("pointerenter", rest);
732
+ node.addEventListener("pointermove", rest);
733
+ node.addEventListener("pointerleave", hideTooltip);
734
+ node.addEventListener("pointerdown", hideTooltip);
735
+ }
736
+ const waiting = () => t("页面不够宽,变宽后展开");
737
+ guideTip(grip, () => [t("收起目录"), t("拖动调整宽度")]);
738
+ // The card's guide: a hand and its darker bar only where the room gives the list (data-fits), else
739
+ // its tooltip says why a click opens nothing (the list comes once there is room).
740
+ const fits = () => {
741
+ const now = listFits();
742
+ strip.guide.toggleAttribute("data-fits", now);
743
+ return now;
744
+ };
745
+ guideTip(strip.guide, () => [t("展开目录"), fits() ? null : waiting()]);
746
+ strip.guide.addEventListener("pointerenter", fits);
747
+ strip.guide.addEventListener("click", (event) => {
748
+ if (event.button !== 0 || form !== "strip") return;
749
+ const room = fits();
750
+ setCollapsed(false);
751
+ if (!room) tipAt(event, t("展开目录"), waiting());
752
+ });
753
+
668
754
 
669
755
  // The page changing: read again on the next frame when it may change what is listed (one reading
670
756
  // a frame however much changed; words typed elsewhere only move the headings: the current one again).
@@ -6,16 +6,30 @@ import { keyAction } from "./tree.mjs";
6
6
  // The outline's rows (outline/index.mjs puts them in its dock, or in the strip's card): a tree as
7
7
  // aria has it (role tree, a treeitem a row with its level, open or folded, the current one
8
8
  // aria-current), one tab stop (the current row, else the first), the keys of tree.mjs keyAction.
9
- // A row's title, cut short, shows whole in the shared tooltip, at the list's left (over the gutter,
10
- // not the rows above); a parent's triangle at its title's left, in a column of its own (a leaf keeps
11
- // the room, so titles of a level line up), folds or unfolds it (a folded one always shows it, an open
12
- // one while the pointer is over the list or the keyboard is in it: STYLE in index.mjs); a click
13
- // elsewhere on the row goes to its heading. The strip (strip.mjs) lights a row from outside (hot): the
14
- // one its line under the pointer stands for, filled as under the pointer, a blue segment on the guide.
9
+ // A row's title, cut short, shows whole in the shared tooltip, left of the list (over the gutter,
10
+ // not the rows above); a parent's triangle just short of its title, out of the row's flow (titles of
11
+ // a level line up, a leaf's too), folds or unfolds it (a folded one always
12
+ // shows it, an open one while the pointer is on the rows or the keyboard is in the list: STYLE in
13
+ // index.mjs); a click elsewhere on the row goes to its heading. The strip (strip.mjs) lights a row
14
+ // from outside (hot): the one its line under the pointer stands for, filled as under the pointer, a
15
+ // blue segment on the guide.
15
16
 
16
17
  export const ROW = 28; // a row's height
17
18
  const MARK = 16; // the current row's segment on the guide (and the hot row's)
18
19
 
20
+ /**
21
+ * The list's geometry (px from its left edge, where the column's right room ends: margin.mjs; index.mjs
22
+ * STYLE draws it): the guide's hairline at `guide` (its grip from MARGIN.grip left of it to 5px right);
23
+ * the rows from `row` (their fill 2px clear of the hairline, a top-level row's a `step` further left to take in
24
+ * its triangle: index.mjs STYLE); a title `text` into its row, `step`
25
+ * further a level (the top level's 26px in, 8px clear of the hairline; a tab's pill 22px in, 4px clear);
26
+ * a parent's triangle `twist` wide (its 16px glyph centered on it) in its row's indentation, `gap`
27
+ * short of its title at every level (the top level's from 11px in, on the hairline and its grip, which it
28
+ * stands over while shown; the next level's from 23px in). Until 2026-11-07 the top level's stood in the
29
+ * list's first 13px, left of the guide, which the user found too far from their titles.
30
+ */
31
+ export const LIST = Object.freeze({ twist: 13, guide: 17, row: 20, text: 6, step: 12, gap: 2 });
32
+
19
33
  // A small solid triangle on the 20-unit grid, pointing right (folded); turned down when open.
20
34
  export const TRIANGLE = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M8 6.5v7l5-3.5z"/></svg>';
21
35
 
@@ -43,16 +57,14 @@ export function createList({ jump, fold, escape, page }) {
43
57
  if (item.kind === "tab") label.append(named);
44
58
  else label.textContent = item.text;
45
59
  const open = !folded.has(item.key);
46
- const twist = item.children.length
47
- ? element("span", { class: "ge-outline-twist", role: "button", "aria-label": open ? t("折叠") : t("展开"), html: TRIANGLE })
48
- : element("span", { class: "ge-outline-slot", "aria-hidden": "true" });
49
- const row = element("div", { class: "ge-outline-row", role: "treeitem", tabindex: "-1", "aria-level": String(depth + 1), "data-index": String(index), "data-kind": item.kind }, twist, label);
60
+ const twist = item.children.length ? [element("span", { class: "ge-outline-twist", role: "button", "aria-label": open ? t("折叠") : t("展开"), html: TRIANGLE })] : [];
61
+ const row = element("div", { class: "ge-outline-row", role: "treeitem", tabindex: "-1", "aria-level": String(depth + 1), "data-index": String(index), "data-kind": item.kind }, ...twist, label);
50
62
  row.style.setProperty("--ge-depth", String(depth));
51
63
  if (item.kind === "tab" && item.shown) row.dataset.shown = "";
52
64
  // The top level's headings (bold): at the top, or right under a tab (a tab's own top level).
53
65
  if (item.kind === "heading" && (parent < 0 || rows[parent]?.node.kind === "tab")) row.dataset.top = "";
54
66
  if (item.children.length) row.setAttribute("aria-expanded", String(open));
55
- tooltip(row, () => (cut(named) ? item.text : null), null, { placement: "left", align: "center", gap: 14 });
67
+ tooltip(row, () => (cut(named) ? item.text : null), null, { placement: "left", align: "center", gap: LIST.row + 8 });
56
68
  return row;
57
69
  }
58
70
 
@@ -10,7 +10,7 @@ import { SHEETS } from "../../components/kit.mjs";
10
10
  // ones too, each with its panel's headings. A heading with no text (just made) has no row yet.
11
11
 
12
12
  export const HEADING = ':is(h1, h2, h3, h4)[data-ge-unit-kind="heading"]';
13
- const LEFT_OUT = "dialog, figure, table, li, .doc-header, template, [data-doc-dev]";
13
+ export const LEFT_OUT = "dialog, figure, table, li, .doc-header, template, [data-doc-dev]";
14
14
  const SCOPES = ".doc-columns, details";
15
15
 
16
16
  // ---------- a formula as a row reads it ----------
@@ -21,6 +21,9 @@ import { ticksOf } from "./tree.mjs";
21
21
  // or a heading chosen by a key closes it too). Esc on the strip itself leaves it as Esc in the list
22
22
  // does (escape). Folding in the card keeps it open; it follows the page's scroll (its current row)
23
23
  // while open, unless the pointer is on it. With no headings the strip shows the outline's icon.
24
+ //
25
+ // The card's guide (its list's hairline) has a grip of its own (`guide`, a few px either side of it):
26
+ // index.mjs makes a click on it take the reader's collapse back (the list in full where the room allows it).
24
27
 
25
28
  // Three lines, the lower two set in: the 目录 icon (the workbench's 更多 row draws the same, icons.ts outline).
26
29
  export const OUTLINE_ICON = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3.75 5.25h12.5M7.25 10h9M7.25 14.75h9"/></svg>';
@@ -39,7 +42,9 @@ const still = () => globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").
39
42
  export function createStrip({ list, opened, closed, escape, choose }) {
40
43
  const button = element("div", { class: "ge-outline-ticks", role: "button", tabindex: "0", "aria-label": t("目录"), "aria-expanded": "false" });
41
44
  const strip = element("div", { class: "ge-outline-strip", "data-doc-dev": "", hidden: "" }, button);
42
- const scroll = element("div", { class: "ge-outline-scroll" });
45
+ // Its guide's grip in its scroller, before the list, placed by the card (as the dock's: index.mjs).
46
+ const guide = element("div", { class: "ge-outline-card-grip", "aria-hidden": "true" });
47
+ const scroll = element("div", { class: "ge-outline-scroll" }, guide);
43
48
  const card = element("nav", { class: "ge-outline-card", "aria-label": t("目录"), "data-doc-dev": "", hidden: "" }, scroll);
44
49
  let open = false;
45
50
  let keyed = false; // opened by a key (the focus in it), not by the pointer
@@ -246,6 +251,7 @@ export function createStrip({ list, opened, closed, escape, choose }) {
246
251
  strip,
247
252
  card,
248
253
  scroll,
254
+ guide,
249
255
  draw,
250
256
  openCard,
251
257
  closeCard,
@@ -1,11 +1,12 @@
1
1
  import { Z } from "../../shared/layers.mjs";
2
2
 
3
- // The editor's one bottom panel: the conflicts' (conflicts.mjs, left) and the lost texts' (notices.mjs,
4
- // right) are the same card: a small muted head with a red dot, items under it, the reader's words as
3
+ // The editor's bottom panels: the conflicts' (conflicts.mjs, left) and the lost texts' (notices.mjs,
4
+ // right) are the same card, and the bar of the Agent's changes (changes/bar.mjs, in the middle) is its
5
+ // buttons in a row: a small muted head with a red dot, items under it, the reader's words as
5
6
  // a quote, one filled primary button, the rest outlined or, looking only, quiet text. No notice covers
6
7
  // one: while one stands, the toast stands above it (notices.mjs, and the workbench's, told the room).
7
8
 
8
- const PANELS = ".ge-conflicts:not([hidden]), .ge-panel";
9
+ const PANELS = ".ge-conflicts:not([hidden]), .ge-panel, .ge-changes:not([hidden])";
9
10
 
10
11
  /**
11
12
  * How far from the window's bottom the highest bottom panel shown reaches, 0 with none shown: what a
@@ -41,7 +42,7 @@ export function panelGone(panel) {
41
42
  told();
42
43
  }
43
44
  export const PANEL_STYLE = `
44
- :is(.ge-conflicts, .ge-panel) { position: fixed; bottom: 16px; z-index: ${Z.panel}; box-sizing: border-box; width: min(400px, calc(100vw - 32px)); max-height: min(70vh, 600px); overflow: auto; border-radius: 10px; background: var(--ge-bg); color: var(--ge-tx); box-shadow: var(--ge-shadow); font: 13px/1.55 var(--doc-font, system-ui, sans-serif); }
45
+ :is(.ge-conflicts, .ge-panel) { position: fixed; bottom: 16px; z-index: ${Z.panel}; box-sizing: border-box; width: min(400px, calc(100vw - 32px)); max-height: min(70vh, 600px); overflow: auto; border-radius: 10px; background: var(--ge-bg); color: var(--ge-tx); box-shadow: var(--ge-shadow); font: 13px/1.55 var(--doc-sans-font, var(--doc-font, system-ui, sans-serif)); }
45
46
  .ge-conflicts { left: 16px; }
46
47
  .ge-panel { right: 16px; }
47
48
  .ge-conflicts[hidden] { display: none; }
@@ -30,7 +30,7 @@ const STYLE = `
30
30
  ${gestureLock("data-ge-resize=\"x\"", { cursor: "ew-resize" })}
31
31
  ${gestureLock("data-ge-resize=\"y\"", { cursor: "ns-resize" })}
32
32
  .ge-resize-line { position: fixed; left: 0; top: 0; z-index: ${Z.guide}; border-radius: 2px; background: rgb(151 197 241); pointer-events: none; }
33
- .ge-resize-label { position: fixed; left: 0; top: 0; z-index: ${Z.label}; padding: 2px 7px; border-radius: 5px; background: var(--doc-accent, #2678d8); color: #fff; font: 12px/18px var(--doc-font, system-ui, sans-serif); white-space: nowrap; pointer-events: none; }
33
+ .ge-resize-label { position: fixed; left: 0; top: 0; z-index: ${Z.label}; padding: 2px 7px; border-radius: 5px; background: var(--doc-accent, #2678d8); color: #fff; font: 12px/18px var(--doc-sans-font, var(--doc-font, system-ui, sans-serif)); white-space: nowrap; pointer-events: none; }
34
34
  `;
35
35
  const GRAB = { inside: 3, outside: 8 }; // how near an edge the pointer takes it, inside and outside the block (px)
36
36
  const SNAP = 14; // px
@@ -10,19 +10,21 @@ import { t } from "../../../shared/i18n/index.mjs";
10
10
  // tab is clicked (undo clicks it back). One undo step, which selects the blocks again. A box that
11
11
  // always holds a block gets an empty paragraph once they are gone (core.mjs tidyEmptied), the caret
12
12
  // in it. That one empty paragraph stays (keptAnyway: it would be put back as it is), so deleting
13
- // only it does nothing and says nothing.
13
+ // only it does nothing and says nothing. A folded heading goes with what it hides, or as the section
14
+ // it covers whole (fold/ takenWith).
14
15
 
15
16
  /**
16
17
  * info: block-info.mjs. selection: selection/index.mjs. struct(parents, touched, mutate): the
17
18
  * editor's undoable structural change (history.mjs). say: the status line. closeMenu(): the block
18
- * menu closes. forget(): the handle forgets the block it stood beside.
19
+ * menu closes. forget(): the handle forgets the block it stood beside. folds: the headings' folds (fold/).
19
20
  */
20
- export function createDelete({ info, selection, struct, say, closeMenu, forget }) {
21
- const { kindOf, nameOf, keepText } = info;
21
+ export function createDelete({ info, selection, struct, say, closeMenu, forget, folds = null }) {
22
+ const { kindOf, nameOf, keepText, named } = info;
22
23
 
23
24
  return function remove(targets = selection.picked) {
24
25
  closeMenu();
25
- const list = ordered([targets].flat().filter(alive)).filter((target) => !keptAnyway(target));
26
+ const picked = ordered([targets].flat().filter(alive));
27
+ const list = (folds ? folds.takenWith(picked) : picked).filter((target) => !keptAnyway(target));
26
28
  if (list.length === 0) return;
27
29
  const refused = list.map((target) => [target, keepText(target, list)]).find(([, reason]) => reason);
28
30
  if (refused) {
@@ -53,7 +55,7 @@ export function createDelete({ info, selection, struct, say, closeMenu, forget }
53
55
  });
54
56
  if (done?.caret) caretAt(done.caret, "start");
55
57
  const emptied = nodes.some((node) => !list.includes(node));
56
- const name = list.length === 1 ? nameOf(nodes[0]) : t("{0} 块", list.length);
58
+ const name = list.length === 1 ? nameOf(nodes[0]) : folds ? folds.namedWith(picked, list, named) : t("{0} 块", list.length);
57
59
  say(t("已删除{0}{1}{2} · Ctrl+Z 撤销", name, emptied ? t("(外层已空,一并删除)") : "", tabs.length > 0 ? t(",连同其中的内容") : ""));
58
60
  };
59
61
  }