@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,288 @@
1
+ import { IN_LAYER } from "../../../shared/layers.mjs";
2
+ import { UNIT, can, isEmpty, isHost, newHost, removalTarget, takeOut, withLeads } from "../core.mjs";
3
+ import { atEdge, caretAt, hosts } from "../caret.mjs";
4
+ import { caretBeside } from "../beside.mjs";
5
+ import { contentOf, extentOf } from "../geometry.mjs";
6
+ import { FOLLOW, task } from "../frames.mjs";
7
+ import { layerOf } from "../overlay-layer.mjs";
8
+ import { onSettle, own } from "../overlays.mjs";
9
+ import { addStyle, element } from "../ui.mjs";
10
+ import { alignClass, shownAlign } from "../align.mjs";
11
+ import { accepts, texOnly } from "../../components/registry.mjs";
12
+
13
+ // The caret after a block (docs/editor.md, "The caret after a block"; the user's, 2026-11-09): a click
14
+ // right of a block that is no text of its own (a picture, a table, a code block, a callout, a divider, a
15
+ // card, an instance of a custom block, a row of columns…: insert/ boxedBeside, beside.mjs) puts a caret
16
+ // at its bottom right, a blinking one, as the browser draws in text. Neither notion-lite nor
17
+ // feishu-lite has one (Gallery's click there selected the block before); this is the plainest rule.
18
+ // The text right below such a block comes back to it (the user's, 2026-11-09: Backspace there went
19
+ // into the block, a table's last cell, a callout's box, or past it, a picture passed over): Backspace
20
+ // at the start of the text block right after it (typing/, after the block's own keys and its
21
+ // alignment), and ← or ↑ there, put this caret after it, an empty block left going as it does
22
+ // (one undo step), one with words staying (no step); a second Backspace is this caret's.
23
+ //
24
+ // Backspace, Delete the block becomes an empty paragraph in its place, whatever follows it (the
25
+ // user's, 2026-11-09; the box it stands in taking one), aligned as the block was
26
+ // shown (a picture centred: centred, align.mjs shownAlign), the caret in it; one
27
+ // more Backspace takes that alignment off (typing/: align.mjs unalign) before
28
+ // anything else
29
+ // Enter an empty paragraph right after it, the caret in it
30
+ // a key typed a paragraph right after it holding what was typed, the caret at its end (an
31
+ // input method composes in the caret, its words going there once it ends)
32
+ // ↑ ←, ↓ → the text before it (its own last text: a table's last cell, a picture's caption),
33
+ // or after it, at its end or start; none there, the caret stays
34
+ // Esc the block selected
35
+ // Ctrl+V a paste goes into a new paragraph after it
36
+ //
37
+ // Each change is one undo step, the words typed going with the paragraph they made (history.mjs
38
+ // typedInto), and undo puts this caret back after the block. The caret is an element of the editor's
39
+ // layer (overlay-layer.mjs: the page's, a dialog's), never the report's: an empty field that takes the
40
+ // focus, in the body text's font, at the block's right edge with its bottom on the block's, kept there
41
+ // as the page moves (a task of frames.mjs); it goes with the focus, its block, or a press elsewhere.
42
+
43
+ const STYLE = `
44
+ .ge-after { z-index: ${IN_LAYER.frame}; display: inline-block; min-width: 2px; margin: 0; padding: 0; border: 0; outline: none; white-space: pre; background: transparent; color: var(--doc-text, currentColor); caret-color: var(--doc-text, currentColor); pointer-events: none; }
45
+ `;
46
+
47
+ /**
48
+ * struct(parents, touched, mutate, options): the editor's undoable structural change (history.mjs).
49
+ * blocks(): the block tools (blocks.mjs). keepText(unit): why a block may not go. removeBlocks(units):
50
+ * the blocks deleted as the selected ones are (where no paragraph can take its place). say: the status
51
+ * line. undo(), redo(): the editor's.
52
+ */
53
+ export function createAfter({ struct, blocks, keepText, removeBlocks, say, undo, redo }) {
54
+ addStyle(STYLE);
55
+ const node = element("span", { class: "ge-after", contenteditable: "true", spellcheck: "false", "data-doc-dev": "" });
56
+ let at = null; // the block the caret stands after
57
+
58
+ const follow = task("after", {
59
+ run() {
60
+ if (!at) return false;
61
+ if (!at.isConnected || at.getClientRects().length === 0) {
62
+ hide();
63
+ return false;
64
+ }
65
+ draw();
66
+ return FOLLOW;
67
+ },
68
+ fail: () => hide(),
69
+ });
70
+
71
+ // At the block's right edge, its bottom on the block's: as tall as a line of the body text of the box it stands in.
72
+ function draw() {
73
+ const rect = extentOf(at);
74
+ if (!rect) return;
75
+ const layer = layerOf(at);
76
+ if (node.parentNode !== layer) layer.append(node);
77
+ const origin = layer.getBoundingClientRect();
78
+ const height = node.offsetHeight || 24;
79
+ node.style.transform = `translate(${Math.round(rect.right + 1 - origin.left)}px, ${Math.round(rect.bottom - height - origin.top)}px)`;
80
+ own(node, "after", { owner: at, alive: () => Boolean(at) && at.isConnected });
81
+ }
82
+
83
+ /** The caret after block `unit`, focused. */
84
+ function place(unit) {
85
+ if (!unit?.isConnected) return;
86
+ blocks().clear();
87
+ at = unit;
88
+ // The body text's font of the box it stands in (what a paragraph there is typed in).
89
+ const style = getComputedStyle(unit.parentElement ?? document.body);
90
+ const size = parseFloat(style.fontSize) || 16;
91
+ const line = parseFloat(style.lineHeight) || size * 1.5;
92
+ Object.assign(node.style, { fontFamily: style.fontFamily, fontSize: `${size}px`, lineHeight: `${line}px`, height: `${line}px` });
93
+ node.textContent = "";
94
+ draw();
95
+ node.focus({ preventScroll: true });
96
+ const range = document.createRange();
97
+ range.setStart(node, 0);
98
+ getSelection().removeAllRanges();
99
+ getSelection().addRange(range);
100
+ follow.wake();
101
+ }
102
+
103
+ function hide() {
104
+ at = null;
105
+ node.textContent = "";
106
+ node.remove();
107
+ }
108
+
109
+ // What the caret stands after, the caret taken away (the focus going where the change puts it).
110
+ function take() {
111
+ const unit = at;
112
+ at = null;
113
+ node.textContent = "";
114
+ if (document.activeElement === node) node.blur();
115
+ node.remove();
116
+ return unit;
117
+ }
118
+
119
+ const takesParagraph = (box) => Boolean(box) && accepts(box.getAttribute("data-ge-box-kind") ?? "flow", "paragraph");
120
+
121
+ // A paragraph right after the block (`text` in it: what was typed), the caret at its end; one undo
122
+ // step, the words typed on into it with it (typedInto), undo putting this caret back after the block.
123
+ function paragraphAfter(text = "") {
124
+ const unit = at;
125
+ const box = unit?.parentElement;
126
+ if (!takesParagraph(box)) return null;
127
+ take();
128
+ const paragraph = newHost(document, "paragraph");
129
+ if (text) paragraph.textContent = text;
130
+ struct([box], [], () => {
131
+ unit.after(paragraph);
132
+ return { touched: [paragraph], focus: paragraph, settle: (backwards) => backwards && queueMicrotask(() => place(unit)) };
133
+ }, { typedInto: text ? paragraph : null });
134
+ caretAt(paragraph, "end");
135
+ return paragraph;
136
+ }
137
+
138
+ // Backspace, Delete: the block becomes an empty paragraph in its place, aligned as it was shown, the
139
+ // caret in it, whatever follows it (the user's, 2026-11-09: it was the text after it that took the
140
+ // caret, no paragraph made); where its box takes none, it is deleted as a selected block is. One undo
141
+ // step, which puts this caret back after the block.
142
+ function remove() {
143
+ const unit = at;
144
+ const reason = keepText(unit);
145
+ if (reason) {
146
+ say(reason, "note", 6000);
147
+ return;
148
+ }
149
+ const box = unit.parentElement;
150
+ if (!takesParagraph(box)) {
151
+ take();
152
+ removeBlocks([unit]);
153
+ return;
154
+ }
155
+ const parts = withLeads(unit);
156
+ const area = contentOf(box.getBoundingClientRect(), getComputedStyle(box));
157
+ const aligned = alignClass(shownAlign(unit.getBoundingClientRect(), area));
158
+ take();
159
+ const paragraph = newHost(document, "paragraph");
160
+ if (aligned) paragraph.classList.add(aligned);
161
+ struct([box], [], () => {
162
+ parts[0].before(paragraph);
163
+ parts.forEach((part) => part.remove());
164
+ return { touched: [paragraph], focus: paragraph, settle: (backwards) => backwards && queueMicrotask(() => place(unit)) };
165
+ });
166
+ caretAt(paragraph, "start");
167
+ }
168
+
169
+ // The block of no text right above text block `host` (one a click right of puts this caret after:
170
+ // beside.mjs caretBeside), shown: the block before it in its box, before a list's first item the one
171
+ // before the list; or null. `host` is the first text of its block (a paragraph, a heading, a list's
172
+ // first item), never a table's cell or a code block's text.
173
+ function above(host) {
174
+ if (!isHost(host) || texOnly(host) || !host.matches(UNIT)) return null;
175
+ let node = host;
176
+ while (!withLeads(node)[0].previousElementSibling && /^(UL|OL)$/.test(node.parentElement?.tagName ?? "") && node.parentElement.matches(UNIT)) node = node.parentElement;
177
+ const block = withLeads(node)[0].previousElementSibling;
178
+ return caretBeside(block) && block.parentElement?.hasAttribute("data-ge-box") && block.getClientRects().length > 0 && block.checkVisibility?.() !== false ? block : null;
179
+ }
180
+
181
+ /**
182
+ * Backspace at the start of text block `host` (typing/, after its own keys): the caret after the
183
+ * block of no text right above it, `host` going when it is empty (one undo step, undo putting the
184
+ * caret back in it, redo this caret after the block), staying when it holds words. Whether it did.
185
+ */
186
+ function back(host) {
187
+ const unit = above(host);
188
+ if (!unit) return false;
189
+ const node = isEmpty(host) && can(host, "remove") ? removalTarget(host) : null;
190
+ if (node && !keepText(node)) {
191
+ struct([node.parentNode], [host], () => {
192
+ takeOut(node);
193
+ return { touched: [], focus: null, settle: (backwards) => !backwards && queueMicrotask(() => place(unit)) };
194
+ });
195
+ }
196
+ place(unit);
197
+ return true;
198
+ }
199
+
200
+ /** ← or ↑ with the caret at the start of text block `host` (editor.mjs): the caret after the block above it, as back(). Whether it did. */
201
+ function up(host, key) {
202
+ if (key !== "ArrowLeft" && key !== "ArrowUp") return false;
203
+ const selection = getSelection();
204
+ if (!selection.rangeCount || !selection.isCollapsed || !atEdge(host, false)) return false;
205
+ const unit = above(host);
206
+ if (!unit) return false;
207
+ place(unit);
208
+ return true;
209
+ }
210
+
211
+ // ↑ ←: the text before the caret (the block's own last, or the one before it), at its end; ↓ →: the
212
+ // first after it, at its start. Formula blocks are passed over (no text to type in).
213
+ function leave(back) {
214
+ const unit = at;
215
+ const list = hosts().filter((host) => !texOnly(host));
216
+ const before = (host) => unit.contains(host) || Boolean(host.compareDocumentPosition(unit) & Node.DOCUMENT_POSITION_FOLLOWING);
217
+ const target = back ? list.filter(before).at(-1) : list.find((host) => !before(host));
218
+ if (!target) return;
219
+ take();
220
+ caretAt(target, back ? "end" : "start");
221
+ }
222
+
223
+ /** A key with the caret after a block (editor.mjs onKeydown, first): whether it was this caret's. */
224
+ function key(event) {
225
+ if (!at || event.target !== node) return false;
226
+ if (event.isComposing || event.keyCode === 229) return true; // an input method's: composed here, put in at its end
227
+ const mod = event.ctrlKey || event.metaKey;
228
+ const name = event.key.toLowerCase();
229
+ if (mod && !event.altKey && (name === "z" || name === "y")) {
230
+ event.preventDefault();
231
+ take();
232
+ name === "y" || event.shiftKey ? redo() : undo();
233
+ return true;
234
+ }
235
+ if (mod && !event.altKey && !event.shiftKey && name === "v") {
236
+ paragraphAfter(); // the paste that follows goes into it
237
+ return true;
238
+ }
239
+ if (mod && name === "s") return false; // saving, the editor's
240
+ if (event.key === "Backspace" || event.key === "Delete") remove();
241
+ else if (event.key === "Enter") paragraphAfter();
242
+ else if (event.key === "Escape") {
243
+ const unit = take();
244
+ blocks().select(unit);
245
+ } else if (event.key.startsWith("Arrow") && !mod && !event.altKey) leave(event.key === "ArrowUp" || event.key === "ArrowLeft");
246
+ else if (!mod && event.key.length === 1) return true; // a character: typed here, put in a paragraph (input)
247
+ else if (mod || event.key === "Tab") event.preventDefault(); // nothing to format or move: the keys do nothing here
248
+ else return true;
249
+ event.preventDefault();
250
+ return true;
251
+ }
252
+
253
+ // What was typed (a key's character, an input method's words once it ends): a paragraph after the
254
+ // block holding it.
255
+ node.addEventListener("input", (event) => {
256
+ if (event.isComposing || !at) return;
257
+ const text = node.textContent;
258
+ node.textContent = ""; // nothing stays in the field, wherever the words go (a box that takes no paragraph: nowhere)
259
+ if (text) paragraphAfter(text);
260
+ });
261
+ node.addEventListener("compositionend", (event) => {
262
+ if (!at) return;
263
+ const text = event.data || node.textContent;
264
+ node.textContent = "";
265
+ if (text) paragraphAfter(text);
266
+ });
267
+ // The focus gone elsewhere on the page (a press, a key that moved it): the caret goes. The window
268
+ // losing it keeps it, as the browser's own caret is kept.
269
+ node.addEventListener("blur", () => {
270
+ if (at && document.hasFocus()) hide();
271
+ });
272
+ // Its block taken off the page (an Agent's write, undo): it goes at once.
273
+ onSettle(() => {
274
+ if (at && !at.isConnected) hide();
275
+ });
276
+ globalThis.__galleryAfter = () => (at && document.activeElement === node ? at : null); // the block the caret stands after (the browser tests)
277
+
278
+ return {
279
+ place,
280
+ key,
281
+ back,
282
+ up,
283
+ /** The block the caret stands after, or null. */
284
+ get block() { return at; },
285
+ /** The editor's UI nodes to keep in the top layer when a modal dialog opens. */
286
+ get nodes() { return node.isConnected ? [node] : []; },
287
+ };
288
+ }
@@ -0,0 +1,91 @@
1
+ import { ALIGNS, ALIGN_TOGGLES, INDENT_MOST, alignLook, alignOf, aligns, indentLook, indentOf, indents } from "../components/align.mjs";
2
+ import { TOGGLES } from "../components/registry.mjs";
3
+ import { svg } from "../components/icons.mjs";
4
+ import { TABLE_ICONS } from "./table/icons.mjs";
5
+ import { t } from "../../shared/i18n/index.mjs";
6
+
7
+ // 对齐与缩进 (docs/editor.md, "Alignment and indent"; Feishu's 缩进和对齐 ›, its block menu's), shared by
8
+ // the tools that use it (block-menu/, typing/, after/): a
9
+ // paragraph's or a heading's alignment and indent, class switches its component takes (components/
10
+ // align.mjs) that the file keeps and the kit draws; a table's frame takes the alignment alone (it is
11
+ // placed on the page left, centred or right, as a text block's words are), by the same row, named 对齐.
12
+ // The block menu's row (block-menu/), for one block or
13
+ // for several selected that all take them, one undo step; Enter carrying them to the block it makes
14
+ // (core.mjs newBlock: components/align.mjs carry), and Backspace at a block's start taking them off before anything else
15
+ // (typing/: unalign), one at a time: a centred or right-aligned block to the left, then its indent a
16
+ // level less (Feishu's text block: 退对齐, then 反缩进, then 合并到上一块), each one undo step, the caret
17
+ // staying at the start. The same for a paragraph left in a block's place by Backspace after it
18
+ // (after/): it takes the alignment the block showed.
19
+
20
+ // Its icons, in the menus' lines (20 units, the table's alignments beside them): text lines the middle
21
+ // two moved in (the row), and the same with an arrow in (增加缩进) or out (减少缩进) at their left.
22
+ const LINES = '<path d="M3.75 4.75h12.5M9.25 8.5h7M9.25 11.75h7M3.75 15.25h12.5"/>';
23
+ export const ALIGN_ICONS = {
24
+ row: svg('<path d="M3.75 4.75h12.5M7.25 8.5h9M7.25 11.75h9M3.75 15.25h12.5"/>'),
25
+ left: TABLE_ICONS["doc-align-left"],
26
+ center: TABLE_ICONS["doc-align-center"],
27
+ right: TABLE_ICONS["doc-align-right"],
28
+ indent: svg(`${LINES}<path d="m3.75 7.9 2.25 2.25-2.25 2.25"/>`),
29
+ outdent: svg(`${LINES}<path d="M6 7.9 3.75 10.15 6 12.4"/>`),
30
+ };
31
+
32
+ const LABELS = { left: t("左对齐"), center: t("居中对齐"), right: t("右对齐") };
33
+
34
+ /** Whether a block takes an alignment: its kind's class switches hold it (`kind`: its unit kind). */
35
+ export const alignable = (kind) => aligns(TOGGLES[kind]);
36
+
37
+ /** Whether a block takes an indent too, as a text block does (`kind`: its unit kind). */
38
+ export const indentable = (kind) => indents(TOGGLES[kind]);
39
+
40
+ /**
41
+ * The block menu's 对齐与缩进 › for `units` (one block, or several selected), when they all take one
42
+ * (kindOf(unit): its unit kind), or none: the three alignments (the one they share checked), then 增加缩进
43
+ * and 减少缩进 (each a level for every one of them, disabled where none can go further), one undo step
44
+ * each (look: the editor's change of classes). Where they do not all take an indent (a table's frame
45
+ * has none) the row is 对齐 › with the three alignments alone.
46
+ */
47
+ export function alignRows(units, { kindOf, look }) {
48
+ if (units.length === 0 || !units.every((unit) => alignable(kindOf(unit)))) return [];
49
+ const shared = units.every((unit) => alignOf(unit) === alignOf(units[0])) ? alignOf(units[0]) : null;
50
+ const withIndent = units.every((unit) => indentable(kindOf(unit)));
51
+ const levels = units.map(indentOf);
52
+ const indent = (step) => look(units, units.map((unit) => indentLook(Math.max(0, Math.min(INDENT_MOST, indentOf(unit) + step)))), units);
53
+ return [{
54
+ label: withIndent ? t("对齐与缩进") : t("对齐"), icon: ALIGN_ICONS.row,
55
+ submenu: () => [
56
+ ...["left", "center", "right"].map((name) => ({ label: LABELS[name], icon: ALIGN_ICONS[name], current: shared === name, run: () => look(units, alignLook(name), units) })),
57
+ ...(withIndent ? [
58
+ { separator: true },
59
+ { label: t("增加缩进"), icon: ALIGN_ICONS.indent, disabled: levels.every((level) => level >= INDENT_MOST), run: () => indent(1) },
60
+ { label: t("减少缩进"), icon: ALIGN_ICONS.outdent, disabled: levels.every((level) => level === 0), run: () => indent(-1) },
61
+ ] : []),
62
+ ],
63
+ }];
64
+ }
65
+
66
+ /**
67
+ * Backspace at the start of text block `host` (typing/): its alignment taken off (to the left), else its
68
+ * indent a level less, one undo step (look), the caret where it is. Returns whether it did either.
69
+ */
70
+ export function unalign(host, look) {
71
+ if (!indentable(host.getAttribute?.("data-ge-unit-kind")) || !ALIGN_TOGGLES.some((name) => host.classList.contains(name))) return false;
72
+ if (alignOf(host) !== "left") return look(host, alignLook("left"));
73
+ return look(host, indentLook(indentOf(host) - 1));
74
+ }
75
+
76
+ /**
77
+ * The alignment a block shows in the text area of the box it stands in (`area`: that area's rect,
78
+ * window coordinates): "center" or "right" when it is narrower and stands there, else "left" (a
79
+ * block as wide as the area has none). How a picture, a card or a table set narrower is placed,
80
+ * whatever says so (its class, its margins): what the reader sees.
81
+ */
82
+ export function shownAlign(rect, area) {
83
+ const left = rect.left - area.left;
84
+ const right = area.right - rect.right;
85
+ if (left <= 2 && right <= 2) return "left";
86
+ if (Math.abs(left - right) <= 2) return "center";
87
+ return right <= 2 ? "right" : "left";
88
+ }
89
+
90
+ /** The class that gives a new paragraph the alignment `name`, or null for the left. */
91
+ export const alignClass = (name) => ALIGNS[name] ?? null;
@@ -6,16 +6,15 @@ import { isHost } from "./core.mjs";
6
6
  // (selection/).
7
7
 
8
8
  /**
9
- * Whether a click to the right of a block, past the text area it stands in (insert/ boxedBeside),
10
- * selects it: a boxed block (a code block, a highlight block, a disclosure, a card, a table, a
11
- * divider…) or a row of columns, as opposed to text and the boxes text flows in (a list, a quote, a
12
- * section).
9
+ * Whether a click to the right of a block (insert/ boxedBeside) puts the caret after it (after/): a
10
+ * boxed block (a code block, a highlight block, a disclosure, a card, a table, a divider…) or a row
11
+ * of columns, as opposed to text and the boxes text flows in (a list, a quote, a section).
13
12
  */
14
- export const selectsBeside = (unit) => BESIDE.has(unit?.getAttribute?.("data-ge-unit-kind"));
13
+ export const caretBeside = (unit) => BESIDE.has(unit?.getAttribute?.("data-ge-unit-kind"));
15
14
 
16
15
  /**
17
16
  * Whether Enter on a block selected (the first of those selected) puts a new paragraph after them
18
17
  * (blocks.mjs, insert.paragraphAfter): a block of no text of its own, those a click to their right
19
- * selects and a 选项卡区块; not a text block among them (a formula block, whose editor it opens).
18
+ * puts the caret after and a 选项卡区块; not a text block among them (a formula block, whose editor it opens).
20
19
  */
21
20
  export const entersAfter = (unit) => Boolean(unit) && !isHost(unit) && ENTERS_AFTER.has(unit.getAttribute("data-ge-unit-kind"));
@@ -10,6 +10,7 @@ import { columnOps, menuRow, pageOfUnit, rowBeside, sectionOps } from "../page-o
10
10
  import { MENU_COLUMNS, MENU_WIDTH, typeGrid } from "../type-grid.mjs";
11
11
  import { rememberType, typeItems } from "../slash-menu/index.mjs";
12
12
  import { COLOR_ICON, COLOR_WIDTH, colorItems } from "./colors.mjs";
13
+ import { alignRows } from "../align.mjs";
13
14
  import { t } from "../../../shared/i18n/index.mjs";
14
15
 
15
16
  // The block menu (docs/editor.md, "Deleting blocks" and "New blocks and conversions"; the card is
@@ -30,7 +31,8 @@ import { t } from "../../../shared/i18n/index.mjs";
30
31
  // "分区与分栏 ›" (组成新分区 for neighbours in the page or a Sheet panel, 解散所在分区 for blocks all in
31
32
  // one section, 分成 n 栏 for neighbours side by side, a column each) and deleting them all. A block that
32
33
  // takes a color (a paragraph, a heading: components/colors.mjs) has "颜色 ›" right below its types,
33
- // as Notion's (colors.mjs); so do several selected ones that all take one. It keeps clear of the block, or stands right of a section's bar.
34
+ // as Notion's (colors.mjs); so do several selected ones that all take one. A paragraph and a heading
35
+ // have "对齐与缩进 ›" right above it (align.mjs, Feishu's 缩进和对齐 ›), several selected that all take one too. It keeps clear of the block, or stands right of a section's bar.
34
36
  // A component whose block has a menu of its own gives it (`menu`): a Sheet tab's is Notion's tab
35
37
  // menu, below the tab (components/sheets/page.mjs): 重命名, 编辑图标 and 删除. A block that turns into
36
38
  // nothing has its name at the head (`nameOf`), with its mark where its component has one (`iconOf`:
@@ -147,7 +149,7 @@ export function createBlockMenu({ info, menu, selection, remove, takenWith = (un
147
149
  }, { columns: COLUMNS });
148
150
  items.push({ node: grid, cells: [...grid.querySelectorAll(".ge-type")], columns: COLUMNS }, { separator: true });
149
151
  }
150
- const colors = colorRow(group);
152
+ const colors = [...alignRows(group, { kindOf, look }), ...colorRow(group)];
151
153
  items.push(...colors, ...(colors.length > 0 ? [{ separator: true }] : []));
152
154
  // Blocks next to each other in the page or a Sheet panel become a section; a selection with gaps does not.
153
155
  // A folded heading among them goes in with what it hides (fold/).
@@ -171,7 +173,8 @@ export function createBlockMenu({ info, menu, selection, remove, takenWith = (un
171
173
  }
172
174
  // A highlight block's icon (添加图标 / 更换图标: components/icon/page.mjs rowBeside) beside its color.
173
175
  const icon = rowBeside(target);
174
- if (colorable(target) || icon) items.push(...(colorable(target) ? colorRow([target]) : []), ...(icon ? [icon] : []), { separator: true });
176
+ const aligned = alignRows([target], { kindOf, look });
177
+ if (aligned.length > 0 || colorable(target) || icon) items.push(...aligned, ...(colorable(target) ? colorRow([target]) : []), ...(icon ? [icon] : []), { separator: true });
175
178
  // A component's own rows (an image's: components/image/page.mjs; a table's, with its size's
176
179
  // 铺满正文宽度…; a card's, a code block's, a Sheet area's), one group, from its page module.
177
180
  const own = pageOfUnit(target)?.menuItems?.(target) ?? [];
@@ -67,7 +67,8 @@ export function createBlocks({ slug = "", units, boxes, active, struct, look, sa
67
67
  const blockMenu = createBlockMenu({ info, menu, selection, remove: (targets) => remove(targets), takenWith: folds.takenWith, turnInto, convert, turnIntoAll, convertAll, below, belowTypes, add, look, copyLink, linkAhead });
68
68
  const handle = createHandle({ info, active, selection, drag, menu, openMenu: blockMenu.open, drawing: () => marquee.drawing, schedule, keeps: (node) => keepsHandle(node) || folds.holds(node), aside: folds.holds, add: (target, where) => { selection.clear(); addNear(target, where); }, notes });
69
69
  const remove = createDelete({ info, selection, struct, say, closeMenu, forget: () => handle.forget(), folds });
70
- // What a component makes with these tools (`blockTools`): a Sheet's tabs as Notion's pills, a click, a drag, their menu, + (components/sheets/pills.mjs).
70
+ // What a component makes with these tools (`blockTools`): a Sheet's tabs as Notion's pills, a click, a drag, their menu, + (components/sheets/pills.mjs);
71
+ // an image's grips and bar while it is the one block selected (components/image/page.mjs), placed in this task's frames (`follow`).
71
72
  const parts = pageObjects().flatMap((page) => page.blockTools?.({ active, drag, menu, openMenu: blockMenu.open, struct, say }) ?? []);
72
73
  // A press on empty space: a box selection, or a click there (marquee.mjs clears the selection).
73
74
  const marquee = createMarquee({
@@ -91,6 +92,7 @@ export function createBlocks({ slug = "", units, boxes, active, struct, look, sa
91
92
  step("selection", () => selection.paint(drag.active), () => selection.unpaint());
92
93
  step("handle", () => handle.place(), () => handle.hide());
93
94
  step("fold", () => folds.place(handle.pointed), () => folds.hide());
95
+ step("parts", () => parts.forEach((part) => part.follow?.(drag.active ? [] : selection.picked)), () => parts.forEach((part) => part.hide()));
94
96
  if (drag.active) return true;
95
97
  return handle.showing || folds.showing || selection.picked.length > 0 ? FOLLOW : false;
96
98
  }
@@ -3,27 +3,31 @@ import { postAnswer } from "../post.mjs";
3
3
  import { say } from "../notices.mjs";
4
4
  import { addStyle, element } from "../ui.mjs";
5
5
 
6
- // The home page's first-open note (notes/plans/component-pages-plan-2026-11-07.md §5.2; docs/render.md
7
- // "A custom page", **The default home page**): at the top of the home page, while the page map says
8
- // `hint` (src/server/routes.mjs: the home page, its note not yet closed in this workspace), a short
9
- // note on its cards: how to move one (the grip at its top-right: drag it), open its menu (a click on
10
- // the same grip: 大小, 设置, 删除), add one (添加自定义块: the first item of the ··· menu at the top right of the workbench's bar; a page open
11
- // alone in a tab has it at the top right of its board) and take one out (the menu's 删除), and that a new
12
- // card or a new page is the Agent's to make, with a sentence the reader can copy and say to one (its 复制
13
- // button). Its × closes it for good, for the workspace (POST
14
- // /__gallery/pages/home/hint: .gallery/hints.json), whichever browser shows the page next. In the
6
+ // A custom page's first-open note (notes/plans/component-pages-plan-2026-11-07.md §5.2; docs/render.md
7
+ // "A custom page", **Notes of a page**): at the top of a page whose page.json says `hint` (the default home
8
+ // page's), while the page map says `hint` (src/server/routes.mjs: its note not yet closed in this
9
+ // workspace), a short note on its cards: how to move one (the grip at its top-right: drag it), open its
10
+ // menu (a click on the same grip: 大小, 设置, 删除), add one (添加自定义块: the first item of the ··· menu at the
11
+ // top right of the workbench's bar; a page open alone in a tab has it at the top right of its board) and
12
+ // take one out (the menu's 删除), and that a new card or a new page is the Agent's to make, with a sentence
13
+ // the reader can copy and say to one (its 复制 button). Its × closes it for good, for the workspace (POST
14
+ // /__gallery/pages/<slug>/hint: .gallery/hints.json), whichever browser shows the page next. In the
15
15
  // editor's look: grays alone, a quiet fill, no colour; the kit's own light and dark through the
16
16
  // editor's tokens (look.mjs).
17
17
  //
18
- // And its note of new default cards (showNews: the plan's 3.4), on a home page made before them (a phase 2
19
- // home: 问候与日期 and 便签 alone), while the page map says `news`: which default cards it does not hold
20
- // that came since (我的待办, 最近编辑, 工作区统计, 编辑热力图), and that 加到首页 puts them in the empty spots
21
- // as 恢复默认卡片 in the home page's menu in the sidebar does (the board's restoreDefaults: what is missing
22
- // alone, the cards there untouched, one step of the page's undo). A home already made is never changed by
23
- // itself: the reader is told once, at the top of the page, where the first-open note stands, and nothing
24
- // moves on the board until they ask. Its × (or 加到首页) says it for the workspace (POST
25
- // /__gallery/pages/home/hint with { cards: true }), which 恢复默认卡片 from the sidebar does too.
26
-
18
+ // Its note of new default cards (showNews: the plan's 3.4), on a page with the default cards (its page.json's
19
+ // defaultCards) made before some of them, while the page map says `news`: which default cards it does not
20
+ // hold that came since, and that 加到这一页 puts them in the empty spots as 恢复默认卡片 in the page's menu
21
+ // does (the board's restoreDefaults: what is missing alone, the cards there untouched, one step of the
22
+ // page's undo). A page already made is never changed by itself: the reader is told once, at the top of
23
+ // the page, where the first-open note stands, and nothing moves on the board until they ask. Its × (or
24
+ // 加到这一页) says it for the workspace (POST /__gallery/pages/<slug>/hint with { cards: true }), which
25
+ // 恢复默认卡片 does too.
26
+ //
27
+ // And the middle of a blank page (showBlank: the workbench's 新建页面, which no one has written yet, while
28
+ // the page map says `blank`): that an Agent writes a custom page, any page it likes, a board of cards or
29
+ // not, with a request the reader can copy and say to one, as the first-open note's. Nothing of it is the
30
+ // file's: once an Agent writes page.html the page is shown again without it.
27
31
  const STYLE = `
28
32
  .ge-home-hint { box-sizing: border-box; max-width: var(--gallery-board-width, 944px); margin: 0 auto 12px; }
29
33
  .ge-home-hint-box { position: relative; box-sizing: border-box; max-width: 720px; padding: 14px 44px 14px 16px; border-radius: 10px;
@@ -47,6 +51,9 @@ const STYLE = `
47
51
  .ge-home-hint-box .ge-home-hint-add { margin-left: -2px; background: var(--ge-btn-primary); color: var(--ge-btn-primary-text); }
48
52
  .ge-home-hint-box .ge-home-hint-add:hover:not(:disabled) { background: var(--ge-btn-primary-hover); color: var(--ge-btn-primary-text); }
49
53
  .ge-home-hint-box .ge-home-hint-add:disabled { opacity: .5; cursor: default; }
54
+ .ge-page-blank { box-sizing: border-box; display: flex; justify-content: center; padding: max(48px, 18vh) 16px 48px; }
55
+ .ge-page-blank .ge-home-hint-box { width: 100%; max-width: 520px; padding: 20px 20px 18px; }
56
+ .ge-page-blank .ge-home-hint-title { font-size: 16px; }
50
57
  `;
51
58
 
52
59
  const DOTS = '<svg viewBox="0 0 10 16" aria-hidden="true"><circle cx="2.5" cy="2" r="1.25"/><circle cx="7.5" cy="2" r="1.25"/><circle cx="2.5" cy="8" r="1.25"/><circle cx="7.5" cy="8" r="1.25"/><circle cx="2.5" cy="14" r="1.25"/><circle cx="7.5" cy="14" r="1.25"/></svg>';
@@ -70,15 +77,15 @@ export function showNews(map, { restore }) {
70
77
  if (!map?.news?.length || !main) return null;
71
78
  addStyle(STYLE);
72
79
  const named = ({ block, name }) => name?.[language === "en" ? "en" : "zh"] ?? name?.zh ?? block;
73
- const add = element("button", { type: "button", class: "ge-home-hint-copy ge-home-hint-add", text: t("加到首页") });
80
+ const add = element("button", { type: "button", class: "ge-home-hint-copy ge-home-hint-add", text: t("加到这一页") });
74
81
  const close = element("button", { type: "button", class: "ge-home-hint-close", "aria-label": t("关闭这条说明"), title: t("关闭"), html: CLOSE });
75
82
  const box = element("div", { class: "ge-home-hint-box" },
76
- element("p", { class: "ge-home-hint-title", text: t("首页有了新的卡片") }),
83
+ element("p", { class: "ge-home-hint-title", text: t("有了新的默认卡片") }),
77
84
  element("p", { text: map.news.map(named).join(t("、")) }),
78
- element("p", { class: "ge-home-hint-ask", text: t("点「加到首页」放进空位,已有的卡片不动。以后也可以在侧栏里首页的菜单中点「恢复默认卡片」。") }),
85
+ element("p", { class: "ge-home-hint-ask", text: t("点「加到这一页」放进空位,已有的卡片不动。以后也可以在这一页的菜单中点「恢复默认卡片」。") }),
79
86
  element("div", { class: "ge-home-hint-act" }, add),
80
87
  close);
81
- const note = noteOf("ge-home-news", t("首页的新卡片"), box);
88
+ const note = noteOf("ge-home-news", t("新的默认卡片"), box);
82
89
  for (const button of [add, close]) button.addEventListener("mousedown", (event) => event.preventDefault());
83
90
  const told = () => postAnswer(`/__gallery/pages/${map.slug}/hint`, map.token, { cards: true });
84
91
  add.addEventListener("click", async () => {
@@ -86,7 +93,7 @@ export function showNews(map, { restore }) {
86
93
  const answer = await restore();
87
94
  if (!answer?.ok) {
88
95
  add.disabled = false;
89
- return say(t("没能把卡片放到首页"), "error");
96
+ return say(t("没能把卡片放到这一页"), "error");
90
97
  }
91
98
  note.remove();
92
99
  const added = answer.added ?? [];
@@ -104,7 +111,7 @@ export function showNews(map, { restore }) {
104
111
  }
105
112
 
106
113
  /**
107
- * The note shown, on the home page whose map (`map`: slug, token) says `hint`, at the top of the page
114
+ * The note shown, on the page whose map (`map`: slug, token) says `hint`, at the top of the page
108
115
  * (`main.gallery-page`, before what the page holds). `hosted`: the page stands in a workbench, whose top
109
116
  * bar's ··· menu has 添加自定义块 (the words say so), else the page's own at the top right of its board. Returns the
110
117
  * note, or null where none is shown.
@@ -114,16 +121,16 @@ export function showHint(map, { hosted = false } = {}) {
114
121
  if (!map?.hint || !main) return null;
115
122
  addStyle(STYLE);
116
123
  // The sentence offered: one that names Gallery, so that the Agent takes up its skill.
117
- const ask = t("用 Gallery 在首页加一张卡片,记我每天喝了几杯水。别动我摆好的卡片。");
124
+ const ask = t("用 Gallery 在「{0}」这一页加一张卡片,记我每天喝了几杯水。别动我摆好的卡片。", map.title ?? document.title);
118
125
  const copy = element("button", { type: "button", class: "ge-home-hint-copy", text: t("复制") });
119
126
  const close = element("button", { type: "button", class: "ge-home-hint-close", "aria-label": t("关闭这条说明"), title: t("关闭"), html: CLOSE });
120
127
  const box = element("div", { class: "ge-home-hint-box" },
121
- element("p", { class: "ge-home-hint-title", text: t("这是你的首页") }),
128
+ element("p", { class: "ge-home-hint-title", text: t("这一页的卡片可以自己摆") }),
122
129
  element("p", {}, ...filled(hosted ? t("拖卡片右上角的 {0} 移动它,点一下它打开菜单(大小、设置、删除),点页面右上角的 ··· 里的「添加自定义块」加一张。") : t("拖卡片右上角的 {0} 移动它,点一下它打开菜单(大小、设置、删除),点页面右上方的「添加自定义块」加一张。"), [icon(DOTS)])),
123
130
  element("p", { class: "ge-home-hint-ask", text: t("想要新卡片或新页面,跟 Agent 说,比如:") }),
124
131
  element("div", { class: "ge-home-hint-say" }, element("span", { text: ask }), copy),
125
132
  close);
126
- const note = noteOf("", t("首页的说明"), box);
133
+ const note = noteOf("", t("这一页的说明"), box);
127
134
  for (const button of [copy, close]) button.addEventListener("mousedown", (event) => event.preventDefault());
128
135
  copy.addEventListener("click", () => {
129
136
  // The frame given the focus first: in a workbench a press its mousedown keeps from moving the focus
@@ -143,3 +150,33 @@ export function showHint(map, { hosted = false } = {}) {
143
150
  main.prepend(note);
144
151
  return note;
145
152
  }
153
+
154
+ /**
155
+ * The middle of a blank page (the page map's `blank`: the workbench's 新建页面, which no one has written yet):
156
+ * that an Agent writes it, any page it likes, with a request to copy and say to one. Returns the note, or
157
+ * null where none is shown (no blank page, or no main.gallery-page to show it in).
158
+ */
159
+ export function showBlank(map) {
160
+ const main = document.querySelector("main.gallery-page");
161
+ if (!map?.blank || !main) return null;
162
+ addStyle(STYLE);
163
+ const title = map.title ?? document.title;
164
+ const ask = t("用 Gallery 把「{0}」这个自定义页写出来:放上我这周的待办,和离月底还有几天。", title);
165
+ const copy = element("button", { type: "button", class: "ge-home-hint-copy", text: t("复制") });
166
+ const box = element("div", { class: "ge-home-hint-box" },
167
+ element("p", { class: "ge-home-hint-title", text: t("这一页还是空的") }),
168
+ element("p", { text: t("自定义页由 Agent 来写:可以是一页卡片,也可以是它为你做的任何网页。") }),
169
+ element("p", { class: "ge-home-hint-ask", text: t("跟 Agent 说你想在这里放什么,比如:") }),
170
+ element("div", { class: "ge-home-hint-say" }, element("span", { text: ask }), copy));
171
+ const note = noteOf("ge-page-blank", t("空白的自定义页"), box);
172
+ copy.addEventListener("mousedown", (event) => event.preventDefault());
173
+ copy.addEventListener("click", () => {
174
+ window.focus(); // as the first-open note's 复制: Chrome writes no clipboard of a document without the focus
175
+ navigator.clipboard?.writeText(ask).then(() => {
176
+ copy.textContent = t("已复制");
177
+ say(t("已复制这句话,发给 Agent 就行"));
178
+ }, () => say(t("没能复制:浏览器不允许写剪贴板。"), "error"));
179
+ });
180
+ main.append(note);
181
+ return note;
182
+ }