@panphora/clayjs 1.15.1 → 1.16.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/README.md +4 -5
- package/THIRD-PARTY-NOTICES.md +16 -0
- package/compat/highlight-11.9.0.min.js +1213 -0
- package/compat/highlight-LICENSE +29 -0
- package/compat/journal-assets.lock.json +22 -0
- package/compat/overtype-2.6.1.min.js +116 -0
- package/compat/overtype-LICENSE +21 -0
- package/dist/all.js +90 -0
- package/dist/clay-dom.js +84 -0
- package/dist/clay-events.js +103 -0
- package/dist/clay-internals.js +85 -0
- package/dist/clay-options.js +93 -0
- package/dist/clay-router.js +98 -0
- package/dist/clay-ui.js +95 -0
- package/dist/clay-utils.js +84 -0
- package/dist/clay.js +146 -0
- package/dist/clay.standalone.js +83090 -58706
- package/dist/m/attrs/add-from-T5LJXFFI.js +1 -0
- package/dist/m/attrs/onaftersave-OKSWEJQF.js +1 -0
- package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +1 -0
- package/dist/m/attrs/save-freeze-FUP4Q4X3.js +1 -0
- package/dist/m/c-2AZDZWCK.js +1 -0
- package/dist/m/c-2JL6E75N.js +1 -0
- package/dist/m/c-2M73252O.js +1 -0
- package/dist/m/c-2SPVQSHC.js +1 -0
- package/dist/m/c-33HUYEDI.js +1 -0
- package/dist/m/c-33JJ6CBX.js +2 -0
- package/dist/m/c-35NBGMZB.js +1 -0
- package/dist/m/c-3GJ4SDE2.js +1 -0
- package/dist/m/c-3IQRECYS.js +1 -0
- package/dist/m/c-3OWDMNJR.js +2 -0
- package/dist/m/c-46XYM55L.js +1 -0
- package/dist/m/c-4B6H4G5V.js +1 -0
- package/dist/m/c-4DRMS4QG.js +9 -0
- package/dist/m/c-4FFHZM32.js +1 -0
- package/dist/m/c-4JZDZFF6.js +1 -0
- package/dist/m/c-4L3BTFRK.js +1 -0
- package/dist/m/c-4Q2XTQ2N.js +2 -0
- package/dist/m/c-4WKFKEKF.js +1 -0
- package/dist/m/c-4YSCRICO.js +1 -0
- package/dist/m/c-54RCDCBU.js +1 -0
- package/dist/m/c-5M4YEKCX.js +4 -0
- package/dist/m/c-5R4PTRG2.js +1 -0
- package/dist/m/c-5V3MOVJT.js +1 -0
- package/dist/m/c-5VYOWDTI.js +1 -0
- package/dist/m/c-5Y6TNX5W.js +34 -0
- package/dist/m/c-5YIHJ3TJ.js +1 -0
- package/dist/m/c-66O32MV3.js +28 -0
- package/dist/m/c-6HMB24EV.js +1 -0
- package/dist/m/c-7DJDKPGL.js +1 -0
- package/dist/m/c-7O6AFR65.js +1 -0
- package/dist/m/c-7SWMYKAI.js +1 -0
- package/dist/m/c-7VGZ7RU5.js +1 -0
- package/dist/m/c-A7LS5UZW.js +1 -0
- package/dist/m/c-ANQTJ3B5.js +2 -0
- package/dist/m/c-AQLFEATB.js +1 -0
- package/dist/m/c-AYVOREBE.js +1 -0
- package/dist/m/c-B2ZXPOS3.js +3 -0
- package/dist/m/c-BBVGTQG7.js +1 -0
- package/dist/m/c-BMIZKMPV.js +1 -0
- package/dist/m/c-BTDI4Y23.js +1 -0
- package/dist/m/c-BWFVSBDF.js +1 -0
- package/dist/m/c-BYTKL6UX.js +1 -0
- package/dist/m/c-CFHW5AMY.js +6 -0
- package/dist/m/c-CJZSLMVQ.js +1 -0
- package/dist/m/c-CYWLPPUS.js +1 -0
- package/dist/m/c-CZUSOB7V.js +1 -0
- package/dist/m/c-DBE6ORSI.js +1 -0
- package/dist/m/c-DCFSEJZG.js +1 -0
- package/dist/m/c-DNZ5IFM6.js +1 -0
- package/dist/m/c-EDHUR4MX.js +1 -0
- package/dist/m/c-EJTGYSLN.js +1 -0
- package/dist/m/c-EY6UOM54.js +1 -0
- package/dist/m/c-F3CKJTJS.js +1 -0
- package/dist/m/c-F7Y6FLPY.js +1 -0
- package/dist/m/c-FA4DZVTC.js +1 -0
- package/dist/m/c-FILLTJA7.js +1 -0
- package/dist/m/c-G2BUZITR.js +1 -0
- package/dist/m/c-GB3JTV6E.js +1 -0
- package/dist/m/c-GBGWXTX4.js +1 -0
- package/dist/m/c-GHBCCVG4.js +1 -0
- package/dist/m/c-GJAEGGXY.js +1 -0
- package/dist/m/c-GOWBDM6J.js +1 -0
- package/dist/m/c-GQ6F6IDK.js +1 -0
- package/dist/m/c-H4LG442Q.js +1 -0
- package/dist/m/c-HA6INT6J.js +1 -0
- package/dist/m/c-HPURYVZS.js +2 -0
- package/dist/m/c-HRDCD3WN.js +67 -0
- package/dist/m/c-HT33EQRN.js +1 -0
- package/dist/m/c-HVYXIS72.js +1 -0
- package/dist/m/c-IB52R237.js +1 -0
- package/dist/m/c-IJUK2GPW.js +1 -0
- package/dist/m/c-IKWNE4R4.js +1 -0
- package/dist/m/c-IU3IYEU6.js +1 -0
- package/dist/m/c-IZ2EBRAJ.js +1 -0
- package/dist/m/c-J7CA54IP.js +1 -0
- package/dist/m/c-JAX2667S.js +6 -0
- package/dist/m/c-KI7M4AY3.js +1 -0
- package/dist/m/c-KIPQJX4B.js +1 -0
- package/dist/m/c-KKWLDOPM.js +1 -0
- package/dist/m/c-KR4UW4KW.js +1 -0
- package/dist/m/c-KY3VJGUA.js +8 -0
- package/dist/m/c-L4WQSC27.js +1 -0
- package/dist/m/c-LFPHCXY3.js +1 -0
- package/dist/m/c-LR4CGEPT.js +91 -0
- package/dist/m/c-LRPUO2MX.js +1 -0
- package/dist/m/c-LTGMXXKO.js +1 -0
- package/dist/m/c-LXENEBV7.js +1 -0
- package/dist/m/c-LZJPNR2W.js +1 -0
- package/dist/m/c-MMUIUUZG.js +2 -0
- package/dist/m/c-MTQHWQC3.js +1 -0
- package/dist/m/c-N326R2JD.js +1 -0
- package/dist/m/c-N7JPUPGC.js +1 -0
- package/dist/m/c-NMJRXKPC.js +8 -0
- package/dist/m/c-NRPJEVJS.js +1 -0
- package/dist/m/c-NUHP2XDB.js +1 -0
- package/dist/m/c-NZMNXVF6.js +1 -0
- package/dist/m/c-OCSZ4QGN.js +1 -0
- package/dist/m/c-OIBUTDXI.js +1 -0
- package/dist/m/c-OIDRZKAF.js +1 -0
- package/dist/m/c-OYTFP5QN.js +1 -0
- package/dist/m/c-P7KNDG34.js +1 -0
- package/dist/m/c-PPOSCAQD.js +1 -0
- package/dist/m/c-PQ5LK6QE.js +993 -0
- package/dist/m/c-PZ4XEPUP.js +2 -0
- package/dist/m/c-QHIWXHRO.js +1 -0
- package/dist/m/c-QHWJ5ZIJ.js +1 -0
- package/dist/m/c-QIX5BJYN.js +1 -0
- package/dist/m/c-QNKREOL6.js +1 -0
- package/dist/m/c-QQHEX6HA.js +1 -0
- package/dist/m/c-R7RMANJI.js +9 -0
- package/dist/m/c-RI4NT75Y.js +1 -0
- package/dist/m/c-RM7J7BV2.js +1 -0
- package/dist/m/c-RUJKSUQW.js +1 -0
- package/dist/m/c-RVMWGXEJ.js +1 -0
- package/dist/m/c-S7H2DRCB.js +1 -0
- package/dist/m/c-S7LK4KUE.js +1 -0
- package/dist/m/c-SGCCC4FW.js +25 -0
- package/dist/m/c-SGIA4IEQ.js +188 -0
- package/dist/m/c-SWRJFCNN.js +1 -0
- package/dist/m/c-T4VO4OCB.js +1 -0
- package/dist/m/c-TCCA5BZN.js +2 -0
- package/dist/m/c-TQEMVJCS.js +1 -0
- package/dist/m/c-TQZ2JYHD.js +1 -0
- package/dist/m/c-TR6RMDQ7.js +3 -0
- package/dist/m/c-TRS6TM6O.js +1 -0
- package/dist/m/c-UGHH4MB3.js +1 -0
- package/dist/m/c-UKHTLWUW.js +1 -0
- package/dist/m/c-UL2YUY52.js +1 -0
- package/dist/m/c-UNN7LWCT.js +1 -0
- package/dist/m/c-UXBMRDZB.js +235 -0
- package/dist/m/c-V7PZZRG7.js +1 -0
- package/dist/m/c-VAK7SG37.js +1 -0
- package/dist/m/c-VDSS4K3Z.js +1 -0
- package/dist/m/c-VFX45IZQ.js +1 -0
- package/dist/m/c-VHNSBD2M.js +1 -0
- package/dist/m/c-VKEDWYDF.js +1 -0
- package/dist/m/c-VL52ADDR.js +1 -0
- package/dist/m/c-WN5AY3OZ.js +1 -0
- package/dist/m/c-WNYBLIOZ.js +1 -0
- package/dist/m/c-WRBKEJD4.js +1 -0
- package/dist/m/c-WWDBYXLK.js +1 -0
- package/dist/m/c-WYFDLITL.js +1 -0
- package/dist/m/c-XN6MBKI6.js +67 -0
- package/dist/m/c-XNNW2CWK.js +1 -0
- package/dist/m/c-XPELB6E5.js +1 -0
- package/dist/m/c-XQXZAO7N.js +1 -0
- package/dist/m/c-Y6USGTS5.js +5 -0
- package/dist/m/c-YDH7Q3NY.js +1 -0
- package/dist/m/c-YG2NJYS6.js +135 -0
- package/dist/m/c-Z36J63PP.js +1 -0
- package/dist/m/c-Z4VRC7GU.js +7 -0
- package/dist/m/c-Z6K52OLP.js +19 -0
- package/dist/m/c-ZDY2MDAA.js +21 -0
- package/dist/m/c-ZE46QDIH.js +1 -0
- package/dist/m/c-ZUCOKOHT.js +1 -0
- package/dist/m/core/admin-attrs-VGNLAUDK.js +1 -0
- package/dist/m/core/autosave-E4PEFPZ6.js +1 -0
- package/dist/m/core/conflict-notice-VQ6Y64PH.js +1 -0
- package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
- package/dist/m/core/host-permission-XN3K2IW3.js +1 -0
- package/dist/m/core/is-edit-mode-JXHDVGUL.js +1 -0
- package/dist/m/core/persist-HKHA66IY.js +3 -0
- package/dist/m/core/recovery-TBBSICE2.js +1 -0
- package/dist/m/core/save-WUCHTQBE.js +1 -0
- package/dist/m/core/save-core-FJVHT5M3.js +1 -0
- package/dist/m/core/snapshot-F3SWDZMM.js +1 -0
- package/dist/m/core/unsaved-warning-STKBHE6G.js +1 -0
- package/dist/m/dom/all-APS5ZSKP.js +1 -0
- package/dist/m/dom/dom-helpers-L7OSR6S4.js +1 -0
- package/dist/m/events/index-23DPTUT4.js +1 -0
- package/dist/m/internals/index-WOVAP3RU.js +1 -0
- package/dist/m/lib/cache-bust-CGWT6ZJI.js +1 -0
- package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
- package/dist/m/loader-HXX6YNH5.js +1 -0
- package/dist/m/manifest-F22DEDAC.js +1 -0
- package/dist/m/manifest-edit-C3C7376B.js +1 -0
- package/dist/m/manifest.json +3491 -0
- package/dist/m/options/options-2LH76JQ3.js +1 -0
- package/dist/m/page/editor-XUTB2HB2.js +189 -0
- package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +6 -0
- package/dist/m/plugins/bind-text-NX46N5UV.js +1 -0
- package/dist/m/plugins/cms-HP3LSTPN.js +2293 -0
- package/dist/m/plugins/demo-ZIBFAA3R.js +1 -0
- package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
- package/dist/m/plugins/indicator-PV4YW2ID.js +1 -0
- package/dist/m/plugins/live-NUTTXSBS.js +1 -0
- package/dist/m/plugins/menu-HCKO3DSP.js +138 -0
- package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
- package/dist/m/plugins/page-KSUH7G6J.js +2 -0
- package/dist/m/plugins/page-view-W73OW7WI.js +1 -0
- package/dist/m/plugins/people-AJJ32YCG.js +1 -0
- package/dist/m/plugins/quickcrop-QNY3PKLD.js +26 -0
- package/dist/m/plugins/quickcrop-QNY3PKLD.js.LEGAL.txt +7 -0
- package/dist/m/plugins/richclay-RC3L2F6X.js +517 -0
- package/dist/m/plugins/richclay-RC3L2F6X.js.LEGAL.txt +1 -0
- package/dist/m/plugins/sortable-YRMQX3ZU.js +1 -0
- package/dist/m/plugins/source-KHK5UUSK.js +1 -0
- package/dist/m/plugins/undo-OF2L3CXU.js +1 -0
- package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
- package/dist/m/plugins/upgrade-ZNYODP5Y.js +1 -0
- package/dist/m/plugins/upload-XSGNILUY.js +1 -0
- package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +21 -0
- package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +1 -0
- package/dist/m/plugins/wire-ZWVWKF76.js +1 -0
- package/dist/m/reader-core-ILWLXVMP.js +68 -0
- package/dist/m/route/route-KOCZSEGN.js +1 -0
- package/dist/m/sync/live-sync-4AUVXVJU.js +1 -0
- package/dist/m/ui/index-EVHKMKBL.js +1 -0
- package/dist/m/utils/index-4TJCVLT7.js +1 -0
- package/dist/m/vendor/Sortable.vendor-A755ITLN.js +3 -0
- package/dist/m/vendor/Sortable.vendor-A755ITLN.js.LEGAL.txt +1 -0
- package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +6 -0
- package/dist/offline-manifest.json +3800 -0
- package/dist/page/themes/light-1.css +406 -0
- package/dist/server/core.cjs +23662 -22860
- package/dist/server/offline-runtime.cjs +4954 -0
- package/dist/server/offline-runtime.mjs +4923 -0
- package/dist/server/rooms.mjs +9 -0
- package/docs/guide.md +1078 -669
- package/entries/all.js +2 -0
- package/entries/clay-data.js +1 -1
- package/entries/clay-dom.js +2 -0
- package/entries/clay-events.js +2 -0
- package/entries/clay-internals.js +2 -0
- package/entries/clay-options.js +2 -0
- package/entries/clay-router.js +2 -0
- package/entries/clay-ui.js +2 -0
- package/entries/clay-utils.js +2 -0
- package/entries/clay.js +2 -0
- package/entries/sap.js +6 -6
- package/fonts/ibm-plex-mono-LICENSE +93 -0
- package/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-LICENSE +93 -0
- package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/fonts/ibm-plex.css +71 -0
- package/fonts/inter-LICENSE +93 -0
- package/fonts/inter-cyrillic-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-cyrillic-wght-normal.woff2 +0 -0
- package/fonts/inter-greek-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-greek-wght-normal.woff2 +0 -0
- package/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/fonts/inter-vietnamese-wght-normal.woff2 +0 -0
- package/fonts/inter.css +62 -0
- package/fonts/newsreader-LICENSE +93 -0
- package/fonts/newsreader-latin-ext-opsz-normal.woff2 +0 -0
- package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
- package/fonts/newsreader-vietnamese-opsz-normal.woff2 +0 -0
- package/fonts/newsreader.css +26 -0
- package/fonts/work-sans-LICENSE +93 -0
- package/fonts/work-sans-latin-wght-normal.woff2 +0 -0
- package/fonts/work-sans.css +8 -0
- package/package.json +20 -3
- package/packed-contract.json +1 -0
- package/src/attrs/add-from.js +27 -4
- package/src/attrs/save-freeze.js +7 -52
- package/src/core/admin-attrs.js +119 -12
- package/src/core/admin-contenteditable.js +12 -10
- package/src/core/admin-inputs.js +14 -15
- package/src/core/admin-onclick.js +2 -0
- package/src/core/autosave.js +74 -21
- package/src/core/conflict-download.js +22 -0
- package/src/core/conflict-notice-ui.js +1173 -0
- package/src/core/conflict-notice.js +140 -697
- package/src/core/edit-mode.js +147 -14
- package/src/core/etag.js +30 -12
- package/src/core/host-meta.js +72 -8
- package/src/core/host-permission.js +29 -0
- package/src/core/is-edit-mode.js +145 -12
- package/src/core/mode-key.js +125 -0
- package/src/core/mode-lifecycle.js +544 -0
- package/src/core/mode-receipt.js +30 -0
- package/src/core/mode-refusal.js +13 -0
- package/src/core/mode-resources.js +52 -0
- package/src/core/mode-support.js +82 -0
- package/src/core/mode-switch.js +259 -0
- package/src/core/mode-transition.js +112 -0
- package/src/core/page-data-write.js +305 -0
- package/src/core/page-data.js +26 -227
- package/src/core/page-format-wire.js +4 -0
- package/src/core/page-format.js +58 -0
- package/src/core/persist.js +37 -9
- package/src/core/preserve-authored.js +114 -0
- package/src/core/recovery.js +772 -0
- package/src/core/runtime-url.js +105 -0
- package/src/core/save-core.js +109 -12
- package/src/core/save-replace.js +72 -0
- package/src/core/save-size-notice.js +134 -0
- package/src/core/save-size.js +140 -0
- package/src/core/save.js +805 -118
- package/src/core/snapshot.js +121 -11
- package/src/core/source-map.js +157 -9
- package/src/core/unsaved-warning.js +14 -4
- package/src/core/upgrade-run.js +408 -0
- package/src/core/version.js +18 -0
- package/src/lib/assets.js +30 -0
- package/src/lib/autosave-debug-gate.js +50 -0
- package/src/lib/autosave-scheduler.js +141 -0
- package/src/lib/boot-imports.js +8 -0
- package/src/lib/derived.js +3 -1
- package/src/lib/dirty-gate.js +15 -0
- package/src/lib/doc-storage.js +33 -0
- package/src/lib/effect.js +9 -4
- package/src/lib/mutation.js +4 -2
- package/src/lib/offline-runtime.js +226 -0
- package/src/lib/preload.js +48 -0
- package/src/lib/root-attrs.js +1 -1
- package/src/lib/trash.js +18 -0
- package/src/lib/undo/capture-move.js +152 -0
- package/src/live/local-channel.js +61 -0
- package/src/live/local-lane.js +60 -9
- package/src/live/server-lane.js +6 -3
- package/src/loader-logic.js +76 -15
- package/src/loader.js +218 -30
- package/src/page/adopt-boot.js +76 -22
- package/src/page/adopt.js +25 -4
- package/src/page/awareness.js +230 -0
- package/src/page/blocks.js +79 -5
- package/src/page/blocksel.js +20 -4
- package/src/page/board-face.js +13 -0
- package/src/page/board-panel.js +5 -17
- package/src/page/board-style.js +2 -13
- package/src/page/board-view.js +1 -11
- package/src/page/board-views.js +17 -0
- package/src/page/board.js +2 -2
- package/src/page/boot-pack.js +26 -0
- package/src/page/boot.js +237 -0
- package/src/page/checklist.js +1 -1
- package/src/page/chrome.js +411 -61
- package/src/page/csv-import.js +17 -2
- package/src/page/document-title-dom.js +40 -0
- package/src/page/document-title-runtime.js +76 -0
- package/src/page/document-title.js +9 -0
- package/src/page/drag.js +48 -13
- package/src/page/drop.js +2 -1
- package/src/page/editor.js +330 -86
- package/src/page/feature-loader.js +72 -0
- package/src/page/features/board.js +12 -0
- package/src/page/features/code.js +13 -0
- package/src/page/features/media.js +290 -0
- package/src/page/features/registry.js +23 -0
- package/src/page/features/table.js +383 -0
- package/src/page/{tool-view.js → features/tool.js} +33 -2
- package/src/page/format-codec.js +215 -0
- package/src/page/format-gate.js +179 -0
- package/src/page/format-level.js +157 -0
- package/src/page/highlight-slot.js +23 -0
- package/src/page/highlight.js +30 -16
- package/src/page/history-intent.js +78 -0
- package/src/page/inline.js +18 -25
- package/src/page/items.js +52 -11
- package/src/page/link.js +1 -0
- package/src/page/markdown.js +86 -1
- package/src/page/media-views.js +3 -0
- package/src/page/media.js +33 -245
- package/src/page/outline-cache.js +11 -0
- package/src/page/placeholder.js +27 -7
- package/src/page/projection.js +196 -50
- package/src/page/reader-patch.js +306 -0
- package/src/page/recover.js +1 -1
- package/src/page/rich/page-tool.js +1 -4
- package/src/page/runtime-owner.js +27 -0
- package/src/page/save-projection.js +7 -4
- package/src/page/schema.js +14 -0
- package/src/page/table-rect-key.js +3 -0
- package/src/page/table-select.js +4 -3
- package/src/page/table-style.js +1 -1
- package/src/page/table-views.js +3 -0
- package/src/page/table.js +8 -293
- package/src/page/themes/light-1.css +139 -2
- package/src/page/themes/light-1.source.css +269 -0
- package/src/page/tool-capability.js +3 -0
- package/src/page/tool-views.js +3 -0
- package/src/page/view.js +390 -107
- package/src/page/ybind/awareness.js +172 -0
- package/src/page/ybind/binding.js +9 -1
- package/src/page/ybind/bridge.js +50 -47
- package/src/page/ybind/decode.js +52 -19
- package/src/page/ybind/doc.js +27 -2
- package/src/page/ybind/group.js +12 -3
- package/src/page/ybind/index.js +2 -0
- package/src/page/ybind/lineage.js +71 -0
- package/src/page/ybind/marks.js +25 -1
- package/src/page/ybind/migrate.js +48 -13
- package/src/page/ybind/moves.js +32 -21
- package/src/page/ybind/positions.js +50 -0
- package/src/page/ybind/provider.js +84 -22
- package/src/page/ybind/recovery-proof.js +191 -0
- package/src/page/ybind/recovery-replay.js +130 -0
- package/src/page/ybind/tokens.js +15 -6
- package/src/page/ybind/walk.js +10 -3
- package/src/page/ybind/writer.js +48 -4
- package/src/plugins/ai-edit.js +167 -32
- package/src/plugins/bind-text.js +218 -0
- package/src/plugins/cms.js +93 -0
- package/src/plugins/indicator.js +57 -5
- package/src/plugins/live.js +282 -34
- package/src/plugins/menu/claygoo.svg.js +1 -0
- package/src/plugins/menu/seo-head.js +323 -0
- package/src/plugins/menu/seo.js +849 -0
- package/src/plugins/menu.js +425 -0
- package/src/plugins/page-view.js +131 -0
- package/src/plugins/page.js +1022 -275
- package/src/plugins/people.js +158 -31
- package/src/plugins/richclay.js +119 -9
- package/src/plugins/sortable.js +370 -60
- package/src/plugins/source.js +189 -366
- package/src/plugins/undo.js +289 -19
- package/src/plugins/unsent.js +132 -0
- package/src/plugins/upgrade-run.js +1 -0
- package/src/plugins/upgrade.js +210 -0
- package/src/plugins/upload-task.js +571 -0
- package/src/plugins/upload-ui.js +265 -0
- package/src/plugins/upload.js +34 -153
- package/src/plugins/visitor-drafts/inert.js +32 -0
- package/src/plugins/visitor-drafts/owner-chip.js +3 -0
- package/src/plugins/visitor-drafts/prompt.js +2 -2
- package/src/plugins/visitor-drafts-owner.js +85 -0
- package/src/plugins/visitor-drafts.js +31 -42
- package/src/plugins/wire.js +82 -1
- package/src/reader-core.js +26 -0
- package/src/recovery/conflicts.js +287 -0
- package/src/recovery/identity.js +154 -0
- package/src/recovery/store.js +792 -0
- package/src/recovery/undo.js +257 -0
- package/src/server/checkpoint/index.js +68 -4
- package/src/server/offline-runtime/index.js +214 -0
- package/src/server/rooms/index.js +2 -1
- package/src/server/rooms/live-data-rooms.js +2 -2
- package/src/server/rooms/page-admission.js +33 -0
- package/src/server/rooms/page-lineage.js +56 -0
- package/src/server/rooms/page-rebase.js +11 -5
- package/src/server/rooms/page-state.js +2 -0
- package/src/server/rooms/page-write-admission.js +9 -3
- package/src/source/session.js +705 -0
- package/src/sync/conflict-revert.js +5 -4
- package/src/sync/conflicts-edit.js +119 -0
- package/src/sync/conflicts-facade.js +49 -0
- package/src/sync/conflicts.js +99 -1
- package/src/sync/editing-marker.js +37 -6
- package/src/sync/editing-sender.js +8 -1
- package/src/sync/live-sync-edit.js +63 -0
- package/src/sync/live-sync.js +364 -119
- package/src/sync/local-presence.js +187 -0
- package/src/sync/merge-declarations.js +10 -0
- package/src/sync/merge-engine.js +12 -0
- package/src/sync/presence-store.js +412 -0
- package/src/sync/presence-target.js +243 -0
- package/src/sync/presence.js +72 -41
- package/src/sync/saved-handoff.js +200 -0
- package/src/sync/saved-stream.js +535 -0
- package/src/sync/version-guard.js +92 -0
- package/src/sync/wire-profile.js +81 -0
- package/src/ui/bevel-controls.js +2 -8
- package/src/ui/page-scheme.js +7 -0
- package/src/ui/skins/cms.js +1 -1
- package/src/ui/theme-config.js +150 -0
- package/src/ui/theme.js +6 -138
- package/src/ui/toast-lazy.js +11 -0
- package/src/vendor/LICENSES-optional.md +253 -0
- package/src/vendor/highlight.vendor.js +3 -0
- package/src/vendor/hljs-bash.vendor.js +2 -0
- package/src/vendor/hljs-css.vendor.js +2 -0
- package/src/vendor/hljs-javascript.vendor.js +2 -0
- package/src/vendor/hljs-json.vendor.js +2 -0
- package/src/vendor/hljs-python.vendor.js +2 -0
- package/src/vendor/hljs-xml.vendor.js +2 -0
- package/src/vendor/hyper-html-api-data.vendor.js +1 -1
- package/src/vendor/hyper-html-api-upgrade.vendor.js +5 -0
- package/src/vendor/hyper-morph-identity.vendor.js +2 -0
- package/src/vendor/hyper-morph-json.vendor.js +3 -0
- package/src/vendor/hyper-morph.vendor.js +6 -6
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +169 -84
- package/src/vendor/marked.vendor.js +68 -0
- package/src/vendor/overtype.vendor.js +994 -0
- package/src/vendor/richclay.vendor.js +17 -17
- package/src/vendor/sap-core.vendor.js +5 -5
- package/src/vendor/sap-ownership.vendor.js +11 -7
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { canEdit, isDraftMode, mode } from "./is-edit-mode.js";
|
|
2
|
+
import { setMode } from "./edit-mode.js";
|
|
3
|
+
import onDomReady from "../lib/dom-ready.js";
|
|
4
|
+
|
|
5
|
+
const mac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
6
|
+
const chord = mac ? "⌘⇧E" : "Ctrl+Shift+E";
|
|
7
|
+
|
|
8
|
+
const NOTE_CSS = `.clay-mode-note {
|
|
9
|
+
position: fixed;
|
|
10
|
+
right: calc(16px + env(safe-area-inset-right, 0px));
|
|
11
|
+
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
|
12
|
+
z-index: 2147482000;
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 10px;
|
|
16
|
+
max-width: min(420px, calc(100vw - 32px));
|
|
17
|
+
padding: 10px 10px 10px 14px;
|
|
18
|
+
border: 1px solid rgba(23, 28, 36, .16);
|
|
19
|
+
border-radius: 12px;
|
|
20
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
21
|
+
font-size: 13px;
|
|
22
|
+
line-height: 1.45;
|
|
23
|
+
text-align: left;
|
|
24
|
+
color: var(--clay-ui-fg, #1F2328);
|
|
25
|
+
background: var(--clay-ui-bg, #F4F4F5);
|
|
26
|
+
box-shadow: 0 8px 24px -12px rgba(23, 28, 36, .45);
|
|
27
|
+
transition: opacity .25s ease, transform .25s ease;
|
|
28
|
+
}
|
|
29
|
+
.clay-mode-note kbd {
|
|
30
|
+
font: inherit;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
padding: 1px 5px;
|
|
33
|
+
border: 1px solid rgba(23, 28, 36, .2);
|
|
34
|
+
border-radius: 5px;
|
|
35
|
+
background: rgba(23, 28, 36, .07);
|
|
36
|
+
}
|
|
37
|
+
.clay-mode-note button {
|
|
38
|
+
flex: none;
|
|
39
|
+
display: inline-flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
justify-content: center;
|
|
42
|
+
width: 24px;
|
|
43
|
+
height: 24px;
|
|
44
|
+
padding: 0;
|
|
45
|
+
border: 0;
|
|
46
|
+
border-radius: 6px;
|
|
47
|
+
font: inherit;
|
|
48
|
+
font-size: 16px;
|
|
49
|
+
line-height: 1;
|
|
50
|
+
color: inherit;
|
|
51
|
+
background: none;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
opacity: .65;
|
|
54
|
+
}
|
|
55
|
+
.clay-mode-note button:hover { opacity: 1; background: rgba(23, 28, 36, .08); }
|
|
56
|
+
@media (prefers-color-scheme: dark) {
|
|
57
|
+
.clay-mode-note {
|
|
58
|
+
border-color: rgba(233, 233, 236, .16);
|
|
59
|
+
color: var(--clay-ui-fg, #E9E9EC);
|
|
60
|
+
background: var(--clay-ui-bg, #26282C);
|
|
61
|
+
box-shadow: 0 8px 24px -12px rgba(0, 0, 0, .7);
|
|
62
|
+
}
|
|
63
|
+
.clay-mode-note kbd { border-color: rgba(233, 233, 236, .22); background: rgba(233, 233, 236, .1); }
|
|
64
|
+
.clay-mode-note button:hover { background: rgba(233, 233, 236, .12); }
|
|
65
|
+
}
|
|
66
|
+
@media (prefers-reduced-motion: reduce) {
|
|
67
|
+
.clay-mode-note { transition: none; }
|
|
68
|
+
}
|
|
69
|
+
@media (max-width: 559px) {
|
|
70
|
+
.clay-mode-note {
|
|
71
|
+
left: calc(16px + env(safe-area-inset-left, 0px));
|
|
72
|
+
right: calc(16px + env(safe-area-inset-right, 0px));
|
|
73
|
+
max-width: none;
|
|
74
|
+
}
|
|
75
|
+
}`;
|
|
76
|
+
|
|
77
|
+
function styles() {
|
|
78
|
+
if (document.getElementById("clay-mode-note-style")) return;
|
|
79
|
+
const style = document.createElement("style");
|
|
80
|
+
style.id = "clay-mode-note-style";
|
|
81
|
+
style.setAttribute("clay", "editor-ui");
|
|
82
|
+
style.textContent = NOTE_CSS;
|
|
83
|
+
document.head.append(style);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onKey(e) {
|
|
87
|
+
if (!(canEdit || mode === "edit")) return;
|
|
88
|
+
if (!(mac ? e.metaKey : e.ctrlKey) || !e.shiftKey || e.altKey || e.code !== "KeyE" || e.isComposing) return;
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
setMode(mode === "edit" ? "view" : "edit").then((r) => {
|
|
91
|
+
if (r.ok) return;
|
|
92
|
+
import("./mode-refusal.js").then(({ modeRefusalText }) => {
|
|
93
|
+
const text = modeRefusalText(r);
|
|
94
|
+
if (text) window.clay?.toast?.(text, "error");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function showNote() {
|
|
100
|
+
styles();
|
|
101
|
+
const note = document.createElement("div");
|
|
102
|
+
note.setAttribute("clay", "editor-ui");
|
|
103
|
+
note.setAttribute("role", "status");
|
|
104
|
+
note.className = "clay-mode-note";
|
|
105
|
+
note.innerHTML = `<span>This page opens in view mode. Press <kbd>${chord}</kbd> to edit.</span><button type="button" aria-label="Dismiss">×</button>`;
|
|
106
|
+
const close = () => note.remove();
|
|
107
|
+
note.querySelector("button").addEventListener("click", close);
|
|
108
|
+
addEventListener("keydown", (e) => { if (e.key === "Escape") close(); }, { once: true });
|
|
109
|
+
setTimeout(close, 12000);
|
|
110
|
+
document.body.append(note);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!isDraftMode) {
|
|
114
|
+
document.addEventListener("keydown", onKey);
|
|
115
|
+
onDomReady(() => {
|
|
116
|
+
if (!canEdit || mode !== "view" || document.querySelector("[clay-mode-toggle]")) return;
|
|
117
|
+
queueMicrotask(() => {
|
|
118
|
+
if (window.clay?.menu) return;
|
|
119
|
+
const store = window.clay?.storage?.local;
|
|
120
|
+
if (!store || store.get("mode-note")) return;
|
|
121
|
+
store.set("mode-note", true);
|
|
122
|
+
showNote();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
// The mode controller: the one module that prepares, enables, quiesces and disposes
|
|
2
|
+
// edit leases. It is a dynamic target, fetched by an explicit initial edit or the first
|
|
3
|
+
// live switch, so a reader that never asks to edit never downloads it.
|
|
4
|
+
//
|
|
5
|
+
// A lease is `{ready, enable, quiesce, resume, dispose}` for one participant. `prepare`
|
|
6
|
+
// may fetch and build inert UI; `enable` is synchronous once every preparation has
|
|
7
|
+
// succeeded; `quiesce` stops admission without losing buffers, so a refused exit can
|
|
8
|
+
// `resume` the very same lease; and `dispose` runs only after a committed exit.
|
|
9
|
+
//
|
|
10
|
+
// The save generation and the exit's barrier are imported when a transition needs them,
|
|
11
|
+
// never at the top: this module's own graph stays what it was.
|
|
12
|
+
import {
|
|
13
|
+
canEdit,
|
|
14
|
+
isDraftMode,
|
|
15
|
+
isEditMode,
|
|
16
|
+
commitModeFacts,
|
|
17
|
+
modeState,
|
|
18
|
+
noteParticipantForget,
|
|
19
|
+
takeModeParticipants,
|
|
20
|
+
} from "./is-edit-mode.js";
|
|
21
|
+
import { supportFor } from "./mode-support.js";
|
|
22
|
+
|
|
23
|
+
let generation = 0;
|
|
24
|
+
let leases = [];
|
|
25
|
+
let roster = [];
|
|
26
|
+
let inFlight = null;
|
|
27
|
+
// A tab that booted in edit started its sessions at evaluation; the first exit adopts them
|
|
28
|
+
// instead of preparing its own.
|
|
29
|
+
let initialSession = false;
|
|
30
|
+
// Whether this boot's sessions have been adopted. `leases` already holds whatever the entry
|
|
31
|
+
// prepared, a preboot participant included, so it says nothing about the boot's own.
|
|
32
|
+
let adoptedInitial = false;
|
|
33
|
+
// The adoption in flight, kept across a refused exit: a participant whose preparation
|
|
34
|
+
// outlives the deadline answers the next exit instead of being asked twice.
|
|
35
|
+
let adopting = null;
|
|
36
|
+
// The file the session was built from. An exit that saved nothing hands it back to the
|
|
37
|
+
// reader, so a second entry has a receipt without waiting for a frame.
|
|
38
|
+
let entryReceipt = null;
|
|
39
|
+
|
|
40
|
+
// One deadline covers the whole transition: every import a participant prepares from,
|
|
41
|
+
// the preparation itself, and the commit.
|
|
42
|
+
const DEADLINE_MS = 15000;
|
|
43
|
+
|
|
44
|
+
const current = () => modeState().mode;
|
|
45
|
+
|
|
46
|
+
const fault = (message, code) => Object.assign(new Error(`clay.modeLifecycle: ${message}`), { code });
|
|
47
|
+
|
|
48
|
+
// A committed change is announced so the engines that follow the session (Sap's undo
|
|
49
|
+
// pair) rebind once, keyed on `state.generation`.
|
|
50
|
+
function announceMode() {
|
|
51
|
+
document.dispatchEvent(new Event("clay:modechange"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function refuse(code, phase = "idle", extra = {}) {
|
|
55
|
+
commitModeFacts({ phase, reason: code });
|
|
56
|
+
return { ok: false, mode: current(), code, ...extra };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// A lease handed nothing back has nothing to take back. Reverse order is the exit's, so a
|
|
60
|
+
// lease enabled last is disposed first.
|
|
61
|
+
function disposeInReverse(list) {
|
|
62
|
+
for (const lease of [...list].reverse()) {
|
|
63
|
+
try {
|
|
64
|
+
lease.dispose?.();
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// A refused entry gives the article back to the reader it took it from: the leases are
|
|
70
|
+
// already disposed in reverse, so the claim and then the queue return to their owner.
|
|
71
|
+
function giveBack(reader) {
|
|
72
|
+
try {
|
|
73
|
+
reader?.reclaimArticle?.();
|
|
74
|
+
} catch {}
|
|
75
|
+
try {
|
|
76
|
+
reader?.resume?.();
|
|
77
|
+
} catch {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function setRootEditMode(on) {
|
|
81
|
+
document.documentElement.setAttribute("editmode", on ? "true" : "false");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The committed mode owns the mode now: a URL that asked for edit is a request this
|
|
85
|
+
// switch has already answered, and a vendor that reads the parameter before the facts
|
|
86
|
+
// (RichClay) would hold the page at the old mode after a live entry. The parameter goes
|
|
87
|
+
// at the commit that changed the mode, and every other parameter and the hash stay.
|
|
88
|
+
function dropEditmodeParam() {
|
|
89
|
+
try {
|
|
90
|
+
const url = new URL(window.location.href);
|
|
91
|
+
if (!url.searchParams.has("editmode")) return;
|
|
92
|
+
url.searchParams.delete("editmode");
|
|
93
|
+
window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.warn("clayjs: the URL could not be rewritten after the mode switch", error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The modules an edit plan resolves, in the order the plan names them.
|
|
100
|
+
const editPlanPaths = (handle) => [
|
|
101
|
+
...(handle?.edit?.core ?? []),
|
|
102
|
+
...(handle?.edit?.plugins ?? []),
|
|
103
|
+
...(handle?.edit?.late ?? []),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// The built-ins a session starts: one participant per loaded edit-plan path whose support
|
|
107
|
+
// row exports one (or is a function of the namespace that answers one, for a module whose
|
|
108
|
+
// participant lives in its edit chunk), in plan order. A path the plan names but the entry
|
|
109
|
+
// could not load contributes nothing, and a neutral row never does.
|
|
110
|
+
function builtinParticipants(handle) {
|
|
111
|
+
const found = [];
|
|
112
|
+
for (const path of editPlanPaths(handle)) {
|
|
113
|
+
const support = supportFor(path);
|
|
114
|
+
if (support !== "export" && typeof support !== "function") continue;
|
|
115
|
+
const mod = handle.loaded?.[path];
|
|
116
|
+
if (!mod) continue;
|
|
117
|
+
found.push(typeof support === "function" ? support(mod) : mod.modeParticipant?.());
|
|
118
|
+
}
|
|
119
|
+
return found;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Participants may be queued as promises, and the queue is where the authored resources'
|
|
123
|
+
// lease arrives. The first claim on an id wins; `order` decides prepare order. A queued
|
|
124
|
+
// entry that is not a participant is a document this release cannot start, and it is
|
|
125
|
+
// found before anything is prepared.
|
|
126
|
+
async function collectRoster(handle) {
|
|
127
|
+
const seen = new Set(roster.map(({ id }) => id));
|
|
128
|
+
const add = (participant) => {
|
|
129
|
+
if (typeof participant?.id !== "string" || typeof participant.prepare !== "function") {
|
|
130
|
+
throw fault("a participant needs an id and a prepare()", "unsupported-plugin");
|
|
131
|
+
}
|
|
132
|
+
if (seen.has(participant.id)) return;
|
|
133
|
+
seen.add(participant.id);
|
|
134
|
+
roster.push(participant);
|
|
135
|
+
};
|
|
136
|
+
if (handle) for (const participant of builtinParticipants(handle)) add(await participant);
|
|
137
|
+
for (const entry of takeModeParticipants()) add(await entry);
|
|
138
|
+
return [...roster].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A registration this page took back while idle in view leaves the roster too, so the next
|
|
142
|
+
// entry never prepares a participant nobody wants. Loaded, the controller is the only
|
|
143
|
+
// owner of the roster; the registry calls this and never fetches the controller for it.
|
|
144
|
+
export function forgetParticipant(id) {
|
|
145
|
+
const index = roster.findIndex((participant) => participant.id === id);
|
|
146
|
+
if (index !== -1) roster.splice(index, 1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
noteParticipantForget(forgetParticipant);
|
|
150
|
+
|
|
151
|
+
// A reader that now represents a different file than the session was built from: the seq
|
|
152
|
+
// decides it when both name one, and a different stamp is a different file when it cannot.
|
|
153
|
+
// Absorbing such a file into the baseline would make a change nobody showed the person
|
|
154
|
+
// read as saved, so the entry refuses instead.
|
|
155
|
+
function superseded(held, represented) {
|
|
156
|
+
if (!held || held.bytes === null) return false;
|
|
157
|
+
const before = represented ?? {};
|
|
158
|
+
if (typeof held.seq === "number" && typeof before.seq === "number" && held.seq !== before.seq) {
|
|
159
|
+
return held.seq > before.seq;
|
|
160
|
+
}
|
|
161
|
+
return (held.etag ?? null) !== (before.etag ?? null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function enter(handle = null) {
|
|
165
|
+
const before = current();
|
|
166
|
+
const editBefore = isEditMode;
|
|
167
|
+
const controller = new AbortController();
|
|
168
|
+
const context = {
|
|
169
|
+
generation: ++generation,
|
|
170
|
+
destination: modeState().destination,
|
|
171
|
+
signal: controller.signal,
|
|
172
|
+
// No reader on this road yet: the entry takes the page as it stands.
|
|
173
|
+
represented: null,
|
|
174
|
+
reader: null,
|
|
175
|
+
initial: false,
|
|
176
|
+
// Abandonment is cancellation as well as a newer generation: a lease that resolves
|
|
177
|
+
// after the deadline belongs to no session and must not be kept.
|
|
178
|
+
isCurrent: () => context.generation === generation && !controller.signal.aborted,
|
|
179
|
+
};
|
|
180
|
+
commitModeFacts({ phase: "entering", generation: context.generation, reason: null });
|
|
181
|
+
// A tab that booted in edit is already running the sessions an entry would prepare, so
|
|
182
|
+
// its exit adopts them rather than starting a second set.
|
|
183
|
+
if (!handle) {
|
|
184
|
+
initialSession = true;
|
|
185
|
+
adoptedInitial = false;
|
|
186
|
+
adopting = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const prepared = [];
|
|
190
|
+
const fresh = [];
|
|
191
|
+
let save = null;
|
|
192
|
+
// The reader a declared document already runs: it hands over the file this session is
|
|
193
|
+
// built from, and it is the only owner of the saved lane until the commit.
|
|
194
|
+
let reader = null;
|
|
195
|
+
// Whether any participant has been asked for a lease. Before that, a document this
|
|
196
|
+
// release cannot start still has its guarded reload; after it, a failure is a refusal.
|
|
197
|
+
let started = false;
|
|
198
|
+
// Preparation answers only for itself: whatever it builds after the deadline is
|
|
199
|
+
// disposed by the continuation that notices the abandonment, never here.
|
|
200
|
+
const preparation = (async () => {
|
|
201
|
+
try {
|
|
202
|
+
// The edit plan's missing modules, imported while the committed mode is still view:
|
|
203
|
+
// every import-time edit half is inert there, so what a module starts is exactly
|
|
204
|
+
// what its lease starts at the commit.
|
|
205
|
+
if (handle) {
|
|
206
|
+
const plan = await import("./mode-switch.js");
|
|
207
|
+
// Before anything is imported: the reader is asked for the file it represents,
|
|
208
|
+
// and a reader without one leaves the caller its guarded reload.
|
|
209
|
+
reader = await plan.prepareReader(handle, context);
|
|
210
|
+
fresh.push(...(await plan.importPlan(handle, handle.edit, { signal: controller.signal })));
|
|
211
|
+
// A file that arrived while the plan imported is applied before the baseline is
|
|
212
|
+
// taken, so the session starts on what the article actually shows.
|
|
213
|
+
await reader?.reconcile();
|
|
214
|
+
context.represented = reader ? reader.represented : null;
|
|
215
|
+
context.reader = reader;
|
|
216
|
+
entryReceipt = context.represented;
|
|
217
|
+
}
|
|
218
|
+
// Inside the deadline, because the generation commits in the boundary below.
|
|
219
|
+
save = handle?.loaded?.["core/save.js"] ?? (await import("./save.js"));
|
|
220
|
+
for (const participant of await collectRoster(handle)) {
|
|
221
|
+
started = true;
|
|
222
|
+
const lease = await participant.prepare(context);
|
|
223
|
+
if (!lease) continue;
|
|
224
|
+
prepared.push(lease);
|
|
225
|
+
if (!context.isCurrent()) throw fault(`"${participant.id}" prepared for a stale generation`, "stale-document");
|
|
226
|
+
if (lease.ready) await lease.ready;
|
|
227
|
+
if (!context.isCurrent()) throw fault(`"${participant.id}" became ready for a stale generation`, "stale-document");
|
|
228
|
+
}
|
|
229
|
+
// A live entry's baseline is the page as it stands: no reader has a receipt to take
|
|
230
|
+
// it from, so one capture settles both baselines and the dirty gate's token. It is
|
|
231
|
+
// taken for an entry with a reader and for one that came from view; only a boot that
|
|
232
|
+
// already starts in edit keeps the baseline it settled when it read the file.
|
|
233
|
+
if (handle || !editBefore) {
|
|
234
|
+
const baseline = save.adoptEntryBaseline({ represented: context.represented });
|
|
235
|
+
if (baseline && baseline.ok === false) {
|
|
236
|
+
throw Object.assign(fault("the page moved since the last session", baseline.code || "pending-input"), baseline);
|
|
237
|
+
}
|
|
238
|
+
// One more look, because a frame can land while the roster prepares: the session
|
|
239
|
+
// is built from the newest file the reader holds, queued or applied, not from an
|
|
240
|
+
// older one it still shows.
|
|
241
|
+
if (superseded(reader?.held ?? reader?.represented, context.represented)) {
|
|
242
|
+
throw fault("the reader holds a newer file than the session was built from", "stale-document");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
disposeInReverse(prepared);
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
})();
|
|
251
|
+
|
|
252
|
+
let timer = null;
|
|
253
|
+
const deadline = new Promise((resolve) => {
|
|
254
|
+
timer = setTimeout(() => {
|
|
255
|
+
controller.abort();
|
|
256
|
+
resolve(false);
|
|
257
|
+
}, DEADLINE_MS);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
let ready;
|
|
261
|
+
try {
|
|
262
|
+
ready = await Promise.race([preparation, deadline]);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
controller.abort();
|
|
266
|
+
// Nothing was prepared, so nothing has changed: the caller keeps its guarded reload
|
|
267
|
+
// and the page goes back to an idle view. Once a lease exists, replacing the page
|
|
268
|
+
// under it is worse than saying no.
|
|
269
|
+
if (!started) {
|
|
270
|
+
// A reader that was already holding the lane gives it back, so the page the guarded
|
|
271
|
+
// reload replaces is not the only owner left.
|
|
272
|
+
giveBack(reader);
|
|
273
|
+
commitModeFacts({ phase: "idle", reason: null });
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
giveBack(reader);
|
|
277
|
+
return refuse(err?.code || "load-failed", "blocked", { message: err?.message });
|
|
278
|
+
}
|
|
279
|
+
clearTimeout(timer);
|
|
280
|
+
if (!ready) {
|
|
281
|
+
// The entry is over; its continuation still holds a lease that has not resolved.
|
|
282
|
+
preparation.catch(() => {});
|
|
283
|
+
// Everything already prepared is a claim on the article and a save hold: it goes
|
|
284
|
+
// before the article is given back, so the reader takes it and the view updates.
|
|
285
|
+
// Whatever the continuation prepares after this is disposed by its own catch.
|
|
286
|
+
disposeInReverse(prepared.splice(0));
|
|
287
|
+
giveBack(reader);
|
|
288
|
+
return refuse("timeout");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// One synchronous boundary, no await between these steps: the mode facts commit while
|
|
292
|
+
// the phase still says `entering` (so `canWrite()` is false), the root attribute and the
|
|
293
|
+
// save generation follow, the leases enable, and only then does the phase go `idle` and
|
|
294
|
+
// the one announcement out. An `enable` therefore sees the committed edit facts.
|
|
295
|
+
// The reader retires first: its lane and the file it represents are this session's now,
|
|
296
|
+
// and the leases that follow start on that file.
|
|
297
|
+
reader?.commit();
|
|
298
|
+
commitModeFacts({ mode: isDraftMode ? "draft" : "edit", isEditMode: true });
|
|
299
|
+
// A boot's own entry keeps the parameter: it is how an owner's reload stays in edit.
|
|
300
|
+
if (handle) dropEditmodeParam();
|
|
301
|
+
setRootEditMode(true);
|
|
302
|
+
save?.setSaveGeneration(context.generation);
|
|
303
|
+
try {
|
|
304
|
+
for (const lease of prepared) lease.enable();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
// The blocked fault: the facts and the root attribute go back, everything prepared is
|
|
307
|
+
// taken back in reverse, and the page keeps the reason. The reader this entry retired
|
|
308
|
+
// is restarted from the file it handed over, so the view keeps receiving files.
|
|
309
|
+
commitModeFacts({ mode: before, isEditMode: editBefore });
|
|
310
|
+
setRootEditMode(false);
|
|
311
|
+
disposeInReverse(prepared);
|
|
312
|
+
controller.abort();
|
|
313
|
+
if (handle) {
|
|
314
|
+
const plan = await import("./mode-switch.js").catch(() => null);
|
|
315
|
+
plan?.startReader(handle, entryReceipt);
|
|
316
|
+
}
|
|
317
|
+
return refuse(err?.code || "load-failed", "blocked", { message: err?.message });
|
|
318
|
+
}
|
|
319
|
+
leases = prepared;
|
|
320
|
+
commitModeFacts({ phase: "idle", reason: null });
|
|
321
|
+
announceMode();
|
|
322
|
+
// One announcement per plugin this entry loaded, after the mode change: a plugin is
|
|
323
|
+
// announced by the boot that loaded it exactly once, and a refused entry announces
|
|
324
|
+
// nothing so the next successful one does.
|
|
325
|
+
for (const path of fresh) handle?.announce?.(path);
|
|
326
|
+
return { ok: true, mode: current(), changed: before !== current() };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// The sessions this tab's explicit initial edit already started, taken as leases over what
|
|
330
|
+
// the imports did rather than started again: a lease that starts nothing, quiesces and
|
|
331
|
+
// disposes the running session. A path that cannot be adopted leaves the caller its flush
|
|
332
|
+
// and reload.
|
|
333
|
+
async function adoptInitial(handle) {
|
|
334
|
+
if (!handle) return [];
|
|
335
|
+
// Only what is loaded now: the boot late tail may still be importing, and an exit waits
|
|
336
|
+
// for no import. A module that lands after this point starts nothing, because it reads
|
|
337
|
+
// the committed mode (or the `leaving` phase) when it evaluates, and it registers for
|
|
338
|
+
// the next entry.
|
|
339
|
+
const participants = [];
|
|
340
|
+
for (const path of editPlanPaths(handle)) {
|
|
341
|
+
const support = supportFor(path);
|
|
342
|
+
if (support !== "export" && typeof support !== "function") continue;
|
|
343
|
+
const mod = handle.loaded?.[path];
|
|
344
|
+
// Not loaded: it started nothing, so there is nothing to hand back.
|
|
345
|
+
if (!mod) continue;
|
|
346
|
+
const participant = await (typeof support === "function" ? support(mod) : mod.modeParticipant?.());
|
|
347
|
+
if (typeof participant?.id !== "string" || typeof participant.prepare !== "function") return null;
|
|
348
|
+
participants.push(participant);
|
|
349
|
+
}
|
|
350
|
+
participants.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
351
|
+
const adopted = [];
|
|
352
|
+
const context = {
|
|
353
|
+
generation,
|
|
354
|
+
destination: modeState().destination,
|
|
355
|
+
signal: undefined,
|
|
356
|
+
represented: null,
|
|
357
|
+
reader: null,
|
|
358
|
+
initial: true,
|
|
359
|
+
isCurrent: () => true,
|
|
360
|
+
};
|
|
361
|
+
for (const participant of participants) {
|
|
362
|
+
let lease = null;
|
|
363
|
+
try {
|
|
364
|
+
lease = await participant.prepare(context);
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
if (lease) adopted.push(lease);
|
|
369
|
+
}
|
|
370
|
+
return adopted;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// The bytes and stamp of this session's last accepted save, or null for a session that
|
|
374
|
+
// never saved: the receipt the restarted reader starts from. `generation` is this
|
|
375
|
+
// session's, so a save an older session made is never handed to the next reader.
|
|
376
|
+
async function acceptedReceipt(handle) {
|
|
377
|
+
const save = handle?.loaded?.["core/save.js"] ?? (await import("./save.js").catch(() => null));
|
|
378
|
+
return save?.acceptedReceipt?.(generation) ?? null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// What the deadline answers with, so a wait can tell it apart from its own result.
|
|
382
|
+
const EXPIRED = Symbol("the exit deadline passed");
|
|
383
|
+
|
|
384
|
+
// The deadline every wait of one exit shares, taken before its first await so nothing it
|
|
385
|
+
// waits on can outlive the transition. One promise, so reaching it once is enough to
|
|
386
|
+
// release all of them.
|
|
387
|
+
function startDeadline() {
|
|
388
|
+
let timer = null;
|
|
389
|
+
const at = Date.now() + DEADLINE_MS;
|
|
390
|
+
const expired = new Promise((resolve) => {
|
|
391
|
+
timer = setTimeout(() => resolve(EXPIRED), DEADLINE_MS);
|
|
392
|
+
});
|
|
393
|
+
return { expired, remaining: () => Math.max(0, at - Date.now()), clear: () => clearTimeout(timer) };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// The view plan's missing modules, imported before anything is quiesced: an explicit
|
|
397
|
+
// Document edit never loaded its reader, and the same failure here refuses with nothing
|
|
398
|
+
// stopped. A live entry has them all, so this imports nothing. The import shares the
|
|
399
|
+
// boot's late tail, which an exit does not wait for forever: what the view needs is its
|
|
400
|
+
// reader, and a module still in flight stays cached and evaluates in view when it lands.
|
|
401
|
+
async function importViewPlan(handle, expired) {
|
|
402
|
+
if (!handle) return;
|
|
403
|
+
const plan = await import("./mode-switch.js");
|
|
404
|
+
const importing = plan.importPlan(handle, handle.view);
|
|
405
|
+
importing.catch(() => {});
|
|
406
|
+
const answer = await Promise.race([importing.then(() => null, (err) => err), expired]);
|
|
407
|
+
if (answer && answer !== EXPIRED) throw answer;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Exit. Nothing here is a public flush: the barrier is the one write an exit may make, and
|
|
411
|
+
// it runs under the document-operation slot this transition takes and holds, so a
|
|
412
|
+
// replacement cannot start between the quiesce and the bytes.
|
|
413
|
+
async function leave(handle = null) {
|
|
414
|
+
const deadline = startDeadline();
|
|
415
|
+
try {
|
|
416
|
+
return await leaveWithin(handle, deadline);
|
|
417
|
+
} finally {
|
|
418
|
+
deadline.clear();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function leaveWithin(handle, deadline) {
|
|
423
|
+
// The boot's own sessions stay this exit's until it commits view: a refusal leaves the
|
|
424
|
+
// tab in edit exactly as it was, and the next exit tries again with the same ones.
|
|
425
|
+
const initial = initialSession;
|
|
426
|
+
commitModeFacts({ phase: "leaving" });
|
|
427
|
+
const transition = await import("./mode-transition.js").catch(() => null);
|
|
428
|
+
if (!transition) return refuse("load-failed");
|
|
429
|
+
let held = leases;
|
|
430
|
+
// Adopting the boot's sessions is the first exit's work, and only while nothing has
|
|
431
|
+
// been adopted yet: a refused exit keeps what it adopted to quiesce and dispose next.
|
|
432
|
+
if (initial && !adoptedInitial) {
|
|
433
|
+
adopting ??= adoptInitial(handle);
|
|
434
|
+
// An adoption is a preparation like any other, so the deadline bounds it too: a
|
|
435
|
+
// participant that awaits a boot that never settles cannot hold the exit open.
|
|
436
|
+
const adopted = await Promise.race([adopting, deadline.expired]);
|
|
437
|
+
if (adopted === EXPIRED) return refuse("timeout");
|
|
438
|
+
if (!adopted) {
|
|
439
|
+
adopting = null;
|
|
440
|
+
commitModeFacts({ phase: "idle", reason: null });
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
adoptedInitial = true;
|
|
444
|
+
held = [...adopted, ...held];
|
|
445
|
+
leases = held;
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
await importViewPlan(handle, deadline.expired);
|
|
449
|
+
} catch (err) {
|
|
450
|
+
return refuse(err?.code || "load-failed", "idle", { message: err?.message });
|
|
451
|
+
}
|
|
452
|
+
const quiesced = [];
|
|
453
|
+
let operation = null;
|
|
454
|
+
try {
|
|
455
|
+
operation = transition.claimOperation("mode");
|
|
456
|
+
if (!operation) throw fault("another document operation is in progress", "busy");
|
|
457
|
+
for (const lease of [...held].reverse()) {
|
|
458
|
+
quiesced.push(lease);
|
|
459
|
+
// A quiesce that never settles is the deadline's business: past it the exit fails
|
|
460
|
+
// and every lease it already asked resumes, so the tab stays in edit rather than
|
|
461
|
+
// stuck in `leaving`.
|
|
462
|
+
const answer = await Promise.race([Promise.resolve(lease.quiesce?.()), deadline.expired]);
|
|
463
|
+
if (answer === EXPIRED) throw fault("the exit deadline passed while a lease quiesced", "timeout");
|
|
464
|
+
// A refusal is a refusal whether it was thrown or answered.
|
|
465
|
+
if (answer && answer.ok === false) {
|
|
466
|
+
throw Object.assign(fault("a lease refused the exit", answer.code || "pending-input"), answer);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// The barrier writes under the time this exit has left, not a fresh 15 s.
|
|
470
|
+
const barrier = await transition.runSaveBarrier({ operation, timeoutMs: deadline.remaining() });
|
|
471
|
+
if (!barrier.ok) throw Object.assign(fault("the exit barrier refused", barrier.code || "save-failed"), barrier);
|
|
472
|
+
} catch (err) {
|
|
473
|
+
for (const lease of [...quiesced].reverse()) {
|
|
474
|
+
try {
|
|
475
|
+
lease.resume?.();
|
|
476
|
+
} catch {}
|
|
477
|
+
}
|
|
478
|
+
commitModeFacts({ phase: "idle", reason: err?.code || "save-failed" });
|
|
479
|
+
return { ok: false, mode: current(), code: err?.code || "save-failed", state: err?.state, pending: err?.pending };
|
|
480
|
+
} finally {
|
|
481
|
+
if (operation) transition.releaseOperation(operation);
|
|
482
|
+
}
|
|
483
|
+
disposeInReverse(held);
|
|
484
|
+
leases = [];
|
|
485
|
+
initialSession = false;
|
|
486
|
+
adoptedInitial = false;
|
|
487
|
+
adopting = null;
|
|
488
|
+
// The reader comes back on the file this session last saved, or on the one it was built
|
|
489
|
+
// from when it saved nothing: a second entry commits without waiting for a frame.
|
|
490
|
+
const accepted = (await acceptedReceipt(handle)) ?? entryReceipt;
|
|
491
|
+
entryReceipt = null;
|
|
492
|
+
if (handle) {
|
|
493
|
+
const plan = await import("./mode-switch.js").catch(() => null);
|
|
494
|
+
plan?.startReader(handle, accepted);
|
|
495
|
+
}
|
|
496
|
+
generation++;
|
|
497
|
+
setRootEditMode(false);
|
|
498
|
+
commitModeFacts({ mode: "view", isEditMode: false, phase: "idle", generation, reason: null });
|
|
499
|
+
dropEditmodeParam();
|
|
500
|
+
announceMode();
|
|
501
|
+
return { ok: true, mode: "view", changed: true };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// The switch. Joining the transition already running for the same target answers with
|
|
505
|
+
// that same promise; asking for the other one while it runs answers busy — including for
|
|
506
|
+
// the mode still committed — and leaves the running transition untouched.
|
|
507
|
+
export function requestMode(target, handle = null) {
|
|
508
|
+
if (target !== "edit" && target !== "view") throw new TypeError(`clay.modeLifecycle: unknown mode "${target}"`);
|
|
509
|
+
if (inFlight) return inFlight.target === target ? inFlight.promise : { ok: false, mode: current(), code: "busy" };
|
|
510
|
+
if (target === current()) return { ok: true, mode: current(), changed: false };
|
|
511
|
+
if (isDraftMode || (target === "edit" && !canEdit)) return { ok: false, mode: current(), code: "not-allowed" };
|
|
512
|
+
const promise = (target === "edit" ? enter(handle) : leave(handle)).finally(() => {
|
|
513
|
+
inFlight = null;
|
|
514
|
+
});
|
|
515
|
+
inFlight = { target, promise };
|
|
516
|
+
return promise;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// The public transaction for a document that signed the live-switch contract: the plan it
|
|
520
|
+
// resolved for an editor is checked first, and a document this release cannot start and
|
|
521
|
+
// take back answers `null` so its caller keeps the guarded reload.
|
|
522
|
+
export async function switchMode(target, plan) {
|
|
523
|
+
// A decision this release cannot fetch leaves the caller its guarded reload, and the
|
|
524
|
+
// failed import is not kept: the next ask asks again.
|
|
525
|
+
const decision = await import("./mode-switch.js").catch(() => null);
|
|
526
|
+
if (!decision || !decision.liveAllowed(plan)) return null;
|
|
527
|
+
// A transition already running answers for itself: it joins or refuses, and this call
|
|
528
|
+
// touches nothing on the way there.
|
|
529
|
+
if (inFlight || target === current()) return requestMode(target, plan);
|
|
530
|
+
if (target === "edit") decision.openResources();
|
|
531
|
+
return requestMode(target, plan);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// The explicit initial edit: this tab already starts in edit or draft, so nothing
|
|
535
|
+
// switches. The preboot queue is drained, prepared and enabled before input instead.
|
|
536
|
+
export async function activateInitial() {
|
|
537
|
+
if (current() === "view") return { ok: true, mode: "view", changed: false };
|
|
538
|
+
if (inFlight) return inFlight.promise;
|
|
539
|
+
const promise = enter().finally(() => {
|
|
540
|
+
inFlight = null;
|
|
541
|
+
});
|
|
542
|
+
inFlight = { target: "edit", promise };
|
|
543
|
+
return promise;
|
|
544
|
+
}
|