@heycys/gallery 0.1.2 → 0.2.0

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 (292) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/browser/editor/components/align.mjs +58 -0
  3. package/browser/editor/components/code/index.mjs +1 -1
  4. package/browser/editor/components/embed/page.mjs +1 -1
  5. package/browser/editor/components/file/page.mjs +4 -4
  6. package/browser/editor/components/heading/index.mjs +7 -6
  7. package/browser/editor/components/icons.mjs +3 -0
  8. package/browser/editor/components/image/index.mjs +2 -0
  9. package/browser/editor/components/image/page.mjs +172 -90
  10. package/browser/editor/components/installed/index.mjs +6 -2
  11. package/browser/editor/components/installed/page.mjs +80 -13
  12. package/browser/editor/components/media/icons.mjs +13 -8
  13. package/browser/editor/components/media/page.mjs +5 -3
  14. package/browser/editor/components/media/toolbar.mjs +12 -3
  15. package/browser/editor/components/paragraph/index.mjs +4 -3
  16. package/browser/editor/components/registry.mjs +17 -5
  17. package/browser/editor/components/table/index.mjs +4 -2
  18. package/browser/editor/page/after/index.mjs +288 -0
  19. package/browser/editor/page/align.mjs +91 -0
  20. package/browser/editor/page/beside.mjs +5 -6
  21. package/browser/editor/page/block-menu/index.mjs +6 -3
  22. package/browser/editor/page/blocks.mjs +3 -1
  23. package/browser/editor/page/board/hint.mjs +64 -27
  24. package/browser/editor/page/board/index.mjs +160 -71
  25. package/browser/editor/page/board/motion.mjs +177 -0
  26. package/browser/editor/page/board/settings.mjs +4 -3
  27. package/browser/editor/page/clipboard/index.mjs +10 -3
  28. package/browser/editor/page/clipboard/own/blocks.mjs +3 -4
  29. package/browser/editor/page/clipboard/own/describe.mjs +6 -2
  30. package/browser/editor/page/commit.mjs +2 -2
  31. package/browser/editor/page/core.mjs +3 -1
  32. package/browser/editor/page/custom-page.mjs +15 -11
  33. package/browser/editor/page/editor.mjs +31 -14
  34. package/browser/editor/page/history.mjs +19 -4
  35. package/browser/editor/page/insert/index.mjs +20 -22
  36. package/browser/editor/page/menu.mjs +15 -7
  37. package/browser/editor/page/page-objects.mjs +3 -7
  38. package/browser/editor/page/present-links.mjs +5 -4
  39. package/browser/editor/page/present-shell.mjs +63 -0
  40. package/browser/editor/page/selection/index.mjs +10 -2
  41. package/browser/editor/page/table/binding.mjs +87 -0
  42. package/browser/editor/page/table/icons.mjs +0 -2
  43. package/browser/editor/page/table/index.mjs +10 -14
  44. package/browser/editor/page/table/views.mjs +5 -52
  45. package/browser/editor/page/table/widths.mjs +82 -10
  46. package/browser/editor/page/typing/index.mjs +44 -12
  47. package/browser/editor/page/typing/leave.mjs +19 -5
  48. package/browser/shared/block-packs.mjs +23 -8
  49. package/browser/shared/component-catalog.mjs +15 -8
  50. package/browser/shared/component-source.mjs +3 -1
  51. package/browser/shared/components/heading.mjs +2 -1
  52. package/browser/shared/components/installed.mjs +1 -1
  53. package/browser/shared/components/page.mjs +2 -0
  54. package/browser/shared/components/paragraph.mjs +8 -1
  55. package/browser/shared/components/table.mjs +1 -0
  56. package/browser/shared/emoji/line-icons.mjs +37 -0
  57. package/browser/shared/http.mjs +9 -0
  58. package/browser/shared/i18n/en.mjs +40 -17
  59. package/browser/shared/outline.mjs +30 -2
  60. package/browser/shared/pack-manifest.mjs +12 -1
  61. package/browser/shared/pages.mjs +169 -57
  62. package/browser/shared/pinyin.mjs +92 -0
  63. package/browser/shared/present-step.mjs +17 -0
  64. package/browser/shared/sidebar-head.mjs +70 -0
  65. package/browser/shared/web-links.mjs +5 -0
  66. package/dist/chunk-2OHANJ6Q.mjs +2 -0
  67. package/dist/{chunk-QOUEFUCH.mjs → chunk-3MBRL2PM.mjs} +1 -1
  68. package/dist/{chunk-NWR2LT5B.mjs → chunk-4OKWZY5E.mjs} +1 -1
  69. package/dist/{chunk-RMECZA2K.mjs → chunk-5K44HLTZ.mjs} +1 -1
  70. package/dist/{chunk-NFMCP2HI.mjs → chunk-5XA2ZBAE.mjs} +2 -2
  71. package/dist/{chunk-OUUP3WKT.mjs → chunk-67N2QJYT.mjs} +1 -1
  72. package/dist/chunk-6KBYRA2C.mjs +2 -0
  73. package/dist/{chunk-SX4LJYT6.mjs → chunk-6N3PX4XX.mjs} +1 -1
  74. package/dist/{chunk-RFDZXFJF.mjs → chunk-7JDX4E65.mjs} +1 -1
  75. package/dist/chunk-7M2BQQGZ.mjs +2 -0
  76. package/dist/{chunk-ZUNVAXLH.mjs → chunk-7OG26PGF.mjs} +1 -1
  77. package/dist/{chunk-B4P2CM2J.mjs → chunk-AHN3NFKJ.mjs} +1 -1
  78. package/dist/chunk-ALZ6ZENZ.mjs +3 -0
  79. package/dist/{chunk-UGWYKHBL.mjs → chunk-AR5D57ZD.mjs} +2 -2
  80. package/dist/{chunk-W3BGBJQW.mjs → chunk-BFJGHSQN.mjs} +1 -1
  81. package/dist/{chunk-HLYV7RIH.mjs → chunk-CMA72MAG.mjs} +1 -1
  82. package/dist/{chunk-D4CKRD3V.mjs → chunk-DAZL73VK.mjs} +2 -2
  83. package/dist/{chunk-6VTU6KEO.mjs → chunk-DJISHPUL.mjs} +1 -1
  84. package/dist/{chunk-BPL64HGA.mjs → chunk-EAIFITAV.mjs} +1 -1
  85. package/dist/chunk-EP25HDAJ.mjs +2 -0
  86. package/dist/{chunk-6EZ7FNIK.mjs → chunk-F4ZA6JRZ.mjs} +1 -1
  87. package/dist/{chunk-3KJHLHUO.mjs → chunk-F6HVDZR4.mjs} +1 -1
  88. package/dist/{chunk-3NJACR4W.mjs → chunk-FDMAXMQA.mjs} +1 -1
  89. package/dist/chunk-FETSREUN.mjs +5 -0
  90. package/dist/{chunk-RKPWLGZ3.mjs → chunk-GHOYKMQ3.mjs} +1 -1
  91. package/dist/chunk-HDWTZ763.mjs +12 -0
  92. package/dist/{chunk-VTKNEYT3.mjs → chunk-HPOI4ZX7.mjs} +1 -1
  93. package/dist/{chunk-OW5NWPOJ.mjs → chunk-HXG7EOPJ.mjs} +1 -1
  94. package/dist/{chunk-V6RIYQZZ.mjs → chunk-ITOH5EGO.mjs} +1 -1
  95. package/dist/{chunk-ZWPMCK7P.mjs → chunk-IV64FFI4.mjs} +1 -1
  96. package/dist/chunk-J43Z6LSS.mjs +2 -0
  97. package/dist/chunk-J5PUIV3Y.mjs +4 -0
  98. package/dist/{chunk-KTKHRC6H.mjs → chunk-JA7G53PC.mjs} +1 -1
  99. package/dist/{chunk-NWFBE2YA.mjs → chunk-JLTZF4LA.mjs} +1 -1
  100. package/dist/{chunk-GY3FAUVM.mjs → chunk-JZT7I2OJ.mjs} +1 -1
  101. package/dist/{chunk-QKXMPUXC.mjs → chunk-K5XATEOO.mjs} +1 -1
  102. package/dist/chunk-L2SUIS5S.mjs +2 -0
  103. package/dist/{chunk-TEZAWDSQ.mjs → chunk-LM4XFQHZ.mjs} +1 -1
  104. package/dist/{chunk-5ZJA7CXT.mjs → chunk-MN7BZL4R.mjs} +1 -1
  105. package/dist/chunk-MTYYHM2G.mjs +15 -0
  106. package/dist/{chunk-HMYTM3GT.mjs → chunk-MXUBKDRO.mjs} +1 -1
  107. package/dist/{chunk-MHW7ZKOU.mjs → chunk-OEXWRJME.mjs} +1 -1
  108. package/dist/{chunk-CRJEG7MT.mjs → chunk-OFHXGWOX.mjs} +1 -1
  109. package/dist/chunk-ORID3AE7.mjs +166 -0
  110. package/dist/{chunk-32ZUEFZ7.mjs → chunk-PGDE24N5.mjs} +1 -1
  111. package/dist/{chunk-UNOYVIYR.mjs → chunk-POTA2FV4.mjs} +1 -1
  112. package/dist/{chunk-N7AZZLGT.mjs → chunk-QE5RG76S.mjs} +18 -17
  113. package/dist/{chunk-N76HWBA3.mjs → chunk-R2ZDLJYT.mjs} +1 -1
  114. package/dist/chunk-SWUTTZCN.mjs +2 -0
  115. package/dist/{chunk-POQSE4C2.mjs → chunk-TR7TWGHB.mjs} +1 -1
  116. package/dist/chunk-U5XL7D6A.mjs +3 -0
  117. package/dist/{chunk-PVER7LSC.mjs → chunk-U7NZI6N4.mjs} +2 -2
  118. package/dist/{chunk-Q47PLKIJ.mjs → chunk-UGWMMEJ5.mjs} +1 -1
  119. package/dist/chunk-UJQJKNGK.mjs +2 -0
  120. package/dist/chunk-UYVCQK3H.mjs +22 -0
  121. package/dist/{chunk-FJYN7RTH.mjs → chunk-VFX2KNHP.mjs} +1 -1
  122. package/dist/{chunk-UBBRTJ2R.mjs → chunk-VS7JIGJW.mjs} +1 -1
  123. package/dist/{chunk-NVRNLK25.mjs → chunk-W7665ZEC.mjs} +2 -2
  124. package/dist/{chunk-NG375ILQ.mjs → chunk-WQDQWCX5.mjs} +1 -1
  125. package/dist/chunk-YLPSMDIS.mjs +5 -0
  126. package/dist/{chunk-VXACTLO3.mjs → chunk-YNAWBAYN.mjs} +1 -1
  127. package/dist/{chunk-ZB4H7ZN4.mjs → chunk-YROX2O22.mjs} +1 -1
  128. package/dist/{chunk-YQGT4AB2.mjs → chunk-ZDLPUOAG.mjs} +1 -1
  129. package/dist/chunk-ZQS6H473.mjs +16 -0
  130. package/dist/{chunk-OJKMINIU.mjs → chunk-ZWWC4KLF.mjs} +15 -15
  131. package/dist/cli.mjs +1 -1
  132. package/dist/server.mjs +1 -1
  133. package/dist/worker.mjs +1 -1
  134. package/kit/board.css +5 -5
  135. package/kit/code.js +15 -15
  136. package/kit/report.css +120 -41
  137. package/kit/runtime.js +45 -7
  138. package/kit/tables.js +2 -0
  139. package/kit/ui.js +2 -0
  140. package/package.json +1 -1
  141. package/packs/gallery/chart-view/kit.js +3 -3
  142. package/packs/gallery/chart-view/writing.md +1 -1
  143. package/packs/gallery/countdown/component.json +1 -1
  144. package/packs/gallery/countdown/kit.js +89 -29
  145. package/packs/gallery/countdown/preview.svg +1 -1
  146. package/packs/gallery/countdown/style.css +88 -100
  147. package/packs/gallery/countdown/writing.md +2 -2
  148. package/packs/gallery/habit/component.json +3 -3
  149. package/packs/gallery/habit/kit.js +114 -75
  150. package/packs/gallery/habit/preview.svg +1 -1
  151. package/packs/gallery/habit/style.css +133 -205
  152. package/packs/gallery/habit/writing.md +2 -2
  153. package/packs/gallery/heatmap/component.json +2 -2
  154. package/packs/gallery/heatmap/kit.js +335 -173
  155. package/packs/gallery/heatmap/preview.svg +1 -1
  156. package/packs/gallery/heatmap/style.css +158 -170
  157. package/packs/gallery/heatmap/writing.md +2 -2
  158. package/packs/gallery/kanban/kit.js +3 -3
  159. package/packs/gallery/kanban/writing.md +1 -1
  160. package/packs/gallery/metric/kit.js +3 -3
  161. package/packs/gallery/metric/writing.md +1 -1
  162. package/packs/gallery/note/component.json +3 -3
  163. package/packs/gallery/note/kit.js +40 -13
  164. package/packs/gallery/note/preview.svg +1 -1
  165. package/packs/gallery/note/style.css +51 -30
  166. package/packs/gallery/note/writing.md +2 -2
  167. package/packs/gallery/recent/component.json +2 -2
  168. package/packs/gallery/recent/kit.js +141 -86
  169. package/packs/gallery/recent/preview.svg +1 -1
  170. package/packs/gallery/recent/style.css +53 -126
  171. package/packs/gallery/recent/writing.md +2 -2
  172. package/packs/gallery/static/component.json +2 -2
  173. package/packs/gallery/static/preview.svg +1 -1
  174. package/packs/gallery/static/style.css +99 -66
  175. package/packs/gallery/static/writing.md +2 -2
  176. package/packs/gallery/stats/component.json +3 -3
  177. package/packs/gallery/stats/kit.js +253 -72
  178. package/packs/gallery/stats/preview.svg +1 -1
  179. package/packs/gallery/stats/style.css +64 -142
  180. package/packs/gallery/stats/writing.md +2 -2
  181. package/packs/gallery/today/component.json +2 -2
  182. package/packs/gallery/today/kit.js +62 -27
  183. package/packs/gallery/today/preview.svg +1 -1
  184. package/packs/gallery/today/style.css +101 -72
  185. package/packs/gallery/today/writing.md +2 -2
  186. package/packs/gallery/todos/component.json +1 -1
  187. package/packs/gallery/todos/kit.js +218 -71
  188. package/packs/gallery/todos/preview.svg +1 -1
  189. package/packs/gallery/todos/style.css +170 -223
  190. package/packs/gallery/todos/writing.md +1 -1
  191. package/packs/gallery/weather/component.json +2 -2
  192. package/packs/gallery/weather/kit.js +165 -90
  193. package/packs/gallery/weather/preview.svg +1 -1
  194. package/packs/gallery/weather/style.css +100 -218
  195. package/packs/gallery/weather/writing.md +1 -1
  196. package/packs/gallery-soft/countdown/component.json +18 -0
  197. package/packs/gallery-soft/countdown/example.html +3 -0
  198. package/packs/gallery-soft/countdown/kit.js +206 -0
  199. package/packs/gallery-soft/countdown/preview.svg +1 -0
  200. package/packs/gallery-soft/countdown/style.css +211 -0
  201. package/packs/gallery-soft/countdown/template.html +3 -0
  202. package/packs/gallery-soft/countdown/writing.md +24 -0
  203. package/packs/gallery-soft/habit/component.json +14 -0
  204. package/packs/gallery-soft/habit/example.html +3 -0
  205. package/packs/gallery-soft/habit/kit.js +366 -0
  206. package/packs/gallery-soft/habit/preview.svg +1 -0
  207. package/packs/gallery-soft/habit/style.css +251 -0
  208. package/packs/gallery-soft/habit/template.html +3 -0
  209. package/packs/gallery-soft/habit/writing.md +23 -0
  210. package/packs/gallery-soft/heatmap/component.json +18 -0
  211. package/packs/gallery-soft/heatmap/example.html +3 -0
  212. package/packs/gallery-soft/heatmap/kit.js +498 -0
  213. package/packs/gallery-soft/heatmap/preview.svg +1 -0
  214. package/packs/gallery-soft/heatmap/style.css +181 -0
  215. package/packs/gallery-soft/heatmap/template.html +3 -0
  216. package/packs/gallery-soft/heatmap/writing.md +23 -0
  217. package/packs/gallery-soft/note/component.json +14 -0
  218. package/packs/gallery-soft/note/example.html +3 -0
  219. package/packs/gallery-soft/note/kit.js +162 -0
  220. package/packs/gallery-soft/note/preview.svg +1 -0
  221. package/packs/gallery-soft/note/style.css +148 -0
  222. package/packs/gallery-soft/note/template.html +3 -0
  223. package/packs/gallery-soft/note/writing.md +20 -0
  224. package/packs/gallery-soft/recent/component.json +13 -0
  225. package/packs/gallery-soft/recent/example.html +1 -0
  226. package/packs/gallery-soft/recent/kit.js +335 -0
  227. package/packs/gallery-soft/recent/preview.svg +1 -0
  228. package/packs/gallery-soft/recent/style.css +160 -0
  229. package/packs/gallery-soft/recent/template.html +1 -0
  230. package/packs/gallery-soft/recent/writing.md +17 -0
  231. package/packs/gallery-soft/static/component.json +13 -0
  232. package/packs/gallery-soft/static/example.html +12 -0
  233. package/packs/gallery-soft/static/preview.svg +1 -0
  234. package/packs/gallery-soft/static/style.css +186 -0
  235. package/packs/gallery-soft/static/template.html +1 -0
  236. package/packs/gallery-soft/static/writing.md +20 -0
  237. package/packs/gallery-soft/stats/component.json +13 -0
  238. package/packs/gallery-soft/stats/example.html +1 -0
  239. package/packs/gallery-soft/stats/kit.js +341 -0
  240. package/packs/gallery-soft/stats/preview.svg +1 -0
  241. package/packs/gallery-soft/stats/style.css +119 -0
  242. package/packs/gallery-soft/stats/template.html +1 -0
  243. package/packs/gallery-soft/stats/writing.md +13 -0
  244. package/packs/gallery-soft/today/component.json +12 -0
  245. package/packs/gallery-soft/today/example.html +1 -0
  246. package/packs/gallery-soft/today/kit.js +222 -0
  247. package/packs/gallery-soft/today/preview.svg +1 -0
  248. package/packs/gallery-soft/today/style.css +178 -0
  249. package/packs/gallery-soft/today/template.html +1 -0
  250. package/packs/gallery-soft/today/writing.md +16 -0
  251. package/packs/gallery-soft/todos/component.json +19 -0
  252. package/packs/gallery-soft/todos/example.html +3 -0
  253. package/packs/gallery-soft/todos/kit.js +527 -0
  254. package/packs/gallery-soft/todos/preview.svg +1 -0
  255. package/packs/gallery-soft/todos/style.css +212 -0
  256. package/packs/gallery-soft/todos/template.html +3 -0
  257. package/packs/gallery-soft/todos/writing.md +32 -0
  258. package/packs/gallery-soft/weather/component.json +19 -0
  259. package/packs/gallery-soft/weather/example.html +3 -0
  260. package/packs/gallery-soft/weather/kit.js +411 -0
  261. package/packs/gallery-soft/weather/preview.svg +1 -0
  262. package/packs/gallery-soft/weather/style.css +185 -0
  263. package/packs/gallery-soft/weather/template.html +3 -0
  264. package/packs/gallery-soft/weather/writing.md +28 -0
  265. package/release.json +9 -9
  266. package/skill/SKILL.md +12 -12
  267. package/skill/references/building-blocks.md +4 -4
  268. package/skill/references/pages-and-data.md +13 -9
  269. package/skill/references/writing-html.md +2 -0
  270. package/web/assets/index-BM_K-Auu.css +1 -0
  271. package/web/assets/index-DRslOBiv.js +15 -0
  272. package/web/index.html +2 -2
  273. package/dist/chunk-2NRBY6AJ.mjs +0 -2
  274. package/dist/chunk-35NQ4PLQ.mjs +0 -2
  275. package/dist/chunk-4AV456RQ.mjs +0 -2
  276. package/dist/chunk-5XYHDBJ5.mjs +0 -2
  277. package/dist/chunk-6A53MZYZ.mjs +0 -2
  278. package/dist/chunk-6EXH3ORZ.mjs +0 -166
  279. package/dist/chunk-BVEFGE4B.mjs +0 -2
  280. package/dist/chunk-D4EURAX4.mjs +0 -4
  281. package/dist/chunk-DQCE7TJY.mjs +0 -5
  282. package/dist/chunk-FWNRPMCJ.mjs +0 -12
  283. package/dist/chunk-FWW4CSAZ.mjs +0 -2
  284. package/dist/chunk-JVHHHGEA.mjs +0 -3
  285. package/dist/chunk-PZDLJJTJ.mjs +0 -3
  286. package/dist/chunk-SXHBYZST.mjs +0 -16
  287. package/dist/chunk-UFNYFFKO.mjs +0 -20
  288. package/dist/chunk-UKSHDZZ6.mjs +0 -2
  289. package/dist/chunk-VJPFVNNW.mjs +0 -15
  290. package/dist/chunk-VS3ZMVGX.mjs +0 -5
  291. package/web/assets/index-BK2e_07w.js +0 -13
  292. package/web/assets/index-CWFI83mF.css +0 -1
@@ -0,0 +1,63 @@
1
+ import { pageAddress } from "../../shared/page-address.mjs";
2
+ import { KEEPS_KEYS, stepSection } from "../../shared/present-step.mjs";
3
+ import { PROTOCOL, PROTOCOLS, VERSION } from "../../shared/session.mjs";
4
+
5
+ // What a page shown in 演示视图 (?present: src/server/routes.mjs, no editor) says to the presentation around it and
6
+ // hears from it. The presentation of a document of this workspace is a frame of the same origin, which it reaches into (its
7
+ // scroll, its keys, its theme); that of a draft of another workspace (docs/workbench.md "Drafts of other workspaces") is a frame
8
+ // of that workspace's server, named by the page's address (?shell=<the workbench's origin>) as the editor's page names it
9
+ // (session.mjs): this page then asks its own server whether that is a Gallery server of this computer (GET
10
+ // /__gallery/workspace/shell) and talks to it alone once it is, holding what it says until then; one that is not hears nothing.
11
+ // It tells where the page is scrolled (`present-view`: { x, y }, with `ready` once the page can be spoken to) and the keys pressed
12
+ // in it that are the presentation's (`present-key`: { key, ctrlKey, metaKey, altKey, kept (a control holds the key), prevented }),
13
+ // and does what it is asked: `theme` (the workbench's, light or dark), `go-section` (上一节 / 下一节) and `scroll` ({ left, top }: the wheel). Every message is the session
14
+ // protocol's envelope (src/shared/session.mjs).
15
+
16
+ /** The origin of a workbench of another origin showing this page (?shell=<origin>), or null: this one's. */
17
+ export const SHELL = (() => {
18
+ const named = new URLSearchParams(location.search).get("shell");
19
+ return named && named !== location.origin ? named : null;
20
+ })();
21
+
22
+ const vouched = (origin) => fetch(`/__gallery/workspace/shell?origin=${encodeURIComponent(origin)}`, { cache: "no-store" })
23
+ .then((answer) => answer.json()).then((answer) => answer?.shell === true, () => false);
24
+
25
+ let parentOrigin = SHELL ? null : location.origin;
26
+ let held = SHELL ? [] : null;
27
+
28
+ /** Tell the presentation around the page (`window.parent`) something; held until its origin is vouched for, dropped when it is not. */
29
+ export function tell(type, fields = {}) {
30
+ if (window.parent === window) return;
31
+ if (held) return void held.push([type, fields]);
32
+ if (!parentOrigin) return;
33
+ window.parent.postMessage({ protocol: PROTOCOL, v: VERSION, type, slug: pageAddress(location.href)?.slug ?? "", ...fields }, parentOrigin);
34
+ }
35
+
36
+ function bridge() {
37
+ let frame = 0;
38
+ const view = (ready = false) => tell("present-view", { x: scrollX, y: scrollY, ...(ready ? { ready: true } : {}) });
39
+ addEventListener("scroll", () => { frame ||= requestAnimationFrame(() => { frame = 0; view(); }); }, { passive: true });
40
+ addEventListener("keydown", (event) => {
41
+ if (event.isComposing) return;
42
+ tell("present-key", { key: event.key, ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, kept: Boolean(event.target?.closest?.(KEEPS_KEYS)), prevented: event.defaultPrevented });
43
+ });
44
+ addEventListener("message", (event) => {
45
+ const message = event.data;
46
+ if (event.source !== window.parent || event.origin !== parentOrigin || !PROTOCOLS.includes(message?.protocol) || message.v !== VERSION || message.type !== "command") return;
47
+ if (message.name === "theme") dispatchEvent(new CustomEvent("doc-theme-request", { detail: { theme: message.theme === "dark" ? "dark" : "light" } })); // the kit's ui.js
48
+ else if (message.name === "go-section") stepSection(window, message.direction > 0 ? 1 : -1);
49
+ else if (message.name === "scroll") scrollBy({ left: Number(message.left) || 0, top: Number(message.top) || 0 }); // the wheel over the presentation's ink
50
+ });
51
+ view(true);
52
+ }
53
+
54
+ if (SHELL && window.parent !== window) {
55
+ vouched(SHELL).then((ok) => {
56
+ const waiting = held;
57
+ held = null;
58
+ if (!ok) return;
59
+ parentOrigin = SHELL;
60
+ for (const [type, fields] of waiting) tell(type, fields);
61
+ bridge();
62
+ });
63
+ }
@@ -5,6 +5,7 @@ import { createFills } from "../fills.mjs";
5
5
  import { FOLDED } from "../folded.mjs";
6
6
  import { describe, note, report } from "../overlays.mjs";
7
7
  import { t } from "../../../shared/i18n/index.mjs";
8
+ import { UNFILLED_ALONE } from "../../components/registry.mjs";
8
9
 
9
10
  // Selected blocks (docs/editor.md, "Selecting blocks"): a set of blocks, none inside another, in
10
11
  // document order. A box selection drawn from empty space gives blocks of one container
@@ -109,10 +110,17 @@ export function createSelection({ info, active, say, closeMenu, schedule }) {
109
110
  // The fills made to match the selection now: those of blocks no longer selected (or no longer on
110
111
  // the page) go at once; each selected block shown gets its fill where it is, reaching past it as
111
112
  // reachOf says, never into a neighbour's (roomOf). One that cannot be drawn takes all the fills
112
- // down (said once): better none than one in the wrong place.
113
+ // down (said once): better none than one in the wrong place. A kind whose own tools show it selected
114
+ // (UNFILLED_ALONE: the image with its picture, its corner grips and toolbar) draws no fill selected alone (the user, 2026-11-09: no pale
115
+ // blue over a picture clicked); among other blocks it is filled as they are.
116
+ const toolsShowIt = () => {
117
+ if (picked.length !== 1 || marked.length !== 0) return false;
118
+ const holds = UNFILLED_ALONE[picked[0].getAttribute?.("data-ge-unit-kind")];
119
+ return holds !== undefined && picked[0].querySelector?.(holds) !== null;
120
+ };
113
121
  function render() {
114
122
  try {
115
- halos.paint(hidden || !active() ? [] : [...picked, ...marked].filter(alive));
123
+ halos.paint(hidden || !active() || toolsShowIt() ? [] : [...picked, ...marked].filter(alive));
116
124
  } catch (error) {
117
125
  unpaint();
118
126
  report("selection fills", error);
@@ -0,0 +1,87 @@
1
+ import { TABLE } from "../../components/kit.mjs";
2
+ import { CHART_SOURCE, TABLE_READERS, VIEW_FROM, nextChartKey } from "../../../shared/table-chart.mjs";
3
+ import { t } from "../../../shared/i18n/index.mjs";
4
+ import { menuRow } from "../page-objects.mjs";
5
+
6
+ // Which table a view reads (docs/editor.md, "Table views"). A view of a table (an instance of a custom
7
+ // block whose needs hold "tables") names its table by a key: VIEW_FROM on the instance, CHART_SOURCE on
8
+ // the table's frame, never by an id or the blocks' order. A view made on the page ("/", 在下方新建, 转换成)
9
+ // is bound to the nearest table above it as it is made, in the same undo step; the table it reads is the
10
+ // reader's to change after that, in the block's 设置 (installed/page.mjs), which lists the document's tables.
11
+ // A table with no key gets one in the same step (the first of t1, t2 … the page does not hold), and so
12
+ // does the second of two an Agent wrote with one key: a view by that key would read the first.
13
+
14
+ const FRAME_WITH_TABLE = (frame) => frame.querySelector("table") !== null && !frame.closest("[data-doc-dev]");
15
+
16
+ /** The tables of the page, in the document's order: their frames (.doc-table-wrap). */
17
+ export const tableFrames = () => [...document.querySelectorAll(TABLE.frame)].filter(FRAME_WITH_TABLE);
18
+
19
+ /** The frame a key names, as the kit reads it (runtime/table-reading.js frameOf) and the checks do: the first that carries it. */
20
+ export const frameOf = (key) => tableFrames().find((frame) => frame.getAttribute(CHART_SOURCE) === key) ?? null;
21
+
22
+ /** The nearest table above `node`, in the document's order, that does not hold it; null where there is none. */
23
+ export function tableAbove(node) {
24
+ let found = null;
25
+ for (const frame of tableFrames()) {
26
+ if (frame.contains(node)) continue;
27
+ if (frame.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING) found = frame;
28
+ }
29
+ return found;
30
+ }
31
+
32
+ // Every key a chart or a view names, or a frame carries.
33
+ const takenKeys = () => new Set([...document.querySelectorAll([CHART_SOURCE, ...TABLE_READERS].map((name) => `[${name}]`).join(", "))]
34
+ .flatMap((node) => [CHART_SOURCE, ...TABLE_READERS].map((name) => node.getAttribute(name)).filter((value) => value !== null)));
35
+
36
+ /** The key `frame` is read by: its own where it names this frame, else a new one (not yet written). */
37
+ export function keyOf(frame) {
38
+ const own = frame.getAttribute(CHART_SOURCE);
39
+ return own !== null && frameOf(own) === frame ? own : nextChartKey(takenKeys());
40
+ }
41
+
42
+ /**
43
+ * `instance` made a view of `frame` (or of none: null takes its key off): its VIEW_FROM set, and the key
44
+ * on the frame where it carried none that names it. Records nothing: the caller's undo step does
45
+ * (`viewFrames`, struct's `attributes`). Returns whether anything changed.
46
+ */
47
+ export function bindView(instance, frame) {
48
+ if (!frame) {
49
+ if (!instance.hasAttribute(VIEW_FROM)) return false;
50
+ instance.removeAttribute(VIEW_FROM);
51
+ return true;
52
+ }
53
+ const key = keyOf(frame);
54
+ let changed = false;
55
+ if (instance.getAttribute(VIEW_FROM) !== key) {
56
+ instance.setAttribute(VIEW_FROM, key);
57
+ changed = true;
58
+ }
59
+ if (frame.getAttribute(CHART_SOURCE) !== key) {
60
+ frame.setAttribute(CHART_SOURCE, key);
61
+ changed = true;
62
+ }
63
+ return changed;
64
+ }
65
+
66
+ /** A view that was made bound to the table above it. */
67
+ export function bindAbove(instance) {
68
+ return bindView(instance, tableAbove(instance));
69
+ }
70
+
71
+ /**
72
+ * What the undo step that makes a block of the menu's key records of the tables' keys (struct's
73
+ * `attributes`): every table's frame when the block reads tables, as the table it is bound to is only
74
+ * known as the block is made; nothing for any other block.
75
+ */
76
+ export const viewFrames = (key) => (menuRow(key)?.readsTables ? tableFrames().map((frame) => [frame, [CHART_SOURCE]]) : []);
77
+
78
+ // A table named for a reader: its place among the document's tables and the words of its first row.
79
+ function labelOf(frame, place) {
80
+ const first = frame.querySelector("tr");
81
+ const words = first ? [...first.cells].map((cell) => cell.textContent.replace(/\s+/g, " ").trim()).filter(Boolean).slice(0, 4).join("、") : "";
82
+ const shown = words.length > 28 ? `${words.slice(0, 27)}…` : words;
83
+ return shown ? t("表格 {0} · {1}", place, shown) : t("表格 {0}", place);
84
+ }
85
+
86
+ /** The document's tables for a list to choose from: [{ frame, label }], the label naming the table by its place and its first row. */
87
+ export const tableChoices = () => tableFrames().map((frame, at) => ({ frame, label: labelOf(frame, at + 1) }));
@@ -37,6 +37,4 @@ export const TABLE_ICONS = {
37
37
  // 一键优化: a magic wand and its sparks; 识别数字列: a number sign.
38
38
  magic: svg('<path d="m3.75 16.25 9-9"/><path d="m11.25 5.75 3 3"/><path d="M15.25 2.5v3M13.75 4h3M5.5 3.25v2M4.5 4.25h2M16 10.75v2M15 11.75h2"/>'),
39
39
  numbers: svg('<path d="M8.25 4 6.75 16M13.75 4l-1.5 12M4.5 8h12M3.75 12.25h12"/>'),
40
- // 视图: three columns of cards, a board drawn from the table.
41
- view: svg('<rect x="3.25" y="3.75" width="3.5" height="7.5" rx="1"/><rect x="8.25" y="3.75" width="3.5" height="12.5" rx="1"/><rect x="13.25" y="3.75" width="3.5" height="5" rx="1"/>'),
42
40
  };
@@ -57,7 +57,7 @@ import { t } from "../../../shared/i18n/index.mjs";
57
57
  // it (the geometry's view), the grips and + points of the rows and columns in sight.
58
58
  // Each change is one undo step; the page writes it as one operation of the table (grid.mjs). The
59
59
  // keys and the cells' bar are cells.mjs's; what a view of the table asks (a custom block reading it:
60
- // set a cell, move, add or take out a row) and 视图 › of its menu, views.mjs's.
60
+ // set a cell, move, add or take out a row), views.mjs's.
61
61
 
62
62
  const STYLE = `
63
63
  .ge-tgrip { display: flex; align-items: center; justify-content: center; cursor: grab; pointer-events: auto; }
@@ -108,15 +108,14 @@ const REACH = { out: 12, in: 8 }; // the pointer this near the table's top (left
108
108
  * active(): whether the tools may show. struct(parents, touched, mutate): the editor's undoable
109
109
  * structural change; look(elements, change, select): a change of classes and sizes as one undo step. selectBlock(unit):
110
110
  * select the table as a block (Esc). resizeTable(event, unit, axis): a drag of the table's width
111
- * ("x") or scroll height ("y") from an armed edge (resize/). say: the status line. cellsOf(data): what
111
+ * ("x") or scroll height ("y") from an armed edge (resize/). say: the status line; settle(): takes it off. cellsOf(data): what
112
112
  * the clipboard holds as rows of cells, { rows, from } (clipboard/ cellsOf), or null. upload(kind,
113
113
  * body): a file sent to the report's folder (POST /__gallery/edit/upload), a picture of a cell's
114
114
  * (pictures.mjs).
115
115
  * typedKey(event): a key that is the "/" or "@" menu's, standing on a cell's text (slash-menu/, at-menu/), comes before the cell's.
116
- * recordStep(host, before): a text step of a text block, what a view's set is (history.mjs). insertBelow(unit, key,
117
- * { attributes, done }): a block of a menu's key put in below a block in one undo step (editor.mjs), what 视图 › makes.
116
+ * recordStep(host, before): a text step of a text block, what a view's set is (history.mjs).
118
117
  */
119
- export function createTableEditing({ active, struct, look, selectBlock, resizeTable = () => false, say, cellsOf = () => null, upload = async () => { throw new Error("upload"); }, typedKey = () => false, recordStep = () => {}, insertBelow = () => {} }) {
118
+ export function createTableEditing({ active, struct, look, selectBlock, resizeTable = () => false, say, settle = () => {}, cellsOf = () => null, upload = async () => { throw new Error("upload"); }, typedKey = () => false, recordStep = () => {} }) {
120
119
  addStyle(STYLE);
121
120
  const menu = createMenu();
122
121
  let hover = null; // { unit, row, column, edges: { row, column } }: the cell under the pointer, and whether it is at the table's left (top) edge (edgesAt)
@@ -268,7 +267,7 @@ export function createTableEditing({ active, struct, look, selectBlock, resizeTa
268
267
  select(null);
269
268
  selectBlock(unit);
270
269
  },
271
- bounds, selectedCells, tableAt, placeOf, cellOf, cellAt, geometry, change, insert, removeRange, duplicate, merge, split, look, struct, say,
270
+ bounds, selectedCells, tableAt, placeOf, cellOf, cellAt, geometry, change, insert, removeRange, duplicate, merge, split, look, struct, say, settle,
272
271
  active,
273
272
  menuOpen: () => menu.isOpen,
274
273
  typedKey,
@@ -927,15 +926,13 @@ export function createTableEditing({ active, struct, look, selectBlock, resizeTa
927
926
 
928
927
  addEventListener("scroll", () => { if (!drag) draw(); }, { passive: true, capture: true });
929
928
 
930
- // What a view of a table asks, and 视图 › (views.mjs).
931
- const views = createViews({ active, change, tableAt, recordStep, insertBelow, drawn: () => draw() });
929
+ // What a view of a table asks (views.mjs).
930
+ createViews({ active, change, tableAt, recordStep, drawn: () => draw() });
932
931
 
933
932
  // The rows of a table the tools manage. 一键优化 is a split row (menu.mjs): its words run it, its ›
934
- // opens two of its parts to run alone, 自动调整列宽 and 识别数字列 (the user's, 2026-11-07); 视图 › last,
935
- // where a custom block reads tables (views.mjs).
933
+ // opens two of its parts to run alone, 自动调整列宽 and 识别数字列 (the user's, 2026-11-07).
936
934
  function toolRows(unit) {
937
935
  const state = tableState(unit);
938
- const view = views.menuRow(unit);
939
936
  return [
940
937
  { label: t("标题行"), icon: ICONS.headerRow, checked: state.headerRow, run: () => headerRow(unit, state) },
941
938
  { label: t("标题列"), icon: ICONS.headerColumn, checked: state.headerColumn, run: () => change(unit, () => setHeaderColumn(unit, !state.headerColumn), state.headerColumn ? t("已取消标题列") : t("已设为标题列")) },
@@ -947,7 +944,6 @@ export function createTableEditing({ active, struct, look, selectBlock, resizeTa
947
944
  ],
948
945
  },
949
946
  { label: t("均分列宽"), icon: ICONS.even, run: () => widths.even(unit) },
950
- ...(view ? [view] : []),
951
947
  ];
952
948
  }
953
949
 
@@ -998,8 +994,8 @@ export function createTableEditing({ active, struct, look, selectBlock, resizeTa
998
994
  * A table's rows of its block menu (notion-lite's 表格 section, under the menu's one title): a
999
995
  * table of the kind these tools manage has 标题行 and 标题列, switches (checked when on); then
1000
996
  * actions, each applied once (no check): 一键优化 › (自动调整列宽, 识别数字列 in its submenu),
1001
- * 均分列宽, 视图 › (where a custom block reads tables: views.mjs); any table in its frame has its
1002
- * size's rows after them (layoutRows). [] for any other block.
997
+ * 均分列宽; any table in its frame has its size's rows after them (layoutRows). [] for any other block.
998
+ * (The table's 对齐 › is the block menu's own row, above 颜色: page/align.mjs, as a text block's.)
1003
999
  */
1004
1000
  menuItems(target) {
1005
1001
  if (!target?.matches?.('[data-ge-unit-kind="table"]')) return [];
@@ -1,11 +1,6 @@
1
- import { TABLE } from "../../components/kit.mjs";
2
1
  import { insertRow, moveRow, removeRows, tableState } from "../../components/table/page.mjs";
3
2
  import { tableOf } from "../../components/table/grid.mjs";
4
3
  import { ownHTML } from "../core.mjs";
5
- import { menuEntry, viewKeys } from "../page-objects.mjs";
6
- import { CHART_SOURCE, TABLE_READERS, VIEW_FROM, nextChartKey } from "../../../shared/table-chart.mjs";
7
- import { t } from "../../../shared/i18n/index.mjs";
8
- import { TABLE_ICONS } from "./icons.mjs";
9
4
 
10
5
  // The views of a table (docs/editor.md, "Table views"; the plan's 阶段 4): an instance of a custom
11
6
  // block whose needs hold "tables", bound to a table of the document by the key its frame carries
@@ -31,25 +26,15 @@ import { TABLE_ICONS } from "./icons.mjs";
31
26
  // are those after its header row: the head's rows, or with no head its body's first row, which a view
32
27
  // reads its columns' names from; no intent puts a row there or takes that one.
33
28
  //
34
- // 视图 › of a table's block menu (menuRow) lists the custom blocks that read tables and that the
35
- // document's menus would offer (their `readsTables` row of the custom blocks' menu variants, which follows
36
- // the 组件列表 switches; page-objects.mjs viewKeys: no other menu lists them, as one put in unbound only
37
- // says it has no table) and puts one in right after the table bound to it, in one undo step: the table's key
38
- // where it names this table (the first frame carrying it, as the kit and the checks read a key), or a new
39
- // one (the first of t1, t2 … the page does not hold) put on its frame in that step (a table made from "/"
40
- // has none; the second of two an Agent wrote with one key is not the one it names). The commit writes
41
- // both, and the server makes sure of the key against the file (source/chart-keys.mjs relinkMade); a key
42
- // it gives the pair anew the page follows, its undo steps too (page/commit.mjs apply, history.mjs rekeyed).
29
+ // Which table a view reads is binding.mjs's: a view made from a menu is bound to the table above it, and
30
+ // its 设置 (components/installed/page.mjs) changes it.
43
31
 
44
32
  /**
45
33
  * active(): whether the page edits now. change(unit, mutate, said, touched): the table tools' one undo
46
34
  * step (index.mjs). tableAt(node): the table unit these tools manage that a node is in. recordStep(host,
47
- * before): a text step of a text block (history.mjs). insertBelow(unit, key, { attributes, done }): a
48
- * block of the menu's `key` put in below a block in one undo step, `attributes` ([element, names]) recorded
49
- * with it and `done(made)` run in it on the new block (editor.mjs insertBelow). drawn(): the tools drawn
50
- * again (a row taken out).
35
+ * before): a text step of a text block (history.mjs). drawn(): the tools drawn again (a row taken out).
51
36
  */
52
- export function createViews({ active, change, tableAt, recordStep, insertBelow, drawn = () => {} }) {
37
+ export function createViews({ active, change, tableAt, recordStep, drawn = () => {} }) {
53
38
  const merged = (rows) => rows.some((row) => [...row.cells].some((cell) => cell.rowSpan !== 1 || cell.colSpan !== 1));
54
39
  const writable = (cell) => cell?.getAttribute("contenteditable") === "true";
55
40
  const holds = (cell, text) => cell.textContent === text && cell.childElementCount === 0;
@@ -114,37 +99,5 @@ export function createViews({ active, change, tableAt, recordStep, insertBelow,
114
99
  if (run(event.detail ?? {})) event.preventDefault();
115
100
  });
116
101
 
117
- // The frame a key names, as the kit reads it (runtime/table-reading.js frameOf) and the checks do: the
118
- // first that carries it, none of a dev block's.
119
- const frameOf = (key) => [...document.querySelectorAll(`[${CHART_SOURCE}]`)].find((node) => node.getAttribute(CHART_SOURCE) === key && node.matches(TABLE.frame) && !node.closest("[data-doc-dev]")) ?? null;
120
- // A view of the block `key` put in right after `unit`, bound to its table: by its key where that names
121
- // it, else by a new one put on it in the same step (a table made from "/" has none; one of two an Agent
122
- // wrote with one key is not the one it names, the first: a view by it would read and change that one).
123
- function insertView(unit, key) {
124
- const own = unit.getAttribute(CHART_SOURCE);
125
- const taken = new Set([...document.querySelectorAll([CHART_SOURCE, ...TABLE_READERS].map((name) => `[${name}]`).join(", "))].flatMap((node) => [CHART_SOURCE, ...TABLE_READERS].map((name) => node.getAttribute(name)).filter((value) => value !== null)));
126
- const bound = own !== null && frameOf(own) === unit ? own : nextChartKey(taken);
127
- insertBelow(unit, key, {
128
- attributes: [[unit, [CHART_SOURCE]]],
129
- done(made) {
130
- made?.setAttribute(VIEW_FROM, bound);
131
- if (bound !== own) unit.setAttribute(CHART_SOURCE, bound);
132
- },
133
- });
134
- }
135
-
136
- return {
137
- /** The 视图 › row of a table's block menu: one for each custom block that reads tables, or null where there is none or the table cannot be read as rows (no frame, a merged cell). */
138
- menuRow(unit) {
139
- const keys = viewKeys();
140
- if (keys.length === 0 || !unit.matches(TABLE.frame) || merged(tableState(unit).rows)) return null;
141
- return {
142
- label: t("视图"), icon: TABLE_ICONS.view,
143
- submenu: () => viewKeys().map((key) => {
144
- const row = menuEntry(key).variant;
145
- return { label: row.label, icon: row.icon, run: () => insertView(unit, key) };
146
- }),
147
- };
148
- },
149
- };
102
+ return {};
150
103
  }
@@ -11,7 +11,8 @@ import { t } from "../../../shared/i18n/index.mjs";
11
11
 
12
12
  // A table's column widths (notion-lite's: a 3px strip on each column's right edge; the plan's D4:
13
13
  // the kit's own widths until the reader sizes a column). Dragging a strip sizes its column (35px at
14
- // least, nothing snapped), shown as it goes (Esc puts it back), written when let go as one undo
14
+ // least; the last one's, the table's right edge, snaps where the table is exactly as wide as the
15
+ // text: SNAP), shown as it goes (Esc puts it back), written when let go as one undo
15
16
  // step: every column's width on the topmost cell of that column alone (--doc-column-width,
16
17
  // index.mjs COLUMN_WIDTH; shared/table-slots.mjs widthHolder: a merged cell holds none), the kit laying the table
17
18
  // out at them (tables/layout.js manualLayout); the first
@@ -50,6 +51,8 @@ const STYLE = `
50
51
  .ge-trows > b > svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; transform: rotate(90deg); }
51
52
  ${gestureLock("data-ge-tresize", { cursor: "col-resize", select: true })}
52
53
  `;
54
+ const SNAP = 6; // px: the last column's edge within this of the width that fills the text lands on it (as the resize tool's: resize/)
55
+ const LIVE = 60000; // ms: the notice saying the table is full stays while the drag lasts (taken off when it ends)
53
56
  const DOUBLE = 500; // two presses on a strip within this many ms are a double click (a press on the armed edge after its click too)
54
57
  const EDGE = 8; // the edges' strips reach this far past the table (the resize tool's grab)
55
58
  const ARROWS = '<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M1.5 6h9M3.5 4 1.5 6l2 2M8.5 4l2 2-2 2"/></svg>';
@@ -62,8 +65,29 @@ export function createColumnWidths(tools) {
62
65
  let armed = null; // an edge clicked: { unit, axis: "x" (right) | "y" (bottom), at: when, shown }, the next drag of it the table's size
63
66
  let quick = null; // the armed right edge pressed soon after its click: { unit, index, x } (let go there, a double click)
64
67
 
65
- /** Each column's width as the table shows it now, px. */
66
- const shown = (unit) => tools.geometry(unit).columns.map((column) => Math.round((column.right - column.left) * 100) / 100);
68
+ /**
69
+ * Each column's width as the table shows it now, px: the width of its cells, not the distance
70
+ * between their edges. A table scrolled sideways keeps its first column in sight (position: sticky,
71
+ * tables.css: a header column or .doc-table--sticky-first), so the edges of those cells are not the
72
+ * columns' and the column under them would read as the part of it in sight: every width then sums
73
+ * to the frame's, the table seeming full, the columns before the border squeezed when it was pressed.
74
+ */
75
+ const shown = (unit) => {
76
+ const { slots } = tableState(unit);
77
+ if (!slots) return tools.geometry(unit).columns.map((column) => Math.round((column.right - column.left) * 100) / 100);
78
+ const cells = [...slots.places].map(([cell, place]) => ({ ...place, width: cell.getBoundingClientRect().width }));
79
+ const widths = Array(slots.columns).fill(null);
80
+ for (const { c, cols, width } of cells) if (cols === 1) widths[c] ??= width;
81
+ // A column no single cell gives a width: what the cells merged over it leave, shared out.
82
+ for (const { c, cols, width } of cells.filter((cell) => cell.cols > 1).sort((a, b) => a.cols - b.cols)) {
83
+ const over = Array.from({ length: cols }, (_, i) => c + i);
84
+ const open = over.filter((i) => widths[i] === null);
85
+ if (open.length === 0) continue;
86
+ const left = Math.max(0, width - over.reduce((sum, i) => sum + (widths[i] ?? 0), 0));
87
+ for (const i of open) widths[i] = left / open.length;
88
+ }
89
+ return widths.map((width) => Math.round((width ?? 0) * 100) / 100);
90
+ };
67
91
  /** The cells holding the columns' widths: per column, its topmost cell alone, or null. */
68
92
  const holders = (unit) => {
69
93
  const { slots } = tableState(unit);
@@ -101,20 +125,56 @@ export function createColumnWidths(tools) {
101
125
  return done;
102
126
  }
103
127
 
104
- // Shown as it goes: the holders' widths set straight (taken back before the undo step).
105
- function preview(unit, widths) {
128
+ // Shown as it goes: the holders' widths set straight (taken back before the undo step), the
129
+ // table's own width (--doc-span) off while it lasts, as the kit would else keep the columns in
130
+ // proportion to it and the table where it was.
131
+ function preview(drag, widths) {
132
+ const { unit } = drag;
133
+ if (drag.span === undefined) {
134
+ drag.span = unit.style.getPropertyValue(SPAN);
135
+ unit.style.removeProperty(SPAN);
136
+ }
106
137
  holders(unit).forEach((cell, c) => cell?.style.setProperty(COLUMN_WIDTH, `${widths[c]}px`));
107
138
  relayout(unit);
108
139
  }
109
- function putBack({ unit, before }) {
140
+ function putBack({ unit, before, span }) {
110
141
  for (const [cell, width] of before) {
111
142
  if (width) cell.style.setProperty(COLUMN_WIDTH, width);
112
143
  else cell.style.removeProperty(COLUMN_WIDTH);
113
144
  if (cell.getAttribute("style") === "") cell.removeAttribute("style");
114
145
  }
146
+ if (span) unit.style.setProperty(SPAN, span);
115
147
  relayout(unit);
116
148
  }
117
149
 
150
+ // The columns' widths together (px) at which the table is exactly as wide as the text (or the
151
+ // column it stands in): the room its frame has, as the kit lays a table out at the reader's widths
152
+ // (tables/layout.js manualLayout: a table of that width or more has the frame at 100%, one less
153
+ // a frame as wide as it). Null where the frame has none.
154
+ function fillOf(unit) {
155
+ const parent = unit.parentElement;
156
+ if (!parent) return null;
157
+ const sides = (node) => {
158
+ const style = getComputedStyle(node);
159
+ return ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"].reduce((sum, name) => sum + (parseFloat(style[name]) || 0), 0);
160
+ };
161
+ const room = parent.getBoundingClientRect().width - sides(parent) - sides(unit);
162
+ return room > 0 ? room : null;
163
+ }
164
+
165
+ // The last column's right edge is the table's: within SNAP of the width that fills the text, the
166
+ // last column takes what the others leave of it (rounded down: never a hair past the frame).
167
+ // True when it did. `widths` is changed in place.
168
+ function snapFull({ last, fill, start, index }, widths) {
169
+ if (!last || fill === null) return false;
170
+ const rest = start.reduce((sum, width, c) => (c === index ? sum : sum + width), 0);
171
+ const wanted = Math.floor((fill - rest) * 100) / 100;
172
+ if (wanted < MIN_COLUMN_WIDTH || Math.abs(rest + widths[index] - fill) > SNAP) return false;
173
+ widths[index] = wanted;
174
+ return true;
175
+ }
176
+ const FULL = () => [t("表格"), t("宽度"), t("铺满正文")];
177
+
118
178
  // A column fitted to its widest cell of it alone, unwrapped (+1px): by its words, not the words an
119
179
  // Agent took out that the page shows in it (changes/: out of the layout while it is measured).
120
180
  function fit(unit, index) {
@@ -221,7 +281,7 @@ export function createColumnWidths(tools) {
221
281
  return fit(unit, index);
222
282
  }
223
283
  const { rows } = tableParts(unit);
224
- drag = { unit, index, x: event.clientX, last, fresh: true, start: shown(unit), before: new Map(rows.flatMap((row) => [...row.cells]).map((cell) => [cell, widthOf(cell)])) };
284
+ drag = { unit, index, x: event.clientX, last, fill: last ? fillOf(unit) : null, told: false, fresh: true, start: shown(unit), before: new Map(rows.flatMap((row) => [...row.cells]).map((cell) => [cell, widthOf(cell)])) };
225
285
  document.documentElement.setAttribute("data-ge-tresize", "");
226
286
  document.documentElement.setPointerCapture?.(event.pointerId);
227
287
  });
@@ -262,8 +322,16 @@ export function createColumnWidths(tools) {
262
322
  drag.moved = true;
263
323
  const widths = [...drag.start];
264
324
  widths[drag.index] = Math.max(MIN_COLUMN_WIDTH, Math.round(drag.start[drag.index] + event.clientX - drag.x));
325
+ // The last column's edge snaps to the table that fills the text; the bottom notice says so while it holds.
326
+ const snapped = snapFull(drag, widths);
265
327
  drag.widths = widths;
266
- preview(drag.unit, widths);
328
+ drag.snapped = snapped;
329
+ preview(drag, widths);
330
+ if (snapped !== drag.told) {
331
+ drag.told = snapped;
332
+ if (snapped) tools.say(t("{0}{1}:{2}", ...FULL()), "note", LIVE);
333
+ else tools.settle();
334
+ }
267
335
  tools.draw();
268
336
  }, true);
269
337
 
@@ -273,9 +341,13 @@ export function createColumnWidths(tools) {
273
341
  document.documentElement.removeAttribute("data-ge-tresize");
274
342
  if (!done) return;
275
343
  putBack(done);
276
- if (!cancel && done.widths) set(done.unit, done.widths);
344
+ if (done.told) tools.settle(); // the notice of the table filling the text: the result's follows, or nothing
345
+ // Back at the widths it had, snapped to a table that was full already (or dragged out and back): nothing to write.
346
+ const unchanged = done.widths?.every((width, c) => Math.abs(width - done.start[c]) < 0.5);
347
+ if (!cancel && done.widths) {
348
+ if (!unchanged && set(done.unit, done.widths) && done.snapped) tools.say(t("已调整{0}{1}:{2} · Ctrl+Z 撤销", ...FULL()));
277
349
  // The last strip clicked (pressed, not moved): the table's right edge armed.
278
- else if (!cancel && done.last) return arm(done.unit, "x");
350
+ } else if (!cancel && done.last) return arm(done.unit, "x");
279
351
  tools.draw();
280
352
  }
281
353
  document.addEventListener("pointerup", () => end(false), true);
@@ -7,6 +7,7 @@ import { joinCode } from "../marks.mjs";
7
7
  import { numberWithUnit, sideOf, WORD } from "../../../shared/inline-spacing.mjs";
8
8
  import { pageOf, pagesAround } from "../page-objects.mjs";
9
9
  import { BLOCK_TYPES, HOSTS, accepts, firstSaysOf, placeholderOf, textFilters, textTypeOf } from "../../components/registry.mjs";
10
+ import { unalign } from "../align.mjs";
10
11
  import { t } from "../../../shared/i18n/index.mjs";
11
12
 
12
13
  // Typing in a block (docs/editor.md, "Browser side"): every key goes first to the component the block
@@ -14,7 +15,7 @@ import { t } from "../../../shared/i18n/index.mjs";
14
15
  // `if (!ctx.def.keys?.enter?.(ctx)) splitBlock(...)`, and then to the rules of every block. Enter
15
16
  // splits it, Backspace and Delete at its edges merge it with the block before or after, or remove it
16
17
  // once empty, the caret going to the neighbouring block. Backspace in a container with nothing
17
- // written in it (`removedEmpty`) deletes the container. Chinese typed next to a Latin letter or
18
+ // written in it (`removedEmpty`) or in an empty heading turns it into an empty paragraph. Chinese typed next to a Latin letter or
18
19
  // digit gets a space between them (autoSpace; not a number and its unit, nor in a block written without), and an empty block under the caret says what it takes
19
20
  // (placeholder). Plain typing and input methods stay with the browser; the keys are routed here by
20
21
  // editor.mjs.
@@ -53,8 +54,11 @@ function writtenTogether(host, node, start, end) {
53
54
  * deleted as the selected ones are (selection/delete.mjs). adding(): the block the "/" menu the
54
55
  * handle's "+" opened is open in (slash-menu/), or null. leave: where Enter and Backspace leave a
55
56
  * container (leave.mjs). showHidden(node): the folds hiding a block opened (fold/ showAround).
57
+ * look(element, change): the editor's undoable change of classes (a block's alignment, align.mjs).
58
+ * afterAbove(host): Backspace at the start of a text block right after a block of no text, the caret
59
+ * put after that block (after/ back); whether it was.
56
60
  */
57
- export function createTyping({ struct, say, keepText, paragraphBefore, removeBlocks, selectBlock = () => {}, adding = () => null, leave, showHidden = () => {} }) {
61
+ export function createTyping({ struct, say, keepText, paragraphBefore, removeBlocks, selectBlock = () => {}, adding = () => null, leave, showHidden = () => {}, look = () => false, afterAbove = () => false }) {
58
62
  // A key, asked of the component of the text block `host` and then of what stands around it, nearest
59
63
  // first (docs/editor.md, "The order of keys"): whether it was theirs. `ctx` is notion-lite's, as
60
64
  // much of it as a key reads, the block being an element here.
@@ -101,8 +105,8 @@ export function createTyping({ struct, say, keepText, paragraphBefore, removeBlo
101
105
  function split(host) {
102
106
  if (HOSTS[host.getAttribute("data-ge-kind")]?.opensBody) return enterBody(host);
103
107
  // The caret at the very start of a block with text: an empty paragraph goes before the block and
104
- // the caret stays (a block that splits does this itself, splitBlock). This comes first: in a code
105
- // block too, whose own Enter breaks the line.
108
+ // the caret stays (a block that splits does this itself, splitBlock). Only hosts that opt in take
109
+ // this rule; code keeps its own Enter, which breaks the line even at the start.
106
110
  const own = HOSTS[host.getAttribute("data-ge-kind")]?.opensBefore; // not a cell, a card's field, the title
107
111
  if (own && !can(host, "split") && !isEmpty(host) && atEdge(host, false) && paragraphBefore(host)) return;
108
112
  // Its component's Enter (a code block's line, a nested item out a level, a title going into its
@@ -152,10 +156,33 @@ export function createTyping({ struct, say, keepText, paragraphBefore, removeBlo
152
156
  return true;
153
157
  }
154
158
 
155
- // An empty container deleted whole (one undo step, which selects it again); the caret at the end of
156
- // the block before it, or at the start of the one after it when it was first (a formula block passed
157
- // over: textNear).
159
+ // Whether a block may not go (keepText): the reason said, a note that stays a while.
160
+ function kept(node) {
161
+ const reason = keepText(node);
162
+ if (reason) say(reason, "note", 6000);
163
+ return Boolean(reason);
164
+ }
165
+
166
+ // An empty container turned into an empty paragraph where it stands, the caret in it (the user's,
167
+ // 2026-11-09: as an empty heading, an empty item and every other empty block go back to body text,
168
+ // not away with the caret on the block before); one undo step, which brings the container back.
169
+ // Where its box takes no paragraph it is deleted whole (one undo step, which selects it again), the
170
+ // caret at the end of the block before it, or at the start of the one after it when it was first (a
171
+ // formula block passed over: textNear).
158
172
  function removeContainer(container) {
173
+ const box = container.parentElement;
174
+ const paragraph = newParagraphHost(document);
175
+ if (box && accepts(boxKind(box), paragraph.getAttribute("data-ge-type"))) {
176
+ if (kept(container)) return true;
177
+ const parts = withLeads(container);
178
+ struct([box], [], () => {
179
+ parts[0].before(paragraph);
180
+ parts.forEach((part) => part.remove());
181
+ return { touched: [paragraph], focus: paragraph };
182
+ });
183
+ caretAt(paragraph, "start");
184
+ return true;
185
+ }
159
186
  const list = hosts();
160
187
  const inside = list.filter((node) => container.contains(node));
161
188
  const before = textNear(inside[0], -1, list);
@@ -181,11 +208,7 @@ export function createTyping({ struct, say, keepText, paragraphBefore, removeBlo
181
208
  ? (next ? [next, "start"] : [previous, "end"])
182
209
  : (previous ? [previous, "end"] : [next, "start"]);
183
210
  const node = removalTarget(host);
184
- const kept = keepText(node);
185
- if (kept) {
186
- say(kept, "note", 6000);
187
- return true;
188
- }
211
+ if (kept(node)) return true;
189
212
  struct([node.parentNode], [host], () => {
190
213
  takeOut(node);
191
214
  return { touched: [], focus: target };
@@ -203,6 +226,9 @@ export function createTyping({ struct, say, keepText, paragraphBefore, removeBlo
203
226
  // Backspace at the start of a block. Returns whether it was handled.
204
227
  function backspace(host) {
205
228
  opened(previousLine(host));
229
+ // A paragraph's or a heading's alignment, then its indent a level at a time, goes before anything
230
+ // else, one undo step each (align.mjs unalign: Feishu's 退对齐 and 反缩进 before a join).
231
+ if (unalign(host, look)) return true;
206
232
  // The block's component (a list item's: a nested item moves out a level, the first or last item of
207
233
  // a top-level list leaves it as body text, an emptied item holding lists stays), then what stands
208
234
  // around it (the item a block stands under; a node, at the start of its first line, holding
@@ -212,6 +238,12 @@ export function createTyping({ struct, say, keepText, paragraphBefore, removeBlo
212
238
  const container = emptyContainer(host);
213
239
  if (container) return removeContainer(container);
214
240
  if (leave.backspace(host)) return true;
241
+ // An empty heading (or small text) is an empty paragraph, where it stands (leave.mjs emptied).
242
+ if (leave.emptied(host)) return true;
243
+ // Right after a block of no text (a picture, a table, a code block, a callout, a divider, a card…):
244
+ // the caret after it, at its bottom right (after/), an empty block going (the user's, 2026-11-09:
245
+ // Backspace there went into the block, or past a picture to the text before it); never a join.
246
+ if (afterAbove(host)) return true;
215
247
  if (isEmpty(host)) return removeHost(host, false);
216
248
  if (merge(host)) return true;
217
249
  // A block holding blocks that did not join the line above says why, as its component puts it.