@heycys/gallery 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) 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/icon/page.mjs +6 -4
  8. package/browser/editor/components/icons.mjs +5 -2
  9. package/browser/editor/components/image/index.mjs +2 -0
  10. package/browser/editor/components/image/page.mjs +172 -90
  11. package/browser/editor/components/installed/index.mjs +6 -2
  12. package/browser/editor/components/installed/page.mjs +80 -13
  13. package/browser/editor/components/media/icons.mjs +13 -8
  14. package/browser/editor/components/media/page.mjs +5 -3
  15. package/browser/editor/components/media/toolbar.mjs +12 -3
  16. package/browser/editor/components/paragraph/index.mjs +4 -3
  17. package/browser/editor/components/registry.mjs +17 -5
  18. package/browser/editor/components/table/index.mjs +4 -2
  19. package/browser/editor/page/after/index.mjs +288 -0
  20. package/browser/editor/page/align.mjs +91 -0
  21. package/browser/editor/page/beside.mjs +5 -6
  22. package/browser/editor/page/block-menu/index.mjs +6 -3
  23. package/browser/editor/page/blocks.mjs +3 -1
  24. package/browser/editor/page/board/hint.mjs +64 -27
  25. package/browser/editor/page/board/index.mjs +160 -71
  26. package/browser/editor/page/board/motion.mjs +177 -0
  27. package/browser/editor/page/board/settings.mjs +4 -3
  28. package/browser/editor/page/clipboard/import/read.mjs +20 -2
  29. package/browser/editor/page/clipboard/index.mjs +10 -3
  30. package/browser/editor/page/clipboard/own/blocks.mjs +3 -4
  31. package/browser/editor/page/clipboard/own/describe.mjs +6 -2
  32. package/browser/editor/page/commit.mjs +2 -2
  33. package/browser/editor/page/core.mjs +3 -1
  34. package/browser/editor/page/custom-page.mjs +15 -11
  35. package/browser/editor/page/editor.mjs +31 -14
  36. package/browser/editor/page/history.mjs +19 -4
  37. package/browser/editor/page/insert/index.mjs +20 -22
  38. package/browser/editor/page/menu.mjs +15 -7
  39. package/browser/editor/page/page-objects.mjs +3 -7
  40. package/browser/editor/page/present-links.mjs +5 -4
  41. package/browser/editor/page/present-shell.mjs +63 -0
  42. package/browser/editor/page/selection/index.mjs +10 -2
  43. package/browser/editor/page/table/binding.mjs +87 -0
  44. package/browser/editor/page/table/icons.mjs +0 -2
  45. package/browser/editor/page/table/index.mjs +10 -14
  46. package/browser/editor/page/table/views.mjs +5 -52
  47. package/browser/editor/page/table/widths.mjs +82 -10
  48. package/browser/editor/page/typing/index.mjs +44 -12
  49. package/browser/editor/page/typing/leave.mjs +19 -5
  50. package/browser/shared/block-packs.mjs +23 -8
  51. package/browser/shared/component-catalog.mjs +15 -8
  52. package/browser/shared/component-source.mjs +3 -1
  53. package/browser/shared/components/heading.mjs +2 -1
  54. package/browser/shared/components/installed.mjs +1 -1
  55. package/browser/shared/components/page.mjs +2 -0
  56. package/browser/shared/components/paragraph.mjs +8 -1
  57. package/browser/shared/components/table.mjs +1 -0
  58. package/browser/shared/document-copy.mjs +43 -0
  59. package/browser/shared/drafts.mjs +10 -3
  60. package/browser/shared/emoji/icon-value.mjs +2 -2
  61. package/browser/shared/emoji/line-icons.mjs +37 -0
  62. package/browser/shared/extensions.mjs +83 -7
  63. package/browser/shared/http.mjs +9 -0
  64. package/browser/shared/i18n/en.mjs +50 -17
  65. package/browser/shared/outline-shape.mjs +6 -0
  66. package/browser/shared/outline.mjs +165 -12
  67. package/browser/shared/pack-manifest.mjs +12 -1
  68. package/browser/shared/pages.mjs +169 -57
  69. package/browser/shared/pinyin.mjs +92 -0
  70. package/browser/shared/present-step.mjs +17 -0
  71. package/browser/shared/sidebar-head.mjs +70 -0
  72. package/browser/shared/web-links.mjs +5 -0
  73. package/browser/shared/workspace-columns.mjs +11 -7
  74. package/dist/{chunk-N7AZZLGT.mjs → chunk-2GKGEVMR.mjs} +29 -24
  75. package/dist/chunk-2OHANJ6Q.mjs +2 -0
  76. package/dist/{chunk-HYMEEXKN.mjs → chunk-2RKIHJI2.mjs} +1 -1
  77. package/dist/{chunk-742D2WWJ.mjs → chunk-36FIRJIF.mjs} +2 -2
  78. package/dist/{chunk-BPL64HGA.mjs → chunk-3A5I2GJK.mjs} +1 -1
  79. package/dist/{chunk-RFDZXFJF.mjs → chunk-3OFXMN6N.mjs} +1 -1
  80. package/dist/chunk-46ACXOU5.mjs +2 -0
  81. package/dist/{chunk-PUOQ65NC.mjs → chunk-4AMTKWDO.mjs} +1 -1
  82. package/dist/{chunk-VTKNEYT3.mjs → chunk-4DNHHJSL.mjs} +1 -1
  83. package/dist/chunk-5DPABPBI.mjs +2 -0
  84. package/dist/chunk-5IIH335D.mjs +3 -0
  85. package/dist/chunk-5L26JUYA.mjs +2 -0
  86. package/dist/{chunk-3NJACR4W.mjs → chunk-5MEXZ2UF.mjs} +1 -1
  87. package/dist/{chunk-ZHEHPMBK.mjs → chunk-6MPKCH3P.mjs} +1 -1
  88. package/dist/{chunk-C564NU2H.mjs → chunk-74RPP2M2.mjs} +1 -1
  89. package/dist/chunk-7M2BQQGZ.mjs +2 -0
  90. package/dist/{chunk-VXACTLO3.mjs → chunk-7M7UPWU6.mjs} +1 -1
  91. package/dist/{chunk-ZB4H7ZN4.mjs → chunk-7N2LPG33.mjs} +1 -1
  92. package/dist/{chunk-UBBRTJ2R.mjs → chunk-7XDYK3UC.mjs} +1 -1
  93. package/dist/chunk-AEMVZYZQ.mjs +166 -0
  94. package/dist/{chunk-POQSE4C2.mjs → chunk-AFGUR7SY.mjs} +1 -1
  95. package/dist/{chunk-QKXMPUXC.mjs → chunk-AMDGVDNV.mjs} +1 -1
  96. package/dist/{chunk-KTKHRC6H.mjs → chunk-ASAXY2DJ.mjs} +1 -1
  97. package/dist/{chunk-NVRNLK25.mjs → chunk-BYSF5UMM.mjs} +2 -2
  98. package/dist/{chunk-U3SSPL4K.mjs → chunk-CCSGFCLZ.mjs} +1 -1
  99. package/dist/{chunk-NWFBE2YA.mjs → chunk-CG3UHDKH.mjs} +1 -1
  100. package/dist/chunk-CPS4SRPF.mjs +5 -0
  101. package/dist/{chunk-B4P2CM2J.mjs → chunk-CZ5KJSGK.mjs} +1 -1
  102. package/dist/chunk-DAMVDXR3.mjs +2 -0
  103. package/dist/{chunk-NG375ILQ.mjs → chunk-DH6GMDNN.mjs} +1 -1
  104. package/dist/chunk-DHB7UZ7F.mjs +3 -0
  105. package/dist/{chunk-MHW7ZKOU.mjs → chunk-DKR7LFIY.mjs} +1 -1
  106. package/dist/chunk-DLFZJJEE.mjs +2 -0
  107. package/dist/{chunk-6EZ7FNIK.mjs → chunk-F4ZA6JRZ.mjs} +1 -1
  108. package/dist/{chunk-6VTU6KEO.mjs → chunk-FCK6NXON.mjs} +1 -1
  109. package/dist/{chunk-ZWPMCK7P.mjs → chunk-FX42XU6J.mjs} +1 -1
  110. package/dist/chunk-FXQBADMN.mjs +5 -0
  111. package/dist/{chunk-W3BGBJQW.mjs → chunk-G4YP7GVC.mjs} +1 -1
  112. package/dist/{chunk-V6XZ4VIR.mjs → chunk-GWOYMENX.mjs} +1 -1
  113. package/dist/chunk-HATA2W7X.mjs +2 -0
  114. package/dist/{chunk-YI43RG4P.mjs → chunk-HBAUKKQH.mjs} +1 -1
  115. package/dist/{chunk-MAFIGVXY.mjs → chunk-HM4E43VM.mjs} +1 -1
  116. package/dist/{chunk-CRJEG7MT.mjs → chunk-JI6T3A4Y.mjs} +1 -1
  117. package/dist/{chunk-NFMCP2HI.mjs → chunk-JJNRIFND.mjs} +2 -2
  118. package/dist/chunk-JL6R4FG4.mjs +4 -0
  119. package/dist/{chunk-UMHUSOPJ.mjs → chunk-JXLSHVJ2.mjs} +1 -1
  120. package/dist/{chunk-FJYN7RTH.mjs → chunk-KFZBYDIS.mjs} +1 -1
  121. package/dist/{chunk-ZTHICSSF.mjs → chunk-KH24LWAT.mjs} +1 -1
  122. package/dist/{chunk-RKPWLGZ3.mjs → chunk-KUILYI7H.mjs} +1 -1
  123. package/dist/{chunk-RMECZA2K.mjs → chunk-L7W624FD.mjs} +1 -1
  124. package/dist/chunk-LE44MD3O.mjs +4 -0
  125. package/dist/{chunk-4K5KC6V2.mjs → chunk-LV24RY2E.mjs} +1 -1
  126. package/dist/{chunk-43B75J5T.mjs → chunk-MKTX5PGV.mjs} +1 -1
  127. package/dist/{chunk-LKX55H76.mjs → chunk-NAGMRVW4.mjs} +1 -1
  128. package/dist/{chunk-D4CKRD3V.mjs → chunk-NFABH3QW.mjs} +2 -2
  129. package/dist/{chunk-PVER7LSC.mjs → chunk-NIW7F7LU.mjs} +2 -2
  130. package/dist/{chunk-PAREWQGB.mjs → chunk-NWRZXELO.mjs} +1 -1
  131. package/dist/{chunk-OJKMINIU.mjs → chunk-OU3S466R.mjs} +15 -15
  132. package/dist/{chunk-5ZJA7CXT.mjs → chunk-PAFSFVYB.mjs} +1 -1
  133. package/dist/chunk-QCEATQC4.mjs +2 -0
  134. package/dist/{chunk-Y34NR5JL.mjs → chunk-QJ3TFJLQ.mjs} +1 -1
  135. package/dist/{chunk-IA4PSIMD.mjs → chunk-QK3VISCA.mjs} +1 -1
  136. package/dist/{chunk-ZUNVAXLH.mjs → chunk-QXVNUASA.mjs} +1 -1
  137. package/dist/{chunk-GRAXKKG4.mjs → chunk-QZ35UAB4.mjs} +1 -1
  138. package/dist/{chunk-UGWYKHBL.mjs → chunk-S2Y7VI4H.mjs} +2 -2
  139. package/dist/{chunk-OW5NWPOJ.mjs → chunk-SAHS56QM.mjs} +1 -1
  140. package/dist/{chunk-QOUEFUCH.mjs → chunk-SOD23RGL.mjs} +1 -1
  141. package/dist/chunk-TDWJKOLN.mjs +2 -0
  142. package/dist/{chunk-V6RIYQZZ.mjs → chunk-TOE6DPLH.mjs} +1 -1
  143. package/dist/chunk-UGO5N3EI.mjs +15 -0
  144. package/dist/{chunk-Q47PLKIJ.mjs → chunk-UGWMMEJ5.mjs} +1 -1
  145. package/dist/{chunk-NWR2LT5B.mjs → chunk-V5JRSGZF.mjs} +1 -1
  146. package/dist/{chunk-BZ5RAF7L.mjs → chunk-VA3KAQZD.mjs} +1 -1
  147. package/dist/chunk-VG5JEIUQ.mjs +12 -0
  148. package/dist/{chunk-UNOYVIYR.mjs → chunk-VGTMLQWZ.mjs} +1 -1
  149. package/dist/chunk-VKSRM7EY.mjs +22 -0
  150. package/dist/{chunk-YQGT4AB2.mjs → chunk-VNUWREXU.mjs} +1 -1
  151. package/dist/{chunk-AGDILNWB.mjs → chunk-VPVOAJ7X.mjs} +1 -1
  152. package/dist/{chunk-HLYV7RIH.mjs → chunk-WCWYNFDF.mjs} +1 -1
  153. package/dist/chunk-WTEGUPFG.mjs +16 -0
  154. package/dist/{chunk-N76HWBA3.mjs → chunk-XKOHV4H6.mjs} +1 -1
  155. package/dist/{chunk-HMYTM3GT.mjs → chunk-XMKHCUI6.mjs} +1 -1
  156. package/dist/{chunk-OUUP3WKT.mjs → chunk-XQRUG4G3.mjs} +1 -1
  157. package/dist/{chunk-32ZUEFZ7.mjs → chunk-YBB5LWZK.mjs} +1 -1
  158. package/dist/{chunk-SX4LJYT6.mjs → chunk-YFJU3O7G.mjs} +1 -1
  159. package/dist/chunk-YPPJVDQI.mjs +2 -0
  160. package/dist/chunk-ZANMKAWS.mjs +2 -0
  161. package/dist/{chunk-6QBHU4DF.mjs → chunk-ZCP7BTOZ.mjs} +1 -1
  162. package/dist/{chunk-ERLAIBOA.mjs → chunk-ZEY2RLBT.mjs} +2 -2
  163. package/dist/{chunk-SQ6Y2ZMA.mjs → chunk-ZO7SFB2E.mjs} +2 -2
  164. package/dist/cli.mjs +1 -1
  165. package/dist/server.mjs +1 -1
  166. package/dist/worker.mjs +1 -1
  167. package/kit/board.css +5 -5
  168. package/kit/code.js +15 -15
  169. package/kit/math.js +1 -1
  170. package/kit/report.css +123 -44
  171. package/kit/runtime.js +45 -7
  172. package/kit/tables.js +2 -0
  173. package/kit/ui.js +2 -0
  174. package/package.json +1 -1
  175. package/packs/gallery/chart-view/kit.js +3 -3
  176. package/packs/gallery/chart-view/writing.md +1 -1
  177. package/packs/gallery/countdown/component.json +1 -1
  178. package/packs/gallery/countdown/kit.js +89 -29
  179. package/packs/gallery/countdown/preview.svg +1 -1
  180. package/packs/gallery/countdown/style.css +88 -100
  181. package/packs/gallery/countdown/writing.md +2 -2
  182. package/packs/gallery/habit/component.json +3 -3
  183. package/packs/gallery/habit/kit.js +114 -75
  184. package/packs/gallery/habit/preview.svg +1 -1
  185. package/packs/gallery/habit/style.css +133 -205
  186. package/packs/gallery/habit/writing.md +2 -2
  187. package/packs/gallery/heatmap/component.json +2 -2
  188. package/packs/gallery/heatmap/kit.js +335 -173
  189. package/packs/gallery/heatmap/preview.svg +1 -1
  190. package/packs/gallery/heatmap/style.css +158 -170
  191. package/packs/gallery/heatmap/writing.md +2 -2
  192. package/packs/gallery/kanban/kit.js +3 -3
  193. package/packs/gallery/kanban/writing.md +1 -1
  194. package/packs/gallery/metric/kit.js +3 -3
  195. package/packs/gallery/metric/writing.md +1 -1
  196. package/packs/gallery/note/component.json +3 -3
  197. package/packs/gallery/note/kit.js +40 -13
  198. package/packs/gallery/note/preview.svg +1 -1
  199. package/packs/gallery/note/style.css +51 -30
  200. package/packs/gallery/note/writing.md +2 -2
  201. package/packs/gallery/recent/component.json +2 -2
  202. package/packs/gallery/recent/kit.js +141 -86
  203. package/packs/gallery/recent/preview.svg +1 -1
  204. package/packs/gallery/recent/style.css +53 -126
  205. package/packs/gallery/recent/writing.md +2 -2
  206. package/packs/gallery/static/component.json +2 -2
  207. package/packs/gallery/static/preview.svg +1 -1
  208. package/packs/gallery/static/style.css +99 -66
  209. package/packs/gallery/static/writing.md +2 -2
  210. package/packs/gallery/stats/component.json +3 -3
  211. package/packs/gallery/stats/kit.js +253 -72
  212. package/packs/gallery/stats/preview.svg +1 -1
  213. package/packs/gallery/stats/style.css +64 -142
  214. package/packs/gallery/stats/writing.md +2 -2
  215. package/packs/gallery/today/component.json +2 -2
  216. package/packs/gallery/today/kit.js +62 -27
  217. package/packs/gallery/today/preview.svg +1 -1
  218. package/packs/gallery/today/style.css +101 -72
  219. package/packs/gallery/today/writing.md +2 -2
  220. package/packs/gallery/todos/component.json +1 -1
  221. package/packs/gallery/todos/kit.js +218 -71
  222. package/packs/gallery/todos/preview.svg +1 -1
  223. package/packs/gallery/todos/style.css +170 -223
  224. package/packs/gallery/todos/writing.md +1 -1
  225. package/packs/gallery/weather/component.json +2 -2
  226. package/packs/gallery/weather/kit.js +165 -90
  227. package/packs/gallery/weather/preview.svg +1 -1
  228. package/packs/gallery/weather/style.css +100 -218
  229. package/packs/gallery/weather/writing.md +1 -1
  230. package/packs/gallery-soft/countdown/component.json +18 -0
  231. package/packs/gallery-soft/countdown/example.html +3 -0
  232. package/packs/gallery-soft/countdown/kit.js +206 -0
  233. package/packs/gallery-soft/countdown/preview.svg +1 -0
  234. package/packs/gallery-soft/countdown/style.css +211 -0
  235. package/packs/gallery-soft/countdown/template.html +3 -0
  236. package/packs/gallery-soft/countdown/writing.md +24 -0
  237. package/packs/gallery-soft/habit/component.json +14 -0
  238. package/packs/gallery-soft/habit/example.html +3 -0
  239. package/packs/gallery-soft/habit/kit.js +366 -0
  240. package/packs/gallery-soft/habit/preview.svg +1 -0
  241. package/packs/gallery-soft/habit/style.css +251 -0
  242. package/packs/gallery-soft/habit/template.html +3 -0
  243. package/packs/gallery-soft/habit/writing.md +23 -0
  244. package/packs/gallery-soft/heatmap/component.json +18 -0
  245. package/packs/gallery-soft/heatmap/example.html +3 -0
  246. package/packs/gallery-soft/heatmap/kit.js +498 -0
  247. package/packs/gallery-soft/heatmap/preview.svg +1 -0
  248. package/packs/gallery-soft/heatmap/style.css +181 -0
  249. package/packs/gallery-soft/heatmap/template.html +3 -0
  250. package/packs/gallery-soft/heatmap/writing.md +23 -0
  251. package/packs/gallery-soft/note/component.json +14 -0
  252. package/packs/gallery-soft/note/example.html +3 -0
  253. package/packs/gallery-soft/note/kit.js +162 -0
  254. package/packs/gallery-soft/note/preview.svg +1 -0
  255. package/packs/gallery-soft/note/style.css +148 -0
  256. package/packs/gallery-soft/note/template.html +3 -0
  257. package/packs/gallery-soft/note/writing.md +20 -0
  258. package/packs/gallery-soft/recent/component.json +13 -0
  259. package/packs/gallery-soft/recent/example.html +1 -0
  260. package/packs/gallery-soft/recent/kit.js +335 -0
  261. package/packs/gallery-soft/recent/preview.svg +1 -0
  262. package/packs/gallery-soft/recent/style.css +160 -0
  263. package/packs/gallery-soft/recent/template.html +1 -0
  264. package/packs/gallery-soft/recent/writing.md +17 -0
  265. package/packs/gallery-soft/static/component.json +13 -0
  266. package/packs/gallery-soft/static/example.html +12 -0
  267. package/packs/gallery-soft/static/preview.svg +1 -0
  268. package/packs/gallery-soft/static/style.css +186 -0
  269. package/packs/gallery-soft/static/template.html +1 -0
  270. package/packs/gallery-soft/static/writing.md +20 -0
  271. package/packs/gallery-soft/stats/component.json +13 -0
  272. package/packs/gallery-soft/stats/example.html +1 -0
  273. package/packs/gallery-soft/stats/kit.js +341 -0
  274. package/packs/gallery-soft/stats/preview.svg +1 -0
  275. package/packs/gallery-soft/stats/style.css +119 -0
  276. package/packs/gallery-soft/stats/template.html +1 -0
  277. package/packs/gallery-soft/stats/writing.md +13 -0
  278. package/packs/gallery-soft/today/component.json +12 -0
  279. package/packs/gallery-soft/today/example.html +1 -0
  280. package/packs/gallery-soft/today/kit.js +222 -0
  281. package/packs/gallery-soft/today/preview.svg +1 -0
  282. package/packs/gallery-soft/today/style.css +178 -0
  283. package/packs/gallery-soft/today/template.html +1 -0
  284. package/packs/gallery-soft/today/writing.md +16 -0
  285. package/packs/gallery-soft/todos/component.json +19 -0
  286. package/packs/gallery-soft/todos/example.html +3 -0
  287. package/packs/gallery-soft/todos/kit.js +527 -0
  288. package/packs/gallery-soft/todos/preview.svg +1 -0
  289. package/packs/gallery-soft/todos/style.css +212 -0
  290. package/packs/gallery-soft/todos/template.html +3 -0
  291. package/packs/gallery-soft/todos/writing.md +32 -0
  292. package/packs/gallery-soft/weather/component.json +19 -0
  293. package/packs/gallery-soft/weather/example.html +3 -0
  294. package/packs/gallery-soft/weather/kit.js +411 -0
  295. package/packs/gallery-soft/weather/preview.svg +1 -0
  296. package/packs/gallery-soft/weather/style.css +185 -0
  297. package/packs/gallery-soft/weather/template.html +3 -0
  298. package/packs/gallery-soft/weather/writing.md +28 -0
  299. package/release.json +10 -10
  300. package/skill/SKILL.md +15 -14
  301. package/skill/references/building-blocks.md +4 -4
  302. package/skill/references/extensions.md +11 -7
  303. package/skill/references/pages-and-data.md +13 -9
  304. package/skill/references/writing-html.md +2 -0
  305. package/web/assets/index-BHC5ZOMh.js +15 -0
  306. package/web/assets/index-xP0uw3a0.css +1 -0
  307. package/web/index.html +2 -2
  308. package/dist/chunk-2NRBY6AJ.mjs +0 -2
  309. package/dist/chunk-35NQ4PLQ.mjs +0 -2
  310. package/dist/chunk-3KJHLHUO.mjs +0 -2
  311. package/dist/chunk-4AV456RQ.mjs +0 -2
  312. package/dist/chunk-5XYHDBJ5.mjs +0 -2
  313. package/dist/chunk-6A53MZYZ.mjs +0 -2
  314. package/dist/chunk-6EXH3ORZ.mjs +0 -166
  315. package/dist/chunk-BVEFGE4B.mjs +0 -2
  316. package/dist/chunk-D4EURAX4.mjs +0 -4
  317. package/dist/chunk-DQCE7TJY.mjs +0 -5
  318. package/dist/chunk-FWNRPMCJ.mjs +0 -12
  319. package/dist/chunk-FWW4CSAZ.mjs +0 -2
  320. package/dist/chunk-GY3FAUVM.mjs +0 -2
  321. package/dist/chunk-JVHHHGEA.mjs +0 -3
  322. package/dist/chunk-L7UP52JO.mjs +0 -2
  323. package/dist/chunk-NAMN3RAF.mjs +0 -2
  324. package/dist/chunk-PZDLJJTJ.mjs +0 -3
  325. package/dist/chunk-SXHBYZST.mjs +0 -16
  326. package/dist/chunk-TEZAWDSQ.mjs +0 -2
  327. package/dist/chunk-UFNYFFKO.mjs +0 -20
  328. package/dist/chunk-UKSHDZZ6.mjs +0 -2
  329. package/dist/chunk-VJPFVNNW.mjs +0 -15
  330. package/dist/chunk-VS3ZMVGX.mjs +0 -5
  331. package/web/assets/index-BK2e_07w.js +0 -13
  332. package/web/assets/index-CWFI83mF.css +0 -1
  333. /package/dist/{chunk-YEY3RSFQ.mjs → chunk-5MMN3N5J.mjs} +0 -0
  334. /package/dist/{chunk-UH4SIXTR.mjs → chunk-74UW7QSL.mjs} +0 -0
  335. /package/dist/{chunk-W7XHE4CT.mjs → chunk-EKGYYOSP.mjs} +0 -0
  336. /package/dist/{chunk-QAIHRH3I.mjs → chunk-KNZJT4ET.mjs} +0 -0
  337. /package/dist/{chunk-A55HN5OK.mjs → chunk-N3DSBH5S.mjs} +0 -0
  338. /package/dist/{chunk-QR6A54KJ.mjs → chunk-ZW2A6GBW.mjs} +0 -0
@@ -3,10 +3,53 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { writeAtomic } from "./files.mjs";
5
5
  import { extensionColumnIds } from "./extensions.mjs";
6
- import { DOCS, entryPattern, extensionGroupId, GROUP_LIMITS, groupOf, isForeign, isTop, newGroupId, topIn, topOf, validGroups } from "./outline-shape.mjs";
7
- import { shownWorkspaces, snapshotOf } from "./workspace-columns.mjs";
8
- import { layoutOf, listDocumentSlugs, listLinkSlugs, pageSlug } from "./workspace.mjs";
6
+ import { DOCS, columnWorkspaceId, entryWorkspace, entryPattern, extensionGroupId, GROUP_LIMITS, groupOf, groupOfTop, isForeign, isTop, newGroupId, topIn, topOf, validGroups } from "./outline-shape.mjs";
7
+ import { storedHead } from "./sidebar-head.mjs";
8
+ import { forgetSnapshot, shownWorkspaces, snapshotOf } from "./workspace-columns.mjs";
9
+ import { defaultWorkspace, layoutOf, listDocumentSlugs, listLinkSlugs, pageSlug } from "./workspace.mjs";
10
+ import { readRegistry, workspaceId, workspaceKey } from "./workspace-registry.mjs";
9
11
 
12
+ /** Registered workspaces share the default workspace's existing tree. Unregistered/temporary
13
+ * workspaces remain standalone; no registry discovery or copying of document files is needed. */
14
+ export function outlineRoot(root) {
15
+ const home = defaultWorkspace();
16
+ return workspaceKey(root) === workspaceKey(home) || readRegistry().workspaces.some(entry => workspaceKey(entry.root) === workspaceKey(root)) ? home : root;
17
+ }
18
+
19
+ export const sharedOutline = root => workspaceKey(outlineRoot(root)) === workspaceKey(defaultWorkspace());
20
+
21
+ /** Moving a reference across owners must use the confirmed copy command. */
22
+ export function ownsOutline(root, value) {
23
+ const self = workspaceId(root);
24
+ const available = new Set([self, ...shownWorkspaces(root).map(workspace => workspace.id)]);
25
+ return Object.entries(value.children).every(([parent, list]) => {
26
+ const owner = isTop(parent) ? columnWorkspaceId(value.groups?.find(group => topOf(group.id) === parent), self) : entryWorkspace(parent, self);
27
+ // Missing legacy references remain removable and must not block edits to healthy documents.
28
+ return list.every(entry => !available.has(entryWorkspace(entry, self)) || entryWorkspace(entry, self) === owner);
29
+ });
30
+ }
31
+
32
+ // Earlier versions allowed mixed-workspace nesting. Return misplaced roots to their owner,
33
+ // retaining same-owner order and descendants. This changes directory positions, never files.
34
+ function ownedTree(tree, self) {
35
+ const children = Object.fromEntries(tree.groups.map(group => [topOf(group.id), []]));
36
+ const ownTop = owner => { const group = tree.groups.find(group => owner === self ? group.id === DOCS : group.workspace === owner); return group ? topOf(group.id) : null; };
37
+ const walk = (parent, owner) => {
38
+ for (const entry of tree.children[parent] ?? []) {
39
+ const belongs = entryWorkspace(entry, self);
40
+ const at = belongs === owner ? parent : ownTop(belongs) ?? parent;
41
+ (children[at] ??= []).push(entry);
42
+ walk(entry, belongs);
43
+ }
44
+ };
45
+ for (const group of tree.groups) if (!group.extension) walk(topOf(group.id), columnWorkspaceId(group, self));
46
+ return { ...tree, children };
47
+ }
48
+
49
+ // Registered workspaces project one shared tree, partitioned by file owner. Their former local files
50
+ // only seed new entries; the default workspace stores subsequent order/hierarchy edits. The format
51
+ // below is also the standalone/legacy reader. Its picked/hidden/left rules are not applied to shared
52
+ // trees, which list every registered document and link. Project-only extensions keep local positions.
10
53
  // The workspace's outline: the sidebar's columns and which entry sits under which, in what order
11
54
  // (<workspace>/.gallery/outline.json, docs/workbench.md "Workspace commands"; its shape: ./outline-shape.mjs).
12
55
  // The file, version 2:
@@ -38,6 +81,9 @@ import { layoutOf, listDocumentSlugs, listLinkSlugs, pageSlug } from "./workspac
38
81
  // got yet comes last (not written until the outline is), one whose extension is gone drops out, and what an
39
82
  // outline says of its entries is passed over (an extension's entries are its own, never the outline's).
40
83
  // Their place is what the outline keeps of them.
84
+ // The file also keeps the order of the sidebar's head rows (`head`: 新建, 搜索 and the custom pages,
85
+ // ./sidebar-head.mjs), which only a drag of a head row and a new page write (writeHead), and every write
86
+ // of the tree keeps as it was (writeOutline).
41
87
  // Written by the workspace's server (src/workbench/service.mjs: the workbench's moves and columns, a new,
42
88
  // copied, restored or imported document, a new link, gallery new --under, gallery move and gallery link
43
89
  // add while it runs, in the queue its saves go through), and by a gallery command when no server runs
@@ -52,10 +98,10 @@ const LOCK_WAIT = 15_000;
52
98
  /**
53
99
  * Run `task` holding the outline's lock of workspace `root`: a file made with "wx", which one
54
100
  * process alone can make, removed after; one older than 10 s is a crashed writer's and is taken over.
55
- * Waits for it 15 s at most, then takes it all the same (a writer that holds it that long holds
56
- * nothing any more).
101
+ * A live writer refreshes its lock. Waits 15 s at most and then refuses rather than stealing it.
57
102
  */
58
- export async function withOutlineLock(root, task) {
103
+ export async function withOutlineLock(root, task, { local = false } = {}) {
104
+ if (!local) root = outlineRoot(root);
59
105
  const { gallery } = layoutOf(root);
60
106
  const lock = path.join(gallery, "outline.lock");
61
107
  const mine = `${process.pid} ${randomBytes(4).toString("hex")}`;
@@ -67,13 +113,17 @@ export async function withOutlineLock(root, task) {
67
113
  } catch (error) {
68
114
  if (error.code !== "EEXIST") throw error;
69
115
  const age = await fs.stat(lock).then(({ mtimeMs }) => Date.now() - mtimeMs, () => 0);
70
- if (age > LOCK_STALE || Date.now() > until) await fs.rm(lock, { force: true });
116
+ if (age > LOCK_STALE) await fs.rm(lock, { force: true });
117
+ else if (Date.now() > until) throw Object.assign(new Error("文档目录刚在别处改过"), { code: "stale" });
71
118
  else await new Promise((resolve) => setTimeout(resolve, 20));
72
119
  }
73
120
  }
121
+ const heartbeat = setInterval(() => fs.utimes(lock, new Date(), new Date()).catch(() => {}), LOCK_STALE / 3);
122
+ heartbeat.unref();
74
123
  try {
75
124
  return await task();
76
125
  } finally {
126
+ clearInterval(heartbeat);
77
127
  // Removed only while it is still this writer's (one taken over meanwhile is the next writer's).
78
128
  if ((await fs.readFile(lock, "utf8").catch(() => null)) === mine) await fs.rm(lock, { force: true });
79
129
  }
@@ -94,7 +144,8 @@ export async function readOutline(root, { others = true } = {}) {
94
144
  */
95
145
  export async function otherWorkspaces(root, { env = process.env } = {}) {
96
146
  const shown = shownWorkspaces(root, { env });
97
- const readOwn = (other) => readOutline(other, { others: false });
147
+ // Project-local files seed entries not yet in the shared tree; never recurse into its projection.
148
+ const readOwn = async (other) => (await localOutlineAndFile(other, { others: false })).outline;
98
149
  const unread = () => ({ documents: new Set(), order: [], parentOf: new Map(), facts: new Map(), failed: true });
99
150
  return Promise.all(shown.map(async (each) => ({ ...each, failed: false, ...(await snapshotOf(each.root, readOwn).catch(unread)) })));
100
151
  }
@@ -109,14 +160,16 @@ export async function otherWorkspaces(root, { env = process.env } = {}) {
109
160
  * place a command or the workbench gave. `others: false`: the workspace's own tree alone (no column of
110
161
  * another workspace made, none filled).
111
162
  */
112
- export async function outlineAndFile(root, { others: withOthers = true } = {}) {
163
+ async function localOutlineAndFile(root, { others: withOthers = true, shared = false } = {}) {
113
164
  const instructions = await pageSlug(root);
114
165
  const documents = (await listDocumentSlugs(root)).filter((slug) => slug !== instructions);
115
166
  const links = await listLinkSlugs(root);
116
167
  const extensions = await extensionColumnIds(root);
117
168
  const stored = storedOf(await readJson(layoutOf(root).outline));
118
169
  const others = withOthers ? await otherWorkspaces(root) : [];
119
- const shown = ordered(stored, documents, links, extensions, others, { drawn: withOthers });
170
+ const base = shared && stored ? { ...stored, groups: stored.groups.map(({ picked, hidden, left, ...group }) => group.workspace || group.extension || group.id === DOCS ? group : { ...group, owner: group.owner ?? workspaceId(root) }) } : stored;
171
+ let shown = ordered(base, documents, links, extensions, others, { drawn: withOthers });
172
+ if (shared) shown = ownedTree(shown, workspaceId(root));
120
173
  const outline = {
121
174
  version: 2, ...shown, links, drafts: Object.values(shown.children).flat().filter(isForeign),
122
175
  shown: others.map(({ id, root: other, isDefault }) => ({ id, root: other, isDefault })),
@@ -124,6 +177,54 @@ export async function outlineAndFile(root, { others: withOthers = true } = {}) {
124
177
  return { outline, stored, others };
125
178
  }
126
179
 
180
+ /** Translate the same tree between the default workspace's ids and a project's local ids.
181
+ * No second ordering is kept: only the current workspace's column is displayed first. */
182
+ function projectTree(value, from, to, { first = false } = {}) {
183
+ if (!value || workspaceKey(from) === workspaceKey(to)) return value;
184
+ const fromId = workspaceId(from), toId = workspaceId(to);
185
+ const own = value.groups.find(group => group.workspace === toId);
186
+ const ownId = own?.id ?? toId;
187
+ const groupId = id => id === DOCS ? fromId : id === ownId ? DOCS : id;
188
+ const entry = key => key.startsWith(`~${toId}/`) ? key.slice(toId.length + 2) : isForeign(key) ? key : `~${fromId}/${key}`;
189
+ const key = value => isTop(value) ? topOf(groupId(groupOfTop(value))) : entry(value);
190
+ let groups = value.groups.map(group => {
191
+ if (group.id === DOCS) return { ...group, id: fromId, workspace: fromId };
192
+ if (group.id === ownId) {
193
+ const { workspace, picked, hidden, left, ...rest } = group;
194
+ return { ...rest, id: DOCS };
195
+ }
196
+ return group;
197
+ });
198
+ if (first) groups = [...groups.filter(group => group.id === DOCS), ...groups.filter(group => group.id !== DOCS)];
199
+ return { ...value, groups, children: Object.fromEntries(Object.entries(value.children).map(([parent, list]) => [key(parent), list.map(entry)])) };
200
+ }
201
+
202
+ export async function outlineAndFile(root, { others: withOthers = true } = {}) {
203
+ const home = outlineRoot(root);
204
+ if (workspaceKey(home) === workspaceKey(root)) return localOutlineAndFile(root, { others: withOthers, shared: sharedOutline(root) });
205
+ // A command may just have created/trashed a local item before placing it in the shared tree.
206
+ forgetSnapshot(root);
207
+ const shared = await localOutlineAndFile(home, { shared: true });
208
+ let outline = projectTree(shared.outline, home, root, { first: true });
209
+ // A project's extensions remain private; public ones still come from the default workspace.
210
+ outline.groups = withExtensions(outline.groups, await extensionColumnIds(root));
211
+ const publicIds = new Set(await extensionColumnIds(home));
212
+ const positions = (await readJson(layoutOf(root).outline))?.extensionPositions;
213
+ for (const position of Array.isArray(positions) ? [...positions].reverse() : []) {
214
+ const group = outline.groups.find(group => group.id === position.id && group.extension && !publicIds.has(group.extension));
215
+ if (!group) continue;
216
+ outline.groups = outline.groups.filter(item => item !== group);
217
+ const at = outline.groups.findIndex(item => item.id === position.before);
218
+ outline.groups.splice(at < 0 ? outline.groups.length : at, 0, group);
219
+ }
220
+ outline.children = Object.fromEntries(Object.entries(outline.children).filter(([key]) => !isTop(key) || outline.groups.some(group => topOf(group.id) === key)));
221
+ for (const group of outline.groups) outline.children[topOf(group.id)] ??= [];
222
+ const ownLinks = await listLinkSlugs(root);
223
+ const others = await otherWorkspaces(root);
224
+ outline = { ...outline, links: ownLinks, drafts: Object.values(outline.children).flat().filter(isForeign), shown: others.map(({ id, root: other, isDefault }) => ({ id, root: other, isDefault })) };
225
+ return { outline, stored: projectTree(shared.stored, home, root, { first: true }), others };
226
+ }
227
+
127
228
  // The own column's name before it was 默认文档 or the workspace's (文档, Documents in English), which a
128
229
  // rename of it confirmed unchanged wrote as its title (the field opens with the name shown): read as no
129
230
  // title, so that the name it goes by now shows. A title of another column, or any other, is the user's.
@@ -137,6 +238,8 @@ const OLD_DOCS_NAMES = ["文档", "Documents"];
137
238
  */
138
239
  export function storedOf(file) {
139
240
  if (!file || typeof file !== "object" || Array.isArray(file)) return null;
241
+ // A file that holds the head's order alone (writeHead's, before any tree was written) holds no tree.
242
+ if (!Object.hasOwn(file, "children") && !Object.hasOwn(file, "groups")) return null;
140
243
  const groups = [];
141
244
  const children = {};
142
245
  const take = (id, own) => {
@@ -367,11 +470,60 @@ export function fileOf(value) {
367
470
  };
368
471
  }
369
472
 
370
- /** Write outline `value` ({ groups?, children }) of workspace `root` (.gallery/ made when there is none yet). */
473
+ /**
474
+ * Write outline `value` ({ groups?, children }) of workspace `root` (.gallery/ made when there is none yet),
475
+ * the head's order the file holds kept as it is (writeHead's).
476
+ */
371
477
  export async function writeOutline(root, value) {
478
+ const home = outlineRoot(root);
479
+ if (sharedOutline(root) && !ownsOutline(root, value)) throw Object.assign(new Error("跨工作区请复制,不能只移动目录位置"), { code: "CROSS_WORKSPACE" });
480
+ if (sharedOutline(root)) value = { ...value, groups: (value.groups ?? [{ id: DOCS }]).map(group => group.id === DOCS || group.workspace || group.extension ? group : { ...group, owner: group.owner ?? workspaceId(root) }) };
481
+ if (workspaceKey(home) !== workspaceKey(root)) {
482
+ const current = (await localOutlineAndFile(home)).outline;
483
+ const translated = projectTree(value, root, home);
484
+ // The current-column-first projection is presentation only. Keep the shared column order;
485
+ // deliberate moves of other columns use the relative order submitted by the client.
486
+ const ownId = workspaceId(root);
487
+ const at = current.groups.findIndex(group => group.workspace === ownId);
488
+ const own = translated.groups.find(group => group.workspace === ownId);
489
+ translated.groups = translated.groups.filter(group => group !== own);
490
+ if (own) translated.groups.splice(Math.max(0, Math.min(at, translated.groups.length)), 0, own);
491
+ const publicIds = new Set(await extensionColumnIds(home));
492
+ const privateGroup = group => group.extension && !publicIds.has(group.extension);
493
+ const positions = value.groups.filter(privateGroup).map(group => ({ id: group.id, before: value.groups.slice(value.groups.indexOf(group) + 1).find(group => !privateGroup(group))?.id ?? null }));
494
+ const localFile = layoutOf(root).outline;
495
+ const local = await readJson(localFile) ?? {};
496
+ if ((positions.length || local.extensionPositions?.length) && JSON.stringify(positions) !== JSON.stringify(local.extensionPositions)) {
497
+ await fs.mkdir(path.dirname(localFile), { recursive: true });
498
+ await writeAtomic(localFile, `${JSON.stringify({ ...local, extensionPositions: positions }, null, 2)}\n`);
499
+ }
500
+ translated.groups = translated.groups.filter(group => !group.extension || publicIds.has(group.extension));
501
+ for (const group of current.groups) if (group.extension && !translated.groups.some(item => item.id === group.id)) translated.groups.push(group);
502
+ value = translated;
503
+ root = home;
504
+ }
505
+ const file = layoutOf(root).outline;
506
+ await fs.mkdir(path.dirname(file), { recursive: true });
507
+ const head = storedHead((await readJson(file))?.head);
508
+ return writeAtomic(file, `${JSON.stringify({ ...fileOf(value), ...(head ? { head } : {}) }, null, 2)}\n`);
509
+ }
510
+
511
+ /** The head's order as workspace `root`'s outline file keeps it (./sidebar-head.mjs storedHead), or null where it keeps none. */
512
+ export async function readStoredHead(root) {
513
+ return storedHead((await readJson(layoutOf(root).outline))?.head);
514
+ }
515
+
516
+ /**
517
+ * Write the head's order `head` (a stored head: ./sidebar-head.mjs) into workspace `root`'s outline file,
518
+ * all else it holds as it was (a file there is none of yet holds it alone, which reads as no tree:
519
+ * storedOf). Run under the outline's lock (withOutlineLock), as every write of the file is.
520
+ */
521
+ export async function writeHead(root, head) {
372
522
  const file = layoutOf(root).outline;
373
523
  await fs.mkdir(path.dirname(file), { recursive: true });
374
- return writeAtomic(file, `${JSON.stringify(fileOf(value), null, 2)}\n`);
524
+ const value = await readJson(file);
525
+ const kept = value && typeof value === "object" && !Array.isArray(value) ? value : {};
526
+ return writeAtomic(file, `${JSON.stringify({ ...kept, head: storedHead(head) ?? [] }, null, 2)}\n`);
375
527
  }
376
528
 
377
529
  /** The key `slug` sits under in `children` (a column's top key at a top level), or null when the outline holds it not. */
@@ -437,6 +589,7 @@ export function moveDocument(root, slug, where) {
437
589
  return withOutlineLock(root, async () => {
438
590
  const { outline, stored } = await outlineAndFile(root);
439
591
  const result = moveIn(outline.children, slug, where);
592
+ if (result.ok && sharedOutline(root) && !ownsOutline(root, outline)) return { ok: false, reason: "cross-workspace" };
440
593
  const written = result.ok && !writtenAs(outline, stored);
441
594
  if (written) await writeOutline(root, outline);
442
595
  return { ...result, ...(result.ok ? { written } : {}) };
@@ -23,9 +23,20 @@ export const PACK_FILE = "pack.json";
23
23
  /** What a pack's id, a scope, may be: as a block id's scope (shared/components/installed.mjs PACKAGE_ID). */
24
24
  export const PACK_ID = /^[a-z0-9][a-z0-9-]*$/;
25
25
 
26
- /** The scope a block of the workspace's own is in (gallery block new local/<name>, or a <name> alone), which no pack takes. */
26
+ /**
27
+ * The scope a block of the workspace's own is in (gallery block new local/<name>, or a <name> alone), which
28
+ * no pack installed takes: the default pack (默认块包, DEFAULT_PACK), where an Agent's block goes when it
29
+ * names no pack, so that a single block needs no pack of its own. It has no pack.json (none may name it):
30
+ * its name and words are these, and 设置 › 组件列表 lists it first, its blocks or none (shared/block-packs.mjs).
31
+ */
27
32
  export const OWN_SCOPE = "local";
28
33
 
34
+ /** What the default pack (OWN_SCOPE's) is called and says of itself, in both languages. */
35
+ export const DEFAULT_PACK = Object.freeze({
36
+ name: Object.freeze({ zh: "默认块包", en: "Default pack" }),
37
+ description: Object.freeze({ zh: "Agent 写自定义块时没有指定块包,块就放在这里。只写一个块,不用另建块包。", en: "Where an Agent's custom block goes when it names no pack: a single block needs no pack of its own." }),
38
+ });
39
+
29
40
  const FIELDS = ["id", "name", "description", "version", "author", "homepage", "icon"];
30
41
  const VERSION = /^\d+\.\d+\.\d+$/;
31
42
  const isTable = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -6,36 +6,49 @@ import { isDeepStrictEqual } from "node:util";
6
6
  import { escapeMarkup, escapeText } from "./escape.mjs";
7
7
  import { writeAtomic } from "./files.mjs";
8
8
  import { laidOut } from "./json-layout.mjs";
9
+ import { readStoredHead, withOutlineLock, writeHead } from "./outline.mjs";
10
+ import { newPageId } from "./page-links.mjs";
11
+ import { headOrder, headPage, headWithPage } from "./sidebar-head.mjs";
9
12
  import { DOCUMENT_FOLDERS, isSlugName, keptName, layoutOf, listTrash, markerOf, slugPattern, slugTaken } from "./workspace.mjs";
10
13
 
11
14
  // A workspace's custom pages (自定义页: notes/plans/component-pages-plan-2026-11-07.md §5, the spec
12
15
  // custom-blocks-spec-2026-11-07.md §5): a page is a folder pages/<slug>/ at the workspace's root, as
13
- // components/ is, holding page.html (the page an Agent writes: its head, its look, and one or more
14
- // boards, <div class="gallery-board">, of cards, each <section class="gallery-card" data-card-id
15
- // data-x data-y data-w data-h> around one instance of a custom block) and page.json ({ title, icon }).
16
- // Only an Agent makes one (gallery page new); the workbench lists them under 新建 and 搜索, shows one
17
- // at #@<slug> and renames or trashes it (src/workbench/service.mjs). A page's slug and a document's
18
- // are one pool (./workspace.mjs slugTaken): no new document takes a page's, nor a new page a
19
- // document's, and `pages` is a name no new document takes. A page writes no script of its own: the
20
- // blocks' scripts and styles are put in by the server as it shows it (src/render/page-html.mjs).
21
- // The home page is pages/home, listed first: the default one (首页, HOME_TITLE, holding a card of each
22
- // of HOME_CARDS) is made once per workspace by src/editor/home-page.mjs, as the server starts and by
23
- // gallery init, and gallery.json's pages.homeMade says it was (markHomeMade); nothing makes it again.
24
- // Node's built-ins only, but ./escape.mjs, ./files.mjs, ./json-layout.mjs and ./workspace.mjs.
16
+ // components/ is, holding page.html (the page an Agent writes, free HTML: its head, its look, its own
17
+ // scripts and frames, and where it wants them one or more boards, <div class="gallery-board">, of
18
+ // cards, each <section class="gallery-card" data-card-id data-x data-y data-w data-h> around one
19
+ // instance of a custom block) and page.json ({ title, icon, defaultCards?, hint? }: pageMetaOf). An Agent
20
+ // makes one (gallery page new), and the workbench's 新建页面 a blank one (blankPageHtml) for an Agent to
21
+ // write; the workbench lists them in the sidebar's head with 新建 and 搜索, in the order the reader drags
22
+ // them to (./sidebar-head.mjs, kept in the outline file), shows one at #@<slug> and renames or trashes
23
+ // it (src/workbench/service.mjs). A page's slug and a document's are one pool (./workspace.mjs
24
+ // slugTaken): no new document takes a page's, nor a new page a document's, and `pages` is a name no new
25
+ // document takes. The blocks' scripts and styles are put in by the server as it shows a page
26
+ // (src/render/page-html.mjs).
27
+ // No page is special by its slug. The default home page (首页, HOME_TITLE, at pages/home, holding a card of
28
+ // each of HOME_CARDS) is a page like any other, made once per workspace by src/editor/home-page.mjs, as
29
+ // the server starts and by gallery init (gallery.json's pages.homeMade says it was: markHomeMade; nothing
30
+ // makes it again), whose page.json says what any page's may: it has the default cards (defaultCards:
31
+ // 恢复默认卡片 in its menu, the note of the default cards it has not got), and its first-open note
32
+ // (hint, until the reader closes it: pageHintClosed).
33
+ // Node's built-ins only, but ./escape.mjs, ./files.mjs, ./json-layout.mjs, ./outline.mjs, ./sidebar-head.mjs
34
+ // and ./workspace.mjs.
25
35
 
26
36
  /** A custom page's two files. */
27
37
  export const PAGE_FILES = { html: "page.html", json: "page.json" };
28
- /** The home page's slug (首页): listed first, never trashed. */
38
+ /** The slug of the default home page (首页), which src/editor/home-page.mjs makes: a page as any other. */
29
39
  export const HOME = "home";
30
40
  /** The default home page's title. */
31
41
  export const HOME_TITLE = "首页";
32
42
  /**
33
- * The blocks of the default home page's cards (the plan's §5.2: 问候与日期, 便签, 我的待办, 最近编辑, 工作区统计,
34
- * 编辑热力图), in the order they are placed, each at the first free spot by its default size (a home of
35
- * 12 x 6 cells, none left empty: shared/board-places.mjs freeSpot); 恢复默认卡片 (the page service's
36
- * restore-defaults) puts back those of them no card of the home page holds.
43
+ * The blocks of the default home page's cards (the plan's §5.2, laid out as the pack Gallery 风格's home page,
44
+ * notes/plans/block-packs-plan-2026-11-09.md §3: 问候与日期, 天气, 倒计时, 习惯打卡, 工作区统计, 我的待办, 编辑热力图,
45
+ * 最近编辑, 便签, 静态卡片), in the order they are placed, each at the first free spot by its size (HOME_SIZES',
46
+ * else its block's default: shared/board-places.mjs freeSpot), a home of 12 x 12 cells, none left empty;
47
+ * 恢复默认卡片 (the page service's restore-defaults) puts back those of them no card of the home page holds.
37
48
  */
38
- export const HOME_CARDS = Object.freeze(["gallery/today", "gallery/note", "gallery/todos", "gallery/recent", "gallery/stats", "gallery/heatmap"]);
49
+ export const HOME_CARDS = Object.freeze(["gallery/today", "gallery/weather", "gallery/countdown", "gallery/habit", "gallery/stats", "gallery/todos", "gallery/heatmap", "gallery/recent", "gallery/note", "gallery/static"]);
50
+ /** The default home page's cards that are not their block's default size, [w, h] each. */
51
+ export const HOME_SIZES = Object.freeze({ "gallery/habit": [2, 2], "gallery/todos": [4, 6], "gallery/note": [4, 4] });
39
52
  /**
40
53
  * The default cards a home page made before the workspace's data came had (the plan's phase 2: 问候与日期
41
54
  * and 便签): what a home whose hints say nothing of its cards was told of, so that it is told once of the
@@ -86,21 +99,51 @@ export async function pageSlugRefused(slug, root) {
86
99
  return null;
87
100
  }
88
101
 
102
+ /**
103
+ * A slug for a new custom page no name of workspace `root` holds (pageSlugRefused): page- and a random id,
104
+ * as a new document's is doc- and one (the workbench's 新建页面, whose title is typed after it is made).
105
+ */
106
+ export async function newPageSlug(root, random = undefined) {
107
+ for (;;) {
108
+ const slug = `page-${newPageId(new Set(), random)}`;
109
+ if (!(await pageSlugRefused(slug, root))) return slug;
110
+ }
111
+ }
112
+
89
113
  const readJson = (file) => fs.readFile(file, "utf8").then((text) => JSON.parse(text.replace(/^/, ""))).catch(() => null);
90
114
 
91
- /** A page.json's fields as a list shows them: { title (the slug when it names none), icon (or null) }. */
115
+ /**
116
+ * A page.json's fields as a list shows them: { title (the slug when it names none), icon (or null),
117
+ * defaultCards (it says `"defaultCards": true`: the page has Gallery's default cards, HOME_CARDS, which
118
+ * 恢复默认卡片 in its menu puts back where it misses them, and its note tells of those that came after
119
+ * it), hint (`"hint": true`: its first-open note on its cards shows until the reader closes it) }.
120
+ * Any page may say either; the default home page says both.
121
+ */
92
122
  export function pageMetaOf(slug, value) {
93
123
  const title = typeof value?.title === "string" && value.title.trim() ? value.title.trim() : slug;
94
124
  const icon = typeof value?.icon === "string" && value.icon.trim() ? value.icon.trim() : null;
95
- return { title, icon };
125
+ return { title, icon, defaultCards: value?.defaultCards === true, hint: value?.hint === true };
96
126
  }
97
127
 
98
- /** Custom page `slug` as a list shows it: { slug, title, icon }, or null when it is not there. */
128
+ /** Custom page `slug` as a list shows it: { slug, title, icon, defaultCards, hint }, or null when it is not there. */
99
129
  export async function readPageMeta(slug, root) {
100
130
  if (!pageExists(slug, root)) return null;
101
131
  return { slug, ...pageMetaOf(slug, await readJson(pageFiles(slug, root).json)) };
102
132
  }
103
133
 
134
+ /** Custom page `slug`'s meta as its page.json says it now (pageMetaOf), read at once; null when it is not there. */
135
+ export function pageMetaNow(slug, root) {
136
+ if (!pageExists(slug, root)) return null;
137
+ let value = null;
138
+ try {
139
+ value = JSON.parse(readFileSync(pageFiles(slug, root).json, "utf8").replace(/^/, ""));
140
+ } catch {}
141
+ return { slug, ...pageMetaOf(slug, value) };
142
+ }
143
+
144
+ /** Whether custom page `slug` has the default cards (its page.json's defaultCards): 恢复默认卡片 runs on it. */
145
+ export const pageDefaults = (slug, root) => pageMetaNow(slug, root)?.defaultCards === true;
146
+
104
147
  /**
105
148
  * What gallery.json says of the custom pages (the spec §2): { order: the sidebar's order of their
106
149
  * slugs, homeMade: whether the default home page was made }; nothing said, or a gallery.json that does
@@ -236,8 +279,9 @@ export function ownLine(text, open, member) {
236
279
  return start > open && /^[ \t]*$/.test(before) ? before : null;
237
280
  }
238
281
 
239
- // .gallery/hints.json: { home: <when it was closed>, homeCards: [the default cards the reader knows of] },
240
- // or {} where there is none or it does not read.
282
+ // .gallery/hints.json: { pages: { <slug>: <when its first-open note was closed> }, homeCards: [the default
283
+ // cards the reader knows of] }, or {} where there is none or it does not read. Its `home` (<when>) is what
284
+ // a Gallery said before every page was alike: the note of pages/home closed, the one page that had one.
241
285
  function hintsOf(root) {
242
286
  try {
243
287
  const value = JSON.parse(readFileSync(layoutOf(root).hints, "utf8"));
@@ -246,19 +290,48 @@ function hintsOf(root) {
246
290
  return {};
247
291
  }
248
292
  }
293
+ const closedOf = (hints) => (hints.pages && typeof hints.pages === "object" && !Array.isArray(hints.pages) ? hints.pages : {});
294
+
295
+ /** What .gallery/hints.json says under `key` (undefined where it says nothing, or does not read). */
296
+ export const hintOf = (root, key) => hintsOf(root)[key];
297
+
298
+ /** .gallery/hints.json with `patch`'s keys written, its others kept. */
299
+ export async function writeHints(root, patch) {
300
+ const { hints, gallery } = layoutOf(root);
301
+ await fs.mkdir(gallery, { recursive: true });
302
+ await writeAtomic(hints, `${JSON.stringify({ ...hintsOf(root), ...patch }, null, 2)}\n`);
303
+ }
249
304
 
250
305
  /**
251
- * Whether the reader closed the home page's first-open note (the plan's §5.2): once for the workspace,
306
+ * Whether the reader closed custom page `slug`'s first-open note (the plan's §5.2): once for the workspace,
252
307
  * whichever browser or port shows it, kept in .gallery/hints.json (what Gallery keeps of its own, never
253
308
  * gallery.json, which is the workspace's settings and wakes every page's watcher as it is written).
254
309
  */
255
- export const homeHintClosed = (root) => typeof hintsOf(root).home === "string";
310
+ export function pageHintClosed(root, slug) {
311
+ const hints = hintsOf(root);
312
+ return typeof closedOf(hints)[slug] === "string" || (slug === HOME && typeof hints.home === "string");
313
+ }
314
+
315
+ /** Whether custom page `slug`'s first-open note shows: its page.json says `hint`, and the reader has not closed it. */
316
+ export const pageHintShown = (slug, root) => pageMetaNow(slug, root)?.hint === true && !pageHintClosed(root, slug);
317
+
318
+ /** Custom page `slug`'s first-open note closed for good (`at`: when, an ISO time), the file's other notes kept. */
319
+ export async function closePageHint(root, slug, at = new Date().toISOString()) {
320
+ const { hints, gallery } = layoutOf(root);
321
+ await fs.mkdir(gallery, { recursive: true });
322
+ const value = hintsOf(root);
323
+ await writeAtomic(hints, `${JSON.stringify({ ...value, pages: { ...closedOf(value), [slug]: at } }, null, 2)}\n`);
324
+ }
325
+
326
+ /** The first-open note of pages/home closed (closePageHint's), and whether it is (pageHintClosed's): the older words for them. */
327
+ export const closeHomeHint = (root, at) => closePageHint(root, HOME, at);
328
+ export const homeHintClosed = (root) => pageHintClosed(root, HOME);
256
329
 
257
330
  /**
258
- * The default cards (of HOME_CARDS) the reader knows of in workspace `root`: those its home page was made
259
- * with, or the reader was told of since (the home page's note of new cards, closed or taken up, and
260
- * 恢复默认卡片 run), kept in .gallery/hints.json as `homeCards`; FIRST_HOME_CARDS where it says none, the
261
- * cards of a home made before the others came.
331
+ * The default cards (of HOME_CARDS) the reader knows of in workspace `root`: those its pages with the
332
+ * default cards were made with, or the reader was told of since (a page's note of new cards, closed or
333
+ * taken up, and 恢复默认卡片 run), kept in .gallery/hints.json as `homeCards`; FIRST_HOME_CARDS where it
334
+ * says none, the cards of a home page made before the others came.
262
335
  */
263
336
  export function homeCardsTold(root) {
264
337
  const told = hintsOf(root).homeCards;
@@ -266,43 +339,65 @@ export function homeCardsTold(root) {
266
339
  }
267
340
 
268
341
  /**
269
- * The default cards the home page's note tells of (the plan's 3.4: a home made before them is not
270
- * changed, and is told once that 恢复默认卡片 brings them): those of HOME_CARDS the reader was not told of
271
- * (homeCardsTold) and no card of the home page holds (`held`: the blocks its cards are). Empty where
272
- * there is none to tell.
342
+ * The default cards a page's note tells of, on a page with the default cards (pageMetaOf's defaultCards;
343
+ * the plan's 3.4: a page made before them is not changed, and is told once that 恢复默认卡片 brings them):
344
+ * those of HOME_CARDS the reader was not told of (homeCardsTold) and no card of the page holds (`held`: the
345
+ * blocks its cards are). Empty where there is none to tell.
273
346
  */
274
347
  export const homeCardsNew = (root, held) => HOME_CARDS.filter((id) => !homeCardsTold(root).includes(id) && !held.includes(id));
275
348
 
276
- /** Every default card told of (HOME_CARDS, kept as hints.json's `homeCards`): the home page's note of new cards says nothing again. */
349
+ /** Every default card told of (HOME_CARDS, kept as hints.json's `homeCards`): a page's note of new cards says nothing again. */
277
350
  export async function tellHomeCards(root) {
278
351
  const { hints, gallery } = layoutOf(root);
279
352
  await fs.mkdir(gallery, { recursive: true });
280
353
  await writeAtomic(hints, `${JSON.stringify({ ...hintsOf(root), homeCards: [...HOME_CARDS] }, null, 2)}\n`);
281
354
  }
282
355
 
283
- /** The home page's first-open note closed for good (`at`: when, an ISO time), the file's other notes kept. */
284
- export async function closeHomeHint(root, at = new Date().toISOString()) {
285
- const { hints, gallery } = layoutOf(root);
286
- await fs.mkdir(gallery, { recursive: true });
287
- await writeAtomic(hints, `${JSON.stringify({ ...hintsOf(root), home: at }, null, 2)}\n`);
288
- }
289
-
290
356
  /**
291
- * Pages in the sidebar's order (the spec §2): home first, then those gallery.json's pages.order
292
- * names, in its order, then the rest by title (then slug). Pure: `pages` [{ slug, title }].
357
+ * Pages in the order a page goes by where the reader gave it no place in the sidebar's head (the spec
358
+ * §2): those gallery.json's pages.order names, in its order, then the rest by title (then slug). Pure:
359
+ * `pages` [{ slug, title }].
293
360
  */
294
361
  export function inPageOrder(pages, order = []) {
295
- const rank = (slug) => (slug === HOME ? -1 : order.includes(slug) ? order.indexOf(slug) : Infinity);
362
+ const rank = (slug) => (order.includes(slug) ? order.indexOf(slug) : Infinity);
296
363
  const collator = new Intl.Collator("zh-CN", { numeric: true });
297
364
  return [...pages].sort((a, b) => rank(a.slug) - rank(b.slug) || collator.compare(a.title, b.title) || (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0));
298
365
  }
299
366
 
300
- /** The custom pages of workspace `root`, in the sidebar's order: [{ slug, title, icon }]. Folders of pages/ that hold no page.html are passed over. */
367
+ /**
368
+ * Pages in the sidebar's order: as the head's rows go (`head`, the stored head: ./sidebar-head.mjs
369
+ * headOrder), those it names not after them in inPageOrder's (`order`: gallery.json's pages.order). Pure.
370
+ */
371
+ export function inHeadOrder(pages, order = [], head = null) {
372
+ const listed = inPageOrder(pages, order);
373
+ const bySlug = new Map(listed.map((page) => [page.slug, page]));
374
+ return headOrder(head, listed.map(({ slug }) => slug)).map(headPage).filter(Boolean).map((slug) => bySlug.get(slug));
375
+ }
376
+
377
+ /** The custom pages of workspace `root`, in the sidebar's order (inHeadOrder): [{ slug, title, icon, defaultCards, hint }]. Folders of pages/ that hold no page.html are passed over. */
301
378
  export async function listPages(root) {
302
379
  const { customPages: pages } = layoutOf(root);
303
380
  const names = await fs.readdir(pages, { withFileTypes: true }).catch(() => []);
304
381
  const found = await Promise.all(names.filter((entry) => entry.isDirectory() && isSlugName(entry.name)).map((entry) => readPageMeta(entry.name, root)));
305
- return inPageOrder(found.filter(Boolean), pagesSettings(root).order);
382
+ return inHeadOrder(found.filter(Boolean), pagesSettings(root).order, await readStoredHead(root));
383
+ }
384
+
385
+ /** The sidebar's head rows of workspace `root` in their order (./sidebar-head.mjs headOrder): new, search and @<slug> of each of `pages` (listPages'). */
386
+ export async function headOf(root, pages = null) {
387
+ return headOrder(await readStoredHead(root), (pages ?? await listPages(root)).map(({ slug }) => slug));
388
+ }
389
+
390
+ /**
391
+ * Custom page `slug` given its row in the sidebar's head, in the outline file under its lock: right after
392
+ * page `after`'s row, else last (a page made: createPage's). A head that was never stored is stored as
393
+ * the sidebar showed it, so that the page made last stands last.
394
+ */
395
+ export async function placePageInHead(root, slug, after = null) {
396
+ await withOutlineLock(root, async () => {
397
+ const stored = await readStoredHead(root);
398
+ const shown = stored ? null : await headOf(root, (await listPages(root)).filter((page) => page.slug !== slug));
399
+ await writeHead(root, headWithPage(stored, shown, slug, after));
400
+ });
306
401
  }
307
402
 
308
403
  /**
@@ -322,10 +417,14 @@ export async function pageVersionNow(slug, root) {
322
417
 
323
418
  /**
324
419
  * The page.html of a new custom page: its title (in `<title>` only: the page draws no big title, its
325
- * name is the sidebar's, the tab's and the top bar's) and one empty board (`main`), the spec §5's
326
- * structure. `lang`: its page's lang ("zh-CN" by default).
420
+ * name is the sidebar's, the tab's and the top bar's) and its <main class="gallery-page">, holding one
421
+ * empty board (`main`, the spec §5's structure: gallery page new's), or nothing (`blank`: the workbench's
422
+ * 新建页面, a page an Agent is to write, isBlankPage). `lang`: its page's lang ("zh-CN" by default).
327
423
  */
328
- export function newPageHtml(slug, title, { lang = "zh-CN" } = {}) {
424
+ export function newPageHtml(slug, title, { lang = "zh-CN", blank = false } = {}) {
425
+ const inside = blank ? "" : `
426
+ <div class="gallery-board" data-board-id="${FIRST_BOARD}">
427
+ </div>`;
329
428
  return `<!doctype html>
330
429
  <html lang="${escapeMarkup(lang)}">
331
430
  <head>
@@ -338,9 +437,7 @@ export function newPageHtml(slug, title, { lang = "zh-CN" } = {}) {
338
437
  </style>
339
438
  </head>
340
439
  <body>
341
- <main class="gallery-page" data-page-id="${slug}">
342
- <div class="gallery-board" data-board-id="${FIRST_BOARD}">
343
- </div>
440
+ <main class="gallery-page" data-page-id="${slug}">${inside}
344
441
  </main>
345
442
  </body>
346
443
  </html>
@@ -348,18 +445,33 @@ export function newPageHtml(slug, title, { lang = "zh-CN" } = {}) {
348
445
  }
349
446
 
350
447
  /**
351
- * Make custom page `slug` in workspace `root`: pages/<slug>/ with page.html (newPageHtml) and page.json
352
- * ({ title, icon? }). Throws (code EEXIST) when pageSlugRefused refuses the slug; resolves to its files.
448
+ * Whether page.html `html` is a blank page, one no one has written yet: its <body> holds an empty
449
+ * <main class="gallery-page"> and nothing else (comments and blanks aside), as the workbench's 新建页面
450
+ * writes it (newPageHtml's `blank`). The workbench then shows, in its middle, that an Agent writes it.
451
+ */
452
+ export function isBlankPage(html) {
453
+ const body = /<body\b[^>]*>([\s\S]*?)<\/body>/i.exec(String(html ?? "").replace(/<!--[\s\S]*?-->/g, ""))?.[1];
454
+ return body !== undefined && /^\s*<main\b[^>]*\bclass\s*=\s*["']?gallery-page["']?[^>]*>\s*<\/main>\s*$/i.test(body);
455
+ }
456
+
457
+ /**
458
+ * Make custom page `slug` in workspace `root`: pages/<slug>/ with page.html (newPageHtml: one empty
459
+ * board, or none where `blank`) and page.json ({ title, icon?, defaultCards?, hint? }: pageMetaOf), and its
460
+ * row in the sidebar's head right after page `after`'s, else last (placePageInHead; a head that cannot be
461
+ * written leaves it where a page the head names not goes). Throws (code EEXIST) when pageSlugRefused
462
+ * refuses the slug; resolves to its files.
353
463
  */
354
- export async function createPage(slug, title, { root, icon = null, lang } = {}) {
464
+ export async function createPage(slug, title, { root, icon = null, lang, blank = false, defaultCards = false, hint = false, after = null } = {}) {
355
465
  const refused = await pageSlugRefused(slug, root);
356
466
  if (refused) throw Object.assign(new Error(refused), { code: "EEXIST" });
357
467
  const files = pageFiles(slug, root);
358
468
  await fs.mkdir(layoutOf(root).customPages, { recursive: true });
359
469
  await fs.mkdir(files.folder, { recursive: false }); // fails when it was made meanwhile
360
470
  const name = title.replace(/\s+/g, " ").trim();
361
- await fs.writeFile(files.html, newPageHtml(slug, name, { lang }));
362
- await fs.writeFile(files.json, `${JSON.stringify({ title: name, ...(icon ? { icon } : {}) }, null, 2)}\n`);
471
+ await fs.writeFile(files.html, newPageHtml(slug, name, { lang, blank }));
472
+ const meta = { title: name, ...(icon ? { icon } : {}), ...(defaultCards ? { defaultCards: true } : {}), ...(hint ? { hint: true } : {}) };
473
+ await fs.writeFile(files.json, `${JSON.stringify(meta, null, 2)}\n`);
474
+ await placePageInHead(root, slug, after).catch(() => {});
363
475
  return files;
364
476
  }
365
477