@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
@@ -1,9 +1,8 @@
1
1
  import { GAP, GRID, cellAt, fitHeight, gridFor, moved, pushDown, sizeFor, spanOf } from "../../../shared/board-places.mjs";
2
2
  import { cellsOf } from "../../../shared/card-size.mjs";
3
3
  import { language, t } from "../../../shared/i18n/index.mjs";
4
- import { BLOCK_ICON, svg } from "../../components/icons.mjs";
4
+ import { BLOCK_ICON, SETTINGS_ICON, svg } from "../../components/icons.mjs";
5
5
  import { instanceAround, instanceOf, packageOf } from "../../components/instances.mjs";
6
- import { edgeScroll } from "../edge-scroll.mjs";
7
6
  import { gestureLock } from "../gesture.mjs";
8
7
  import { ICONS, createMenu } from "../menu.mjs";
9
8
  import { say } from "../notices.mjs";
@@ -12,7 +11,8 @@ import { tooltip } from "../tooltip.mjs";
12
11
  import { addStyle, element, mount } from "../ui.mjs";
13
12
  import { createClient } from "./client.mjs";
14
13
  import { createHistory } from "./history.mjs";
15
- import { showHint, showNews } from "./hint.mjs";
14
+ import { createEdgeScroll, createSettle, glide, isGliding, stopGlide } from "./motion.mjs";
15
+ import { showBlank, showHint, showNews } from "./hint.mjs";
16
16
  import { closeSettings, formOpen, openSettings } from "./settings.mjs";
17
17
 
18
18
  // A custom page's board edited (docs/render.md "A custom page"; the plan's §5.3, the spec §6 and §8):
@@ -31,20 +31,33 @@ import { closeSettings, formOpen, openSettings } from "./settings.mjs";
31
31
  // one that moves less, or a key (Enter, Space) on the grip, opens the card's menu: what is dragged
32
32
  // and selected in a card is its block's (DocKit.drag never moves a card). A bare card (its block's
33
33
  // card.bare) shows where it stands by a dashed line of the guides' gray.
34
- // - Moving. The card follows the pointer, and where it would go, the whole cells nearest to it
35
- // (shared/board-places.mjs cellAt), shows as a quiet spot (a fill of its own, no line; no guide
36
- // lines stand where its edge meets another card's); the cards it would meet are drawn pushed down
37
- // under it (pushDown, the service's own rule: DocBoard.draft), never up. Escape, or the pointer taken
38
- // away, puts all back; a drop sends `place`, and the board stands as the service answers. The rows
39
- // above a board's first card are not drawn (DocBoard.skipped): the pointer's row is the drawn one
40
- // plus the rows skipped as the drag began, so that a card let go where it was writes nothing and one
41
- // held above the first row goes above it (drawn at the top, the others under it).
34
+ // - The board while a card is moved or resized (the user's word of 2026-11-09): no landing shadow
35
+ // under it; the board's lattice drawn as faint grey dots, one where the gaps between four cells
36
+ // cross (the board's outer edge too), from the gesture's start to its end (.ge-board-dots, light
37
+ // and dark), and the card snapping to the cells it would land at as it comes within SNAP px of them,
38
+ // each axis on its own: where it snaps is where a drop puts it.
39
+ // - Its motion (./motion.mjs; the user's word of 2026-11-09, an iPhone's home-screen widgets): the card
40
+ // held follows the pointer at once and settles onto the cells it snaps to (or off them) on a spring in
41
+ // SETTLE ms, never a jump; the cards making way, the card let go (to its cells, or back on Escape), a
42
+ // resize's and 大小 ›'s, an undo's and a redo's glide from where they were seen to their new place on
43
+ // that spring (FLIP, GLIDE ms), each starting again from where it is seen when the target changes under
44
+ // it; near the window's top or bottom edge the page scrolls, timed by the frames, faster nearer the edge
45
+ // and never past FASTEST px a second. prefers-reduced-motion: no glide or settling.
46
+ // - Moving. The card follows the pointer, and where it would go is the whole cells nearest to it
47
+ // (shared/board-places.mjs cellAt), which the card itself is drawn at (DocBoard.draft), moved off
48
+ // them by the pointer's offset but within SNAP px of them on an axis, where it stands on them; the
49
+ // cards it would meet are drawn pushed down under it (pushDown, the service's own rule), never up.
50
+ // Escape, or the pointer taken away, puts all back; a drop sends `place`, and the board stands as
51
+ // the service answers. The rows above a board's first card are not drawn (DocBoard.skipped): the
52
+ // pointer's row is the drawn one plus the rows skipped as the drag began, so that a card let go
53
+ // where it was writes nothing and one held above the first row goes above it (drawn at the top, the
54
+ // others under it).
42
55
  // - Resizing. A card whose block says card.resize has a grip at its bottom-right: it follows the
43
- // pointer, the spot showing the size it snaps to, the nearest of card.sizes, else whole cells
44
- // between card.min and card.max (sizeFor), never past the row's end; one of fit content only
45
- // gets wider (its height is its content's); one whose manifest frees it (`any`) reaches every
46
- // whole size. The menu's 大小 › gives the listed sizes (the common ones), in a card of rows of
47
- // words alone, only as wide as they are.
56
+ // pointer, snapping to the size a drop gives (within SNAP px of its width or height), the nearest of
57
+ // card.sizes, else whole cells between card.min and card.max (sizeFor), never past the row's end;
58
+ // one of fit content only gets wider (its height is its content's); one whose manifest frees it
59
+ // (`any`) reaches every whole size. The menu's 大小 › gives the listed sizes (the common ones), in a
60
+ // card of rows of words alone, only as wide as they are.
48
61
  // - The card's menu (the grip's click): 设置… (a block with settings: ./settings.mjs, written by the
49
62
  // service's `settings`), 大小 ›, 删除 (`remove`).
50
63
  // - 添加自定义块: in a workbench (`hosted`) the workbench's top bar has its button, which asks by the
@@ -53,7 +66,8 @@ import { closeSettings, formOpen, openSettings } from "./settings.mjs";
53
66
  // centred, and adds to the page's FIRST board; a page open alone in a tab has the button of its own
54
67
  // at the top right of each board, above it. The library (the service's GET library: the blocks
55
68
  // whose places hold page and are not turned off, with their previews, Gallery's own not yet in the
56
- // workspace too, which the service puts in): the card put at the first free spot by its block's
69
+ // workspace too, which the service puts in), in a section of each pack under its name (no pack's
70
+ // name in grey after a block's): the card put at the first free spot by its block's
57
71
  // default size (`add`); the page is then shown again (the host's hidden-frame swap), which brings
58
72
  // the block's script and style.
59
73
  // - A block's own write (context.edit, the kit's cancellable doc-component-edit): taken here, its
@@ -88,12 +102,14 @@ import { closeSettings, formOpen, openSettings } from "./settings.mjs";
88
102
  const BOARD = ".gallery-board";
89
103
  const CARD = ".gallery-card";
90
104
  const THRESHOLD = 4; // px a press moves before it is a drag
105
+ const SNAP = 14; // px from the place (or the size) a drop gives within which a moved (or resized) card stands on it
106
+ const DOT = 3; // px of room around each of the board's dots while a card is moved or resized (their tile's corner)
91
107
  const GRIP = { size: 28, inset: 6 }; // the grip's box, and its room from the card's top and right edges (inside the card)
92
108
  const FIELDS = "input, textarea, select, [contenteditable]:not([contenteditable=false])";
93
109
 
94
110
  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>';
95
111
  const CORNER = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M13 6.5 6.5 13M13 10.5 10.5 13"/></svg>';
96
- const GEAR = svg('<circle cx="10" cy="10" r="2.25"/><path d="M10 3.25v1.5M10 15.25v1.5M3.25 10h1.5M15.25 10h1.5M5.2 5.2l1.06 1.06M13.74 13.74l1.06 1.06M5.2 14.8l1.06-1.06M13.74 6.26l1.06-1.06"/>');
112
+ const GEAR = SETTINGS_ICON;
97
113
  const SIZE = svg('<rect x="3.25" y="3.25" width="13.5" height="13.5" rx="2.5"/><path d="M10 3.25v13.5M3.25 10H10"/>');
98
114
  const BLOCK = BLOCK_ICON;
99
115
 
@@ -118,10 +134,21 @@ const STYLE = `
118
134
  .ge-board-add:hover { background: var(--ge-hover); color: var(--ge-tx); }
119
135
  .ge-board-add:active, .ge-board-add[data-pressed] { background: var(--ge-press); color: var(--ge-tx); }
120
136
  .ge-board-add svg { display: block; width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; }
121
- .gallery-board > .ge-board-spot { box-sizing: border-box; grid-column: calc(var(--gallery-x, 0) + 1) / span var(--gallery-w, 4); grid-row: calc(var(--gallery-y, 0) + 1) / span var(--gallery-h, 2);
122
- border-radius: var(--gallery-card-radius, 12px); background: light-dark(rgb(55 53 47 / 7%), rgb(255 255 255 / 8%)); pointer-events: none; }
123
- .gallery-board > .gallery-card[data-ge-lifted] { z-index: 3; box-shadow: var(--ge-shadow-pop); cursor: grabbing; }
124
- .gallery-board > .gallery-card[data-ge-sizing] { z-index: 3; }
137
+ /* The board's lattice while a card is moved or resized: a faint grey dot where the gaps between four cells cross, under the cards (first in the board, which stands as their containing block meanwhile), placed by dotsOn. */
138
+ .gallery-board[data-ge-dotted] { position: relative; }
139
+ .gallery-board > .ge-board-dots { position: absolute; pointer-events: none; background-image: radial-gradient(circle at ${DOT}px ${DOT}px, light-dark(rgb(55 53 47 / 24%), rgb(255 255 255 / 20%)) 0 1.1px, transparent 1.6px); background-repeat: repeat; animation: ge-board-dots-in .14s ease-out; }
140
+ @keyframes ge-board-dots-in { from { opacity: 0; } }
141
+ @media (prefers-reduced-motion: reduce) { .gallery-board > .ge-board-dots { animation: none; } }
142
+ /* The card held lifts: its own shadow kept as it is, layer for layer, and a soft lift added after it, so
143
+ that only the lift eases in and out (a menu's shadow in its place made each of the card's layers turn
144
+ into another's, the shadow swelling and shrinking on the way: the user, 2026-11-09); a frameless card
145
+ takes the lift alone. One let go stays over the others while it glides to its cells. */
146
+ .gallery-board { --ge-board-lift: 0 10px 28px -10px light-dark(rgb(15 15 15 / 16%), rgb(0 0 0 / 55%)), 0 3px 10px -4px light-dark(rgb(15 15 15 / 8%), rgb(0 0 0 / 30%)); }
147
+ .gallery-board > .gallery-card[data-ge-lifted], .gallery-board > .gallery-card[data-ge-landing] { transition: box-shadow .2s ease-out; }
148
+ .gallery-board > .gallery-card[data-ge-lifted] { z-index: 3; box-shadow: var(--gallery-card-shadow), var(--ge-board-lift); cursor: grabbing; }
149
+ .gallery-board > .gallery-card[data-ge-bare][data-ge-lifted] { box-shadow: var(--ge-board-lift); }
150
+ .gallery-board > .gallery-card[data-ge-sizing], .gallery-board > .gallery-card[data-ge-landing] { z-index: 3; }
151
+ @media (prefers-reduced-motion: reduce) { .gallery-board > .gallery-card[data-ge-lifted], .gallery-board > .gallery-card[data-ge-landing] { transition: none; } }
125
152
  .gallery-board > .gallery-card[data-ge-bare][data-ge-hover] { outline: 1px dashed var(--ge-guide); outline-offset: 2px; }
126
153
  /* The card the grip stands in: a head's quiet note in its corner fades (the kit's --gallery-grip-shown, board.css). */
127
154
  .gallery-board > .gallery-card[data-ge-hover] { --gallery-grip-shown: 1; }
@@ -218,7 +245,7 @@ export function startBoard({ map, reload, hosted = false }) {
218
245
  };
219
246
  const drawn = (card) => globalThis.DocBoard?.drawn(card) ?? cellsOf(card);
220
247
  // How many rows above its first card the board does not draw (the kit's DocBoard.skipped): its drawn row
221
- // r is the file's row r + skipped, which the spot, a dragged card's place and a drop are reckoned by.
248
+ // r is the file's row r + skipped, which a dragged card's place and a drop are reckoned by.
222
249
  const skipped = (board) => globalThis.DocBoard?.skipped?.(board) ?? 0;
223
250
  const draft = (board, cells) => globalThis.DocBoard?.draft(board, cells);
224
251
 
@@ -226,16 +253,20 @@ export function startBoard({ map, reload, hosted = false }) {
226
253
  // has not, or one the page has that it does not name, is someone else's doing: the page is shown again.
227
254
  function took(answer, { own = [] } = {}) {
228
255
  const named = new Set();
229
- for (const card of answer.cards ?? []) {
230
- named.add(card.id);
231
- const node = cardById(card.id);
232
- if (!node) {
233
- if (!own.includes(card.id) && !leaving.has(card.id)) wantSwap(); // one the page took out meanwhile, its remove asked, is its own
234
- continue;
256
+ // Where the page drew a card elsewhere than the service put it, it glides there (a held card follows the pointer).
257
+ glide(allCards(), () => {
258
+ for (const card of answer.cards ?? []) {
259
+ named.add(card.id);
260
+ const node = cardById(card.id);
261
+ if (!node) {
262
+ if (!own.includes(card.id) && !leaving.has(card.id)) wantSwap(); // one the page took out meanwhile, its remove asked, is its own
263
+ continue;
264
+ }
265
+ setCells(node, card);
266
+ node.dataset.geBase = card.base;
235
267
  }
236
- setCells(node, card);
237
- node.dataset.geBase = card.base;
238
- }
268
+ if (!gesture) for (const board of boards()) globalThis.DocBoard?.layout(board);
269
+ }, { still: gesture?.card ?? null, settled: placeAgain });
239
270
  if (allCards().some((card) => !named.has(card.dataset.cardId))) wantSwap();
240
271
  history.know(basesNow());
241
272
  }
@@ -403,7 +434,8 @@ export function startBoard({ map, reload, hosted = false }) {
403
434
  if (resizable(card) && sizes.length > 1) {
404
435
  items.push({
405
436
  label: t("大小"), icon: SIZE, icons: false, compact: true,
406
- submenu: () => sizes.map(([w, h]) => ({ label: `${w} × ${h}`, current: w === cells.w && h === cells.h, run: () => placeCard(card, { ...cells, w, h }) })),
437
+ // By width, then height (the user's word of 2026-11-09; the manifest lists its default first).
438
+ submenu: () => [...sizes].sort(([aw, ah], [bw, bh]) => aw - bw || ah - bh).map(([w, h]) => ({ label: `${w} × ${h}`, current: w === cells.w && h === cells.h, run: () => layOut(board, () => placeCard(card, { ...cells, w, h }), { lift: card }) })),
407
439
  });
408
440
  }
409
441
  if (items.length) items.push({ separator: true });
@@ -435,37 +467,58 @@ export function startBoard({ map, reload, hosted = false }) {
435
467
  }, true);
436
468
 
437
469
  // ---------- moving and resizing ----------
438
- const spot = element("div", { class: "ge-board-spot", "data-doc-dev": "" });
439
- // Near the window's top or bottom edge a drag or a resize scrolls the page (the editor's edge-scroll.mjs
440
- // rule, faster the nearer the edge), each frame, and follows what came into view.
470
+ const dots = element("div", { class: "ge-board-dots", "data-doc-dev": "", "aria-hidden": "true" });
471
+ // The board's dots over all it draws now (its rows grow as a draft puts a card lower), each at a point of
472
+ // its lattice: where the gaps between four cells cross, and as far out on its edges, a DOT px room
473
+ // around each (the tile's corner the gradient draws it in).
474
+ const dotsOn = (board) => {
475
+ const { rect, step } = geometry(board);
476
+ const box = board.getBoundingClientRect();
477
+ const columns = Math.round((rect.right - rect.left + GAP) / step);
478
+ const rows = Math.max(1, Math.round((board.clientHeight + GAP) / step));
479
+ Object.assign(dots.style, {
480
+ left: `${rect.left - box.left - board.clientLeft - GAP / 2 - DOT}px`, top: `${-GAP / 2 - DOT}px`,
481
+ width: `${columns * step + 2 * DOT}px`, height: `${rows * step + 2 * DOT}px`, backgroundSize: `${step}px ${step}px`,
482
+ });
483
+ };
484
+ // Each frame of a gesture: near the window's top or bottom edge the page scrolls (./motion.mjs
485
+ // createEdgeScroll: timed by the frames, faster nearer the edge, never past FASTEST px a second) and the
486
+ // gesture follows what came into view; a card settling onto (or off) what it snaps to is drawn again.
441
487
  let pointer = null;
442
488
  let scrolling = 0;
443
- const scrollNear = () => {
489
+ const edge = createEdgeScroll();
490
+ const frame = () => {
444
491
  scrolling = 0;
445
492
  if (!gesture || !pointer) return;
446
- if (edgeScroll(pointer.clientX, pointer.clientY)) follow(pointer);
447
- scrolling = requestAnimationFrame(scrollNear);
448
- };
449
- // The spot at cells of the file, drawn where its board draws them.
450
- const placeSpot = (board, cells) => {
451
- for (const axis of ["x", "y", "w", "h"]) spot.style.setProperty(`--gallery-${axis}`, String(axis === "y" ? cells.y - skipped(board) : cells[axis]));
493
+ if (edge.frame(pointer.clientY, performance.now())) follow(pointer);
494
+ else if (gesture.settle.gliding()) draw();
495
+ scrolling = requestAnimationFrame(frame);
452
496
  };
497
+ // The cards of `board` laid out by `change` (cells set), those it moves gliding there (./motion.mjs), the grip
498
+ // put again on its card once they stand.
499
+ const layOut = (board, change, options = {}) => glide(cardsIn(board), () => {
500
+ const result = change();
501
+ globalThis.DocBoard?.layout(board);
502
+ return result;
503
+ }, { settled: placeAgain, ...options });
453
504
 
454
505
  function begin(kind, card, event) {
506
+ stopGlide(card); // still gliding from a drop: taken up where its cells are
455
507
  const board = card.parentElement;
456
508
  const file = cellsOf(card);
457
509
  const rect = card.getBoundingClientRect();
458
510
  // skip: the rows the board skipped as the gesture began, which the kit keeps while its draft is shown
459
511
  // (a card held above the first row skips fewer): the pointer's row is reckoned by it throughout.
460
- gesture = { kind, card, board, file, skip: skipped(board), target: { ...file }, grab: { x: event.clientX - rect.left, y: event.clientY - rect.top }, start: { x: event.clientX, y: event.clientY, width: rect.width, height: rect.height, scroll: scrollY } };
512
+ gesture = { kind, card, board, file, skip: skipped(board), target: { ...file }, grab: { x: event.clientX - rect.left, y: event.clientY - rect.top }, start: { x: event.clientX, y: event.clientY, width: rect.width, height: rect.height, scroll: scrollY }, settle: createSettle(SNAP), size: null };
461
513
  document.documentElement.setAttribute(kind === "move" ? "data-ge-dragging" : "data-ge-sizing", "");
462
514
  card.setAttribute(kind === "move" ? "data-ge-lifted" : "data-ge-sizing", "");
463
- placeSpot(board, drawn(card));
464
- board.append(spot);
515
+ board.setAttribute("data-ge-dotted", "");
516
+ board.prepend(dots);
517
+ dotsOn(board);
465
518
  grip.removeAttribute("data-shown");
466
519
  corner.removeAttribute("data-shown");
467
520
  follow(event);
468
- scrolling = requestAnimationFrame(scrollNear);
521
+ scrolling = requestAnimationFrame(frame);
469
522
  }
470
523
  function follow(event) {
471
524
  const { kind, card, board, file, skip, grab, start } = gesture;
@@ -479,7 +532,7 @@ export function startBoard({ map, reload, hosted = false }) {
479
532
  target = { ...file, x: clamp(cellAt(left, cell), 0, 12 - file.w), y: Math.max(0, cellAt(top, cell) + skip) };
480
533
  } else {
481
534
  // The card follows the pointer between the least and the most it can snap to (its listed sizes', or
482
- // its min and max), never past its row's end; the spot shows what it snaps to.
535
+ // its min and max), never past its row's end, standing at the size a drop gives where it is near it.
483
536
  const block = infoOf(card)?.card ?? {};
484
537
  const listed = block.any ? null : block.sizes?.filter(([w]) => file.x + w <= 12); // a card its block frees (any) takes every whole size
485
538
  const least = listed?.length ? { w: Math.min(...listed.map(([w]) => w)), h: Math.min(...listed.map(([, h]) => h)) } : { w: block.min?.w ?? 1, h: block.min?.h ?? 1 };
@@ -490,36 +543,59 @@ export function startBoard({ map, reload, hosted = false }) {
490
543
  const asked = { w: Math.max(1, Math.round((width + GAP) / step)), h: Math.max(1, Math.round((height + GAP) / step)) };
491
544
  const size = listed?.length ? sizeFor({ sizes: listed }, asked) : sizeFor(block, { w: Math.min(asked.w, 12 - file.x), h: asked.h }, 12 - file.x);
492
545
  target = { ...file, w: size.w, h: fits ? file.h : size.h };
493
- card.style.width = `${Math.round(width)}px`;
494
- if (!fits) card.style.height = `${Math.round(height)}px`;
546
+ gesture.size = { width, height, w: spanOf(size.w, cell), h: spanOf(size.h, cell), fits };
495
547
  }
496
548
  if (!cellsSame(target, gesture.target) || !gesture.drafted) {
497
549
  gesture.target = target;
498
550
  gesture.drafted = true;
499
- draft(board, new Map([[card, target]]));
500
- placeSpot(board, drawn(card));
551
+ // The cards it meets make way, gliding (the held card follows the pointer: draw).
552
+ glide(cardsIn(board), () => draft(board, new Map([[card, target]])), { still: card });
501
553
  }
554
+ draw();
555
+ dotsOn(board);
556
+ }
557
+ // The held card drawn where the pointer has it: moved, at the cells it would land at (the draft's), off them
558
+ // by the pointer's offset but where that is within SNAP px; resized, at the pointer's size but where that is
559
+ // within SNAP px of the size a drop gives. Each axis settles onto (or off) what it snaps to (createSettle).
560
+ function draw() {
561
+ const { kind, card, board, grab, settle, size } = gesture;
562
+ const time = performance.now();
502
563
  if (kind === "move") {
564
+ const { rect, step } = geometry(board);
503
565
  const now = drawn(card);
504
- card.style.translate = `${Math.round(event.clientX - grab.x - rect.left - now.x * step)}px ${Math.round(event.clientY - grab.y - rect.top - (now.y - skipped(board)) * step)}px`;
566
+ const cells = { x: now.x * step, y: (now.y - skipped(board)) * step };
567
+ const x = settle.at("x", pointer.clientX - grab.x - rect.left, cells.x, time);
568
+ const y = settle.at("y", pointer.clientY - grab.y - rect.top, cells.y, time);
569
+ card.style.translate = `${Math.round(x - cells.x)}px ${Math.round(y - cells.y)}px`;
570
+ } else {
571
+ card.style.width = `${Math.round(settle.at("w", size.width, size.w, time))}px`;
572
+ if (!size.fits) card.style.height = `${Math.round(settle.at("h", size.height, size.h, time))}px`;
505
573
  }
506
574
  }
507
575
  function finish(drop) {
508
576
  const done = gesture;
509
577
  gesture = null;
510
578
  cancelAnimationFrame(scrolling);
579
+ edge.stop();
511
580
  pointer = null;
512
581
  const { card, board, file, target, kind } = done;
513
582
  document.documentElement.removeAttribute("data-ge-dragging");
514
583
  document.documentElement.removeAttribute("data-ge-sizing");
584
+ card.setAttribute("data-ge-landing", ""); // its lifted shadow eases out (and it stays over the others while it glides)
515
585
  card.removeAttribute("data-ge-lifted");
516
586
  card.removeAttribute("data-ge-sizing");
517
- card.style.translate = "";
518
- card.style.width = "";
519
- card.style.height = "";
520
- spot.remove();
521
- if (drop && !cellsSame(target, file)) placeCard(card, target, { kind });
522
- draft(board, null);
587
+ // Let go: it glides from where it is seen to its cells (dropped) or back to its own (Escape), over the
588
+ // others meanwhile, and they to theirs.
589
+ glide(cardsIn(board), () => {
590
+ card.style.translate = "";
591
+ card.style.width = "";
592
+ card.style.height = "";
593
+ dots.remove();
594
+ board.removeAttribute("data-ge-dotted");
595
+ if (drop && !cellsSame(target, file)) placeCard(card, target, { kind });
596
+ draft(board, null);
597
+ }, { lift: card, settled: placeAgain });
598
+ if (!isGliding(card)) setTimeout(() => { if (!isGliding(card)) card.removeAttribute("data-ge-landing"); }, 220);
523
599
  trySwap();
524
600
  hideSoon(); // the grip comes back where the pointer is
525
601
  }
@@ -722,14 +798,26 @@ export function startBoard({ map, reload, hosted = false }) {
722
798
  say(t("连不上本地服务"), "error");
723
799
  return false;
724
800
  }
725
- const rows = (text = "") => listed.filter((block) => {
726
- const words = `${said(block.name)} ${said(block.description)} ${block.id}`.toLowerCase();
727
- return !text.trim() || words.includes(text.trim().toLowerCase());
728
- }).map((block) => ({
729
- label: said(block.name) || block.id, icon: BLOCK, hint: block.installed ? `${block.size.w} × ${block.size.h}` : t("自带"), ...(block.of ? { of: block.of } : {}),
730
- ...(block.preview || block.description ? { preview: block.preview ? { src: block.preview, says: said(block.description) } : { says: said(block.description) } } : {}),
731
- run: () => addBlock(board, block),
732
- }));
801
+ // A section of each pack (the user's word of 2026-11-09), its name over its blocks, in the service's order
802
+ // (the packs as 设置 › 组件列表 has them, each's blocks by name); what is typed keeps the sections it finds blocks in.
803
+ const rows = (text = "") => {
804
+ const items = [];
805
+ let pack;
806
+ for (const block of listed) {
807
+ const words = `${said(block.name)} ${said(block.description)} ${block.id}`.toLowerCase();
808
+ if (text.trim() && !words.includes(text.trim().toLowerCase())) continue;
809
+ if (block.pack?.id !== pack) {
810
+ pack = block.pack?.id;
811
+ if (pack) items.push({ title: said(block.pack.name) || pack });
812
+ }
813
+ items.push({
814
+ label: said(block.name) || block.id, icon: BLOCK, hint: block.installed ? `${block.size.w} × ${block.size.h}` : t("自带"),
815
+ ...(block.preview || block.description ? { preview: block.preview ? { src: block.preview, says: said(block.description) } : { says: said(block.description) } } : {}),
816
+ run: () => addBlock(board, block),
817
+ });
818
+ }
819
+ return items;
820
+ };
733
821
  button?.setAttribute("data-pressed", "");
734
822
  const closed = () => menu.close();
735
823
  if (!button) addEventListener("blur", closed);
@@ -773,7 +861,7 @@ export function startBoard({ map, reload, hosted = false }) {
773
861
  });
774
862
 
775
863
  // ---------- 恢复默认卡片 ----------
776
- // The home page's default cards it misses put back (the service's restore-defaults), a task as every
864
+ // The default cards a page with them misses put back (the service's restore-defaults), a task as every
777
865
  // op is: one step of undo for all of them (a group of adds), the page shown again with their blocks'
778
866
  // scripts and styles. Gives the service's answer ({ ok, added: [{ card, block, name }], skipped }),
779
867
  // which the workbench says, or its refusal.
@@ -822,7 +910,7 @@ export function startBoard({ map, reload, hosted = false }) {
822
910
  const cards = step.cards.filter(({ id }) => cardById(id));
823
911
  if (cards.length === 0) return false;
824
912
  const to = way === "undo" ? "before" : "after";
825
- for (const one of cards) setCells(cardById(one.id), one[to]);
913
+ layOut(cardById(cards[0].id).parentElement, () => { for (const one of cards) setCells(cardById(one.id), one[to]); });
826
914
  const answer = await send({ op: "arrange", cards: cards.map((one) => ({ card: one.id, ...one[to], base: cardById(one.id).dataset.geBase })) });
827
915
  if (!answer?.ok) return failed(answer) ?? false;
828
916
  took(answer);
@@ -960,10 +1048,11 @@ export function startBoard({ map, reload, hosted = false }) {
960
1048
  watch.observe(document.documentElement, { attributes: true, attributeFilter: ["data-doc-drawn"] });
961
1049
  }
962
1050
  }
963
- // The home page's first-open note, while its map says so, and its note of new default cards (put in as
964
- // 恢复默认卡片 puts them: restoreDefaults).
1051
+ // The page's first-open note, while its map says so, its note of new default cards (put in as
1052
+ // 恢复默认卡片 puts them: restoreDefaults), and the middle of a blank page (an Agent writes it).
965
1053
  if (map.hint) showHint(map, { hosted });
966
1054
  if (map.news) showNews(map, { restore: restoreDefaults });
1055
+ if (map.blank) showBlank(map);
967
1056
  // Its blocks may write from now on (the kit's context.canEdit, editable).
968
1057
  dispatchEvent(new Event("doc-editable"));
969
1058
  return { swap: wantSwap, undo, redo, idle: () => Promise.all([client.idle(), todos.idle()]), restoreDefaults, undoRestore, addCard };
@@ -0,0 +1,177 @@
1
+ // How a board's cards move while they are edited (./index.mjs; docs/render.md "Its cards edited"), the user's
2
+ // word of 2026-11-09: as smooth as the widgets of an iPhone's home screen, nothing that jumps or stutters.
3
+ //
4
+ // - glide(): the cards a change of the layout moves (a draft while a card is dragged or resized, a drop, an
5
+ // undo, 大小 ›) glide from where they were seen to where the layout now puts them (FLIP: their box before
6
+ // and after, a transform from the one to the other run to none) on a spring's ease (spring: a little
7
+ // less than critically damped, its overshoot under 2%), GLIDE ms; a card whose size changed glides its
8
+ // width and height too. A card already gliding to where the layout still puts it is left to go on; one
9
+ // the change sends elsewhere starts again from where it is seen then, so a target that changes under a
10
+ // drag never makes a card jump (interruptible).
11
+ // - createSettle(): a held card's place (or a resized card's size), axis by axis: the pointer's, or within
12
+ // `snap` px of the cells (or the size) a drop gives, those; where that changes (the card comes within the
13
+ // snap or leaves it, or the cells it stands on move) it settles from where it was carried to the new
14
+ // place in SETTLE ms on the same spring, the pointer's own motion never delayed.
15
+ // - createEdgeScroll(): near the window's top or bottom edge a gesture scrolls the page, timed by the frames'
16
+ // clock (not by the pointer's events): within BAND px of the edge, faster as the pointer is nearer it (the
17
+ // square of how far into the band, past the edge the most), FASTEST px a second at the most (12 a frame
18
+ // at 60 frames a second), and growing from nothing over RAMP ms once the pointer comes into the band, so
19
+ // that a card picked up near the edge does not set the page running.
20
+ // - prefers-reduced-motion: no glide and no settling (the layout and the snap at once); the scroll as it is.
21
+
22
+ export const GLIDE = 280; // ms a card glides to where a layout puts it
23
+ export const SETTLE = 180; // ms a held (or resized) card settles on what it snaps to, or comes off it
24
+ export const BAND = 64; // px from the window's top or bottom edge within which a gesture scrolls the page
25
+ export const FASTEST = 720; // px a second the page scrolls at the most
26
+ export const RAMP = 320; // ms over which that speed grows from nothing once the pointer is in the band
27
+
28
+ export const reduced = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
29
+
30
+ // The step response of a spring (damping ZETA, settled within 0.2% at t = 1): 0 at 0, 1 at 1, still at both
31
+ // ends, a crest of under 2% past 1 at t ≈ 0.65.
32
+ const ZETA = 0.8;
33
+ const OMEGA = 6.4 / ZETA;
34
+ const DAMPED = OMEGA * Math.sqrt(1 - ZETA * ZETA);
35
+ export function spring(t) {
36
+ if (t <= 0) return 0;
37
+ if (t >= 1) return 1;
38
+ return 1 - Math.exp(-ZETA * OMEGA * t) * (Math.cos(DAMPED * t) + ((ZETA * OMEGA) / DAMPED) * Math.sin(DAMPED * t));
39
+ }
40
+ // The same as an easing a Web Animation takes (CSS linear() of 40 points), else a close cubic Bézier.
41
+ const EASE = (() => {
42
+ const points = Array.from({ length: 41 }, (_, index) => Math.round(spring(index / 40) * 10000) / 10000).join(", ");
43
+ const curve = `linear(${points})`;
44
+ return typeof CSS !== "undefined" && CSS.supports?.("transition-timing-function", curve) ? curve : "cubic-bezier(.2, .9, .25, 1)";
45
+ })();
46
+
47
+ const running = new WeakMap(); // a card -> its glide under way
48
+ // Where the kit's board script draws a card now: its cells as the CSS takes them (the board's row skip in).
49
+ const keyOf = (card) => ["x", "y", "w", "h"].map((name) => card.style.getPropertyValue(`--gallery-${name}`)).join();
50
+
51
+ /** Whether a card is gliding now. */
52
+ export const isGliding = (card) => running.has(card);
53
+
54
+ /** A card's glide stopped where it is (a gesture takes it up): it stands where the layout puts it. */
55
+ export function stopGlide(card) {
56
+ running.get(card)?.cancel();
57
+ }
58
+
59
+ /**
60
+ * `change()` run (it lays the board out at once: DocBoard.draft or .layout), and each of `cards` it moves
61
+ * glided from where it was seen to its new place. `still`: a card left out (the one a gesture holds, which
62
+ * follows the pointer); `lift`: a card kept over the others while it glides (data-ge-landing: one let go);
63
+ * `settled()`: run once every glide this started has ended (at once where none started). Gives what change() gave.
64
+ */
65
+ export function glide(cards, change, { still = null, lift = null, duration = GLIDE, settled = null } = {}) {
66
+ if (reduced()) {
67
+ const result = change();
68
+ settled?.();
69
+ return result;
70
+ }
71
+ const started = [];
72
+ const before = cards.filter((card) => card !== still).map((card) => ({ card, key: keyOf(card), box: card.getBoundingClientRect() }));
73
+ const result = change();
74
+ for (const { card, key, box } of before) {
75
+ if (!card.isConnected) continue;
76
+ const going = running.get(card);
77
+ if (going && keyOf(card) === key) continue; // on its way to where it still goes
78
+ going?.cancel();
79
+ const now = card.getBoundingClientRect();
80
+ const dx = box.left - now.left;
81
+ const dy = box.top - now.top;
82
+ const sized = Math.abs(box.width - now.width) > 0.5 || Math.abs(box.height - now.height) > 0.5;
83
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5 && !sized) continue;
84
+ const from = { transform: `translate(${dx}px, ${dy}px)` };
85
+ const to = { transform: "translate(0px, 0px)" };
86
+ if (sized) {
87
+ Object.assign(from, { width: `${box.width}px`, height: `${box.height}px` });
88
+ Object.assign(to, { width: `${now.width}px`, height: `${now.height}px` });
89
+ }
90
+ const animation = card.animate([from, to], { duration, easing: EASE });
91
+ running.set(card, animation);
92
+ if (card === lift) card.setAttribute("data-ge-landing", "");
93
+ const end = () => {
94
+ if (running.get(card) !== animation) return;
95
+ running.delete(card);
96
+ card.removeAttribute("data-ge-landing");
97
+ };
98
+ animation.addEventListener("finish", end);
99
+ animation.addEventListener("cancel", end);
100
+ started.push(animation.finished.catch(() => {}));
101
+ }
102
+ if (settled) {
103
+ if (started.length) Promise.all(started).then(settled);
104
+ else settled();
105
+ }
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * A held card's place, or a resized card's size, axis by axis (`name`: "x", "y", "w", "h"): at(name, raw,
111
+ * target, time) gives what is drawn for the pointer's `raw` and the `target` a drop gives (the cells' place
112
+ * or size, px): the target within `snap` px of it, else raw; a change of which (or of the target while on
113
+ * it) settles from where the card was carried, in `duration` ms. gliding(): whether any axis still settles.
114
+ */
115
+ export function createSettle(snap, duration = SETTLE) {
116
+ const axes = new Map();
117
+ return {
118
+ at(name, raw, target, time) {
119
+ const on = Math.abs(raw - target) <= snap;
120
+ const want = on ? target : raw;
121
+ const axis = axes.get(name);
122
+ if (!axis) {
123
+ axes.set(name, { raw, target, on, shown: want, glide: null });
124
+ return want;
125
+ }
126
+ // Where it would be had it gone with the pointer since the last time: what a settling starts from.
127
+ const carried = axis.shown + (raw - axis.raw);
128
+ if (on !== axis.on || (on && target !== axis.target)) axis.glide = reduced() ? null : { offset: carried - want, start: time };
129
+ Object.assign(axis, { raw, target, on });
130
+ let shown = want;
131
+ if (axis.glide) {
132
+ const progress = (time - axis.glide.start) / duration;
133
+ if (progress >= 1) axis.glide = null;
134
+ else shown = want + axis.glide.offset * (1 - spring(progress));
135
+ }
136
+ axis.shown = shown;
137
+ return shown;
138
+ },
139
+ gliding: () => [...axes.values()].some((axis) => axis.glide !== null),
140
+ };
141
+ }
142
+
143
+ /**
144
+ * The page scrolled near the window's edge while a gesture lasts: frame(y, time) once a frame (the pointer's
145
+ * y in the window, the frame's time), whether the page moved; stop() as the gesture ends.
146
+ */
147
+ export function createEdgeScroll({ band = BAND, fastest = FASTEST, ramp = RAMP } = {}) {
148
+ let since = null; // when the pointer came into the band
149
+ let last = null; // the last frame's time
150
+ let carry = 0; // px asked and not yet scrolled (a frame's share is seldom whole)
151
+ return {
152
+ frame(y, time) {
153
+ const step = last === null ? 0 : Math.min(34, Math.max(0, time - last)); // a stalled frame scrolls no more than two
154
+ last = time;
155
+ const into = y < band ? -(band - y) / band : y > innerHeight - band ? (y - (innerHeight - band)) / band : 0;
156
+ if (into === 0) {
157
+ since = null;
158
+ carry = 0;
159
+ return false;
160
+ }
161
+ since ??= time;
162
+ const depth = Math.min(1, Math.abs(into));
163
+ carry += Math.sign(into) * fastest * depth * depth * Math.min(1, (time - since) / ramp) * (step / 1000);
164
+ const whole = Math.trunc(carry);
165
+ if (!whole) return false;
166
+ carry -= whole;
167
+ const was = scrollY;
168
+ scrollBy(0, whole);
169
+ return scrollY !== was;
170
+ },
171
+ stop() {
172
+ since = null;
173
+ last = null;
174
+ carry = 0;
175
+ },
176
+ };
177
+ }
@@ -14,7 +14,8 @@ import { addStyle, button, element, mount } from "../ui.mjs";
14
14
  // workspace's documents by title (the workbench's outline: GET /__gallery/workspace/outline, its web
15
15
  // links no documents), the slug what is kept. A choice from a list (select, document) is a field that
16
16
  // opens the editor's own menu card under it (menu.mjs: rows, the chosen one checked, ↑/↓ Enter Escape from
17
- // the field), not a native <select>: the page cannot style that one's popup, which a browser draws in
17
+ // the field), a choice of the caller's (type "choice", `choices`: [[value, words]], what a document block adds
18
+ // to its fields: the table a view reads) too, not a native <select>: the page cannot style that one's popup, which a browser draws in
18
19
  // the system's colours (a white list with pale words in a dark page, the user's picture of 2026-11-09).
19
20
  // While the form stands, the page is not shown again under it (the board's idle).
20
21
 
@@ -92,9 +93,9 @@ export async function openSettings({ fields, values, name, anchor, save }) {
92
93
  continue;
93
94
  }
94
95
  let control;
95
- const choice = field.type === "select" || field.type === "document"; // chosen from a list
96
+ const choice = field.type === "select" || field.type === "document" || field.type === "choice"; // chosen from a list
96
97
  if (choice) {
97
- const options = field.type === "select" ? field.options.map((option) => [String(option), String(option)]) : [["", t("(未选择)")], ...listed.map(({ slug, title }) => [slug, title])];
98
+ const options = field.type === "select" ? field.options.map((option) => [String(option), String(option)]) : field.type === "choice" ? field.choices : [["", t("(未选择)")], ...listed.map(({ slug, title }) => [slug, title])];
98
99
  if (field.type === "document" && value && !listed.some(({ slug }) => slug === value)) options.push([String(value), String(value)]);
99
100
  const text = element("span", { class: "ge-board-pick-text" });
100
101
  control = element("button", { type: "button", class: "ge-board-pick", "data-key": field.key, "aria-haspopup": "menu", "aria-expanded": "false" }, text, element("span", { html: CHEVRON }));