@panphora/clayjs 1.15.2 → 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,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* presence-target.js — the one target union behind `clay.presence`.
|
|
3
|
+
*
|
|
4
|
+
* Kanban's record targets and a Document's page targets travel on the same wire
|
|
5
|
+
* field (`editing`) and through the same facade. This module is the contract
|
|
6
|
+
* alone: what a target is, what bounds it, how a roster session is read, and the
|
|
7
|
+
* exact request body a target turns into. It imports no Yjs, no ProseMirror, no
|
|
8
|
+
* page code and no visual code, so a reader can validate a frame without any of
|
|
9
|
+
* them, and the host can validate the same value without decoding Yjs.
|
|
10
|
+
*
|
|
11
|
+
* Three shapes are accepted for `editing`:
|
|
12
|
+
*
|
|
13
|
+
* null nobody is anywhere
|
|
14
|
+
* "1.2" the legacy dotted block path, unchanged
|
|
15
|
+
* {kind:"record", id, field?} a Kanban/Journal record (fieldless journals too)
|
|
16
|
+
* {kind:"page", v:1, epoch, generation, selection} a Document's page target
|
|
17
|
+
*
|
|
18
|
+
* Anything else is refused rather than repaired: a frame this module cannot read
|
|
19
|
+
* draws nothing, and a target it cannot read is never published.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { EDITING_PATH } from './editing-path.js';
|
|
23
|
+
|
|
24
|
+
// The extended envelope's version. A body without `v` is the legacy shape.
|
|
25
|
+
export const PRESENCE_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
// Bounds, shared by this client and the server contract it speaks to.
|
|
28
|
+
export const MAX_TARGET_BYTES = 8 * 1024;
|
|
29
|
+
export const MAX_PAGE_ID_BYTES = 256;
|
|
30
|
+
export const MAX_REL_BYTES = 1024;
|
|
31
|
+
export const MAX_RECORD_ID = 128;
|
|
32
|
+
export const MAX_SELECTION_IDS = 128;
|
|
33
|
+
|
|
34
|
+
// Kanban's documented field enum. A record target without a field is valid.
|
|
35
|
+
export const RECORD_FIELDS = Object.freeze([
|
|
36
|
+
'title', 'description', 'labels', 'members', 'dates', 'checklist', 'comment', 'attachments',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Coalesce selection moves to one every 250 ms, renew a standing target every 5 s,
|
|
40
|
+
// and let it expire 15 s after the server received it. The server accepts at most
|
|
41
|
+
// 10 updates per second, with a burst of 20, per admitted session and document.
|
|
42
|
+
export const COALESCE_MS = 250;
|
|
43
|
+
export const RENEW_MS = 5000;
|
|
44
|
+
export const EXPIRE_MS = 15000;
|
|
45
|
+
export const RATE_PER_SECOND = 10;
|
|
46
|
+
export const RATE_BURST = 20;
|
|
47
|
+
|
|
48
|
+
const RECORD_KEYS = new Set(['kind', 'id', 'field']);
|
|
49
|
+
const PAGE_KEYS = new Set(['kind', 'v', 'epoch', 'generation', 'selection']);
|
|
50
|
+
const TEXT_KEYS = new Set(['kind', 'anchor', 'head']);
|
|
51
|
+
const BLOCKS_KEYS = new Set(['kind', 'ids']);
|
|
52
|
+
const CELL_KEYS = new Set(['kind', 'block', 'row', 'col']);
|
|
53
|
+
const POSITION_KEYS = new Set(['block', 'rel']);
|
|
54
|
+
|
|
55
|
+
// Kanban's ids are ASCII and at most 128 characters. Nothing else is an id.
|
|
56
|
+
const RECORD_ID = /^[\x20-\x7e]+$/;
|
|
57
|
+
// A base64 relative position: valid alphabet, valid padding, decodable length.
|
|
58
|
+
const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
59
|
+
const CONTROL = /[\u0000-\u001f\u007f]/;
|
|
60
|
+
|
|
61
|
+
const refuse = (reason) => ({ ok: false, reason });
|
|
62
|
+
const accept = (target) => ({ ok: true, target });
|
|
63
|
+
|
|
64
|
+
const isObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
65
|
+
|
|
66
|
+
const onlyKeys = (value, allowed) => {
|
|
67
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) return false;
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// UTF-8 length by hand: the host bounds bytes, and neither a test environment nor an
|
|
72
|
+
// old browser is guaranteed a TextEncoder.
|
|
73
|
+
function utf8Length(text) {
|
|
74
|
+
let length = 0;
|
|
75
|
+
for (const character of text) {
|
|
76
|
+
const code = character.codePointAt(0);
|
|
77
|
+
length += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
|
|
78
|
+
}
|
|
79
|
+
return length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pageId(value) {
|
|
83
|
+
return typeof value === 'string'
|
|
84
|
+
&& value.length > 0
|
|
85
|
+
&& !CONTROL.test(value)
|
|
86
|
+
&& utf8Length(value) <= MAX_PAGE_ID_BYTES;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function relativePosition(value) {
|
|
90
|
+
if (typeof value !== 'string' || !value.length || value.length % 4 !== 0 || !BASE64.test(value)) return false;
|
|
91
|
+
const padding = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;
|
|
92
|
+
return (value.length / 4) * 3 - padding <= MAX_REL_BYTES;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function positionOf(value) {
|
|
96
|
+
if (!isObject(value) || !onlyKeys(value, POSITION_KEYS)) return null;
|
|
97
|
+
if (!pageId(value.block) || !relativePosition(value.rel)) return null;
|
|
98
|
+
return { block: value.block, rel: value.rel };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function selectionOf(value) {
|
|
102
|
+
if (!isObject(value)) return null;
|
|
103
|
+
|
|
104
|
+
if (value.kind === 'text') {
|
|
105
|
+
if (!onlyKeys(value, TEXT_KEYS)) return null;
|
|
106
|
+
const anchor = positionOf(value.anchor);
|
|
107
|
+
const head = positionOf(value.head);
|
|
108
|
+
return anchor && head ? { kind: 'text', anchor, head } : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (value.kind === 'blocks') {
|
|
112
|
+
if (!onlyKeys(value, BLOCKS_KEYS) || !Array.isArray(value.ids)) return null;
|
|
113
|
+
if (!value.ids.length || value.ids.length > MAX_SELECTION_IDS) return null;
|
|
114
|
+
for (const id of value.ids) if (!pageId(id)) return null;
|
|
115
|
+
return { kind: 'blocks', ids: [...value.ids] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (value.kind === 'cell') {
|
|
119
|
+
if (!onlyKeys(value, CELL_KEYS)) return null;
|
|
120
|
+
if (!pageId(value.block) || !pageId(value.row) || !pageId(value.col)) return null;
|
|
121
|
+
return { kind: 'cell', block: value.block, row: value.row, col: value.col };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function withinSize(target) {
|
|
128
|
+
return utf8Length(JSON.stringify(target)) <= MAX_TARGET_BYTES;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function recordTarget(value) {
|
|
132
|
+
if (!onlyKeys(value, RECORD_KEYS)) return refuse('key');
|
|
133
|
+
const id = value.id;
|
|
134
|
+
if (typeof id !== 'string' || !id.length || id.length > MAX_RECORD_ID || !RECORD_ID.test(id)) return refuse('id');
|
|
135
|
+
if (value.field === undefined) {
|
|
136
|
+
const target = { kind: 'record', id };
|
|
137
|
+
return withinSize(target) ? accept(target) : refuse('size');
|
|
138
|
+
}
|
|
139
|
+
if (typeof value.field !== 'string' || !RECORD_FIELDS.includes(value.field)) return refuse('field');
|
|
140
|
+
const target = { kind: 'record', id, field: value.field };
|
|
141
|
+
return withinSize(target) ? accept(target) : refuse('size');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pageTarget(value) {
|
|
145
|
+
if (!onlyKeys(value, PAGE_KEYS)) return refuse('key');
|
|
146
|
+
if (value.v !== PRESENCE_VERSION) return refuse('version');
|
|
147
|
+
if (!pageId(value.epoch)) return refuse('epoch');
|
|
148
|
+
if (!Number.isSafeInteger(value.generation) || value.generation < 0) return refuse('generation');
|
|
149
|
+
const selection = selectionOf(value.selection);
|
|
150
|
+
if (!selection) return refuse('selection');
|
|
151
|
+
const target = { kind: 'page', v: PRESENCE_VERSION, epoch: value.epoch, generation: value.generation, selection };
|
|
152
|
+
return withinSize(target) ? accept(target) : refuse('size');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Read one `editing` value.
|
|
157
|
+
*
|
|
158
|
+
* @param {*} value
|
|
159
|
+
* @returns {{ok: true, target: null|string|Object}|{ok: false, reason: string}}
|
|
160
|
+
*/
|
|
161
|
+
export function validateTarget(value) {
|
|
162
|
+
if (value === null) return accept(null);
|
|
163
|
+
if (typeof value === 'string') return EDITING_PATH.test(value) ? accept(value) : refuse('path');
|
|
164
|
+
if (!isObject(value)) return refuse('shape');
|
|
165
|
+
if (value.kind === 'record') return recordTarget(value);
|
|
166
|
+
if (value.kind === 'page') return pageTarget(value);
|
|
167
|
+
return refuse('kind');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The person-level `editing` value: the legacy string or a record target. Page
|
|
172
|
+
* coordinates live only in `sessions`, never on a person.
|
|
173
|
+
*/
|
|
174
|
+
export function normalizeEditing(value) {
|
|
175
|
+
const result = validateTarget(value === undefined ? null : value);
|
|
176
|
+
if (!result.ok) return null;
|
|
177
|
+
if (result.target !== null && typeof result.target === 'object' && result.target.kind !== 'record') return null;
|
|
178
|
+
return result.target;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A stable key for a validated target, so the sender can tell whether the target
|
|
183
|
+
* it holds is the one it sent. Expects `validateTarget`'s output.
|
|
184
|
+
*/
|
|
185
|
+
export function targetKey(target) {
|
|
186
|
+
if (target === null || target === undefined) return '';
|
|
187
|
+
if (typeof target === 'string') return `path:${target}`;
|
|
188
|
+
return `target:${JSON.stringify(target)}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Read one public session. `n` is monotonic within a session: a value below the
|
|
193
|
+
* one already seen for that session id cannot overwrite the newer one.
|
|
194
|
+
*
|
|
195
|
+
* @param {Array} value
|
|
196
|
+
* @param {Map<string, Object>} [previous]
|
|
197
|
+
*/
|
|
198
|
+
export function normalizeSessions(value, previous = null) {
|
|
199
|
+
const list = Array.isArray(value) ? value : [];
|
|
200
|
+
const sessions = [];
|
|
201
|
+
for (const item of list) {
|
|
202
|
+
if (!isObject(item)) continue;
|
|
203
|
+
const id = pageId(item.id) ? item.id : null;
|
|
204
|
+
if (!id) continue;
|
|
205
|
+
const n = Number.isSafeInteger(item.n) && item.n >= 0 ? item.n : 0;
|
|
206
|
+
const seen = previous && previous.get(id);
|
|
207
|
+
if (seen && n < seen.n) {
|
|
208
|
+
sessions.push(seen);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const editing = validateTarget(item.editing === undefined ? null : item.editing);
|
|
212
|
+
sessions.push({ id, you: item.you === true, n, editing: editing.ok ? editing.target : null });
|
|
213
|
+
}
|
|
214
|
+
return sessions;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The private recipient-specific transport block. Never published in a snapshot. */
|
|
218
|
+
export function normalizeSelfSession(value) {
|
|
219
|
+
if (!isObject(value)) return null;
|
|
220
|
+
const id = pageId(value.id) ? value.id : null;
|
|
221
|
+
const token = typeof value.token === 'string' && value.token.length ? value.token : null;
|
|
222
|
+
return id && token ? { id, token } : null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The exact body a target is sent as.
|
|
227
|
+
*
|
|
228
|
+
* The legacy dotted path and a clear keep the single-field shape every host
|
|
229
|
+
* already accepts, character for character. A record or page target uses the
|
|
230
|
+
* extended envelope once the connection has issued a session token; before that a
|
|
231
|
+
* record may still travel in the single-field shape, and a page target — whose
|
|
232
|
+
* coordinates need the envelope — has no body at all.
|
|
233
|
+
*
|
|
234
|
+
* @param {{target: *, session?: ?{token: string}, n?: number}} request
|
|
235
|
+
* @returns {?Object}
|
|
236
|
+
*/
|
|
237
|
+
export function requestBody({ target = null, session = null, n = 0 }) {
|
|
238
|
+
if (target === null || target === undefined || typeof target === 'string') return { editing: target ?? null };
|
|
239
|
+
const token = session && typeof session.token === 'string' && session.token.length ? session.token : null;
|
|
240
|
+
if (!token) return target.kind === 'record' ? { editing: target } : null;
|
|
241
|
+
if (!Number.isSafeInteger(n) || n < 0) return null;
|
|
242
|
+
return { v: PRESENCE_VERSION, session: token, n, editing: target };
|
|
243
|
+
}
|
package/src/sync/presence.js
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
* recipient, from that recipient's own access; nothing here asks for a name and
|
|
7
7
|
* nothing here can widen what arrived.
|
|
8
8
|
*
|
|
9
|
+
* The roster itself is not here: it lives in `presence-store.js`, which is what
|
|
10
|
+
* `clay.presence` hands to a page and what a caret adapter reads. This module is
|
|
11
|
+
* the chrome a reader sees — the squares, the count and the tip — and the visual
|
|
12
|
+
* library behind it stays a first-use download, so a reader who draws nothing
|
|
13
|
+
* still has a roster and still fetches none of it.
|
|
14
|
+
*
|
|
9
15
|
* THE RULE THIS FILE EXISTS TO GET RIGHT.
|
|
10
16
|
*
|
|
11
17
|
* captureForSaveAndComparison() clones the document, hands that clone to peers
|
|
@@ -39,15 +45,39 @@
|
|
|
39
45
|
import { hostSupports } from '../core/host-meta.js';
|
|
40
46
|
import { make, set } from '../lib/hostile-css.js';
|
|
41
47
|
import { initialsOf, personColor } from '../lib/person-face.js';
|
|
42
|
-
import {
|
|
43
|
-
import { TOKENS, FONT_MONO, SHADOW } from '../ui/bevel.js';
|
|
48
|
+
import { presence as store } from './presence-store.js';
|
|
44
49
|
|
|
45
50
|
// Two initials at 11px need about 15px of the square, so the overlap has to
|
|
46
51
|
// leave that much clear or the neighbouring square eats the second letter.
|
|
47
52
|
const SIZE = 30;
|
|
48
53
|
const OVERLAP = 6;
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
|
|
55
|
+
// The palette, the marking token and the two faces are the Bevel subset, which is a
|
|
56
|
+
// first-use download: a reader alone on a document never draws a square, so it never
|
|
57
|
+
// fetches them. Everything here is read after loadUI() resolves.
|
|
58
|
+
let ui = null;
|
|
59
|
+
let uiLoad = null;
|
|
60
|
+
|
|
61
|
+
function loadUI() {
|
|
62
|
+
uiLoad ??= Promise.all([import('../ui/bevel-controls.js'), import('../ui/bevel.js')])
|
|
63
|
+
.then(([controls, bevel]) => (ui = {
|
|
64
|
+
pageScheme: controls.pageScheme,
|
|
65
|
+
RUNTIME_ONLY: controls.RUNTIME_ONLY,
|
|
66
|
+
TOKENS: bevel.TOKENS,
|
|
67
|
+
SHADOW: bevel.SHADOW,
|
|
68
|
+
FONT: `600 11px/1 ${bevel.FONT_MONO}`,
|
|
69
|
+
LABEL_FONT: `500 11px/1.4 ${bevel.FONT_MONO}`,
|
|
70
|
+
}))
|
|
71
|
+
.catch((error) => {
|
|
72
|
+
uiLoad = null;
|
|
73
|
+
throw error;
|
|
74
|
+
});
|
|
75
|
+
return uiLoad;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A roster frame never rejects its way back to the stream: a chunk that failed to
|
|
79
|
+
// arrive leaves the corner empty and the next frame asks again.
|
|
80
|
+
const uiReady = () => loadUI().then(() => true, () => false);
|
|
51
81
|
|
|
52
82
|
// all:initial first on every element, so a page rule has nothing left to reach; it
|
|
53
83
|
// resets color-scheme and direction too, which are put back straight after.
|
|
@@ -60,30 +90,18 @@ const FACE = '#FFFCF6';
|
|
|
60
90
|
|
|
61
91
|
/** Every element this module creates, marked the same way. */
|
|
62
92
|
function runtimeRoot(el) {
|
|
63
|
-
el.setAttribute('clay', RUNTIME_ONLY);
|
|
93
|
+
el.setAttribute('clay', ui.RUNTIME_ONLY);
|
|
64
94
|
return el;
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
98
|
+
* What this module draws from a snapshot. The store keeps the rest — the target
|
|
99
|
+
* each person holds and their sessions — because none of it is chrome: a frame that
|
|
100
|
+
* only moved somebody's caret must leave every square where it is.
|
|
71
101
|
*/
|
|
72
|
-
function
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
for (const person of list) {
|
|
76
|
-
if (!person || typeof person.id !== 'string' || !person.id) continue;
|
|
77
|
-
const name = typeof person.name === 'string' && person.name.trim() ? person.name.trim() : 'Someone';
|
|
78
|
-
people.push({
|
|
79
|
-
id: person.id,
|
|
80
|
-
name,
|
|
81
|
-
you: person.you === true,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
const count = data && data.anonymous;
|
|
85
|
-
const anonymous = typeof count === 'number' && Number.isFinite(count) && count > 0 ? Math.floor(count) : 0;
|
|
86
|
-
return { people, anonymous };
|
|
102
|
+
function faces(snapshot) {
|
|
103
|
+
const people = snapshot.people.map((person) => ({ id: person.id, name: person.name, you: person.you }));
|
|
104
|
+
return { people, anonymous: snapshot.anonymous };
|
|
87
105
|
}
|
|
88
106
|
|
|
89
107
|
class Presence {
|
|
@@ -99,10 +117,21 @@ class Presence {
|
|
|
99
117
|
/**
|
|
100
118
|
* Does this host run presence at all? Asked once, of /_/meta, and never
|
|
101
119
|
* inferred from a frame arriving: the answer decides whether the chrome is
|
|
102
|
-
* built, so a host that says nothing draws nothing forever.
|
|
120
|
+
* built, so a host that says nothing draws nothing forever. The two awareness
|
|
121
|
+
* capabilities travel with it, because the store publishes them in the same
|
|
122
|
+
* snapshot and a page reads them before it publishes a target.
|
|
103
123
|
*/
|
|
104
124
|
supported() {
|
|
105
|
-
if (!this._gate)
|
|
125
|
+
if (!this._gate) {
|
|
126
|
+
this._gate = Promise.all([
|
|
127
|
+
hostSupports('presence'),
|
|
128
|
+
hostSupports('record-awareness-v1'),
|
|
129
|
+
hostSupports('page-awareness-v1'),
|
|
130
|
+
]).then(([supported, recordAwareness, pageAwareness]) => {
|
|
131
|
+
store.setCapabilities({ supported, recordAwareness, pageAwareness });
|
|
132
|
+
return supported === true;
|
|
133
|
+
}, () => false);
|
|
134
|
+
}
|
|
106
135
|
return this._gate;
|
|
107
136
|
}
|
|
108
137
|
|
|
@@ -115,19 +144,20 @@ class Presence {
|
|
|
115
144
|
* @returns {Promise<void>}
|
|
116
145
|
*/
|
|
117
146
|
async update(data) {
|
|
118
|
-
const next =
|
|
147
|
+
const next = faces(store.receive(data));
|
|
119
148
|
const same = this.root && JSON.stringify(next) === JSON.stringify(this.roster);
|
|
120
149
|
this.roster = next;
|
|
121
150
|
if (!(await this.supported())) return;
|
|
122
151
|
if (same) {
|
|
123
|
-
set(this.root, 'color-scheme', pageScheme());
|
|
152
|
+
set(this.root, 'color-scheme', ui.pageScheme());
|
|
124
153
|
return;
|
|
125
154
|
}
|
|
126
|
-
this.render();
|
|
155
|
+
await this.render();
|
|
127
156
|
}
|
|
128
157
|
|
|
129
158
|
/** The stream went away, so the roster is no longer true about anything. */
|
|
130
159
|
clear() {
|
|
160
|
+
store.clear();
|
|
131
161
|
this.roster = { people: [], anonymous: 0 };
|
|
132
162
|
if (!this.root) return;
|
|
133
163
|
this.root.remove();
|
|
@@ -137,7 +167,7 @@ class Presence {
|
|
|
137
167
|
this.tip = null;
|
|
138
168
|
}
|
|
139
169
|
|
|
140
|
-
render() {
|
|
170
|
+
async render() {
|
|
141
171
|
const { people, anonymous } = this.roster;
|
|
142
172
|
|
|
143
173
|
// A person alone on a document is told nothing at all. The count is the
|
|
@@ -148,10 +178,11 @@ class Presence {
|
|
|
148
178
|
this.hide();
|
|
149
179
|
return;
|
|
150
180
|
}
|
|
181
|
+
if (!(await uiReady())) return;
|
|
151
182
|
if (!this.build()) return;
|
|
152
183
|
|
|
153
184
|
this.hideTip();
|
|
154
|
-
set(this.root, 'color-scheme', pageScheme());
|
|
185
|
+
set(this.root, 'color-scheme', ui.pageScheme());
|
|
155
186
|
this.stack.textContent = '';
|
|
156
187
|
for (const person of people) {
|
|
157
188
|
this.stack.appendChild(this.avatar(person));
|
|
@@ -184,14 +215,14 @@ class Presence {
|
|
|
184
215
|
|
|
185
216
|
this.root = runtimeRoot(make('div', [
|
|
186
217
|
...RESET,
|
|
187
|
-
`color-scheme:${pageScheme()}`,
|
|
218
|
+
`color-scheme:${ui.pageScheme()}`,
|
|
188
219
|
'position:fixed',
|
|
189
220
|
'top:calc(12px + env(safe-area-inset-top,0px))',
|
|
190
221
|
'right:calc(12px + env(safe-area-inset-right,0px))',
|
|
191
222
|
'z-index:2147483000',
|
|
192
223
|
'display:flex', 'align-items:center', 'gap:8px',
|
|
193
224
|
'max-width:calc(100vw - 24px)',
|
|
194
|
-
`font:${LABEL_FONT}`, `color:${TOKENS.ink}`,
|
|
225
|
+
`font:${ui.LABEL_FONT}`, `color:${ui.TOKENS.ink}`,
|
|
195
226
|
// Only the squares take a pointer. A fixed corner that swallowed clicks
|
|
196
227
|
// would take a bite out of whatever the page put underneath it.
|
|
197
228
|
'pointer-events:none',
|
|
@@ -202,10 +233,10 @@ class Presence {
|
|
|
202
233
|
...RESET,
|
|
203
234
|
'box-sizing:border-box', 'flex:none', 'white-space:nowrap',
|
|
204
235
|
'padding:3px 8px', 'border-radius:0',
|
|
205
|
-
`background:${TOKENS.surface}`, `color:${TOKENS.muted}`,
|
|
206
|
-
`border:1px solid ${TOKENS['line-2']}`,
|
|
207
|
-
`font:${LABEL_FONT}`, 'letter-spacing:.02em',
|
|
208
|
-
`box-shadow:${SHADOW}`,
|
|
236
|
+
`background:${ui.TOKENS.surface}`, `color:${ui.TOKENS.muted}`,
|
|
237
|
+
`border:1px solid ${ui.TOKENS['line-2']}`,
|
|
238
|
+
`font:${ui.LABEL_FONT}`, 'letter-spacing:.02em',
|
|
239
|
+
`box-shadow:${ui.SHADOW}`,
|
|
209
240
|
'display:none',
|
|
210
241
|
'pointer-events:none',
|
|
211
242
|
]));
|
|
@@ -224,9 +255,9 @@ class Presence {
|
|
|
224
255
|
'position:absolute', 'top:100%', 'right:0', 'margin-top:6px',
|
|
225
256
|
'box-sizing:border-box', 'white-space:nowrap', 'pointer-events:none',
|
|
226
257
|
'padding:5px 9px', 'border-radius:0',
|
|
227
|
-
`background:${TOKENS.ink}`, `color:${TOKENS.ground}`,
|
|
228
|
-
`font:${LABEL_FONT}`, 'letter-spacing:.02em',
|
|
229
|
-
`box-shadow:${SHADOW}`,
|
|
258
|
+
`background:${ui.TOKENS.ink}`, `color:${ui.TOKENS.ground}`,
|
|
259
|
+
`font:${ui.LABEL_FONT}`, 'letter-spacing:.02em',
|
|
260
|
+
`box-shadow:${ui.SHADOW}`,
|
|
230
261
|
'display:none',
|
|
231
262
|
]));
|
|
232
263
|
this.tip.setAttribute('data-clay-presence-tip', '');
|
|
@@ -245,10 +276,10 @@ class Presence {
|
|
|
245
276
|
'box-sizing:border-box', 'flex:none',
|
|
246
277
|
`width:${SIZE}px`, `height:${SIZE}px`, 'border-radius:0',
|
|
247
278
|
'display:flex', 'align-items:center', 'justify-content:center',
|
|
248
|
-
`font:${FONT}`, 'letter-spacing:.02em',
|
|
279
|
+
`font:${ui.FONT}`, 'letter-spacing:.02em',
|
|
249
280
|
`margin-left:-${OVERLAP}px`,
|
|
250
281
|
'border-width:2px', 'border-style:solid', 'background-image:none',
|
|
251
|
-
`box-shadow:${SHADOW}`,
|
|
282
|
+
`box-shadow:${ui.SHADOW}`,
|
|
252
283
|
'user-select:none', 'cursor:default', 'pointer-events:auto',
|
|
253
284
|
...skin,
|
|
254
285
|
], initialsOf(person.name)));
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// The reader's half of a live switch: what the saved lane hands to an entry without
|
|
2
|
+
// stopping, and what it takes back when the entry is refused.
|
|
3
|
+
//
|
|
4
|
+
// A reader owns the saved lane — the transport it opened, or the frames generic sync
|
|
5
|
+
// applies to the page — and a switch needs that one lane, never a second stream. So
|
|
6
|
+
// preparation holds the queue instead of closing it: an apply already on the wire
|
|
7
|
+
// finishes, a file that arrives meanwhile keeps its place, and a repair the host asked
|
|
8
|
+
// for stays owed. `reconcile` applies what was held, `resume` gives the lane back
|
|
9
|
+
// exactly as it was, and `commit` retires it only onto a receipt a successor can build
|
|
10
|
+
// from. Nothing here clears a buffer, aborts a request or stops a stream before commit.
|
|
11
|
+
//
|
|
12
|
+
// This is a dynamic target: a reader that never enters edit never fetches it.
|
|
13
|
+
|
|
14
|
+
const fail = (message, code) => Object.assign(new Error(`clayjs: ${message}`), { code });
|
|
15
|
+
|
|
16
|
+
const newer = (staged, candidate) =>
|
|
17
|
+
!staged || candidate.seq === null || staged.seq === null || candidate.seq > staged.seq ? candidate : staged;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A transport's handoff, built out of the transport's own takeover points: `adopt`
|
|
21
|
+
* moves the acknowledged sink here, so nothing is applied and the receipt still moves
|
|
22
|
+
* only onto bytes a sink took. `stop` is never called before commit.
|
|
23
|
+
*
|
|
24
|
+
* `yieldArticle` gives the article up to a switch that mounts over it and
|
|
25
|
+
* `reclaimArticle` takes it back when that switch is refused; both are the reader's
|
|
26
|
+
* own, so the queue and the receipt never move while the article is not its.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} lane
|
|
29
|
+
* @param {Object} lane.stream - the transport this reader opened
|
|
30
|
+
* @param {function(Object): boolean} lane.apply - apply one file to the article
|
|
31
|
+
* @param {function(): boolean} lane.retire - give up the article and its claim
|
|
32
|
+
* @param {function(): boolean} lane.yieldArticle - release the article, keep the queue
|
|
33
|
+
* @param {function(): boolean} lane.reclaimArticle - take the article back and repaint
|
|
34
|
+
*/
|
|
35
|
+
function streamHandoff({ stream, apply, retire, yieldArticle = () => false, reclaimArticle = () => false }) {
|
|
36
|
+
let staged = null;
|
|
37
|
+
let yielded = false;
|
|
38
|
+
stream.adopt({
|
|
39
|
+
onSnapshot: (candidate) => {
|
|
40
|
+
staged = newer(staged, candidate);
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const take = () => {
|
|
45
|
+
const candidate = staged;
|
|
46
|
+
staged = null;
|
|
47
|
+
return candidate;
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
get represented() {
|
|
51
|
+
return stream.represented;
|
|
52
|
+
},
|
|
53
|
+
// The newest file this reader holds: a snapshot the transport took while the
|
|
54
|
+
// switch prepared is a file the session was not built from.
|
|
55
|
+
get held() {
|
|
56
|
+
return staged ?? stream.represented;
|
|
57
|
+
},
|
|
58
|
+
yieldArticle() {
|
|
59
|
+
if (yielded || !yieldArticle()) return false;
|
|
60
|
+
yielded = true;
|
|
61
|
+
return true;
|
|
62
|
+
},
|
|
63
|
+
reclaimArticle() {
|
|
64
|
+
if (!yielded || !reclaimArticle()) return false;
|
|
65
|
+
yielded = false;
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
reconcile() {
|
|
69
|
+
const candidate = take();
|
|
70
|
+
if (candidate && !apply(candidate)) return Promise.reject(fail("the switch could not apply the saved file", "load-failed"));
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
},
|
|
73
|
+
resume() {
|
|
74
|
+
stream.adopt({ onSnapshot: apply });
|
|
75
|
+
const candidate = take();
|
|
76
|
+
if (candidate) apply(candidate);
|
|
77
|
+
},
|
|
78
|
+
commit() {
|
|
79
|
+
// Whatever the old owner was never shown goes on screen first: the receipt a
|
|
80
|
+
// successor is built from and the article it replaces must name one file. An
|
|
81
|
+
// article already yielded belongs to the switch: it reconciled the queue and
|
|
82
|
+
// refused anything newer before asking for this commit, so nothing staged is
|
|
83
|
+
// its to paint, and the read-only half only stops the stream and retires.
|
|
84
|
+
if (!yielded) {
|
|
85
|
+
const candidate = take();
|
|
86
|
+
if (candidate && !apply(candidate)) return false;
|
|
87
|
+
}
|
|
88
|
+
if (stream.represented.bytes === null) return false;
|
|
89
|
+
stream.stop();
|
|
90
|
+
return retire();
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A reader's handoff: its own transport, or the engine that feeds the page.
|
|
97
|
+
*
|
|
98
|
+
* @param {Object} reader
|
|
99
|
+
* @param {?Object} reader.stream - the transport this reader owns, if any
|
|
100
|
+
* @param {Object} reader.options - `{ signal, deadline }` for the lane
|
|
101
|
+
* @param {function(Object): boolean} reader.apply - apply one file to the article
|
|
102
|
+
* @param {function(): boolean} reader.retire - give up the article and its claim
|
|
103
|
+
* @param {function(): boolean} reader.yieldArticle - release the article, keep the queue
|
|
104
|
+
* @param {function(): boolean} reader.reclaimArticle - take the article back and repaint
|
|
105
|
+
* @returns {Promise<Object>}
|
|
106
|
+
*/
|
|
107
|
+
export async function readerHandoff({ stream, options, apply, retire, yieldArticle = () => false, reclaimArticle = () => false }) {
|
|
108
|
+
if (stream) return streamHandoff({ stream, apply, retire, yieldArticle, reclaimArticle });
|
|
109
|
+
const lane = await import("./live-sync.js").then(({ liveSync }) => liveSync.prepareHandoff(options));
|
|
110
|
+
return {
|
|
111
|
+
get represented() {
|
|
112
|
+
return lane.represented;
|
|
113
|
+
},
|
|
114
|
+
get held() {
|
|
115
|
+
return lane.held;
|
|
116
|
+
},
|
|
117
|
+
yieldArticle,
|
|
118
|
+
reclaimArticle,
|
|
119
|
+
reconcile: () => lane.reconcile(),
|
|
120
|
+
resume: () => lane.resume(),
|
|
121
|
+
commit: () => lane.commit() && retire(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A sync engine's handoff: hold the saved lane's frames and the repair it owes, and
|
|
127
|
+
* answer with the file it last applied.
|
|
128
|
+
*
|
|
129
|
+
* @param {Object} lane - the engine instance
|
|
130
|
+
* @param {Object} options - `{ signal, deadline }`
|
|
131
|
+
* @returns {Promise<Object>}
|
|
132
|
+
*/
|
|
133
|
+
export async function engineHandoff(lane, { signal, deadline = 15000 } = {}) {
|
|
134
|
+
const until = Date.now() + deadline;
|
|
135
|
+
// An apply already in flight is the one thing preparation waits for: its bytes are
|
|
136
|
+
// the file the switch will be built from.
|
|
137
|
+
while (lane._morphInFlight) {
|
|
138
|
+
if (signal?.aborted) {
|
|
139
|
+
lane._handoffHeld = false;
|
|
140
|
+
throw fail("the switch was cancelled", "aborted");
|
|
141
|
+
}
|
|
142
|
+
if (Date.now() >= until) {
|
|
143
|
+
lane._handoffHeld = false;
|
|
144
|
+
throw fail("the apply did not settle", "timeout");
|
|
145
|
+
}
|
|
146
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
147
|
+
}
|
|
148
|
+
const queued = () => lane._pendingHtml != null || lane._pendingExternal != null;
|
|
149
|
+
return {
|
|
150
|
+
get represented() {
|
|
151
|
+
return lane._represented;
|
|
152
|
+
},
|
|
153
|
+
// The newest file the lane holds, queued or applied: a frame the engine took
|
|
154
|
+
// while the switch prepared is newer than the one on the page, and a session
|
|
155
|
+
// built from the older one must refuse rather than stop the lane over it.
|
|
156
|
+
get held() {
|
|
157
|
+
let newest = lane._represented;
|
|
158
|
+
if (lane._pendingHtml != null) {
|
|
159
|
+
newest = newer(newest, {
|
|
160
|
+
bytes: lane._pendingHtml,
|
|
161
|
+
etag: lane._pendingEtag ?? null,
|
|
162
|
+
seq: lane._pendingSeq ?? null,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (lane._pendingExternal) {
|
|
166
|
+
newest = newer(newest, {
|
|
167
|
+
bytes: lane._pendingExternal.html,
|
|
168
|
+
etag: lane._pendingExternal.etag ?? null,
|
|
169
|
+
seq: lane._pendingExternal.seq ?? null,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return newest;
|
|
173
|
+
},
|
|
174
|
+
async reconcile() {
|
|
175
|
+
if (!lane._handoff || lane.isDestroyed) return;
|
|
176
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
177
|
+
if (lane._morphInFlight) {
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (!queued()) return;
|
|
182
|
+
await lane._runPending();
|
|
183
|
+
}
|
|
184
|
+
if (queued()) throw fail("the held frame could not be applied", "load-failed");
|
|
185
|
+
},
|
|
186
|
+
resume() {
|
|
187
|
+
if (!lane._handoff) return;
|
|
188
|
+
lane._handoff = null;
|
|
189
|
+
lane._handoffHeld = false;
|
|
190
|
+
lane._scheduleNextFrame();
|
|
191
|
+
},
|
|
192
|
+
commit() {
|
|
193
|
+
if (!lane._represented.bytes) return false;
|
|
194
|
+
lane._handoff = null;
|
|
195
|
+
lane._handoffHeld = false;
|
|
196
|
+
lane.stop();
|
|
197
|
+
return true;
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|