@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.
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/browser/editor/components/align.mjs +58 -0
- package/browser/editor/components/code/index.mjs +1 -1
- package/browser/editor/components/embed/page.mjs +1 -1
- package/browser/editor/components/file/page.mjs +4 -4
- package/browser/editor/components/heading/index.mjs +7 -6
- package/browser/editor/components/icon/page.mjs +6 -4
- package/browser/editor/components/icons.mjs +5 -2
- package/browser/editor/components/image/index.mjs +2 -0
- package/browser/editor/components/image/page.mjs +172 -90
- package/browser/editor/components/installed/index.mjs +6 -2
- package/browser/editor/components/installed/page.mjs +80 -13
- package/browser/editor/components/media/icons.mjs +13 -8
- package/browser/editor/components/media/page.mjs +5 -3
- package/browser/editor/components/media/toolbar.mjs +12 -3
- package/browser/editor/components/paragraph/index.mjs +4 -3
- package/browser/editor/components/registry.mjs +17 -5
- package/browser/editor/components/table/index.mjs +4 -2
- package/browser/editor/page/after/index.mjs +288 -0
- package/browser/editor/page/align.mjs +91 -0
- package/browser/editor/page/beside.mjs +5 -6
- package/browser/editor/page/block-menu/index.mjs +6 -3
- package/browser/editor/page/blocks.mjs +3 -1
- package/browser/editor/page/board/hint.mjs +64 -27
- package/browser/editor/page/board/index.mjs +160 -71
- package/browser/editor/page/board/motion.mjs +177 -0
- package/browser/editor/page/board/settings.mjs +4 -3
- package/browser/editor/page/clipboard/import/read.mjs +20 -2
- package/browser/editor/page/clipboard/index.mjs +10 -3
- package/browser/editor/page/clipboard/own/blocks.mjs +3 -4
- package/browser/editor/page/clipboard/own/describe.mjs +6 -2
- package/browser/editor/page/commit.mjs +2 -2
- package/browser/editor/page/core.mjs +3 -1
- package/browser/editor/page/custom-page.mjs +15 -11
- package/browser/editor/page/editor.mjs +31 -14
- package/browser/editor/page/history.mjs +19 -4
- package/browser/editor/page/insert/index.mjs +20 -22
- package/browser/editor/page/menu.mjs +15 -7
- package/browser/editor/page/page-objects.mjs +3 -7
- package/browser/editor/page/present-links.mjs +5 -4
- package/browser/editor/page/present-shell.mjs +63 -0
- package/browser/editor/page/selection/index.mjs +10 -2
- package/browser/editor/page/table/binding.mjs +87 -0
- package/browser/editor/page/table/icons.mjs +0 -2
- package/browser/editor/page/table/index.mjs +10 -14
- package/browser/editor/page/table/views.mjs +5 -52
- package/browser/editor/page/table/widths.mjs +82 -10
- package/browser/editor/page/typing/index.mjs +44 -12
- package/browser/editor/page/typing/leave.mjs +19 -5
- package/browser/shared/block-packs.mjs +23 -8
- package/browser/shared/component-catalog.mjs +15 -8
- package/browser/shared/component-source.mjs +3 -1
- package/browser/shared/components/heading.mjs +2 -1
- package/browser/shared/components/installed.mjs +1 -1
- package/browser/shared/components/page.mjs +2 -0
- package/browser/shared/components/paragraph.mjs +8 -1
- package/browser/shared/components/table.mjs +1 -0
- package/browser/shared/document-copy.mjs +43 -0
- package/browser/shared/drafts.mjs +10 -3
- package/browser/shared/emoji/icon-value.mjs +2 -2
- package/browser/shared/emoji/line-icons.mjs +37 -0
- package/browser/shared/extensions.mjs +83 -7
- package/browser/shared/http.mjs +9 -0
- package/browser/shared/i18n/en.mjs +50 -17
- package/browser/shared/outline-shape.mjs +6 -0
- package/browser/shared/outline.mjs +165 -12
- package/browser/shared/pack-manifest.mjs +12 -1
- package/browser/shared/pages.mjs +169 -57
- package/browser/shared/pinyin.mjs +92 -0
- package/browser/shared/present-step.mjs +17 -0
- package/browser/shared/sidebar-head.mjs +70 -0
- package/browser/shared/web-links.mjs +5 -0
- package/browser/shared/workspace-columns.mjs +11 -7
- package/dist/{chunk-N7AZZLGT.mjs → chunk-2GKGEVMR.mjs} +29 -24
- package/dist/chunk-2OHANJ6Q.mjs +2 -0
- package/dist/{chunk-HYMEEXKN.mjs → chunk-2RKIHJI2.mjs} +1 -1
- package/dist/{chunk-742D2WWJ.mjs → chunk-36FIRJIF.mjs} +2 -2
- package/dist/{chunk-BPL64HGA.mjs → chunk-3A5I2GJK.mjs} +1 -1
- package/dist/{chunk-RFDZXFJF.mjs → chunk-3OFXMN6N.mjs} +1 -1
- package/dist/chunk-46ACXOU5.mjs +2 -0
- package/dist/{chunk-PUOQ65NC.mjs → chunk-4AMTKWDO.mjs} +1 -1
- package/dist/{chunk-VTKNEYT3.mjs → chunk-4DNHHJSL.mjs} +1 -1
- package/dist/chunk-5DPABPBI.mjs +2 -0
- package/dist/chunk-5IIH335D.mjs +3 -0
- package/dist/chunk-5L26JUYA.mjs +2 -0
- package/dist/{chunk-3NJACR4W.mjs → chunk-5MEXZ2UF.mjs} +1 -1
- package/dist/{chunk-ZHEHPMBK.mjs → chunk-6MPKCH3P.mjs} +1 -1
- package/dist/{chunk-C564NU2H.mjs → chunk-74RPP2M2.mjs} +1 -1
- package/dist/chunk-7M2BQQGZ.mjs +2 -0
- package/dist/{chunk-VXACTLO3.mjs → chunk-7M7UPWU6.mjs} +1 -1
- package/dist/{chunk-ZB4H7ZN4.mjs → chunk-7N2LPG33.mjs} +1 -1
- package/dist/{chunk-UBBRTJ2R.mjs → chunk-7XDYK3UC.mjs} +1 -1
- package/dist/chunk-AEMVZYZQ.mjs +166 -0
- package/dist/{chunk-POQSE4C2.mjs → chunk-AFGUR7SY.mjs} +1 -1
- package/dist/{chunk-QKXMPUXC.mjs → chunk-AMDGVDNV.mjs} +1 -1
- package/dist/{chunk-KTKHRC6H.mjs → chunk-ASAXY2DJ.mjs} +1 -1
- package/dist/{chunk-NVRNLK25.mjs → chunk-BYSF5UMM.mjs} +2 -2
- package/dist/{chunk-U3SSPL4K.mjs → chunk-CCSGFCLZ.mjs} +1 -1
- package/dist/{chunk-NWFBE2YA.mjs → chunk-CG3UHDKH.mjs} +1 -1
- package/dist/chunk-CPS4SRPF.mjs +5 -0
- package/dist/{chunk-B4P2CM2J.mjs → chunk-CZ5KJSGK.mjs} +1 -1
- package/dist/chunk-DAMVDXR3.mjs +2 -0
- package/dist/{chunk-NG375ILQ.mjs → chunk-DH6GMDNN.mjs} +1 -1
- package/dist/chunk-DHB7UZ7F.mjs +3 -0
- package/dist/{chunk-MHW7ZKOU.mjs → chunk-DKR7LFIY.mjs} +1 -1
- package/dist/chunk-DLFZJJEE.mjs +2 -0
- package/dist/{chunk-6EZ7FNIK.mjs → chunk-F4ZA6JRZ.mjs} +1 -1
- package/dist/{chunk-6VTU6KEO.mjs → chunk-FCK6NXON.mjs} +1 -1
- package/dist/{chunk-ZWPMCK7P.mjs → chunk-FX42XU6J.mjs} +1 -1
- package/dist/chunk-FXQBADMN.mjs +5 -0
- package/dist/{chunk-W3BGBJQW.mjs → chunk-G4YP7GVC.mjs} +1 -1
- package/dist/{chunk-V6XZ4VIR.mjs → chunk-GWOYMENX.mjs} +1 -1
- package/dist/chunk-HATA2W7X.mjs +2 -0
- package/dist/{chunk-YI43RG4P.mjs → chunk-HBAUKKQH.mjs} +1 -1
- package/dist/{chunk-MAFIGVXY.mjs → chunk-HM4E43VM.mjs} +1 -1
- package/dist/{chunk-CRJEG7MT.mjs → chunk-JI6T3A4Y.mjs} +1 -1
- package/dist/{chunk-NFMCP2HI.mjs → chunk-JJNRIFND.mjs} +2 -2
- package/dist/chunk-JL6R4FG4.mjs +4 -0
- package/dist/{chunk-UMHUSOPJ.mjs → chunk-JXLSHVJ2.mjs} +1 -1
- package/dist/{chunk-FJYN7RTH.mjs → chunk-KFZBYDIS.mjs} +1 -1
- package/dist/{chunk-ZTHICSSF.mjs → chunk-KH24LWAT.mjs} +1 -1
- package/dist/{chunk-RKPWLGZ3.mjs → chunk-KUILYI7H.mjs} +1 -1
- package/dist/{chunk-RMECZA2K.mjs → chunk-L7W624FD.mjs} +1 -1
- package/dist/chunk-LE44MD3O.mjs +4 -0
- package/dist/{chunk-4K5KC6V2.mjs → chunk-LV24RY2E.mjs} +1 -1
- package/dist/{chunk-43B75J5T.mjs → chunk-MKTX5PGV.mjs} +1 -1
- package/dist/{chunk-LKX55H76.mjs → chunk-NAGMRVW4.mjs} +1 -1
- package/dist/{chunk-D4CKRD3V.mjs → chunk-NFABH3QW.mjs} +2 -2
- package/dist/{chunk-PVER7LSC.mjs → chunk-NIW7F7LU.mjs} +2 -2
- package/dist/{chunk-PAREWQGB.mjs → chunk-NWRZXELO.mjs} +1 -1
- package/dist/{chunk-OJKMINIU.mjs → chunk-OU3S466R.mjs} +15 -15
- package/dist/{chunk-5ZJA7CXT.mjs → chunk-PAFSFVYB.mjs} +1 -1
- package/dist/chunk-QCEATQC4.mjs +2 -0
- package/dist/{chunk-Y34NR5JL.mjs → chunk-QJ3TFJLQ.mjs} +1 -1
- package/dist/{chunk-IA4PSIMD.mjs → chunk-QK3VISCA.mjs} +1 -1
- package/dist/{chunk-ZUNVAXLH.mjs → chunk-QXVNUASA.mjs} +1 -1
- package/dist/{chunk-GRAXKKG4.mjs → chunk-QZ35UAB4.mjs} +1 -1
- package/dist/{chunk-UGWYKHBL.mjs → chunk-S2Y7VI4H.mjs} +2 -2
- package/dist/{chunk-OW5NWPOJ.mjs → chunk-SAHS56QM.mjs} +1 -1
- package/dist/{chunk-QOUEFUCH.mjs → chunk-SOD23RGL.mjs} +1 -1
- package/dist/chunk-TDWJKOLN.mjs +2 -0
- package/dist/{chunk-V6RIYQZZ.mjs → chunk-TOE6DPLH.mjs} +1 -1
- package/dist/chunk-UGO5N3EI.mjs +15 -0
- package/dist/{chunk-Q47PLKIJ.mjs → chunk-UGWMMEJ5.mjs} +1 -1
- package/dist/{chunk-NWR2LT5B.mjs → chunk-V5JRSGZF.mjs} +1 -1
- package/dist/{chunk-BZ5RAF7L.mjs → chunk-VA3KAQZD.mjs} +1 -1
- package/dist/chunk-VG5JEIUQ.mjs +12 -0
- package/dist/{chunk-UNOYVIYR.mjs → chunk-VGTMLQWZ.mjs} +1 -1
- package/dist/chunk-VKSRM7EY.mjs +22 -0
- package/dist/{chunk-YQGT4AB2.mjs → chunk-VNUWREXU.mjs} +1 -1
- package/dist/{chunk-AGDILNWB.mjs → chunk-VPVOAJ7X.mjs} +1 -1
- package/dist/{chunk-HLYV7RIH.mjs → chunk-WCWYNFDF.mjs} +1 -1
- package/dist/chunk-WTEGUPFG.mjs +16 -0
- package/dist/{chunk-N76HWBA3.mjs → chunk-XKOHV4H6.mjs} +1 -1
- package/dist/{chunk-HMYTM3GT.mjs → chunk-XMKHCUI6.mjs} +1 -1
- package/dist/{chunk-OUUP3WKT.mjs → chunk-XQRUG4G3.mjs} +1 -1
- package/dist/{chunk-32ZUEFZ7.mjs → chunk-YBB5LWZK.mjs} +1 -1
- package/dist/{chunk-SX4LJYT6.mjs → chunk-YFJU3O7G.mjs} +1 -1
- package/dist/chunk-YPPJVDQI.mjs +2 -0
- package/dist/chunk-ZANMKAWS.mjs +2 -0
- package/dist/{chunk-6QBHU4DF.mjs → chunk-ZCP7BTOZ.mjs} +1 -1
- package/dist/{chunk-ERLAIBOA.mjs → chunk-ZEY2RLBT.mjs} +2 -2
- package/dist/{chunk-SQ6Y2ZMA.mjs → chunk-ZO7SFB2E.mjs} +2 -2
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/board.css +5 -5
- package/kit/code.js +15 -15
- package/kit/math.js +1 -1
- package/kit/report.css +123 -44
- package/kit/runtime.js +45 -7
- package/kit/tables.js +2 -0
- package/kit/ui.js +2 -0
- package/package.json +1 -1
- package/packs/gallery/chart-view/kit.js +3 -3
- package/packs/gallery/chart-view/writing.md +1 -1
- package/packs/gallery/countdown/component.json +1 -1
- package/packs/gallery/countdown/kit.js +89 -29
- package/packs/gallery/countdown/preview.svg +1 -1
- package/packs/gallery/countdown/style.css +88 -100
- package/packs/gallery/countdown/writing.md +2 -2
- package/packs/gallery/habit/component.json +3 -3
- package/packs/gallery/habit/kit.js +114 -75
- package/packs/gallery/habit/preview.svg +1 -1
- package/packs/gallery/habit/style.css +133 -205
- package/packs/gallery/habit/writing.md +2 -2
- package/packs/gallery/heatmap/component.json +2 -2
- package/packs/gallery/heatmap/kit.js +335 -173
- package/packs/gallery/heatmap/preview.svg +1 -1
- package/packs/gallery/heatmap/style.css +158 -170
- package/packs/gallery/heatmap/writing.md +2 -2
- package/packs/gallery/kanban/kit.js +3 -3
- package/packs/gallery/kanban/writing.md +1 -1
- package/packs/gallery/metric/kit.js +3 -3
- package/packs/gallery/metric/writing.md +1 -1
- package/packs/gallery/note/component.json +3 -3
- package/packs/gallery/note/kit.js +40 -13
- package/packs/gallery/note/preview.svg +1 -1
- package/packs/gallery/note/style.css +51 -30
- package/packs/gallery/note/writing.md +2 -2
- package/packs/gallery/recent/component.json +2 -2
- package/packs/gallery/recent/kit.js +141 -86
- package/packs/gallery/recent/preview.svg +1 -1
- package/packs/gallery/recent/style.css +53 -126
- package/packs/gallery/recent/writing.md +2 -2
- package/packs/gallery/static/component.json +2 -2
- package/packs/gallery/static/preview.svg +1 -1
- package/packs/gallery/static/style.css +99 -66
- package/packs/gallery/static/writing.md +2 -2
- package/packs/gallery/stats/component.json +3 -3
- package/packs/gallery/stats/kit.js +253 -72
- package/packs/gallery/stats/preview.svg +1 -1
- package/packs/gallery/stats/style.css +64 -142
- package/packs/gallery/stats/writing.md +2 -2
- package/packs/gallery/today/component.json +2 -2
- package/packs/gallery/today/kit.js +62 -27
- package/packs/gallery/today/preview.svg +1 -1
- package/packs/gallery/today/style.css +101 -72
- package/packs/gallery/today/writing.md +2 -2
- package/packs/gallery/todos/component.json +1 -1
- package/packs/gallery/todos/kit.js +218 -71
- package/packs/gallery/todos/preview.svg +1 -1
- package/packs/gallery/todos/style.css +170 -223
- package/packs/gallery/todos/writing.md +1 -1
- package/packs/gallery/weather/component.json +2 -2
- package/packs/gallery/weather/kit.js +165 -90
- package/packs/gallery/weather/preview.svg +1 -1
- package/packs/gallery/weather/style.css +100 -218
- package/packs/gallery/weather/writing.md +1 -1
- package/packs/gallery-soft/countdown/component.json +18 -0
- package/packs/gallery-soft/countdown/example.html +3 -0
- package/packs/gallery-soft/countdown/kit.js +206 -0
- package/packs/gallery-soft/countdown/preview.svg +1 -0
- package/packs/gallery-soft/countdown/style.css +211 -0
- package/packs/gallery-soft/countdown/template.html +3 -0
- package/packs/gallery-soft/countdown/writing.md +24 -0
- package/packs/gallery-soft/habit/component.json +14 -0
- package/packs/gallery-soft/habit/example.html +3 -0
- package/packs/gallery-soft/habit/kit.js +366 -0
- package/packs/gallery-soft/habit/preview.svg +1 -0
- package/packs/gallery-soft/habit/style.css +251 -0
- package/packs/gallery-soft/habit/template.html +3 -0
- package/packs/gallery-soft/habit/writing.md +23 -0
- package/packs/gallery-soft/heatmap/component.json +18 -0
- package/packs/gallery-soft/heatmap/example.html +3 -0
- package/packs/gallery-soft/heatmap/kit.js +498 -0
- package/packs/gallery-soft/heatmap/preview.svg +1 -0
- package/packs/gallery-soft/heatmap/style.css +181 -0
- package/packs/gallery-soft/heatmap/template.html +3 -0
- package/packs/gallery-soft/heatmap/writing.md +23 -0
- package/packs/gallery-soft/note/component.json +14 -0
- package/packs/gallery-soft/note/example.html +3 -0
- package/packs/gallery-soft/note/kit.js +162 -0
- package/packs/gallery-soft/note/preview.svg +1 -0
- package/packs/gallery-soft/note/style.css +148 -0
- package/packs/gallery-soft/note/template.html +3 -0
- package/packs/gallery-soft/note/writing.md +20 -0
- package/packs/gallery-soft/recent/component.json +13 -0
- package/packs/gallery-soft/recent/example.html +1 -0
- package/packs/gallery-soft/recent/kit.js +335 -0
- package/packs/gallery-soft/recent/preview.svg +1 -0
- package/packs/gallery-soft/recent/style.css +160 -0
- package/packs/gallery-soft/recent/template.html +1 -0
- package/packs/gallery-soft/recent/writing.md +17 -0
- package/packs/gallery-soft/static/component.json +13 -0
- package/packs/gallery-soft/static/example.html +12 -0
- package/packs/gallery-soft/static/preview.svg +1 -0
- package/packs/gallery-soft/static/style.css +186 -0
- package/packs/gallery-soft/static/template.html +1 -0
- package/packs/gallery-soft/static/writing.md +20 -0
- package/packs/gallery-soft/stats/component.json +13 -0
- package/packs/gallery-soft/stats/example.html +1 -0
- package/packs/gallery-soft/stats/kit.js +341 -0
- package/packs/gallery-soft/stats/preview.svg +1 -0
- package/packs/gallery-soft/stats/style.css +119 -0
- package/packs/gallery-soft/stats/template.html +1 -0
- package/packs/gallery-soft/stats/writing.md +13 -0
- package/packs/gallery-soft/today/component.json +12 -0
- package/packs/gallery-soft/today/example.html +1 -0
- package/packs/gallery-soft/today/kit.js +222 -0
- package/packs/gallery-soft/today/preview.svg +1 -0
- package/packs/gallery-soft/today/style.css +178 -0
- package/packs/gallery-soft/today/template.html +1 -0
- package/packs/gallery-soft/today/writing.md +16 -0
- package/packs/gallery-soft/todos/component.json +19 -0
- package/packs/gallery-soft/todos/example.html +3 -0
- package/packs/gallery-soft/todos/kit.js +527 -0
- package/packs/gallery-soft/todos/preview.svg +1 -0
- package/packs/gallery-soft/todos/style.css +212 -0
- package/packs/gallery-soft/todos/template.html +3 -0
- package/packs/gallery-soft/todos/writing.md +32 -0
- package/packs/gallery-soft/weather/component.json +19 -0
- package/packs/gallery-soft/weather/example.html +3 -0
- package/packs/gallery-soft/weather/kit.js +411 -0
- package/packs/gallery-soft/weather/preview.svg +1 -0
- package/packs/gallery-soft/weather/style.css +185 -0
- package/packs/gallery-soft/weather/template.html +3 -0
- package/packs/gallery-soft/weather/writing.md +28 -0
- package/release.json +10 -10
- package/skill/SKILL.md +15 -14
- package/skill/references/building-blocks.md +4 -4
- package/skill/references/extensions.md +11 -7
- package/skill/references/pages-and-data.md +13 -9
- package/skill/references/writing-html.md +2 -0
- package/web/assets/index-BHC5ZOMh.js +15 -0
- package/web/assets/index-xP0uw3a0.css +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-2NRBY6AJ.mjs +0 -2
- package/dist/chunk-35NQ4PLQ.mjs +0 -2
- package/dist/chunk-3KJHLHUO.mjs +0 -2
- package/dist/chunk-4AV456RQ.mjs +0 -2
- package/dist/chunk-5XYHDBJ5.mjs +0 -2
- package/dist/chunk-6A53MZYZ.mjs +0 -2
- package/dist/chunk-6EXH3ORZ.mjs +0 -166
- package/dist/chunk-BVEFGE4B.mjs +0 -2
- package/dist/chunk-D4EURAX4.mjs +0 -4
- package/dist/chunk-DQCE7TJY.mjs +0 -5
- package/dist/chunk-FWNRPMCJ.mjs +0 -12
- package/dist/chunk-FWW4CSAZ.mjs +0 -2
- package/dist/chunk-GY3FAUVM.mjs +0 -2
- package/dist/chunk-JVHHHGEA.mjs +0 -3
- package/dist/chunk-L7UP52JO.mjs +0 -2
- package/dist/chunk-NAMN3RAF.mjs +0 -2
- package/dist/chunk-PZDLJJTJ.mjs +0 -3
- package/dist/chunk-SXHBYZST.mjs +0 -16
- package/dist/chunk-TEZAWDSQ.mjs +0 -2
- package/dist/chunk-UFNYFFKO.mjs +0 -20
- package/dist/chunk-UKSHDZZ6.mjs +0 -2
- package/dist/chunk-VJPFVNNW.mjs +0 -15
- package/dist/chunk-VS3ZMVGX.mjs +0 -5
- package/web/assets/index-BK2e_07w.js +0 -13
- package/web/assets/index-CWFI83mF.css +0 -1
- /package/dist/{chunk-YEY3RSFQ.mjs → chunk-5MMN3N5J.mjs} +0 -0
- /package/dist/{chunk-UH4SIXTR.mjs → chunk-74UW7QSL.mjs} +0 -0
- /package/dist/{chunk-W7XHE4CT.mjs → chunk-EKGYYOSP.mjs} +0 -0
- /package/dist/{chunk-QAIHRH3I.mjs → chunk-KNZJT4ET.mjs} +0 -0
- /package/dist/{chunk-A55HN5OK.mjs → chunk-N3DSBH5S.mjs} +0 -0
- /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 {
|
|
8
|
-
import {
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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);
|
package/browser/shared/pages.mjs
CHANGED
|
@@ -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,
|
|
14
|
-
// boards, <div class="gallery-board">, of
|
|
15
|
-
// data-x data-y data-w data-h> around one
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
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
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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/
|
|
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
|
-
/**
|
|
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: {
|
|
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
|
|
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
|
|
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
|
|
259
|
-
* with, or the reader was told of since (
|
|
260
|
-
* 恢复默认卡片 run), kept in .gallery/hints.json as `homeCards`; FIRST_HOME_CARDS where it
|
|
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
|
|
270
|
-
* changed, and is told once that 恢复默认卡片 brings them):
|
|
271
|
-
* (homeCardsTold) and no card of the
|
|
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`):
|
|
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
|
|
292
|
-
* names, in its order, then the rest by title (then slug). Pure:
|
|
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) => (
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
326
|
-
*
|
|
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
|
-
*
|
|
352
|
-
*
|
|
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
|
-
|
|
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
|
|