@panphora/clayjs 1.15.2 → 1.17.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 +84291 -57611
- 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-ZUE7PSCI.js +1 -0
- package/dist/m/attrs/save-freeze-MYNVXREN.js +1 -0
- package/dist/m/c-2AZDZWCK.js +1 -0
- package/dist/m/c-2SPVQSHC.js +1 -0
- package/dist/m/c-2YYDMLNZ.js +1 -0
- package/dist/m/c-336SGU5W.js +1 -0
- package/dist/m/c-33JJ6CBX.js +2 -0
- package/dist/m/c-3GJ4SDE2.js +1 -0
- package/dist/m/c-3H6BZDNJ.js +6 -0
- package/dist/m/c-3OWDMNJR.js +2 -0
- package/dist/m/c-3RUU4TPX.js +91 -0
- package/dist/m/c-3XIRD4NU.js +1 -0
- package/dist/m/c-46XYM55L.js +1 -0
- package/dist/m/c-4DFEJ2PO.js +3 -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-56QRZTZH.js +1 -0
- package/dist/m/c-5J4E7O4Y.js +4 -0
- package/dist/m/c-5LKDWQTL.js +19 -0
- package/dist/m/c-5QWQ7A5L.js +1 -0
- package/dist/m/c-5RJFTLSF.js +1 -0
- package/dist/m/c-5V3MOVJT.js +1 -0
- package/dist/m/c-5VYOWDTI.js +1 -0
- package/dist/m/c-5YIHJ3TJ.js +1 -0
- package/dist/m/c-6AZOGAPE.js +1 -0
- package/dist/m/c-6CRUUXOX.js +5 -0
- package/dist/m/c-6HMB24EV.js +1 -0
- package/dist/m/c-6KEVEQ7L.js +1 -0
- package/dist/m/c-6NC5I2TV.js +1 -0
- package/dist/m/c-74MGYWZF.js +1 -0
- package/dist/m/c-7DJDKPGL.js +1 -0
- package/dist/m/c-7OLLJPWP.js +1 -0
- package/dist/m/c-7SWMYKAI.js +1 -0
- package/dist/m/c-A3E3NIMC.js +1 -0
- package/dist/m/c-A7LS5UZW.js +1 -0
- package/dist/m/c-AMSNPOCX.js +1 -0
- package/dist/m/c-AQLFEATB.js +1 -0
- package/dist/m/c-B3JM5ZXC.js +1 -0
- package/dist/m/c-BBVGTQG7.js +1 -0
- package/dist/m/c-BPF2NSPV.js +1 -0
- package/dist/m/c-BQ6LOBEE.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-CJ3A7ZKE.js +1 -0
- package/dist/m/c-CQHPGKX2.js +1 -0
- package/dist/m/c-CYWLPPUS.js +1 -0
- package/dist/m/c-CZUSOB7V.js +1 -0
- package/dist/m/c-DCFSEJZG.js +1 -0
- package/dist/m/c-DCXOXBF3.js +1 -0
- package/dist/m/c-DFS4XZJV.js +2 -0
- package/dist/m/c-DNENWY4O.js +1 -0
- package/dist/m/c-DNZ5IFM6.js +1 -0
- package/dist/m/c-E4LIR5X5.js +1 -0
- package/dist/m/c-EJTGYSLN.js +1 -0
- package/dist/m/c-EVCQ3BKA.js +1 -0
- package/dist/m/c-EZU56NQV.js +34 -0
- package/dist/m/c-FDNHHZY5.js +1 -0
- package/dist/m/c-FILLTJA7.js +1 -0
- package/dist/m/c-FRLHSY7E.js +1 -0
- package/dist/m/c-FROJUZMV.js +1 -0
- package/dist/m/c-G2JPKCTP.js +1 -0
- package/dist/m/c-GB6TU7WV.js +1 -0
- package/dist/m/c-GFJJEBTU.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-GSFIGKOP.js +1 -0
- package/dist/m/c-GY422N3Q.js +1 -0
- package/dist/m/c-H4LG442Q.js +1 -0
- package/dist/m/c-HA6INT6J.js +1 -0
- package/dist/m/c-HAWFNBYG.js +1 -0
- package/dist/m/c-HDDEEL77.js +1 -0
- package/dist/m/c-HFKUVACH.js +1 -0
- package/dist/m/c-HHDFCNHV.js +1 -0
- package/dist/m/c-HPURYVZS.js +2 -0
- package/dist/m/c-I2ZATQCD.js +2 -0
- package/dist/m/c-IEM4NTCS.js +1 -0
- package/dist/m/c-IJUK2GPW.js +1 -0
- package/dist/m/c-IW6GGOU2.js +1 -0
- package/dist/m/c-JAX2667S.js +6 -0
- package/dist/m/c-JDCX26UP.js +25 -0
- package/dist/m/c-JRKY44DK.js +1 -0
- package/dist/m/c-JU3NK25H.js +1 -0
- package/dist/m/c-KAU6E43D.js +1 -0
- package/dist/m/c-KBS3UV6B.js +1 -0
- package/dist/m/c-KGVGROZT.js +1 -0
- package/dist/m/c-KI7M4AY3.js +1 -0
- package/dist/m/c-KIPQJX4B.js +1 -0
- package/dist/m/c-KQ6TYJAP.js +135 -0
- package/dist/m/c-KU3ZFADW.js +1 -0
- package/dist/m/c-L4WQSC27.js +1 -0
- package/dist/m/c-LFPHCXY3.js +1 -0
- package/dist/m/c-LRPUO2MX.js +1 -0
- package/dist/m/c-LWLBHLVC.js +3 -0
- package/dist/m/c-LZJPNR2W.js +1 -0
- package/dist/m/c-M2L47S2X.js +7 -0
- package/dist/m/c-MFP2ZWD4.js +1 -0
- package/dist/m/c-MKSQ4Y57.js +1 -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-NRPJEVJS.js +1 -0
- package/dist/m/c-NUFFJVVJ.js +1 -0
- package/dist/m/c-NUHP2XDB.js +1 -0
- package/dist/m/c-NUHSPT5P.js +1 -0
- package/dist/m/c-O72HVYSA.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-OW5QIGG3.js +1 -0
- package/dist/m/c-P7KNDG34.js +1 -0
- package/dist/m/c-PGRCK73F.js +1 -0
- package/dist/m/c-PQ5LK6QE.js +993 -0
- package/dist/m/c-PS5ARVWR.js +9 -0
- package/dist/m/c-PZ4XEPUP.js +2 -0
- package/dist/m/c-Q622NS2Y.js +1 -0
- package/dist/m/c-Q7NQD65V.js +1 -0
- package/dist/m/c-QH63LQOS.js +1 -0
- package/dist/m/c-QHIWXHRO.js +1 -0
- package/dist/m/c-QHWJ5ZIJ.js +1 -0
- package/dist/m/c-QICP2YR5.js +1 -0
- package/dist/m/c-QJ4OM3G6.js +1 -0
- package/dist/m/c-QNKREOL6.js +1 -0
- package/dist/m/c-R3L7ZJGB.js +2 -0
- package/dist/m/c-R7IWVINQ.js +1 -0
- package/dist/m/c-RM7J7BV2.js +1 -0
- package/dist/m/c-RSMOODDU.js +1 -0
- package/dist/m/c-RVMWGXEJ.js +1 -0
- package/dist/m/c-RYUXUUBX.js +1 -0
- package/dist/m/c-S4ZEH46B.js +9 -0
- package/dist/m/c-S7H2DRCB.js +1 -0
- package/dist/m/c-S7LK4KUE.js +1 -0
- package/dist/m/c-SDAXDM2D.js +1 -0
- package/dist/m/c-SGIA4IEQ.js +188 -0
- package/dist/m/c-SMBZG5X4.js +1 -0
- package/dist/m/c-SMGU7RT3.js +235 -0
- package/dist/m/c-SPIORRYS.js +1 -0
- package/dist/m/c-SWLRT66R.js +1 -0
- package/dist/m/c-SZGYW5HT.js +1 -0
- package/dist/m/c-TCCA5BZN.js +2 -0
- package/dist/m/c-TCJTVAFN.js +1 -0
- package/dist/m/c-TJ6WOJL3.js +67 -0
- package/dist/m/c-TQEMVJCS.js +1 -0
- package/dist/m/c-TRS6TM6O.js +1 -0
- package/dist/m/c-TYLXX4NF.js +21 -0
- package/dist/m/c-U62OB4BT.js +1 -0
- package/dist/m/c-U6YWUAR3.js +8 -0
- package/dist/m/c-UDCSU5G5.js +2 -0
- package/dist/m/c-UDUUJPFZ.js +1 -0
- package/dist/m/c-UGHH4MB3.js +1 -0
- package/dist/m/c-UL2YUY52.js +1 -0
- package/dist/m/c-VAK7SG37.js +1 -0
- package/dist/m/c-VDSS4K3Z.js +1 -0
- package/dist/m/c-VE6ZMYMC.js +1 -0
- package/dist/m/c-VG5XIN4T.js +1 -0
- package/dist/m/c-VHM5XQHK.js +1 -0
- package/dist/m/c-VHNSBD2M.js +1 -0
- package/dist/m/c-VI5RMSVY.js +1 -0
- package/dist/m/c-VKEDWYDF.js +1 -0
- package/dist/m/c-VRZN56XY.js +1 -0
- package/dist/m/c-W5MCHEGA.js +1 -0
- package/dist/m/c-WDC5GVKU.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-WWXIDH4O.js +28 -0
- package/dist/m/c-WYFDLITL.js +1 -0
- package/dist/m/c-XCNTK76U.js +1 -0
- package/dist/m/c-XMS6DPBR.js +1 -0
- package/dist/m/c-XN6MBKI6.js +67 -0
- package/dist/m/c-XPELB6E5.js +1 -0
- package/dist/m/c-XQXZAO7N.js +1 -0
- package/dist/m/c-Y5HBBIQ4.js +1 -0
- package/dist/m/c-ZE46QDIH.js +1 -0
- package/dist/m/c-ZUCOKOHT.js +1 -0
- package/dist/m/c-ZVPVXTJF.js +4 -0
- package/dist/m/c-ZVRKXKM7.js +1 -0
- package/dist/m/c-ZWIOJGMT.js +1 -0
- package/dist/m/core/admin-attrs-QHOKPJ7W.js +1 -0
- package/dist/m/core/autosave-HRYGUWV6.js +1 -0
- package/dist/m/core/conflict-notice-6MROJN2L.js +1 -0
- package/dist/m/core/draft-mode-WSQAT67S.js +1 -0
- package/dist/m/core/host-permission-FNXVEFNE.js +1 -0
- package/dist/m/core/is-edit-mode-KTWA3O5N.js +1 -0
- package/dist/m/core/persist-JFYRHMRJ.js +3 -0
- package/dist/m/core/recovery-FLTCC2OC.js +1 -0
- package/dist/m/core/save-HQZ4S667.js +1 -0
- package/dist/m/core/save-core-KGQWFUNH.js +1 -0
- package/dist/m/core/snapshot-EGQZNT45.js +1 -0
- package/dist/m/core/unsaved-warning-IDJLD66H.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-RN2JNGBV.js +1 -0
- package/dist/m/internals/index-PVXGPUDA.js +1 -0
- package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
- package/dist/m/lib/region-policy-SB2AWVG5.js +1 -0
- package/dist/m/loader-S4I7GKXR.js +1 -0
- package/dist/m/manifest-4C419D67.js +1 -0
- package/dist/m/manifest-edit-B077F288.js +1 -0
- package/dist/m/manifest.json +3988 -0
- package/dist/m/options/options-EJMS3K24.js +1 -0
- package/dist/m/page/editor-NNJA4OKD.js +202 -0
- package/dist/m/plugins/ai-edit-QN4PMMNL.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-RM5IR6AO.js +1 -0
- package/dist/m/plugins/derive-UAAA4V4S.js +1 -0
- package/dist/m/plugins/indicator-URDBBOQO.js +1 -0
- package/dist/m/plugins/live-HFWD4OIH.js +1 -0
- package/dist/m/plugins/menu-3HIEQSI6.js +138 -0
- package/dist/m/plugins/message-LQ6EDH4V.js +1 -0
- package/dist/m/plugins/page-JOKB6XPR.js +2 -0
- package/dist/m/plugins/page-view-NXOJHNW6.js +1 -0
- package/dist/m/plugins/people-SGPJKANK.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-NRQDSWX6.js +517 -0
- package/dist/m/plugins/richclay-NRQDSWX6.js.LEGAL.txt +1 -0
- package/dist/m/plugins/sortable-GWT4PWDV.js +1 -0
- package/dist/m/plugins/source-4ZRSAIYM.js +1 -0
- package/dist/m/plugins/undo-VDA5P7FH.js +1 -0
- package/dist/m/plugins/unsent-HJFLQJ2O.js +1 -0
- package/dist/m/plugins/upgrade-W7FG5VFG.js +1 -0
- package/dist/m/plugins/upload-WLYKK6TL.js +1 -0
- package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +21 -0
- package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +1 -0
- package/dist/m/plugins/wire-4N6GYAF6.js +1 -0
- package/dist/m/reader-core-DXYCURFF.js +68 -0
- package/dist/m/route/route-KOCZSEGN.js +1 -0
- package/dist/m/sync/live-sync-J7VMYTKS.js +1 -0
- package/dist/m/ui/index-3JLVI27O.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 +4034 -0
- package/dist/page/themes/light-1.css +430 -0
- package/dist/server/core.cjs +23748 -22855
- 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/boot-phase.js +2 -1
- package/src/core/conflict-download.js +19 -23
- package/src/core/conflict-notice-ui.js +1173 -0
- package/src/core/conflict-notice.js +140 -697
- package/src/core/download.js +32 -0
- package/src/core/edit-mode.js +147 -14
- package/src/core/etag.js +30 -12
- package/src/core/host-meta.js +90 -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 +113 -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 +806 -119
- package/src/core/snapshot.js +159 -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 +140 -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 +113 -6
- 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 +3 -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/chart-tables.js +11 -0
- package/src/page/chart.js +6 -10
- package/src/page/checklist.js +1 -1
- package/src/page/checkpoint-proof.js +289 -0
- package/src/page/chrome.js +431 -63
- package/src/page/csv-import.js +17 -2
- package/src/page/document-title-dom.js +52 -0
- package/src/page/document-title-runtime.js +90 -0
- package/src/page/document-title.js +12 -0
- package/src/page/drag.js +48 -13
- package/src/page/drop.js +2 -1
- package/src/page/editor.js +399 -90
- package/src/page/emoji-data.js +162 -0
- package/src/page/feature-loader.js +72 -0
- package/src/page/features/board.js +21 -0
- package/src/page/features/chart.js +11 -0
- package/src/page/features/code.js +13 -0
- package/src/page/features/dates.js +180 -0
- package/src/page/features/emoji.js +187 -0
- package/src/page/features/media.js +290 -0
- package/src/page/features/mentions.js +188 -0
- package/src/page/features/registry.js +28 -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-command.js +9 -0
- package/src/page/format-gate.js +179 -0
- package/src/page/format-host.js +54 -0
- package/src/page/format-level.js +157 -0
- package/src/page/format-native-commands.js +142 -0
- package/src/page/format-native.js +42 -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 +45 -28
- package/src/page/items.js +67 -14
- package/src/page/link.js +1 -0
- package/src/page/markdown.js +112 -3
- package/src/page/media-views.js +3 -0
- package/src/page/media.js +33 -245
- package/src/page/mention-picker.js +99 -0
- package/src/page/merge.js +1 -1
- package/src/page/outline-cache.js +11 -0
- package/src/page/placeholder.js +27 -7
- package/src/page/projection.js +240 -51
- 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 +46 -4
- package/src/page/save-states.js +1 -1
- package/src/page/schema.js +47 -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/content.css +25 -1
- package/src/page/themes/light-1.css +163 -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-hooks.js +32 -0
- package/src/page/view.js +390 -107
- package/src/page/ybind/awareness.js +172 -0
- package/src/page/ybind/binding.js +40 -5
- 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 +26 -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 +97 -24
- package/src/page/ybind/recovery-proof.js +191 -0
- package/src/page/ybind/recovery-replay.js +488 -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 +319 -35
- 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 +1131 -270
- 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/intro.js +2 -1
- 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 +544 -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,105 @@
|
|
|
1
|
+
import { setCloneCanonicalizer } from "./snapshot.js";
|
|
2
|
+
import {
|
|
3
|
+
LOCAL_PREFIX,
|
|
4
|
+
canonicalizeRuntimeURL,
|
|
5
|
+
parseRuntimeURL,
|
|
6
|
+
} from "../lib/offline-runtime.js";
|
|
7
|
+
|
|
8
|
+
// The release's exact aliases, from compat/journal-assets.lock.json: the only foreign URLs a
|
|
9
|
+
// saved page may get back for a local runtime address. A unit test keeps the two equal.
|
|
10
|
+
export const RUNTIME_ALIASES = Object.freeze({
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/overtype@2.6.1/dist/overtype.min.js": "compat/overtype-2.6.1.min.js",
|
|
12
|
+
"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js": "compat/highlight-11.9.0.min.js",
|
|
13
|
+
"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;1,400;1,500&family=IBM+Plex+Sans:wght@400;500;600&display=swap": "fonts/ibm-plex.css",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const RUNTIME_MAP_ID = "clay-runtime-map";
|
|
17
|
+
|
|
18
|
+
const RUNTIME_REL = new Set(["stylesheet", "preload", "modulepreload"]);
|
|
19
|
+
|
|
20
|
+
let cachedText = null;
|
|
21
|
+
let cachedDescriptor = null;
|
|
22
|
+
|
|
23
|
+
function parseDescriptor(text) {
|
|
24
|
+
let value;
|
|
25
|
+
try {
|
|
26
|
+
value = JSON.parse(text);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (!value || typeof value !== "object" || value.schema !== 1) return null;
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function runtimeDescriptor() {
|
|
35
|
+
if (typeof document === "undefined") return null;
|
|
36
|
+
const element = document.getElementById(RUNTIME_MAP_ID);
|
|
37
|
+
if (!element) return null;
|
|
38
|
+
const text = element.textContent ?? "";
|
|
39
|
+
if (text === cachedText) return cachedDescriptor;
|
|
40
|
+
cachedText = text;
|
|
41
|
+
cachedDescriptor = parseDescriptor(text);
|
|
42
|
+
return cachedDescriptor;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const RUNTIME_SELECTOR = `script[src*="${LOCAL_PREFIX}"], link[href*="${LOCAL_PREFIX}"], #${RUNTIME_MAP_ID}`;
|
|
46
|
+
|
|
47
|
+
// Native selector first: this runs on every capture, dirty checks included, and almost every page
|
|
48
|
+
// has nothing to canonicalize.
|
|
49
|
+
function holdsRuntime(root) {
|
|
50
|
+
if (root.querySelector(RUNTIME_SELECTOR)) return true;
|
|
51
|
+
for (const template of root.querySelectorAll("template")) {
|
|
52
|
+
if (holdsRuntime(template.content)) return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function elements(root) {
|
|
58
|
+
const found = [];
|
|
59
|
+
const stack = [root];
|
|
60
|
+
while (stack.length) {
|
|
61
|
+
const node = stack.pop();
|
|
62
|
+
for (const child of node.children ?? []) {
|
|
63
|
+
found.push(child);
|
|
64
|
+
stack.push(child);
|
|
65
|
+
if (child.content) stack.push(child.content);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function candidateAttribute(element) {
|
|
72
|
+
if (element.tagName === "SCRIPT" && element.hasAttribute("src")) return "src";
|
|
73
|
+
if (element.tagName !== "LINK" || !element.hasAttribute("href")) return null;
|
|
74
|
+
const rel = (element.getAttribute("rel") ?? "").toLowerCase();
|
|
75
|
+
return rel.split(/\s+/).some((token) => RUNTIME_REL.has(token)) ? "href" : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function canonicalizeRuntimeClone(clone) {
|
|
79
|
+
if (!clone || !holdsRuntime(clone)) return;
|
|
80
|
+
|
|
81
|
+
const candidates = [];
|
|
82
|
+
let runtime = false;
|
|
83
|
+
for (const element of elements(clone)) {
|
|
84
|
+
if (element.getAttribute("id") === RUNTIME_MAP_ID) {
|
|
85
|
+
element.remove();
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const name = candidateAttribute(element);
|
|
89
|
+
if (!name) continue;
|
|
90
|
+
candidates.push({ element, name });
|
|
91
|
+
if (!runtime && (element.getAttribute(name) ?? "").includes(LOCAL_PREFIX)) runtime = true;
|
|
92
|
+
}
|
|
93
|
+
if (!runtime) return;
|
|
94
|
+
|
|
95
|
+
const documentURL = location.href;
|
|
96
|
+
const descriptor = runtimeDescriptor();
|
|
97
|
+
for (const { element, name } of candidates) {
|
|
98
|
+
const value = element.getAttribute(name);
|
|
99
|
+
const parsed = parseRuntimeURL(value, { documentURL });
|
|
100
|
+
if (!parsed || parsed.kind !== "local") continue;
|
|
101
|
+
element.setAttribute(name, canonicalizeRuntimeURL(value, { descriptor, documentURL, aliases: RUNTIME_ALIASES }));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setCloneCanonicalizer(canonicalizeRuntimeClone);
|
package/src/core/save-core.js
CHANGED
|
@@ -13,11 +13,16 @@ import { consumeUserDriven, consumeExplicitSave, markUserDriven } from "../lib/u
|
|
|
13
13
|
import { saveToken } from "./host-attrs.js";
|
|
14
14
|
import { lastSeenEtag, conditionalSaves, recordEtag } from "./etag.js";
|
|
15
15
|
import { hostMeta } from "./host-meta.js";
|
|
16
|
+
import { saveSize, storedFile, knownSaveLimit, saveLimit, announceSaveSize } from "./save-size.js";
|
|
17
|
+
import { PAGE_FORMAT, PAGE_FORMAT_HEADER, knownPageFormatFloor, withPageFormatFloor } from "./page-format.js";
|
|
18
|
+
import "./runtime-url.js";
|
|
16
19
|
import {
|
|
17
20
|
getPageContents,
|
|
18
21
|
onSnapshot,
|
|
19
22
|
addDocumentTransform,
|
|
20
|
-
currentCaptureId
|
|
23
|
+
currentCaptureId,
|
|
24
|
+
captureArtifact,
|
|
25
|
+
releaseCaptureArtifact
|
|
21
26
|
} from "./snapshot.js";
|
|
22
27
|
|
|
23
28
|
// =============================================================================
|
|
@@ -100,10 +105,16 @@ export function onSaveAccepted(callback) {
|
|
|
100
105
|
saveAcceptedHooks.push(callback);
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
function notifySaveAccepted(html, captureId) {
|
|
108
|
+
function notifySaveAccepted(html, captureId, artifact = null) {
|
|
104
109
|
for (const hook of saveAcceptedHooks) {
|
|
105
110
|
try {
|
|
106
|
-
|
|
111
|
+
// The artifact is the capture's own private model of these bytes, and it is
|
|
112
|
+
// offered only when it exists: a raw save and a capture without one keep the
|
|
113
|
+
// shape they had.
|
|
114
|
+
const accepted = artifact
|
|
115
|
+
? { captureId: captureId ?? null, artifact }
|
|
116
|
+
: { captureId: captureId ?? null };
|
|
117
|
+
hook(html, accepted);
|
|
107
118
|
} catch (err) {
|
|
108
119
|
console.error('clayjs: onSaveAccepted hook failed:', err);
|
|
109
120
|
}
|
|
@@ -242,7 +253,7 @@ export function saveFateIsUnknown() {
|
|
|
242
253
|
* zeroed array is the failure that would do exactly that, silently, so a missing
|
|
243
254
|
* crypto implementation falls back to something random rather than to zeros.
|
|
244
255
|
*/
|
|
245
|
-
function mintSaveId() {
|
|
256
|
+
export function mintSaveId() {
|
|
246
257
|
const uuid = window.crypto?.randomUUID?.();
|
|
247
258
|
if (uuid) return uuid;
|
|
248
259
|
if (window.crypto?.getRandomValues) {
|
|
@@ -339,6 +350,49 @@ function skippedResult(msg) {
|
|
|
339
350
|
return { ok: false, msg, msgType: 'skipped', code: null, etag: null };
|
|
340
351
|
}
|
|
341
352
|
|
|
353
|
+
// A body the host will not store, refused here instead of on the wire. It is an
|
|
354
|
+
// ordinary error result with one more fact on it, because the caller's answer to
|
|
355
|
+
// "too large" is the same answer it gives any failed save: keep the writing, do not
|
|
356
|
+
// mark it saved, and let the person shrink or download it.
|
|
357
|
+
function tooLargeResult(size) {
|
|
358
|
+
const err = new Error(`This document is ${size.bytes} bytes; this host saves at most ${size.maxBytes}`);
|
|
359
|
+
err.code = 'save-too-large';
|
|
360
|
+
return { ...errorResult(err), sent: false, size };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ============================================
|
|
364
|
+
// THE DOCUMENT-OPERATION SLOT
|
|
365
|
+
// ============================================
|
|
366
|
+
//
|
|
367
|
+
// One slot for the operations that speak for the document as a whole: a mode switch's
|
|
368
|
+
// barrier, an upgrade, and a whole-document replacement. Two of them at once is how a
|
|
369
|
+
// generation gets flushed and then replaced by a later one, so the second asks and is
|
|
370
|
+
// told no instead of interleaving. Kinds are open strings; the slot only cares that it
|
|
371
|
+
// is exclusive, and that the token it hands back is the one that releases it.
|
|
372
|
+
let documentOperation = null;
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Take the slot, or learn it is taken.
|
|
376
|
+
*
|
|
377
|
+
* @param {string} kind - 'mode' | 'replace' | 'upgrade'
|
|
378
|
+
* @returns {?Object} an opaque token to release with, or null when an operation holds it
|
|
379
|
+
*/
|
|
380
|
+
export function claimDocumentOperation(kind) {
|
|
381
|
+
if (documentOperation) return null;
|
|
382
|
+
documentOperation = { kind };
|
|
383
|
+
return documentOperation;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Release the slot. Only the token that holds it does anything; idempotent. */
|
|
387
|
+
export function releaseDocumentOperation(token) {
|
|
388
|
+
if (documentOperation === token) documentOperation = null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Which operation holds the document, or null. */
|
|
392
|
+
export function operationKind() {
|
|
393
|
+
return documentOperation ? documentOperation.kind : null;
|
|
394
|
+
}
|
|
395
|
+
|
|
342
396
|
// The metadata a caller attaches to a capture was refused here, before anything
|
|
343
397
|
// went out. The host's own grammar is the only definition of valid, so an
|
|
344
398
|
// `invalid-header` here is the same answer a host would have given, without the
|
|
@@ -437,7 +491,7 @@ function formatConversionHeader(conversion) {
|
|
|
437
491
|
* @param {?string} conversionHeader - the `Page-Conversion` value for this logical save, or null
|
|
438
492
|
* @returns {{url: string, options: Object}}
|
|
439
493
|
*/
|
|
440
|
-
function buildSaveRequest(content, userDriven, signal, saveId, etag, keepalive = false, conversionHeader = null) {
|
|
494
|
+
export function buildSaveRequest(content, userDriven, signal, saveId, etag, keepalive = false, conversionHeader = null) {
|
|
441
495
|
const token = saveToken();
|
|
442
496
|
const path = token ? `${SAVE_PATH}/${token}` : SAVE_PATH;
|
|
443
497
|
const options = {
|
|
@@ -457,7 +511,11 @@ function buildSaveRequest(content, userDriven, signal, saveId, etag, keepalive =
|
|
|
457
511
|
// gating it on discovery would mean holding the save until an async lookup
|
|
458
512
|
// resolved. Nothing is inferred from the header being accepted: only a host
|
|
459
513
|
// that ANSWERS with the id has said anything.
|
|
460
|
-
'Save-ID': saveId
|
|
514
|
+
'Save-ID': saveId,
|
|
515
|
+
// The page format this client writes. R understands floor 1 and says so on
|
|
516
|
+
// every page save; a host reads it as protocol capability, never as
|
|
517
|
+
// authorization, and refuses a lower one before mutation.
|
|
518
|
+
[PAGE_FORMAT_HEADER]: String(PAGE_FORMAT)
|
|
461
519
|
},
|
|
462
520
|
body: content
|
|
463
521
|
};
|
|
@@ -595,7 +653,11 @@ function sendOnce(content, saveId, etag, { captureId = null, conversionHeader =
|
|
|
595
653
|
}
|
|
596
654
|
if (!res.ok) {
|
|
597
655
|
const error = new Error(data.msg || data.error || `HTTP ${res.status}: ${res.statusText}`);
|
|
598
|
-
|
|
656
|
+
// A 413 is the host refusing these bytes for their size, whatever it called
|
|
657
|
+
// the refusal: the one code a caller can act on is the same one the local
|
|
658
|
+
// guard gives. No limit is invented from it — a refusal is not an
|
|
659
|
+
// announcement, and the measurement the caller sees is this attempt's own.
|
|
660
|
+
error.code = res.status === 413 ? 'save-too-large' : (data.code ?? null);
|
|
599
661
|
error.status = res.status;
|
|
600
662
|
// Spec §6: a conflict may name what moved the document. Only the host can
|
|
601
663
|
// know, and it often cannot, so this rides through as-is and the notice
|
|
@@ -819,11 +881,25 @@ async function sendSave(content, { captureId = null, conversionHeader = null, at
|
|
|
819
881
|
// One private implementation behind every entry point, so the hooks see the same
|
|
820
882
|
// thing whether the bytes came from a bound capture or a raw caller. `captureId`
|
|
821
883
|
// is null for a raw save: it belongs to no capture and can confirm none.
|
|
822
|
-
|
|
884
|
+
//
|
|
885
|
+
// `pageFormatFloor` is internal: one save may carry a floor this tab has not
|
|
886
|
+
// noted yet, which is how the format gate's activation writes the marker. It
|
|
887
|
+
// raises these bytes only, never what the tab knows the document at.
|
|
888
|
+
function runSave(html, captureId, callback, conversion, capture = null, pageFormatFloor = 0) {
|
|
889
|
+
// A capture taken before the floor rose cannot drop the marker by omitting it:
|
|
890
|
+
// the bytes that go out carry the floor this tab last saw. At floor 0 this is
|
|
891
|
+
// the string itself, unchanged.
|
|
892
|
+
html = withPageFormatFloor(html, Math.max(knownPageFormatFloor(), pageFormatFloor));
|
|
823
893
|
return new Promise((resolve) => {
|
|
824
894
|
const attempts = [];
|
|
895
|
+
// Read once, before the first request: the artifact belongs to the capture that
|
|
896
|
+
// produced these bytes, and it is released however this save settles. A timeout
|
|
897
|
+
// recovered through the host's receipt reaches `accepted` like any other success,
|
|
898
|
+
// so it adopts the artifact of the request that landed.
|
|
899
|
+
const artifact = captureArtifact(capture);
|
|
825
900
|
const finished = (result, accepted = false) => {
|
|
826
|
-
if (accepted) notifySaveAccepted(html, captureId ?? null);
|
|
901
|
+
if (accepted) notifySaveAccepted(html, captureId ?? null, artifact);
|
|
902
|
+
releaseCaptureArtifact(capture);
|
|
827
903
|
notifySaveOutcome({
|
|
828
904
|
captureId: captureId ?? null,
|
|
829
905
|
result,
|
|
@@ -859,6 +935,20 @@ function runSave(html, captureId, callback, conversion) {
|
|
|
859
935
|
return;
|
|
860
936
|
}
|
|
861
937
|
|
|
938
|
+
// The one measurement every entry point shares: the exact string about to be
|
|
939
|
+
// handed to the backend, once per attempt, whichever lane asked. Not a second
|
|
940
|
+
// capture — these are the bytes 05b already took — and not `outerHTML`, which is
|
|
941
|
+
// a different string. The limit is whatever discovery last said; unknown refuses
|
|
942
|
+
// nothing, so a host that advertises no ceiling, or has not answered yet, gets the
|
|
943
|
+
// save and stays the authority on it.
|
|
944
|
+
saveLimit();
|
|
945
|
+
const size = saveSize(storedFile(html), knownSaveLimit());
|
|
946
|
+
announceSaveSize(size);
|
|
947
|
+
if (size.state === 'over') {
|
|
948
|
+
saveInProgress = false;
|
|
949
|
+
return finished(tooLargeResult(size));
|
|
950
|
+
}
|
|
951
|
+
|
|
862
952
|
sendSave(html, { captureId, conversionHeader: formatted.header, attempts })
|
|
863
953
|
.then(successResult, errorResult)
|
|
864
954
|
// Clear the flag BEFORE handing the result back, so a caller that queues a
|
|
@@ -866,7 +956,11 @@ function runSave(html, captureId, callback, conversion) {
|
|
|
866
956
|
// is busy by the save that just finished.
|
|
867
957
|
.then(result => {
|
|
868
958
|
saveInProgress = false;
|
|
869
|
-
|
|
959
|
+
// A host's 413 is the same refusal the guard above makes, so it carries the
|
|
960
|
+
// same measurement. Its own answer about the size wins over nothing: the bytes
|
|
961
|
+
// are this attempt's, and the limit is still only what discovery advertised.
|
|
962
|
+
if (result.code === 'save-too-large' && !result.size) result.size = size;
|
|
963
|
+
if (result.ok) notifySaveAccepted(html, captureId ?? null, artifact);
|
|
870
964
|
finished(result);
|
|
871
965
|
});
|
|
872
966
|
});
|
|
@@ -904,8 +998,8 @@ export function saveHtml(html, callback = () => {}) {
|
|
|
904
998
|
* @param {Object} [options.conversion] - {source:'file', base} or {source:'room', base, epoch}
|
|
905
999
|
* @returns {Promise<Object>}
|
|
906
1000
|
*/
|
|
907
|
-
export function saveCapturedHtml(html, captureId, callback = () => {}, { conversion } = {}) {
|
|
908
|
-
return runSave(html, captureId, callback, conversion);
|
|
1001
|
+
export function saveCapturedHtml(html, captureId, callback = () => {}, { conversion, capture = null, pageFormatFloor } = {}) {
|
|
1002
|
+
return runSave(html, captureId, callback, conversion, capture, pageFormatFloor);
|
|
909
1003
|
}
|
|
910
1004
|
|
|
911
1005
|
/**
|
|
@@ -936,15 +1030,22 @@ export function saveConversion({ html, captureId, conversion }) {
|
|
|
936
1030
|
*/
|
|
937
1031
|
export function replacePageWith(url, callback = () => {}) {
|
|
938
1032
|
return new Promise((resolve) => {
|
|
1033
|
+
let operation = null;
|
|
939
1034
|
const done = (result) => {
|
|
1035
|
+
releaseDocumentOperation(operation);
|
|
940
1036
|
if (typeof callback === 'function') callback(result);
|
|
941
1037
|
resolve(result);
|
|
942
1038
|
};
|
|
943
1039
|
|
|
944
1040
|
if (!isEditMode) return done(skippedResult('Not in edit mode'));
|
|
1041
|
+
if (operationKind()) return done(skippedResult('Another document operation is in progress'));
|
|
945
1042
|
if (saveInProgress) return done(skippedResult('Save already in progress'));
|
|
946
1043
|
if (isDraftMode) return done(false);
|
|
947
1044
|
|
|
1045
|
+
// A replacement writes the whole document, so it holds the slot from the fetch to
|
|
1046
|
+
// the save: a mode switch cannot start its barrier in the middle of one.
|
|
1047
|
+
operation = claimDocumentOperation('replace');
|
|
1048
|
+
|
|
948
1049
|
// Resolved against the document's real location, not a <base href> the page
|
|
949
1050
|
// may have set.
|
|
950
1051
|
fetch(new URL(url, window.location.href).href)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// One conditional save of bytes this page did not build: the upgrade path's write
|
|
2
|
+
// (plan 05, Step 6). It is deliberately not part of the save core every editor
|
|
3
|
+
// loads. There is no capture, no beforeSave, no snapshot and no retry: the bytes
|
|
4
|
+
// handed in are the whole document, already composed by the caller, and the only
|
|
5
|
+
// question this answers is whether the host took them.
|
|
6
|
+
//
|
|
7
|
+
// It speaks through the same request builder as every other save, so the endpoint,
|
|
8
|
+
// the headers and the token route are the save lane's own. `If-Match` comes from the
|
|
9
|
+
// caller (the stamp the page was loaded with or last saved at) and falls back to the
|
|
10
|
+
// tab's stamp. A host that did not announce `conditional` gets no request at all:
|
|
11
|
+
// without the capability the header means nothing, and a write this large is not
|
|
12
|
+
// worth sending to a host that cannot refuse it.
|
|
13
|
+
import { buildSaveRequest, mintSaveId } from "./save-core.js";
|
|
14
|
+
import { conditionalSaves, lastSeenEtag, recordEtag, seedEtag } from "./etag.js";
|
|
15
|
+
|
|
16
|
+
const TIMEOUT_MS = 30000;
|
|
17
|
+
|
|
18
|
+
function failed() {
|
|
19
|
+
return { ok: false, code: "failed" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Save exactly `html` over the document, conditional on the version it replaces.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} html - the whole document, as it should be stored
|
|
26
|
+
* @param {Object} [options]
|
|
27
|
+
* @param {?string} [options.etag] - the stamp to replace, defaulting to this tab's
|
|
28
|
+
* @returns {Promise<{ok: true, etag: ?string}|{ok: false, code: "conflict"|"forbidden"|"unsupported"|"failed"}>}
|
|
29
|
+
*/
|
|
30
|
+
export async function replace(html, { etag } = {}) {
|
|
31
|
+
if (typeof html !== "string" || html === "") return failed();
|
|
32
|
+
|
|
33
|
+
const given = typeof etag === "string" && etag ? etag : null;
|
|
34
|
+
// Discovery is what says whether the capability exists, and it is also where a
|
|
35
|
+
// tab that has never saved learns the stamp it is about to replace.
|
|
36
|
+
if (!conditionalSaves()) await seedEtag();
|
|
37
|
+
if (!conditionalSaves()) return { ok: false, code: "unsupported" };
|
|
38
|
+
|
|
39
|
+
// With no stamp the write would be unconditional, which is exactly what this lane
|
|
40
|
+
// promises never to send. A reload gives the tab the stamp it is missing.
|
|
41
|
+
const stamp = given || lastSeenEtag();
|
|
42
|
+
if (!stamp) return { ok: false, code: "conflict" };
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
45
|
+
try {
|
|
46
|
+
const { url, options } = buildSaveRequest(html, true, controller.signal, mintSaveId(), stamp);
|
|
47
|
+
const res = await fetch(url, options);
|
|
48
|
+
const text = await res.text();
|
|
49
|
+
let data = {};
|
|
50
|
+
if (text) {
|
|
51
|
+
try {
|
|
52
|
+
data = JSON.parse(text);
|
|
53
|
+
} catch (_) {
|
|
54
|
+
data = {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
if (res.status === 412) return { ok: false, code: "conflict" };
|
|
59
|
+
if (res.status === 401 || res.status === 403) return { ok: false, code: "forbidden" };
|
|
60
|
+
return failed();
|
|
61
|
+
}
|
|
62
|
+
const stored = typeof data.etag === "string" && data.etag ? data.etag : null;
|
|
63
|
+
// The write landed, so the stamp it replaced describes bytes the host no longer
|
|
64
|
+
// stores: the answer's own stamp is the one this tab holds from here on.
|
|
65
|
+
recordEtag(stored);
|
|
66
|
+
return { ok: true, etag: stored };
|
|
67
|
+
} catch (_) {
|
|
68
|
+
return failed();
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timeoutId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { isEditMode } from "./is-edit-mode.js";
|
|
2
|
+
import { downloadStored } from "./conflict-download.js";
|
|
3
|
+
import { bevelSurface, bevelText, bevelButton, pageScheme, setShown } from "../ui/bevel-controls.js";
|
|
4
|
+
import { set } from "../lib/hostile-css.js";
|
|
5
|
+
|
|
6
|
+
// The one quiet word about how large this document is.
|
|
7
|
+
//
|
|
8
|
+
// `clay:save-size` is a measurement of a real save attempt, never a live reading, so
|
|
9
|
+
// every sentence here says "At the last save attempt": the number is the bytes the last
|
|
10
|
+
// attempt measured, and nothing claims the page is that size now. Most documents never
|
|
11
|
+
// reach the host's advertised limit, so save.js fetches this module only when one does,
|
|
12
|
+
// and it draws nothing until then.
|
|
13
|
+
//
|
|
14
|
+
// One element, updated in place: a save on a phone fires often, and a notice per save
|
|
15
|
+
// would be a toast storm. A later measurement below the near threshold takes it away,
|
|
16
|
+
// and so does leaving edit: the notice is editor chrome, not part of the page.
|
|
17
|
+
//
|
|
18
|
+
// The download is the safe copy this library already offers for the current document
|
|
19
|
+
// (conflict-download.js's `downloadStored`), a file openable in a browser rather than a
|
|
20
|
+
// save the host would refuse.
|
|
21
|
+
|
|
22
|
+
const NEAR_MESSAGE = "Download a copy, then reduce its size; saving still works.";
|
|
23
|
+
const OVER_MESSAGE = "Download a copy to keep your work.";
|
|
24
|
+
|
|
25
|
+
let root = null;
|
|
26
|
+
let sentence = null;
|
|
27
|
+
let copy = null;
|
|
28
|
+
let listeners = null;
|
|
29
|
+
|
|
30
|
+
// A size a person can read at a glance. Kilobyte precision under a megabyte, one
|
|
31
|
+
// decimal above it: the question is "how close is this to the limit", not the exact byte.
|
|
32
|
+
function readable(bytes) {
|
|
33
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) return "an unknown size";
|
|
34
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
35
|
+
if (bytes >= 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
36
|
+
return `${bytes} bytes`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function words({ bytes, maxBytes, state }) {
|
|
40
|
+
const measured = `At the last save attempt: ${readable(bytes)} of ${readable(maxBytes)}.`;
|
|
41
|
+
return state === "over"
|
|
42
|
+
? `This host can't save a file this size. ${measured} ${OVER_MESSAGE}`
|
|
43
|
+
: `This file is getting large. ${measured} ${NEAR_MESSAGE}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// A phone keyboard shrinks the visual viewport but leaves fixed elements pinned to the
|
|
47
|
+
// layout viewport, so a bottom-anchored bar parks itself behind the keyboard. Same fix
|
|
48
|
+
// as the conflict notice.
|
|
49
|
+
function place() {
|
|
50
|
+
if (!root) return;
|
|
51
|
+
const vv = window.visualViewport;
|
|
52
|
+
const lift = vv ? Math.max(0, window.innerHeight - vv.height - vv.offsetTop) : 0;
|
|
53
|
+
set(root, "bottom", `calc(${16 + lift}px + env(safe-area-inset-bottom,0px))`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The safe copy the conflict notice offers, for the same reason: a file the person can
|
|
57
|
+
// open has to exist outside a host that will not store these bytes. It already reports
|
|
58
|
+
// its own failure.
|
|
59
|
+
function download() {
|
|
60
|
+
downloadStored({});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function build() {
|
|
64
|
+
root = bevelSurface("div", [
|
|
65
|
+
"position:fixed", "left:50%", "transform:translateX(-50%)",
|
|
66
|
+
"z-index:2147483000", "display:flex", "align-items:center", "gap:10px 12px",
|
|
67
|
+
"width:max-content", "max-width:calc(100vw - 24px)", "flex-wrap:wrap",
|
|
68
|
+
"padding:8px 8px 8px 14px", "text-align:left", `color-scheme:${pageScheme()}`,
|
|
69
|
+
]);
|
|
70
|
+
// An injected element is in no document on disk, and `editor-ui` is the bundle that
|
|
71
|
+
// says so: not saved, not watched, not in any snapshot, not undoable, not data. The
|
|
72
|
+
// controls inside carry the same markers, as every other notice this library draws.
|
|
73
|
+
root.setAttribute("clay", "editor-ui");
|
|
74
|
+
root.setAttribute("data-clay-save-size", "");
|
|
75
|
+
root.setAttribute("role", "status");
|
|
76
|
+
sentence = bevelText("span", [], "");
|
|
77
|
+
copy = bevelButton("Download my copy", { small: true, onClick: download });
|
|
78
|
+
setShown(copy, false, "inline-flex");
|
|
79
|
+
root.append(sentence, copy);
|
|
80
|
+
document.body.appendChild(root);
|
|
81
|
+
place();
|
|
82
|
+
window.visualViewport?.addEventListener("resize", place);
|
|
83
|
+
window.visualViewport?.addEventListener("scroll", place);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function render(size) {
|
|
87
|
+
if (!root) build();
|
|
88
|
+
sentence.textContent = words(size);
|
|
89
|
+
// Only an oversize document offers the copy: a near one still saves, and its text
|
|
90
|
+
// already recommends downloading one.
|
|
91
|
+
setShown(copy, size.state === "over", "inline-flex");
|
|
92
|
+
place();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function clear() {
|
|
96
|
+
if (!root) return;
|
|
97
|
+
window.visualViewport?.removeEventListener("resize", place);
|
|
98
|
+
window.visualViewport?.removeEventListener("scroll", place);
|
|
99
|
+
root.remove();
|
|
100
|
+
root = null;
|
|
101
|
+
sentence = null;
|
|
102
|
+
copy = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function onSaveSize(event) {
|
|
106
|
+
const size = event?.detail;
|
|
107
|
+
if (!size) return;
|
|
108
|
+
if (size.state === "near" || size.state === "over") render(size);
|
|
109
|
+
else clear();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onModeChange() {
|
|
113
|
+
if (!isEditMode) stop();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// announceSaveSize (save-size.js) asks for this file on the first measurement that is near or over,
|
|
117
|
+
// and hands it the event that asked: it is replayed here, after the listeners are on,
|
|
118
|
+
// exactly as if it had arrived on a page that already held the notice.
|
|
119
|
+
export function start(trigger) {
|
|
120
|
+
if (!isEditMode) return;
|
|
121
|
+
listeners ||= new AbortController();
|
|
122
|
+
document.addEventListener("clay:save-size", onSaveSize, { signal: listeners.signal });
|
|
123
|
+
document.addEventListener("clay:modechange", onModeChange, { signal: listeners.signal });
|
|
124
|
+
if (trigger?.type === "clay:save-size") onSaveSize(trigger);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A committed exit takes the notice with it: the handlers, the node it drew and the
|
|
128
|
+
// measurement it was drawing. A later entry starts again from whatever the next save
|
|
129
|
+
// measures.
|
|
130
|
+
export function stop() {
|
|
131
|
+
listeners?.abort();
|
|
132
|
+
listeners = null;
|
|
133
|
+
clear();
|
|
134
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* save-size.js — how large a save's exact bytes are, and the host's own ceiling (§9).
|
|
3
|
+
*
|
|
4
|
+
* `document.maxBytes` in discovery is the one place a host states what it will store:
|
|
5
|
+
* the maximum uncompressed UTF-8 size of the HTML save body, inclusive. Only a
|
|
6
|
+
* positive safe integer is an answer; missing, zero, negative, fractional and
|
|
7
|
+
* nonnumeric all mean "unknown". Unknown refuses nothing: the save goes out and the
|
|
8
|
+
* server stays authoritative. Nothing here guesses a ceiling from a hostname,
|
|
9
|
+
* compression or a browser storage quota, and `document.upload.maxBytes` is a
|
|
10
|
+
* different promise about different bytes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { hostMeta } from "./host-meta.js";
|
|
14
|
+
|
|
15
|
+
// The limit the last discovery answer announced. Memoized beside `hostMeta`'s own
|
|
16
|
+
// memoized promise, because a save reads it synchronously: the request a person feels
|
|
17
|
+
// is the save, and holding it for a discovery round trip would make a first save wait
|
|
18
|
+
// out a timeout on a host that has no `/_/meta` at all. Until an answer lands the
|
|
19
|
+
// limit is unknown, which is the honest reading and refuses nothing.
|
|
20
|
+
let limit = null;
|
|
21
|
+
let measures = null;
|
|
22
|
+
let asked = null;
|
|
23
|
+
|
|
24
|
+
function advertised(meta) {
|
|
25
|
+
const value = meta?.document?.maxBytes;
|
|
26
|
+
return Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function adopt(meta) {
|
|
30
|
+
limit = advertised(meta);
|
|
31
|
+
measures = meta?.document?.maxBytesMeasures === 'file' ? 'file' : null;
|
|
32
|
+
return limit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A Document's live state rides in one <script merge="page"> tag that Hyperclay
|
|
36
|
+
// stores apart from the file, and its limit (`maxBytesMeasures: 'file'`) is judged on
|
|
37
|
+
// the body without that tag, the way the host's splitPage() cuts it: the whole line
|
|
38
|
+
// when the tag stands alone on it, otherwise the tag itself. Where the host would not
|
|
39
|
+
// split (no tag, two tags), the cut here can only make the count smaller, so the
|
|
40
|
+
// host's own 413 stays the answer and nothing is refused that the host would store.
|
|
41
|
+
const PAGE_TAG = /<script\b[^>]*\bmerge=(["'])page\1[^>]*>[\s\S]*?<\/script\s*>/i;
|
|
42
|
+
|
|
43
|
+
export function storedFile(html, measure = measures) {
|
|
44
|
+
if (measure !== 'file' || typeof html !== 'string') return html;
|
|
45
|
+
const match = PAGE_TAG.exec(html);
|
|
46
|
+
if (!match) return html;
|
|
47
|
+
let start = match.index;
|
|
48
|
+
let end = start + match[0].length;
|
|
49
|
+
const from = html.lastIndexOf('\n', start - 1) + 1;
|
|
50
|
+
const newline = html.indexOf('\n', end);
|
|
51
|
+
const to = newline === -1 ? html.length : newline + 1;
|
|
52
|
+
if (!/\S/.test(html.slice(from, start)) && !/\S/.test(html.slice(end, to))) { start = from; end = to; }
|
|
53
|
+
return html.slice(0, start) + html.slice(end);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* How big these exact bytes are, against a limit that may not be known.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} html - the bytes a save would send
|
|
60
|
+
* @param {number} [maxBytes] - the host's advertised ceiling, if it advertised one
|
|
61
|
+
* @returns {{bytes: number, maxBytes: ?number, state: 'unknown'|'ok'|'near'|'over'}}
|
|
62
|
+
*/
|
|
63
|
+
export function saveSize(html, maxBytes) {
|
|
64
|
+
const bytes = new TextEncoder().encode(html).byteLength;
|
|
65
|
+
const known = Number.isSafeInteger(maxBytes) && maxBytes > 0;
|
|
66
|
+
return {
|
|
67
|
+
bytes, maxBytes: known ? maxBytes : null,
|
|
68
|
+
state: !known ? 'unknown' : bytes > maxBytes ? 'over' :
|
|
69
|
+
bytes >= Math.ceil(maxBytes * 0.8) ? 'near' : 'ok'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The limit the last discovery answer advertised, without asking the host.
|
|
75
|
+
*
|
|
76
|
+
* @returns {?number}
|
|
77
|
+
*/
|
|
78
|
+
export function knownSaveLimit() {
|
|
79
|
+
return limit;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ask the host what it accepts, once per page.
|
|
84
|
+
*
|
|
85
|
+
* Joins the memoized discovery answer, so this never costs a second request. `fresh`
|
|
86
|
+
* asks again, for a caller that wants the ceiling as it stands now.
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} [options]
|
|
89
|
+
* @param {boolean} [options.fresh]
|
|
90
|
+
* @returns {Promise<?number>} the advertised limit, or null when it announced none
|
|
91
|
+
*/
|
|
92
|
+
export function saveLimit({ fresh = false } = {}) {
|
|
93
|
+
if (fresh) {
|
|
94
|
+
asked = hostMeta({ fresh: true }).then(adopt);
|
|
95
|
+
return asked;
|
|
96
|
+
}
|
|
97
|
+
if (!asked) asked = hostMeta().then(adopt);
|
|
98
|
+
return asked;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `clay.save.limits`: the host's save ceiling, and whether it said one at all.
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} [options]
|
|
105
|
+
* @param {boolean} [options.fresh]
|
|
106
|
+
* @returns {Promise<{maxBytes: ?number}>}
|
|
107
|
+
*/
|
|
108
|
+
export function saveLimits({ fresh = false } = {}) {
|
|
109
|
+
return saveLimit({ fresh }).then((maxBytes) => ({ maxBytes }));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* `clay.save.inspect`: measure bytes that are not this page's document.
|
|
114
|
+
*
|
|
115
|
+
* No capture, no send, no baseline, no save claim, and nothing said to anybody about
|
|
116
|
+
* what this page currently holds: an import preview asks about candidate bytes.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} html
|
|
119
|
+
* @returns {Promise<{bytes: number, maxBytes: ?number, state: string}>}
|
|
120
|
+
*/
|
|
121
|
+
export async function inspectSaveSize(html) {
|
|
122
|
+
const maxBytes = await saveLimit();
|
|
123
|
+
return saveSize(storedFile(html), maxBytes);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Say what the last real save attempt measured, on the document.
|
|
128
|
+
*
|
|
129
|
+
* @param {{bytes: number, maxBytes: ?number, state: string}} size
|
|
130
|
+
*/
|
|
131
|
+
export function announceSaveSize(size) {
|
|
132
|
+
const event = new CustomEvent('clay:save-size', { detail: size });
|
|
133
|
+
document.dispatchEvent(event);
|
|
134
|
+
// The notice is fetched only by a measurement near or over the ceiling, so most edit
|
|
135
|
+
// boots never download it; once loaded it listens for itself, and this replay of the
|
|
136
|
+
// event that fetched it only redraws in place.
|
|
137
|
+
if (size.state === 'near' || size.state === 'over') {
|
|
138
|
+
import('./save-size-notice.js').then((notice) => notice.start(event), () => {});
|
|
139
|
+
}
|
|
140
|
+
}
|