@heycys/gallery 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/columns/page.mjs +4 -4
- package/browser/editor/components/embed/index.mjs +2 -2
- package/browser/editor/components/embed/page.mjs +1 -1
- package/browser/editor/components/file/page.mjs +4 -4
- package/browser/editor/components/flowchart/add.mjs +7 -6
- package/browser/editor/components/flowchart/page.mjs +1 -1
- package/browser/editor/components/heading/index.mjs +7 -6
- package/browser/editor/components/icons.mjs +11 -0
- package/browser/editor/components/image/crop.mjs +6 -9
- 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 +15 -4
- package/browser/editor/components/installed/page.mjs +103 -25
- package/browser/editor/components/installed/served.mjs +6 -5
- package/browser/editor/components/instances.mjs +18 -0
- package/browser/editor/components/list/page.mjs +3 -6
- 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 +30 -9
- package/browser/editor/components/table/index.mjs +61 -2
- package/browser/editor/components/table/page.mjs +1 -1
- package/browser/editor/page/after/index.mjs +288 -0
- package/browser/editor/page/align.mjs +91 -0
- package/browser/editor/page/annotate/card.mjs +3 -6
- package/browser/editor/page/autosave.mjs +310 -37
- 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/client.mjs +57 -0
- package/browser/editor/page/board/hint.mjs +182 -0
- package/browser/editor/page/board/history.mjs +119 -0
- package/browser/editor/page/board/index.mjs +1059 -0
- package/browser/editor/page/board/motion.mjs +177 -0
- package/browser/editor/page/board/settings.mjs +210 -0
- package/browser/editor/page/caret.mjs +1 -1
- package/browser/editor/page/changes/bar.mjs +6 -14
- package/browser/editor/page/changes/index.mjs +2 -2
- package/browser/editor/page/clipboard/import/html.mjs +29 -6
- package/browser/editor/page/clipboard/import/inline.mjs +32 -0
- package/browser/editor/page/clipboard/import/markdown.mjs +58 -13
- package/browser/editor/page/clipboard/import/read.mjs +6 -1
- package/browser/editor/page/clipboard/import/to-blocks.mjs +44 -12
- package/browser/editor/page/clipboard/import/yuque.mjs +622 -0
- package/browser/editor/page/clipboard/index.mjs +133 -15
- package/browser/editor/page/clipboard/own/blocks.mjs +13 -14
- package/browser/editor/page/clipboard/own/chart-keys.mjs +41 -28
- package/browser/editor/page/clipboard/own/describe.mjs +6 -2
- package/browser/editor/page/clipboard/own/selection.mjs +1 -1
- package/browser/editor/page/columns/index.mjs +48 -46
- package/browser/editor/page/commit.mjs +80 -22
- package/browser/editor/page/conflicts.mjs +7 -7
- package/browser/editor/page/core.mjs +34 -16
- package/browser/editor/page/custom-page.mjs +110 -0
- package/browser/editor/page/editor.mjs +71 -17
- package/browser/editor/page/emoji-picker/styles.mjs +7 -12
- package/browser/editor/page/emoji-picker/upload.mjs +2 -2
- package/browser/editor/page/handle/index.mjs +5 -8
- package/browser/editor/page/history.mjs +41 -6
- package/browser/editor/page/insert/index.mjs +20 -22
- package/browser/editor/page/links/index.mjs +5 -3
- package/browser/editor/page/links/pages.mjs +27 -9
- package/browser/editor/page/look.mjs +54 -3
- package/browser/editor/page/margin.mjs +18 -3
- package/browser/editor/page/media-menu/index.mjs +6 -7
- package/browser/editor/page/menu.mjs +33 -14
- package/browser/editor/page/notices.mjs +2 -2
- package/browser/editor/page/page-objects.mjs +9 -2
- package/browser/editor/page/panel.mjs +4 -9
- package/browser/editor/page/present-links.mjs +15 -5
- package/browser/editor/page/present-shell.mjs +63 -0
- package/browser/editor/page/resize/index.mjs +42 -29
- package/browser/editor/page/selection/index.mjs +10 -2
- package/browser/editor/page/served.mjs +14 -0
- package/browser/editor/page/session.mjs +48 -4
- package/browser/editor/page/slash-menu/index.mjs +3 -3
- package/browser/editor/page/table/binding.mjs +87 -0
- package/browser/editor/page/table/cells.mjs +102 -46
- package/browser/editor/page/table/index.mjs +24 -9
- package/browser/editor/page/table/pictures.mjs +299 -0
- package/browser/editor/page/table/views.mjs +103 -0
- package/browser/editor/page/table/widths.mjs +82 -10
- package/browser/editor/page/text-select/index.mjs +26 -12
- package/browser/editor/page/todo-writes.mjs +75 -0
- package/browser/editor/page/typing/index.mjs +97 -15
- package/browser/editor/page/typing/leave.mjs +19 -5
- package/browser/editor/page/ui.mjs +13 -0
- package/browser/shared/block-packs.mjs +419 -0
- package/browser/shared/board-places.mjs +224 -0
- package/browser/shared/card-size.mjs +59 -0
- package/browser/shared/colors.mjs +3 -2
- package/browser/shared/component-catalog.mjs +330 -46
- package/browser/shared/component-source.mjs +39 -0
- package/browser/shared/components/heading.mjs +2 -1
- package/browser/shared/components/index.mjs +12 -3
- package/browser/shared/components/installed.mjs +35 -3
- package/browser/shared/components/page.mjs +20 -8
- package/browser/shared/components/paragraph.mjs +8 -1
- package/browser/shared/components/state.mjs +5 -1
- package/browser/shared/components/table.mjs +1 -0
- package/browser/shared/document-look.mjs +10 -5
- package/browser/shared/drafts.mjs +432 -0
- package/browser/shared/emoji/line-icons.mjs +37 -0
- package/browser/shared/extension-sidebar.mjs +126 -0
- package/browser/shared/extensions.mjs +79 -0
- package/browser/shared/file-table.mjs +101 -0
- package/browser/shared/http.mjs +9 -0
- package/browser/shared/i18n/en.mjs +330 -27
- package/browser/shared/json-layout.mjs +47 -0
- package/browser/shared/net-cache.mjs +133 -0
- package/browser/shared/net-keys.mjs +32 -0
- package/browser/shared/net-policy.mjs +177 -0
- package/browser/shared/outline-shape.mjs +128 -0
- package/browser/shared/outline.mjs +359 -67
- package/browser/shared/pack-manifest.mjs +88 -0
- package/browser/shared/page-address.mjs +6 -3
- package/browser/shared/page-cards.mjs +308 -0
- package/browser/shared/page-links.mjs +69 -6
- package/browser/shared/pages.mjs +500 -0
- package/browser/shared/paths.mjs +6 -0
- package/browser/shared/pinyin.mjs +92 -0
- package/browser/shared/present-step.mjs +17 -0
- package/browser/shared/preview-server.mjs +3 -7
- package/browser/shared/session.mjs +8 -1
- package/browser/shared/shipped-blocks.mjs +299 -0
- package/browser/shared/sidebar-head.mjs +70 -0
- package/browser/shared/table-chart.mjs +33 -1
- package/browser/shared/todos.mjs +83 -0
- package/browser/shared/user-directory.mjs +24 -0
- package/browser/shared/versions.mjs +14 -0
- package/browser/shared/web-links.mjs +139 -0
- package/browser/shared/workspace-columns.mjs +120 -0
- package/browser/shared/workspace-name.mjs +154 -0
- package/browser/shared/workspace-query.mjs +125 -0
- package/browser/shared/workspace-registry.mjs +272 -0
- package/browser/shared/workspace.mjs +171 -20
- package/dist/chunk-2OHANJ6Q.mjs +2 -0
- package/dist/chunk-3MBRL2PM.mjs +16 -0
- package/dist/chunk-4OKWZY5E.mjs +2 -0
- package/dist/chunk-4OZWXZE5.mjs +5 -0
- package/dist/chunk-5K44HLTZ.mjs +2 -0
- package/dist/{chunk-5ZKJ3H4P.mjs → chunk-5XA2ZBAE.mjs} +3 -3
- package/dist/{chunk-4FS7Z7WR.mjs → chunk-67N2QJYT.mjs} +1 -1
- package/dist/chunk-6KBYRA2C.mjs +2 -0
- package/dist/chunk-6N3PX4XX.mjs +8 -0
- package/dist/{chunk-V5JTW2LH.mjs → chunk-6QBHU4DF.mjs} +1 -1
- package/dist/chunk-7JDX4E65.mjs +13 -0
- package/dist/chunk-7M2BQQGZ.mjs +2 -0
- package/dist/chunk-7OG26PGF.mjs +2 -0
- package/dist/chunk-AHN3NFKJ.mjs +3 -0
- package/dist/chunk-ALZ6ZENZ.mjs +3 -0
- package/dist/chunk-AR5D57ZD.mjs +170 -0
- package/dist/{chunk-W3BGBJQW.mjs → chunk-BFJGHSQN.mjs} +1 -1
- package/dist/{chunk-LS4F7JUM.mjs → chunk-BZ5RAF7L.mjs} +1 -1
- package/dist/chunk-CMA72MAG.mjs +2 -0
- package/dist/{chunk-LCTHTZIL.mjs → chunk-DAZL73VK.mjs} +91 -91
- package/dist/{chunk-Y6RLRPG3.mjs → chunk-DJISHPUL.mjs} +1 -1
- package/dist/chunk-DR73Y2UJ.mjs +3 -0
- package/dist/{chunk-FDLXYYG4.mjs → chunk-EAIFITAV.mjs} +1 -1
- package/dist/{chunk-TMYZB6AV.mjs → chunk-EKNHM3P4.mjs} +1 -1
- package/dist/chunk-EP25HDAJ.mjs +2 -0
- package/dist/{chunk-GB5SOYQJ.mjs → chunk-ERLAIBOA.mjs} +2 -2
- package/dist/chunk-EURLTOTZ.mjs +4 -0
- package/dist/chunk-F4ZA6JRZ.mjs +3 -0
- package/dist/chunk-F6HVDZR4.mjs +2 -0
- package/dist/chunk-FDMAXMQA.mjs +2 -0
- package/dist/chunk-FETSREUN.mjs +5 -0
- package/dist/chunk-GHOYKMQ3.mjs +4 -0
- package/dist/{chunk-2B5E4CDJ.mjs → chunk-GPIWQ4X4.mjs} +1 -1
- package/dist/chunk-HDWTZ763.mjs +12 -0
- package/dist/chunk-HPOI4ZX7.mjs +6 -0
- package/dist/{chunk-BGGDRNYQ.mjs → chunk-HXG7EOPJ.mjs} +1 -1
- package/dist/chunk-IA4PSIMD.mjs +2 -0
- package/dist/{chunk-E7LWZRXY.mjs → chunk-ITOH5EGO.mjs} +1 -1
- package/dist/chunk-IV64FFI4.mjs +2 -0
- package/dist/chunk-J43Z6LSS.mjs +2 -0
- package/dist/chunk-J5PUIV3Y.mjs +4 -0
- package/dist/chunk-JA7G53PC.mjs +2 -0
- package/dist/chunk-JEUK3ZO7.mjs +2 -0
- package/dist/chunk-JLTZF4LA.mjs +2 -0
- package/dist/chunk-JZT7I2OJ.mjs +2 -0
- package/dist/chunk-K5XATEOO.mjs +2 -0
- package/dist/chunk-L2SUIS5S.mjs +2 -0
- package/dist/chunk-L7UP52JO.mjs +2 -0
- package/dist/chunk-LM4XFQHZ.mjs +2 -0
- package/dist/chunk-MN7BZL4R.mjs +2 -0
- package/dist/chunk-MTYYHM2G.mjs +15 -0
- package/dist/chunk-MXUBKDRO.mjs +15 -0
- package/dist/chunk-NAMN3RAF.mjs +2 -0
- package/dist/{chunk-SYGN7GLU.mjs → chunk-OEXWRJME.mjs} +1 -1
- package/dist/chunk-OFHXGWOX.mjs +2 -0
- package/dist/chunk-ORID3AE7.mjs +166 -0
- package/dist/chunk-PAREWQGB.mjs +4 -0
- package/dist/chunk-PGDE24N5.mjs +2 -0
- package/dist/chunk-POTA2FV4.mjs +2 -0
- package/dist/chunk-PUOQ65NC.mjs +2 -0
- package/dist/chunk-Q4UEPUWF.mjs +2 -0
- package/dist/chunk-QE5RG76S.mjs +549 -0
- package/dist/chunk-R2ZDLJYT.mjs +10 -0
- package/dist/{chunk-7DTINW6U.mjs → chunk-SQ6Y2ZMA.mjs} +2 -2
- package/dist/chunk-SWUTTZCN.mjs +2 -0
- package/dist/chunk-TANZRINB.mjs +2 -0
- package/dist/chunk-TR7TWGHB.mjs +2 -0
- package/dist/chunk-U5XL7D6A.mjs +3 -0
- package/dist/{chunk-XNROSH2T.mjs → chunk-U7NZI6N4.mjs} +2 -2
- package/dist/chunk-UGWMMEJ5.mjs +15 -0
- package/dist/chunk-UJQJKNGK.mjs +2 -0
- package/dist/chunk-UYVCQK3H.mjs +22 -0
- package/dist/chunk-VFX2KNHP.mjs +4 -0
- package/dist/chunk-VS7JIGJW.mjs +24 -0
- package/dist/chunk-W7665ZEC.mjs +6 -0
- package/dist/chunk-WQDQWCX5.mjs +2 -0
- package/dist/chunk-YI43RG4P.mjs +2 -0
- package/dist/chunk-YLPSMDIS.mjs +5 -0
- package/dist/chunk-YNAWBAYN.mjs +3 -0
- package/dist/chunk-YROX2O22.mjs +2 -0
- package/dist/chunk-ZDLPUOAG.mjs +6 -0
- package/dist/{chunk-IVDLMMBD.mjs → chunk-ZHEHPMBK.mjs} +1 -1
- package/dist/chunk-ZQS6H473.mjs +16 -0
- package/dist/chunk-ZTHICSSF.mjs +2 -0
- package/dist/chunk-ZWWC4KLF.mjs +20 -0
- package/dist/cli.mjs +1 -1
- package/dist/server.mjs +1 -1
- package/dist/{chunk-U6KNPJPR.mjs → update-fetch.mjs} +1 -1
- package/dist/worker.mjs +1 -1
- package/kit/board.css +95 -0
- package/kit/board.js +250 -0
- package/kit/charts.js +5 -71
- package/kit/code.js +17 -17
- package/kit/report.css +335 -18
- package/kit/runtime.js +1638 -125
- package/kit/state.js +10 -1
- package/kit/tables.js +130 -43
- package/kit/ui.js +79 -35
- package/package.json +2 -1
- package/packs/gallery/card/component.json +14 -0
- package/packs/gallery/card/example.html +10 -0
- package/packs/gallery/card/preview.svg +1 -0
- package/packs/gallery/card/style.css +6 -0
- package/packs/gallery/card/template.html +6 -0
- package/packs/gallery/card/writing.md +14 -0
- package/packs/gallery/chart-card/component.json +15 -0
- package/packs/gallery/chart-card/example.html +14 -0
- package/packs/gallery/chart-card/kit.js +130 -0
- package/packs/gallery/chart-card/preview.svg +1 -0
- package/packs/gallery/chart-card/style.css +42 -0
- package/packs/gallery/chart-card/template.html +13 -0
- package/packs/gallery/chart-card/writing.md +20 -0
- package/packs/gallery/chart-view/component.json +20 -0
- package/packs/gallery/chart-view/example.html +13 -0
- package/packs/gallery/chart-view/kit.js +355 -0
- package/packs/gallery/chart-view/preview.svg +1 -0
- package/packs/gallery/chart-view/style.css +94 -0
- package/packs/gallery/chart-view/template.html +3 -0
- package/packs/gallery/chart-view/writing.md +22 -0
- package/packs/gallery/countdown/component.json +18 -0
- package/packs/gallery/countdown/example.html +3 -0
- package/packs/gallery/countdown/kit.js +216 -0
- package/packs/gallery/countdown/preview.svg +1 -0
- package/packs/gallery/countdown/style.css +118 -0
- package/packs/gallery/countdown/template.html +3 -0
- package/packs/gallery/countdown/writing.md +22 -0
- package/packs/gallery/habit/component.json +14 -0
- package/packs/gallery/habit/example.html +3 -0
- package/packs/gallery/habit/kit.js +334 -0
- package/packs/gallery/habit/preview.svg +1 -0
- package/packs/gallery/habit/style.css +174 -0
- package/packs/gallery/habit/template.html +3 -0
- package/packs/gallery/habit/writing.md +21 -0
- package/packs/gallery/heatmap/component.json +18 -0
- package/packs/gallery/heatmap/example.html +3 -0
- package/packs/gallery/heatmap/kit.js +513 -0
- package/packs/gallery/heatmap/preview.svg +1 -0
- package/packs/gallery/heatmap/style.css +189 -0
- package/packs/gallery/heatmap/template.html +3 -0
- package/packs/gallery/heatmap/writing.md +21 -0
- package/packs/gallery/kanban/component.json +18 -0
- package/packs/gallery/kanban/example.html +12 -0
- package/packs/gallery/kanban/kit.js +540 -0
- package/packs/gallery/kanban/preview.svg +1 -0
- package/packs/gallery/kanban/style.css +212 -0
- package/packs/gallery/kanban/template.html +3 -0
- package/packs/gallery/kanban/writing.md +20 -0
- package/packs/gallery/metric/component.json +22 -0
- package/packs/gallery/metric/example.html +15 -0
- package/packs/gallery/metric/kit.js +248 -0
- package/packs/gallery/metric/preview.svg +1 -0
- package/packs/gallery/metric/style.css +92 -0
- package/packs/gallery/metric/template.html +3 -0
- package/packs/gallery/metric/writing.md +23 -0
- package/packs/gallery/note/component.json +14 -0
- package/packs/gallery/note/example.html +3 -0
- package/packs/gallery/note/kit.js +129 -0
- package/packs/gallery/note/preview.svg +1 -0
- package/packs/gallery/note/style.css +97 -0
- package/packs/gallery/note/template.html +3 -0
- package/packs/gallery/note/writing.md +18 -0
- package/packs/gallery/pack.json +8 -0
- package/packs/gallery/recent/component.json +13 -0
- package/packs/gallery/recent/example.html +1 -0
- package/packs/gallery/recent/kit.js +293 -0
- package/packs/gallery/recent/preview.svg +1 -0
- package/packs/gallery/recent/style.css +85 -0
- package/packs/gallery/recent/template.html +1 -0
- package/packs/gallery/recent/writing.md +15 -0
- package/packs/gallery/static/component.json +13 -0
- package/packs/gallery/static/example.html +11 -0
- package/packs/gallery/static/preview.svg +1 -0
- package/packs/gallery/static/style.css +171 -0
- package/packs/gallery/static/template.html +1 -0
- package/packs/gallery/static/writing.md +18 -0
- package/packs/gallery/stats/component.json +13 -0
- package/packs/gallery/stats/example.html +1 -0
- package/packs/gallery/stats/kit.js +347 -0
- package/packs/gallery/stats/preview.svg +1 -0
- package/packs/gallery/stats/style.css +73 -0
- package/packs/gallery/stats/template.html +1 -0
- package/packs/gallery/stats/writing.md +11 -0
- package/packs/gallery/today/component.json +12 -0
- package/packs/gallery/today/example.html +1 -0
- package/packs/gallery/today/kit.js +220 -0
- package/packs/gallery/today/preview.svg +1 -0
- package/packs/gallery/today/style.css +149 -0
- package/packs/gallery/today/template.html +1 -0
- package/packs/gallery/today/writing.md +14 -0
- package/packs/gallery/todos/component.json +19 -0
- package/packs/gallery/todos/example.html +3 -0
- package/packs/gallery/todos/kit.js +489 -0
- package/packs/gallery/todos/preview.svg +1 -0
- package/packs/gallery/todos/style.css +214 -0
- package/packs/gallery/todos/template.html +3 -0
- package/packs/gallery/todos/writing.md +30 -0
- package/packs/gallery/weather/component.json +19 -0
- package/packs/gallery/weather/example.html +3 -0
- package/packs/gallery/weather/kit.js +387 -0
- package/packs/gallery/weather/preview.svg +1 -0
- package/packs/gallery/weather/style.css +140 -0
- package/packs/gallery/weather/template.html +3 -0
- package/packs/gallery/weather/writing.md +26 -0
- 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/pack.json +8 -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 +12 -10
- package/sdk/doc-kit.d.ts +396 -5
- package/sdk/doc-state.d.ts +2 -0
- package/skill/SKILL.md +56 -23
- package/skill/references/block-script.md +140 -0
- package/skill/references/building-blocks.md +257 -0
- package/skill/references/extensions.md +59 -0
- package/skill/references/pages-and-data.md +225 -0
- package/skill/references/writing-html.md +8 -4
- package/skill/sdk/doc-kit.d.ts +396 -5
- package/skill/sdk/doc-state.d.ts +2 -0
- package/web/assets/index-BM_K-Auu.css +1 -0
- package/web/assets/index-DRslOBiv.js +15 -0
- package/web/index.html +2 -2
- package/dist/chunk-3HIRPVLU.mjs +0 -2
- package/dist/chunk-5MHMDLZD.mjs +0 -4
- package/dist/chunk-64K7IPLO.mjs +0 -255
- package/dist/chunk-6QXGZM36.mjs +0 -8
- package/dist/chunk-6RGJOMJP.mjs +0 -16
- package/dist/chunk-AJ73XJF3.mjs +0 -2
- package/dist/chunk-AUZGSZJH.mjs +0 -13
- package/dist/chunk-BFE45AKL.mjs +0 -2
- package/dist/chunk-BXO2ZY5P.mjs +0 -2
- package/dist/chunk-CEH4MRCC.mjs +0 -2
- package/dist/chunk-CY2FNNAB.mjs +0 -16
- package/dist/chunk-DFUDV4LX.mjs +0 -2
- package/dist/chunk-DKXG4XDG.mjs +0 -2
- package/dist/chunk-EZQ34M3F.mjs +0 -124
- package/dist/chunk-EZY3WC6Z.mjs +0 -15
- package/dist/chunk-FUKQNOQS.mjs +0 -9
- package/dist/chunk-JPOSLGOC.mjs +0 -6
- package/dist/chunk-JVHHHGEA.mjs +0 -3
- package/dist/chunk-L3QKH4OK.mjs +0 -26
- package/dist/chunk-LAE5NQ4B.mjs +0 -2
- package/dist/chunk-LEF6DRCK.mjs +0 -59
- package/dist/chunk-NO5NJCHO.mjs +0 -2
- package/dist/chunk-NQPVAU63.mjs +0 -2
- package/dist/chunk-NSKUTQ5J.mjs +0 -5
- package/dist/chunk-O35FIXRB.mjs +0 -2
- package/dist/chunk-P2CAZPDP.mjs +0 -3
- package/dist/chunk-PL6GHLGD.mjs +0 -15
- package/dist/chunk-QJIR2BLP.mjs +0 -6
- package/dist/chunk-QUPLVPB2.mjs +0 -7
- package/dist/chunk-R7BNENLY.mjs +0 -4
- package/dist/chunk-S6O4UHLC.mjs +0 -2
- package/dist/chunk-SGJWQKEI.mjs +0 -2
- package/dist/chunk-TX6JH7TR.mjs +0 -2
- package/dist/chunk-TYK6VNAB.mjs +0 -10
- package/dist/chunk-UALRNVSE.mjs +0 -4
- package/dist/chunk-V3HXVQEM.mjs +0 -2
- package/dist/chunk-V5VXUJKG.mjs +0 -2
- package/dist/chunk-VBXMADMP.mjs +0 -2
- package/dist/chunk-VNV55RUA.mjs +0 -2
- package/dist/chunk-VPD2PS35.mjs +0 -2
- package/dist/chunk-W4VKN3GF.mjs +0 -7
- package/dist/chunk-W5BXXPDJ.mjs +0 -3
- package/dist/chunk-XUNHRCOG.mjs +0 -6
- package/skill/references/building-components.md +0 -367
- package/web/assets/index-Bh74DfsV.css +0 -1
- package/web/assets/index-UW0RyFFt.js +0 -13
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { iconKind } from "./emoji/icon-value.mjs";
|
|
2
|
+
|
|
3
|
+
// A web link of the sidebar (docs/workbench.md "Web links"): a folder at the workspace's root, named by
|
|
4
|
+
// a slug from the pool documents and custom pages share, holding link.json alone:
|
|
5
|
+
// { "url": "https://open-meteo.com/en/docs", "title": "天气接口文档", "icon": "🌤️", "siteIcon": "data:image/png;base64,…" }
|
|
6
|
+
// `url` an http or https address of 2048 characters at most, `title` 1 to 200 characters, `icon` an
|
|
7
|
+
// emoji or an icon of the 图标 tab, the user's (no picture: a link keeps no files), `siteIcon` the site's
|
|
8
|
+
// own icon, fetched once as the link is added (src/editor/unfurl.mjs siteIcon) and kept in the file as a
|
|
9
|
+
// data: address (PNG, ICO, SVG, JPEG, WebP or GIF, 16 KB at most: SITE_ICON_BYTES), shown when the
|
|
10
|
+
// user chose none (the grid globe when neither is there). The workbench shows it in its
|
|
11
|
+
// content area in a sandboxed frame (features/link/LinkView.tsx), never as a document: no session, no
|
|
12
|
+
// export, no check, no search of its text. A folder holding doc.html (or report.html) is a document
|
|
13
|
+
// whatever else it holds. What is read and checked here is the same on the server, in the gallery
|
|
14
|
+
// command and in the workbench; reading and writing the file is ./workspace.mjs's (Node's).
|
|
15
|
+
|
|
16
|
+
/** A link's file in its folder. */
|
|
17
|
+
export const LINK_FILE = "link.json";
|
|
18
|
+
export const LINK_LIMITS = { url: 2048, title: 200 };
|
|
19
|
+
/** The largest site icon a link keeps, in bytes (its data: address holds them in base64). */
|
|
20
|
+
export const SITE_ICON_BYTES = 16 * 1024;
|
|
21
|
+
/** The media types of a site icon a link keeps. */
|
|
22
|
+
export const SITE_ICON_TYPES = ["image/png", "image/x-icon", "image/vnd.microsoft.icon", "image/svg+xml", "image/jpeg", "image/webp", "image/gif"];
|
|
23
|
+
|
|
24
|
+
/** Whether `value` is a site icon a link keeps: a base64 data: address of one of SITE_ICON_TYPES, SITE_ICON_BYTES at most. */
|
|
25
|
+
export function isSiteIcon(value) {
|
|
26
|
+
if (typeof value !== "string") return false;
|
|
27
|
+
const match = /^data:([a-z+./-]+);base64,([A-Za-z0-9+/]+={0,2})$/.exec(value);
|
|
28
|
+
if (!match || !SITE_ICON_TYPES.includes(match[1])) return false;
|
|
29
|
+
const bytes = (match[2].length * 3) / 4 - (match[2].endsWith("==") ? 2 : match[2].endsWith("=") ? 1 : 0);
|
|
30
|
+
return bytes > 0 && bytes <= SITE_ICON_BYTES;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The sandbox of a web page's frame, the embed block's (Notion's): scripts, its own origin (a page of
|
|
35
|
+
* another site, which can then not reach the workbench's), forms, popups that leave the sandbox, a
|
|
36
|
+
* top navigation only by the reader's own click.
|
|
37
|
+
*/
|
|
38
|
+
export const WEB_SANDBOX = "allow-scripts allow-popups allow-top-navigation-by-user-activation allow-forms allow-same-origin allow-storage-access-by-user-activation allow-popups-to-escape-sandbox";
|
|
39
|
+
|
|
40
|
+
/** Login/error headings are not document names (also used for browser-provided clipboard titles). */
|
|
41
|
+
export function loginTitle(title) {
|
|
42
|
+
return /(?:^|[|·—–-]\s*)(?:钉钉)?统一身份认证\s*$|^(?:(?:登录|登入|sign[ -]?in|log[ -]?in)(?:$|\s*(?:[|·—–-]|to\b))|(?:401|403)(?:\s|$)|unauthorized|access denied|forbidden|无权访问|访问受限)|(?:[|·—–-]\s*)(?:登录|登入|sign[ -]?in|log[ -]?in)\s*$/i.test(title);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tidy = (value) => (typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "");
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Address `value` as a link takes it: { url } (the URL's own spelling, a scheme written in) or
|
|
49
|
+
* { refused, message } — "url" (no address), "scheme" (not http or https: javascript:, file:, data:…),
|
|
50
|
+
* "long" (over 2048 characters). An address written without a scheme (example.com/a) is taken as https.
|
|
51
|
+
*/
|
|
52
|
+
export function linkAddress(value) {
|
|
53
|
+
const written = tidy(value);
|
|
54
|
+
if (!written) return { refused: "url", message: "网址不能是空的" };
|
|
55
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:/i.test(written) ? written : `https://${written}`;
|
|
56
|
+
let url;
|
|
57
|
+
try {
|
|
58
|
+
url = new URL(withScheme);
|
|
59
|
+
} catch {
|
|
60
|
+
return { refused: "url", message: "这不是一个网址" };
|
|
61
|
+
}
|
|
62
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return { refused: "scheme", message: "只能链接 http 或 https 的网址" };
|
|
63
|
+
if (!url.hostname) return { refused: "url", message: "这不是一个网址" };
|
|
64
|
+
if (url.href.length > LINK_LIMITS.url) return { refused: "long", message: "网址太长了(最多 2048 个字符)" };
|
|
65
|
+
return { url: url.href };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether `icon` is one a link takes: an emoji or an icon of the 图标 tab. */
|
|
69
|
+
export const linkIcon = (icon) => ["emoji", "glyph"].includes(iconKind(icon));
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A link's file as it is to be written: { value: { url, title, icon?, siteIcon? } } or { refused, message }
|
|
73
|
+
* (linkAddress's, "title": empty or over 200 characters, "icon": no emoji nor icon of the 图标 tab). A site
|
|
74
|
+
* icon that is none (isSiteIcon) is left out, never refused: the add never fails for it.
|
|
75
|
+
*/
|
|
76
|
+
export function linkValue({ url, title, icon = null, siteIcon = null }) {
|
|
77
|
+
const address = linkAddress(url);
|
|
78
|
+
if (address.refused) return address;
|
|
79
|
+
const name = tidy(title);
|
|
80
|
+
if (!name || name.length > LINK_LIMITS.title) return { refused: "title", message: "名称不能是空的,也不能超过 200 个字" };
|
|
81
|
+
if (icon !== null && icon !== undefined && icon !== "" && !linkIcon(icon)) return { refused: "icon", message: "图标只能是表情或图标页签里的图标" };
|
|
82
|
+
return { value: { url: address.url, title: name, ...(icon ? { icon } : {}), ...(isSiteIcon(siteIcon) ? { siteIcon } : {}) } };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What link.json's text says, as a link: { url, title, icon?, siteIcon? }, or null for a file that is no link (one written by hand wrong). */
|
|
86
|
+
export function readLinkText(text) {
|
|
87
|
+
let value;
|
|
88
|
+
try {
|
|
89
|
+
value = JSON.parse(String(text).replace(/^/, ""));
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
94
|
+
const made = linkValue({ url: value.url, title: value.title, icon: linkIcon(value.icon) ? value.icon : null, siteIcon: value.siteIcon });
|
|
95
|
+
return made.value ?? null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether address `url` is this Gallery's own (`port` its server's): the workbench's host by any of
|
|
100
|
+
* its names (127.0.0.1, localhost, ::1) and port. A link there is refused: framed with the sandbox's
|
|
101
|
+
* allow-same-origin and scripts, the workbench's own page could reach the workbench.
|
|
102
|
+
*/
|
|
103
|
+
export function ownAddress(url, port) {
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = new URL(url);
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
111
|
+
const given = parsed.port || (parsed.protocol === "https:" ? "443" : "80");
|
|
112
|
+
return ["127.0.0.1", "localhost", "::1", "0.0.0.0"].includes(host) && String(port) === given;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A slug made of words: their Latin letters and digits, lowercase, kebab-case, 60 characters at most
|
|
117
|
+
* (cut where a word ends); "" when none of them is a word of three letters (a title in Chinese, or one
|
|
118
|
+
* whose Latin is a version alone: "… v2.2 …"). gallery import's, and a link's.
|
|
119
|
+
*/
|
|
120
|
+
export const kebab = (text) => {
|
|
121
|
+
const words = String(text ?? "").normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
122
|
+
if (!/[a-z]{3}/.test(words)) return "";
|
|
123
|
+
return words.length <= 60 ? words : words.slice(0, 61).replace(/-[^-]*$/, "") || words.slice(0, 60);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The stem of a new link's slug (a number added after it when it is taken: the workspace's freeSlug):
|
|
128
|
+
* its title's Latin words, else its site's name (open-meteo of open-meteo.com, www passed over), else "link".
|
|
129
|
+
*/
|
|
130
|
+
export function linkStem(title, url) {
|
|
131
|
+
const words = kebab(title);
|
|
132
|
+
if (words) return words;
|
|
133
|
+
let host = "";
|
|
134
|
+
try {
|
|
135
|
+
host = new URL(url).hostname.replace(/^www\./, "");
|
|
136
|
+
} catch {}
|
|
137
|
+
const site = kebab(host.split(".")[0] ?? "");
|
|
138
|
+
return site || "link";
|
|
139
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { iconKind } from "./emoji/icon-value.mjs";
|
|
4
|
+
import { isWorkspace, readRegistry, workspaceId, workspaceKey } from "./workspace-registry.mjs";
|
|
5
|
+
import { defaultWorkspace, documentFiles, isSlugName } from "./workspace.mjs";
|
|
6
|
+
|
|
7
|
+
// The other workspaces a workspace's sidebar shows a column of (notes/plans/workspaces-links-plan-2026-11-08.md
|
|
8
|
+
// §7.2; docs/workbench.md "Columns"): every workspace of the machine's registry (./workspace-registry.mjs) but
|
|
9
|
+
// this one, a clash of ids (two registered folders sharing one: never guessed) and one whose folder is no
|
|
10
|
+
// workspace now (an external drive away), in this order: the default workspace (./workspace.mjs
|
|
11
|
+
// defaultWorkspace, GALLERY_DEFAULT_WORKSPACE honoured) first, the rest as they were registered. The outline
|
|
12
|
+
// (./outline.mjs) gives each a column the first time it reads it (a "whole" one, all its documents, laid out
|
|
13
|
+
// as its own sidebar has them) and the place of its documents no column holds; the user's order of the
|
|
14
|
+
// columns is the outline's after that.
|
|
15
|
+
//
|
|
16
|
+
// What a workspace's sidebar is read with (snapshotOf): its documents in its own sidebar's order, the document
|
|
17
|
+
// each is under there (a web link or another workspace's draft passed over upward), and what its doc.json
|
|
18
|
+
// says (title, description, an icon of the emoji or 图标 kind: no picture of another workspace's folder). It is
|
|
19
|
+
// read from its files, never written, and kept SNAPSHOT_KEPT ms at most, so that the reads of a burst (a
|
|
20
|
+
// command and the workbench's read after it, the list of 添加其他 Gallery 项目…) read each folder once while a
|
|
21
|
+
// change there shows at the next read after that; the server drops one at once when that workspace's own
|
|
22
|
+
// server tells of a change (src/workbench/service.mjs, through ./elsewhere.mjs). Reads files only.
|
|
23
|
+
|
|
24
|
+
/** How long a snapshot of another workspace is believed (ms). */
|
|
25
|
+
export const SNAPSHOT_KEPT = 2000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The registered workspaces by their ids: a Map of id to the roots registered under it (two for one id: a clash
|
|
29
|
+
* of the hash's six digits, which names neither). `registry`: readRegistry()'s.
|
|
30
|
+
*/
|
|
31
|
+
export function idsOf(registry = readRegistry()) {
|
|
32
|
+
const ids = new Map();
|
|
33
|
+
for (const { root } of registry.workspaces) {
|
|
34
|
+
const id = workspaceId(root);
|
|
35
|
+
const roots = ids.get(id) ?? [];
|
|
36
|
+
if (!roots.some((each) => workspaceKey(each) === workspaceKey(root))) roots.push(root);
|
|
37
|
+
ids.set(id, roots);
|
|
38
|
+
}
|
|
39
|
+
return ids;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The other workspaces whose columns the sidebar of workspace `root` shows: [{ id, root, isDefault }], the
|
|
44
|
+
* default workspace first (when it is registered and not `root`), then the registry's order.
|
|
45
|
+
*/
|
|
46
|
+
export function shownWorkspaces(root, { registry = readRegistry(), env = process.env } = {}) {
|
|
47
|
+
const own = workspaceKey(root);
|
|
48
|
+
const home = workspaceKey(defaultWorkspace({ env }));
|
|
49
|
+
const shown = [];
|
|
50
|
+
for (const [id, roots] of idsOf(registry)) {
|
|
51
|
+
if (roots.length !== 1) continue;
|
|
52
|
+
const [each] = roots;
|
|
53
|
+
if (workspaceKey(each) === own || !isWorkspace(each)) continue;
|
|
54
|
+
shown.push({ id, root: each, isDefault: workspaceKey(each) === home });
|
|
55
|
+
}
|
|
56
|
+
return [...shown.filter(({ isDefault }) => isDefault), ...shown.filter(({ isDefault }) => !isDefault)];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const oneLine = (value) => (typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "");
|
|
60
|
+
/** A draft's page icon as another workspace's sidebar may show it: an emoji or a glyph of the 图标 tab. */
|
|
61
|
+
export const shownIcon = (value) => (typeof value === "string" && ["emoji", "glyph"].includes(iconKind(value)) ? value : null);
|
|
62
|
+
|
|
63
|
+
const kept = new Map(); // a workspace's key -> { at, value: Promise }
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What the sidebar reads of workspace `root` (another's): { documents: Set of slugs, order: [slug] (its own
|
|
67
|
+
* sidebar's order, column after column, each entry followed by those under it), parentOf: Map of slug to the
|
|
68
|
+
* document it is under there (null at a column's top), facts: Map of slug to { title, description, icon, path
|
|
69
|
+
* (its doc.html, an old one's report.html) } }. `readOwn(root)`: its outline as its own sidebar has it (no
|
|
70
|
+
* column of any other workspace: ./outline.mjs readOutline with others false). Kept SNAPSHOT_KEPT ms.
|
|
71
|
+
*/
|
|
72
|
+
export function snapshotOf(root, readOwn, { now = Date.now() } = {}) {
|
|
73
|
+
const key = workspaceKey(root);
|
|
74
|
+
const known = kept.get(key);
|
|
75
|
+
if (known && now - known.at < SNAPSHOT_KEPT) return known.value;
|
|
76
|
+
const value = read(root, readOwn);
|
|
77
|
+
kept.set(key, { at: now, value });
|
|
78
|
+
value.catch(() => kept.delete(key));
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Forget what was read of workspace `root` (none: of every workspace), so that the next read reads it again. */
|
|
83
|
+
export function forgetSnapshot(root = null) {
|
|
84
|
+
if (root === null) kept.clear();
|
|
85
|
+
else kept.delete(workspaceKey(root));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function read(root, readOwn) {
|
|
89
|
+
const own = await readOwn(root);
|
|
90
|
+
const documents = new Set();
|
|
91
|
+
const order = [];
|
|
92
|
+
const parentOf = new Map();
|
|
93
|
+
// Its documents alone: its web links, the drafts of others its tree shows and a folder named by no slug are none.
|
|
94
|
+
const isDocument = (key) => !own.links.includes(key) && !key.startsWith("~") && isSlugName(key);
|
|
95
|
+
const parentIn = (child) => {
|
|
96
|
+
for (const [key, list] of Object.entries(own.children)) if (list.includes(child)) return key;
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
const above = (slug) => {
|
|
100
|
+
let up = parentIn(slug);
|
|
101
|
+
while (up !== null && !(up === "" || up.startsWith(":")) && !isDocument(up)) up = parentIn(up);
|
|
102
|
+
return up === null || up === "" || up.startsWith(":") ? null : up;
|
|
103
|
+
};
|
|
104
|
+
const walk = (key) => (own.children[key] ?? []).forEach((slug) => {
|
|
105
|
+
if (isDocument(slug) && !documents.has(slug)) {
|
|
106
|
+
documents.add(slug);
|
|
107
|
+
order.push(slug);
|
|
108
|
+
parentOf.set(slug, above(slug));
|
|
109
|
+
}
|
|
110
|
+
walk(slug);
|
|
111
|
+
});
|
|
112
|
+
for (const { id } of own.groups) walk(id === "docs" ? "" : `:${id}`);
|
|
113
|
+
const facts = new Map(await Promise.all(order.map(async (slug) => {
|
|
114
|
+
const files = documentFiles(path.join(root, slug));
|
|
115
|
+
const metadata = await fs.readFile(files.json, "utf8").then((text) => JSON.parse(text.replace(/^/, "")), () => null);
|
|
116
|
+
const value = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {};
|
|
117
|
+
return [slug, { title: oneLine(value.title) || slug, description: oneLine(value.description), icon: shownIcon(value.icon), path: files.html }];
|
|
118
|
+
})));
|
|
119
|
+
return { documents, order, parentOf, facts };
|
|
120
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
6
|
+
import { writeAtomic } from "./files.mjs";
|
|
7
|
+
import { objectAt, ownLine } from "./pages.mjs";
|
|
8
|
+
import { documentsFolder, layoutOf, markerOf, WORKSPACE_MARK } from "./workspace.mjs";
|
|
9
|
+
|
|
10
|
+
// What a workspace is called where Gallery names it beside others (gallery workspaces, gallery list
|
|
11
|
+
// --all, and the sidebar's column of another workspace: notes/plans/workspaces-links-plan-2026-11-08.md
|
|
12
|
+
// §0): its own name, `name` in its gallery.json (gallery init --name, gallery workspaces --name, the
|
|
13
|
+
// user in the sidebar); else its folder's name, climbing past a name that says nothing of the project
|
|
14
|
+
// (GENERIC_NAMES: D:\proj\outputs\gallery is "proj") but never to the user's home, their Documents
|
|
15
|
+
// folder or a drive's root (~/Documents/Gallery stays "Gallery"; a workspace that is a drive's root
|
|
16
|
+
// is called by the drive, "E:"). Two of the same name are told apart by the folder above the one
|
|
17
|
+
// named, which the sidebar writes after the name in grey (workspaceLabels), and where that is called
|
|
18
|
+
// alike too, by the workspace's own path; the path is otherwise shown on hover and in the menus, never
|
|
19
|
+
// as the name, and no workspace is numbered. Reads files only, but writeWorkspaceName.
|
|
20
|
+
|
|
21
|
+
/** The folders' names that say nothing of a project, passed over upward (upper and lower case alike). */
|
|
22
|
+
export const GENERIC_NAMES = ["gallery", "docs", "outputs", "reports", "文档"];
|
|
23
|
+
/** The longest name a workspace is given, in characters. */
|
|
24
|
+
export const NAME_LIMIT = 80;
|
|
25
|
+
|
|
26
|
+
const isGeneric = (name) => GENERIC_NAMES.includes(name.toLowerCase());
|
|
27
|
+
const keyOf = (folder, platform) => (platform === "win32" ? path.resolve(folder).toLowerCase() : path.resolve(folder));
|
|
28
|
+
const oneLine = (value) => (typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "");
|
|
29
|
+
// A folder's name; a drive's root, which has none, by the drive ("E:", "/").
|
|
30
|
+
const folderName = (folder) => path.basename(folder) || folder.replace(/[\\/]+$/, "") || folder;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* What a name given to a workspace is wrong in, or null: empty, longer than NAME_LIMIT characters, or
|
|
34
|
+
* holding a control character (a line break among them).
|
|
35
|
+
*/
|
|
36
|
+
export function nameProblem(name) {
|
|
37
|
+
if (typeof name !== "string" || !name.trim()) return "A workspace's name holds a word at least.";
|
|
38
|
+
if (/[\u0000-\u001f\u007f]/.test(name)) return "A workspace's name is one line, with no control character.";
|
|
39
|
+
if ([...name.trim()].length > NAME_LIMIT) return `A workspace's name is ${NAME_LIMIT} characters at most.`;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The workspace's own name at `root` (gallery.json's `name`, on one line), or null: none, or a gallery.json that is not ours or does not read. */
|
|
44
|
+
export function ownName(root, { readText = (file) => readFileSync(file, "utf8") } = {}) {
|
|
45
|
+
let text;
|
|
46
|
+
try {
|
|
47
|
+
text = readText(layoutOf(root).marker);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const { value, ours } = markerOf(text);
|
|
52
|
+
const name = ours ? oneLine(value?.name) : "";
|
|
53
|
+
return name ? [...name].slice(0, NAME_LIMIT).join("") : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The folders no name climbs to: the user's home and their Documents folder (each workspace's name is found against these). */
|
|
57
|
+
export function nameStops({ home = os.homedir(), documents = documentsFolder({ home }) } = {}) {
|
|
58
|
+
return [home, documents].filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The folder whose name a workspace at `root` goes by when it has none of its own: `root`, or the
|
|
63
|
+
* nearest folder above it whose name is not generic, climbing no higher than below one of `stops` and
|
|
64
|
+
* never to a drive's root (a folder there keeps its name, generic or not).
|
|
65
|
+
*/
|
|
66
|
+
export function namedFolder(root, { stops = [], platform = process.platform } = {}) {
|
|
67
|
+
const stopped = new Set(stops.map((stop) => keyOf(stop, platform)));
|
|
68
|
+
let folder = path.resolve(root);
|
|
69
|
+
for (;;) {
|
|
70
|
+
const parent = path.dirname(folder);
|
|
71
|
+
const atRoot = parent === folder || path.dirname(parent) === parent;
|
|
72
|
+
if (!isGeneric(path.basename(folder)) || atRoot || stopped.has(keyOf(parent, platform))) return folder;
|
|
73
|
+
folder = parent;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What the workspace at `root` is called: { name, own: whether it is gallery.json's name (else its
|
|
79
|
+
* folder's), folder: the folder named (the workspace's own for a name of its own), above: that
|
|
80
|
+
* folder's parent's name (what tells two of the same name apart), parent: that parent's path }.
|
|
81
|
+
*/
|
|
82
|
+
export function workspaceName(root, { stops = [], platform = process.platform, readText } = {}) {
|
|
83
|
+
const own = ownName(root, readText ? { readText } : undefined);
|
|
84
|
+
const folder = own ? path.resolve(root) : namedFolder(root, { stops, platform });
|
|
85
|
+
const parent = path.dirname(folder);
|
|
86
|
+
return { name: own ?? folderName(folder), own: Boolean(own), folder, above: parent === folder ? null : path.basename(parent) || parent, parent };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The names of several workspaces as the sidebar and the commands write them side by side: each of
|
|
91
|
+
* `named` (workspaceName's, with its `root`, its path) as { root, name, own, above }, `above` null
|
|
92
|
+
* but where another has the same name (upper and lower case alike): then the name of the folder above
|
|
93
|
+
* the one named, and where another of them has that too (two workspaces side by side, or two in one
|
|
94
|
+
* project climbing to its folder), or there is none (a drive's root), the workspace's own path, which
|
|
95
|
+
* no other has.
|
|
96
|
+
*/
|
|
97
|
+
export function workspaceLabels(named) {
|
|
98
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
99
|
+
return named.map((entry) => {
|
|
100
|
+
const label = { root: entry.root, name: entry.name, own: entry.own, above: null };
|
|
101
|
+
const others = named.filter((other) => other !== entry && same(other.name, entry.name));
|
|
102
|
+
if (!others.length) return label;
|
|
103
|
+
const told = entry.above !== null && !others.some((other) => other.above !== null && same(other.above, entry.above));
|
|
104
|
+
return { ...label, above: told ? entry.above : entry.root };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* gallery.json at `root` with `name` as the workspace's name (namedIn: in place of the one it had,
|
|
110
|
+
* else right after what marks it; all else it holds kept, and the file's own layout with it), written
|
|
111
|
+
* at once; the workspace must be there and its gallery.json ours (ensureWorkspace first). Resolves to
|
|
112
|
+
* { name: as written, before: the name it had, or null }; throws an Error saying why when the name is
|
|
113
|
+
* wrong (nameProblem), the file is not ours or does not read, or it kept changing under the write.
|
|
114
|
+
*/
|
|
115
|
+
export async function writeWorkspaceName(root, name) {
|
|
116
|
+
const problem = nameProblem(name);
|
|
117
|
+
if (problem) throw new Error(problem);
|
|
118
|
+
const wanted = name.trim();
|
|
119
|
+
const file = layoutOf(root).marker;
|
|
120
|
+
for (let tries = 0; tries < 3; tries++) {
|
|
121
|
+
const text = await fs.readFile(file, "utf8");
|
|
122
|
+
const { value, ours, reads } = markerOf(text);
|
|
123
|
+
if (!ours || !value || typeof value !== "object" || Array.isArray(value)) throw new Error(reads ? `${file} is not Gallery's.` : `${file} does not read as JSON: mend it first.`);
|
|
124
|
+
const before = oneLine(value.name) || null;
|
|
125
|
+
if (value.name === wanted) return { name: wanted, before };
|
|
126
|
+
let next = null;
|
|
127
|
+
try {
|
|
128
|
+
next = namedIn(text, wanted);
|
|
129
|
+
} catch {}
|
|
130
|
+
// What the text says once written is checked: any doubt, the whole file written as JSON.stringify lays it out.
|
|
131
|
+
if (next === null || !isDeepStrictEqual(markerOf(next).value, { ...value, name: wanted })) {
|
|
132
|
+
const { gallery, version, name: _old, ...rest } = value;
|
|
133
|
+
next = `${JSON.stringify({ gallery: gallery ?? WORKSPACE_MARK.gallery, version: version ?? WORKSPACE_MARK.version, name: wanted, ...rest }, null, 2)}\n`;
|
|
134
|
+
}
|
|
135
|
+
if (await writeAtomic(file, next, { expect: text })) return { name: wanted, before };
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`${file} kept changing while its name was written: try again.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// gallery.json's text (`text`, Gallery's, a JSON object that reads) with `name` as its name, written
|
|
141
|
+
// into it as it stands, as ./pages.mjs writes pages.homeMade (its line ends, indentation, BOM, the order
|
|
142
|
+
// of its keys and what stands on one line kept): the name it had replaced in its place, else one added
|
|
143
|
+
// right after the mark (`gallery`, or `version` where that comes after it), laid out as that member is
|
|
144
|
+
// (on a line of its own at its indentation, or on its line after it, as tightly).
|
|
145
|
+
function namedIn(text, name) {
|
|
146
|
+
const top = objectAt(text, text.indexOf("{"));
|
|
147
|
+
const had = top.members.findLast(({ key }) => key === "name");
|
|
148
|
+
if (had) return `${text.slice(0, had.value)}${JSON.stringify(name)}${text.slice(had.end)}`;
|
|
149
|
+
const mark = top.members.filter(({ key }) => key === "gallery" || key === "version").at(-1);
|
|
150
|
+
if (!mark) return null;
|
|
151
|
+
const indent = ownLine(text, top.open, mark);
|
|
152
|
+
const gap = indent !== null ? (text.includes("\r\n") ? "\r\n" : "\n") + indent : /[ \t]*$/.exec(text.slice(0, mark.from))[0];
|
|
153
|
+
return `${text.slice(0, mark.end)},${gap}"name":${mark.colon}${JSON.stringify(name)}${text.slice(mark.end)}`;
|
|
154
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { CHART_KEY } from "./table-chart.mjs";
|
|
2
|
+
|
|
3
|
+
// What a block reads of the workspace (the spec custom-blocks-spec-2026-11-07.md §3, §4; docs/render.md
|
|
4
|
+
// "A custom block on a page", docs/workbench.md "The workspace's facts"): the kinds of
|
|
5
|
+
// context.workspace.query, how many items each gives, the options each takes, and where the kit asks for
|
|
6
|
+
// them, the one list the kit's runtime (render/kit/runtime/workspace.js), the server's facts
|
|
7
|
+
// (src/workbench/index.mjs) and the event stream's "data <kind>" (src/server/routes.mjs) go by; and how a
|
|
8
|
+
// block checks a to-do of another document (workspace.setTodo: the plan's 5.1), what an export embeds of
|
|
9
|
+
// the workspace (the spec §9). Pure: no DOM, no Node.
|
|
10
|
+
|
|
11
|
+
/** The kinds workspace.query answers: the workspace's facts, and `table`, a keyed table of a document (the plan's 5.2). */
|
|
12
|
+
export const QUERY_KINDS = Object.freeze(["todos", "recent", "stats", "activity", "table"]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The kinds of the workspace's facts a change may change, each said to the pages as "data <kind>":
|
|
16
|
+
* `table` whenever a document's text changed (any keyed table of it may have).
|
|
17
|
+
*/
|
|
18
|
+
export const FACT_KINDS = Object.freeze(["todos", "recent", "stats", "activity", "table"]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The codes workspace.query rejects with (the spec §3), never another: the server's own refusals of a query
|
|
22
|
+
* (kind, options, missing: no such document, or no table of that key in it, or one with merged cells; and
|
|
23
|
+
* unsupported, which no server of this version says) as it says them; offline where there is no server to ask
|
|
24
|
+
* (an export without that kind in its snapshot) or it does not answer; failed where it answered and could
|
|
25
|
+
* not (a read that failed, a request it refused, no JSON).
|
|
26
|
+
*/
|
|
27
|
+
export const QUERY_CODES = Object.freeze(["kind", "options", "unsupported", "missing", "offline", "failed"]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How many items a query gives where its options say none, and the most it gives: to-dos, documents edited
|
|
31
|
+
* last, days of the record (as many as it keeps, src/workbench/activity.mjs KEEP_DAYS: the 编辑热力图's 53
|
|
32
|
+
* weeks), rows of a table.
|
|
33
|
+
*/
|
|
34
|
+
export const QUERY_LIMITS = Object.freeze({ todos: { default: 200, most: 1000 }, recent: { default: 10, most: 50 }, activity: { default: 365, most: 371 }, table: { default: 200, most: 1000 } });
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What an export embeds of the workspace for a document whose blocks read it (the spec §9: its snapshot's
|
|
38
|
+
* kinds beside `net`), each kind's whole answer to so many at most: to-dos (done or not), documents edited
|
|
39
|
+
* last, days of the record, and of the tables its instances asked for (the last `tables` of them), rows each.
|
|
40
|
+
*/
|
|
41
|
+
export const SNAPSHOT_LIMITS = Object.freeze({ todos: 200, recent: 50, activity: 371, tables: 20, rows: 1000 });
|
|
42
|
+
|
|
43
|
+
/** Where a page asks for its to-dos to be checked (POST { slug, items: [{ ref, done }] }, the token's: the workspace's service). */
|
|
44
|
+
export const TODO_ROUTE = "/__gallery/workspace/todo";
|
|
45
|
+
|
|
46
|
+
/** What workspace.setTodo asks whoever shows the page (cancelable, on the window: { ask: { slug, ref, done }, answer(promise) }). */
|
|
47
|
+
export const TODO_EVENT = "doc-todo-set";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The codes workspace.setTodo rejects with, never another: ref (not a to-do's ref as query gave it, or
|
|
51
|
+
* `done` no true or false), changed (the item is not as the ref read it: its words changed, or it is gone:
|
|
52
|
+
* the conflict, which the block reads again after), ambiguous (no block id, the file changed since, and
|
|
53
|
+
* more than one item of its words under its headings), locked (the item is there as read, in a place the
|
|
54
|
+
* document's own page cannot check: a list in a table cell, a block of the document's own), missing (no such document), busy (the file kept
|
|
55
|
+
* changing, or stayed half written), offline (nothing on the page writes the workspace: an exported file,
|
|
56
|
+
* the 演示视图, a page on its own without a token; or the server does not answer), failed (anything else).
|
|
57
|
+
*/
|
|
58
|
+
export const TODO_CODES = Object.freeze(["ref", "changed", "ambiguous", "locked", "missing", "busy", "offline", "failed"]);
|
|
59
|
+
|
|
60
|
+
/** How many to-dos one write of a document carries at most (the page merges what waits into one). */
|
|
61
|
+
export const TODO_MOST = 50;
|
|
62
|
+
|
|
63
|
+
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
64
|
+
const BLOCK = /^[A-Za-z0-9_-]{1,64}$/;
|
|
65
|
+
const VERSION = /^[a-f0-9]{16}$/;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `ref` as a write carries it, when it is a to-do's ref as workspace.query("todos") gives it ({ slug,
|
|
69
|
+
* block, text, path, nth, version }: `version` the hash of the file the facts read it from, which may be
|
|
70
|
+
* missing), or null.
|
|
71
|
+
*/
|
|
72
|
+
export function todoRef(ref) {
|
|
73
|
+
if (!ref || typeof ref !== "object" || Array.isArray(ref)) return null;
|
|
74
|
+
const { slug, block = null, text, path, nth, version = null } = ref;
|
|
75
|
+
if (typeof slug !== "string" || !SLUG.test(slug) || (block !== null && (typeof block !== "string" || !BLOCK.test(block)))) return null;
|
|
76
|
+
if (typeof text !== "string" || text.length > 4000 || !Array.isArray(path) || path.length > 6 || !path.every((step) => typeof step === "string" && step.length <= 4000)) return null;
|
|
77
|
+
if (!Number.isInteger(nth) || nth < 0 || (version !== null && (typeof version !== "string" || !VERSION.test(version)))) return null;
|
|
78
|
+
return { slug, block, text, path: [...path], nth, version };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Where a query is asked (GET, ?kind=<kind>&options=<JSON>): the workspace's service. */
|
|
82
|
+
export const QUERY_ROUTE = "/__gallery/workspace/query";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The global a page the server serves holds QUERY_ROUTE in (src/server/routes.mjs, a script of the
|
|
86
|
+
* server's that no export keeps), which the kit's runtime asks: none there, the workspace is offline.
|
|
87
|
+
*/
|
|
88
|
+
export const WORKSPACE_GLOBAL = "__galleryWorkspace";
|
|
89
|
+
|
|
90
|
+
/** What workspace.open asks whoever shows the page to open (a cancelable event on the window, detail { slug, block } or { page }). */
|
|
91
|
+
export const OPEN_EVENT = "doc-open-request";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The options of a query of `kind` as they are used: { options } (todos: done, slugs, limit; recent:
|
|
95
|
+
* limit; activity: days; every count held to its most), or { problem } (what is wrong, in English).
|
|
96
|
+
*/
|
|
97
|
+
export function queryOptions(kind, options) {
|
|
98
|
+
const given = options ?? {};
|
|
99
|
+
if (typeof given !== "object" || Array.isArray(given)) return { problem: "its options are an object" };
|
|
100
|
+
const count = (name, { default: fallback, most }) => {
|
|
101
|
+
const value = given[name];
|
|
102
|
+
if (value === undefined) return fallback;
|
|
103
|
+
if (!Number.isInteger(value) || value < 1) throw new Error(`${name} is a whole number from 1`);
|
|
104
|
+
return Math.min(value, most);
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
if (kind === "todos") {
|
|
108
|
+
if (given.done !== undefined && typeof given.done !== "boolean") return { problem: "done is true or false" };
|
|
109
|
+
if (given.slugs !== undefined && !(Array.isArray(given.slugs) && given.slugs.every((slug) => typeof slug === "string"))) return { problem: "slugs is a list of documents' slugs" };
|
|
110
|
+
return { options: { done: given.done, slugs: given.slugs ?? null, limit: count("limit", QUERY_LIMITS.todos) } };
|
|
111
|
+
}
|
|
112
|
+
if (kind === "recent") return { options: { limit: count("limit", QUERY_LIMITS.recent) } };
|
|
113
|
+
if (kind === "table") {
|
|
114
|
+
if (typeof given.slug !== "string" || !SLUG.test(given.slug)) return { problem: "slug is a document's slug" };
|
|
115
|
+
if (typeof given.key !== "string" || !CHART_KEY.test(given.key)) return { problem: "key is a table's key (data-doc-chart-source)" };
|
|
116
|
+
const where = given.where ?? {};
|
|
117
|
+
if (typeof where !== "object" || Array.isArray(where) || !Object.values(where).every((value) => typeof value === "string")) return { problem: "where maps each column to the words its cells are to hold" };
|
|
118
|
+
return { options: { slug: given.slug, key: given.key, where: { ...where }, limit: count("limit", QUERY_LIMITS.table) } };
|
|
119
|
+
}
|
|
120
|
+
if (kind === "activity") return { options: { days: count("days", QUERY_LIMITS.activity) } };
|
|
121
|
+
return { options: {} };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return { problem: error.message };
|
|
124
|
+
}
|
|
125
|
+
}
|