@panphora/clayjs 1.16.0 → 1.18.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/dist/all.js +1 -1
- package/dist/clay-dom.js +1 -1
- package/dist/clay-events.js +1 -1
- package/dist/clay-internals.js +1 -1
- package/dist/clay-options.js +1 -1
- package/dist/clay-router.js +1 -1
- package/dist/clay-ui.js +1 -1
- package/dist/clay-utils.js +1 -1
- package/dist/clay.js +1 -1
- package/dist/clay.standalone.js +72519 -59839
- package/dist/m/attrs/refetch-on-save-ZUE7PSCI.js +1 -0
- package/dist/m/attrs/save-freeze-LWGF6XUN.js +1 -0
- package/dist/m/c-2GRUM77Q.js +30 -0
- package/dist/m/c-2KIEGNC4.js +1 -0
- package/dist/m/{c-5R4PTRG2.js → c-2KL3BD52.js} +1 -1
- package/dist/m/c-2YYDMLNZ.js +1 -0
- package/dist/m/c-2ZZBC45O.js +1 -0
- package/dist/m/c-372N6H3B.js +1 -0
- package/dist/m/{c-4WKFKEKF.js → c-3G2DGZUP.js} +1 -1
- package/dist/m/c-3LDGIBMN.js +1 -0
- package/dist/m/c-3O7OBDCM.js +1 -0
- package/dist/m/c-4ASQPRQL.js +1 -0
- package/dist/m/c-4TXNLX4K.js +6 -0
- package/dist/m/c-4YU3BR4D.js +6 -0
- package/dist/m/c-5734MF2P.js +1 -0
- package/dist/m/c-57XDY6HC.js +1 -0
- package/dist/m/{c-VFX45IZQ.js → c-5B4YBZXC.js} +1 -1
- package/dist/m/c-5ONTYIUI.js +1 -0
- package/dist/m/c-5OOVFCJM.js +1 -0
- package/dist/m/c-5T6NZHNM.js +1 -0
- package/dist/m/{c-YDH7Q3NY.js → c-5ZQV7KC4.js} +1 -1
- package/dist/m/{c-Z36J63PP.js → c-6DB2OLRJ.js} +1 -1
- package/dist/m/c-6DNRCPNM.js +1 -0
- package/dist/m/c-6UGWFMQC.js +2 -0
- package/dist/m/c-7K4SCQB7.js +1 -0
- package/dist/m/c-7MG5RU63.js +1 -0
- package/dist/m/c-7PRJCA3E.js +1 -0
- package/dist/m/c-A6L3FMUS.js +1 -0
- package/dist/m/c-AMU3BPPR.js +4 -0
- package/dist/m/c-AMYEFA2E.js +1 -0
- package/dist/m/c-AQYCG7QW.js +1 -0
- package/dist/m/c-ATKAOH23.js +1 -0
- package/dist/m/c-B3JM5ZXC.js +1 -0
- package/dist/m/c-B5CS55WR.js +1 -0
- package/dist/m/c-B5UJQT7W.js +1 -0
- package/dist/m/c-B7J4K2AE.js +3 -0
- package/dist/m/c-BHZYSHW4.js +1 -0
- package/dist/m/c-BIJALSRD.js +1 -0
- package/dist/m/c-BJ7YQ5QZ.js +1 -0
- package/dist/m/c-BPF2NSPV.js +1 -0
- package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
- package/dist/m/c-BUAYFEV5.js +1 -0
- package/dist/m/c-C6IPRG34.js +1 -0
- package/dist/m/{c-2JL6E75N.js → c-CAS3NND4.js} +1 -1
- package/dist/m/{c-4DRMS4QG.js → c-CCQ25GFY.js} +1 -1
- package/dist/m/c-CJFDLD43.js +1 -0
- package/dist/m/c-COPTL5WB.js +1 -0
- package/dist/m/c-D3E4WJ4M.js +166 -0
- package/dist/m/c-D5IKEKXW.js +7 -0
- package/dist/m/c-DHBRA6DL.js +1 -0
- package/dist/m/c-DKLS74XB.js +72 -0
- package/dist/m/c-DVYGFTSK.js +3 -0
- package/dist/m/c-E3ZXARWA.js +1 -0
- package/dist/m/c-EK63G3CG.js +1 -0
- package/dist/m/c-ERQSNR6K.js +13 -0
- package/dist/m/c-EVYAISP3.js +1 -0
- package/dist/m/c-EXKHPOVC.js +3 -0
- package/dist/m/{c-7VGZ7RU5.js → c-FWKHM4E7.js} +1 -1
- package/dist/m/{c-4Q2XTQ2N.js → c-GBKB5WDL.js} +1 -1
- package/dist/m/c-GNDL3TKB.js +1 -0
- package/dist/m/c-GRWMD6O5.js +1 -0
- package/dist/m/c-GRXHHFPO.js +1 -0
- package/dist/m/{c-OYTFP5QN.js → c-GY422N3Q.js} +1 -1
- package/dist/m/c-HAWFNBYG.js +1 -0
- package/dist/m/c-HG2C77BM.js +4 -0
- package/dist/m/c-HHGJBU4D.js +1 -0
- package/dist/m/c-HMV6P4RX.js +1 -0
- package/dist/m/c-HMYWIHFG.js +1 -0
- package/dist/m/c-HT2DKBV2.js +1 -0
- package/dist/m/c-I2SCCUEW.js +1 -0
- package/dist/m/{c-HRDCD3WN.js → c-ICJYHABF.js} +2 -2
- package/dist/m/c-IMZCWYHR.js +7 -0
- package/dist/m/c-IVMD3FEM.js +1 -0
- package/dist/m/c-IVTRVENZ.js +1 -0
- package/dist/m/c-IW6GGOU2.js +1 -0
- package/dist/m/c-JKZJIUIP.js +1 -0
- package/dist/m/{c-NZMNXVF6.js → c-JNLX3RJB.js} +1 -1
- package/dist/m/c-JRKY44DK.js +1 -0
- package/dist/m/c-K3ADYNLU.js +1 -0
- package/dist/m/c-KBS3UV6B.js +1 -0
- package/dist/m/c-KGHVFRR3.js +1 -0
- package/dist/m/c-KJJPV77R.js +1 -0
- package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
- package/dist/m/{c-5Y6TNX5W.js → c-KP64LT2A.js} +2 -2
- package/dist/m/c-KQ7E3PYY.js +1 -0
- package/dist/m/c-L3F5ZLIV.js +1 -0
- package/dist/m/{c-LR4CGEPT.js → c-L42OLXWM.js} +3 -2
- package/dist/m/c-L5VFATT6.js +1 -0
- package/dist/m/c-L7JL5TL3.js +3 -0
- package/dist/m/c-LKSL4AUJ.js +1 -0
- package/dist/m/c-M6FNQCTQ.js +1 -0
- package/dist/m/c-MCOOHCSG.js +1 -0
- package/dist/m/c-MHI3GYKF.js +1 -0
- package/dist/m/c-MWXFLW4J.js +1 -0
- package/dist/m/c-NEL7HQW3.js +1 -0
- package/dist/m/c-NQJRVJ56.js +1 -0
- package/dist/m/c-NWX3MFT4.js +1 -0
- package/dist/m/{c-HPURYVZS.js → c-OJYM642Q.js} +1 -1
- package/dist/m/c-OKXI52N7.js +1 -0
- package/dist/m/{c-MMUIUUZG.js → c-OPLUZR3A.js} +1 -1
- package/dist/m/{c-J7CA54IP.js → c-OTMKN2OO.js} +1 -1
- package/dist/m/{c-LTGMXXKO.js → c-OYRZ7XG2.js} +1 -1
- package/dist/m/c-P3OFYUMA.js +1 -0
- package/dist/m/c-PLN76GF3.js +6 -0
- package/dist/m/c-PLP4JKFC.js +1 -0
- package/dist/m/c-Q26MME6N.js +1 -0
- package/dist/m/{c-S7H2DRCB.js → c-Q3XOWZEV.js} +1 -1
- package/dist/m/{c-SWRJFCNN.js → c-QIKT6SKM.js} +1 -1
- package/dist/m/c-QIXT3Q7C.js +28 -0
- package/dist/m/c-QO4HNGCQ.js +1 -0
- package/dist/m/c-R27DWQKX.js +1 -0
- package/dist/m/{c-HVYXIS72.js → c-R2XUODVY.js} +1 -1
- package/dist/m/{c-QNKREOL6.js → c-R7LBFMF5.js} +1 -1
- package/dist/m/c-RND3G2S2.js +1 -0
- package/dist/m/c-S2U6NJU4.js +1 -0
- package/dist/m/c-SPZPGD3R.js +1 -0
- package/dist/m/c-SW4YEG2D.js +8 -0
- package/dist/m/c-SZGYW5HT.js +1 -0
- package/dist/m/c-T637T7SN.js +1 -0
- package/dist/m/c-TJAXGIRV.js +1 -0
- package/dist/m/{c-KKWLDOPM.js → c-TNEM3RNM.js} +1 -1
- package/dist/m/c-TTB5EK4S.js +32 -0
- package/dist/m/c-U7SHNWDH.js +1 -0
- package/dist/m/c-UDHIVLYH.js +20 -0
- package/dist/m/{c-BBVGTQG7.js → c-UGNV5MFB.js} +1 -1
- package/dist/m/{c-TQZ2JYHD.js → c-UORXLSD3.js} +1 -1
- package/dist/m/c-UPD33ZPE.js +1 -0
- package/dist/m/c-URSMSFCB.js +1 -0
- package/dist/m/c-UY4QX42B.js +1 -0
- package/dist/m/c-V44KWI2L.js +4 -0
- package/dist/m/c-VG5XIN4T.js +1 -0
- package/dist/m/c-VILENZTD.js +1 -0
- package/dist/m/{c-33HUYEDI.js → c-VJYEVRYK.js} +1 -1
- package/dist/m/c-VR2BJ4AN.js +1 -0
- package/dist/m/c-W4JCVLAB.js +1 -0
- package/dist/m/c-W6DQSHXI.js +135 -0
- package/dist/m/c-WBGWGOM7.js +1 -0
- package/dist/m/{c-4L3BTFRK.js → c-WCXLG3MV.js} +1 -1
- package/dist/m/c-WGW764WX.js +1 -0
- package/dist/m/c-WMRJKQ2V.js +5 -0
- package/dist/m/c-WNTYKCDG.js +1 -0
- package/dist/m/c-WQWIRWO3.js +72 -0
- package/dist/m/c-X5LNHET5.js +1 -0
- package/dist/m/c-X5TSXZYJ.js +2 -0
- package/dist/m/c-XGH4GS2T.js +1 -0
- package/dist/m/c-Y6F2FORL.js +1 -0
- package/dist/m/c-YBKHUAK7.js +10 -0
- package/dist/m/c-YBSS55KU.js +20 -0
- package/dist/m/c-YCLU4UNT.js +2 -0
- package/dist/m/c-YVZ2L7CE.js +1 -0
- package/dist/m/c-ZK4GIZH2.js +1 -0
- package/dist/m/c-ZRXJD4G6.js +1 -0
- package/dist/m/c-ZWBI4HYK.js +1 -0
- package/dist/m/{c-HT33EQRN.js → c-ZWIOJGMT.js} +1 -1
- package/dist/m/core/admin-attrs-YY7T5FGU.js +1 -0
- package/dist/m/core/autosave-S6OMNDCK.js +1 -0
- package/dist/m/core/conflict-notice-IWHMHH43.js +1 -0
- package/dist/m/core/host-permission-6EVJGNPL.js +1 -0
- package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
- package/dist/m/core/{persist-HKHA66IY.js → persist-UKXV5KLA.js} +1 -1
- package/dist/m/core/recovery-DRFZRJED.js +1 -0
- package/dist/m/core/save-LYY7K6U7.js +1 -0
- package/dist/m/core/save-core-MKRPSEEO.js +1 -0
- package/dist/m/core/snapshot-NU3YM6GR.js +1 -0
- package/dist/m/core/unsaved-warning-7QTKEN7Z.js +1 -0
- package/dist/m/events/index-RN2JNGBV.js +1 -0
- package/dist/m/internals/index-TGLQINN3.js +1 -0
- package/dist/m/lib/cache-bust-MUMFXZK6.js +1 -0
- package/dist/m/loader-3BBNZZGM.js +1 -0
- package/dist/m/manifest-F449FF98.js +1 -0
- package/dist/m/manifest-edit-4396A009.js +1 -0
- package/dist/m/manifest.json +2509 -1474
- package/dist/m/options/options-EJMS3K24.js +1 -0
- package/dist/m/page/editor-6LRB3GE4.js +304 -0
- package/dist/m/plugins/ai-edit-UTV7YO2I.js +6 -0
- package/dist/m/plugins/{cms-HP3LSTPN.js → cms-LRAOZHT7.js} +9 -9
- package/dist/m/plugins/demo-OYO5GCAO.js +1 -0
- package/dist/m/plugins/{derive-UAAA4V4S.js → derive-FRE5JJQE.js} +1 -1
- package/dist/m/plugins/indicator-4VDXWYTD.js +1 -0
- package/dist/m/plugins/live-AD4H6XSL.js +1 -0
- package/dist/m/plugins/menu-M5WW7DBF.js +1 -0
- package/dist/m/plugins/page-MD3T2ZX3.js +2 -0
- package/dist/m/plugins/page-view-PAWVZNFY.js +1 -0
- package/dist/m/plugins/people-JCHVPF7P.js +1 -0
- package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
- package/dist/m/plugins/{richclay-RC3L2F6X.js → richclay-KLGETK77.js} +2 -2
- package/dist/m/plugins/sortable-4S6SQC3D.js +1 -0
- package/dist/m/plugins/source-TJXOIOYY.js +1 -0
- package/dist/m/plugins/undo-NLPGCJQY.js +1 -0
- package/dist/m/plugins/{unsent-HJFLQJ2O.js → unsent-OA4RGBUT.js} +1 -1
- package/dist/m/plugins/upgrade-HSUNDNJD.js +1 -0
- package/dist/m/plugins/upload-NHGJXZK2.js +1 -0
- package/dist/m/plugins/visitor-drafts-BW43DGY3.js +20 -0
- package/dist/m/plugins/visitor-drafts-owner-7ODQG7JX.js +1 -0
- package/dist/m/plugins/wire-VBWXEXU2.js +1 -0
- package/dist/m/reader-core-4W2KYEWQ.js +77 -0
- package/dist/m/route/route-62CHXL5B.js +1 -0
- package/dist/m/sync/live-sync-MO5DH33W.js +1 -0
- package/dist/m/ui/index-IVWN2QC4.js +1 -0
- package/dist/m/vendor/sap-core.vendor-RN4ZVY4X.js +6 -0
- package/dist/offline-manifest.json +1560 -889
- package/dist/page/themes/light-1.css +33 -8
- package/dist/server/core.cjs +1554 -311
- package/dist/server/offline-runtime.cjs +63 -63
- package/dist/server/offline-runtime.mjs +63 -63
- package/dist/server/page-compaction-worker.cjs +3710 -0
- package/dist/server/rooms.mjs +5 -0
- package/docs/guide.md +653 -101
- package/entries/sap.js +5 -5
- package/package.json +1 -1
- package/src/core/admin-contenteditable.js +5 -2
- package/src/core/admin-inputs.js +1 -0
- package/src/core/admin-onclick.js +2 -1
- package/src/core/boot-phase.js +2 -1
- package/src/core/conflict-download.js +3 -29
- package/src/core/conflict-notice-ui.js +4 -7
- package/src/core/download.js +32 -0
- package/src/core/host-meta.js +56 -0
- package/src/core/is-edit-mode.js +17 -1
- package/src/core/mode-key.js +125 -19
- package/src/core/mode-support.js +5 -0
- package/src/core/mode-transition.js +20 -6
- package/src/core/preserve-authored.js +4 -4
- package/src/core/recovery.js +121 -33
- package/src/core/save-accepted.js +33 -0
- package/src/core/save-core.js +111 -32
- package/src/core/save-job.js +108 -0
- package/src/core/save-prepare.js +53 -0
- package/src/core/save-size-notice.js +3 -1
- package/src/core/save-slices.js +15 -0
- package/src/core/save.js +557 -127
- package/src/core/serialize-steps.js +109 -0
- package/src/core/snapshot.js +209 -40
- package/src/core/source-map.js +83 -38
- package/src/core/stale-host-notice.js +1 -0
- package/src/core/upgrade-run.js +59 -8
- package/src/dom/active-element.js +5 -0
- package/src/lib/assets.js +1 -0
- package/src/lib/autosave-scheduler.js +3 -4
- package/src/lib/slice.js +65 -0
- package/src/lib/uimotion.js +774 -0
- package/src/lib/unsaved-state.js +2 -2
- package/src/live/fast-lane.js +10 -3
- package/src/live/local-lane.js +202 -11
- package/src/live/ops.js +16 -0
- package/src/live/page-compaction.js +162 -0
- package/src/live/server-lane.js +1 -0
- package/src/live/store.js +11 -3
- package/src/loader-logic.js +3 -1
- package/src/loader.js +14 -4
- package/src/page/adopt-boot.js +2 -1
- package/src/page/awareness.js +33 -2
- package/src/page/blocks.js +122 -27
- package/src/page/board-views.js +2 -16
- package/src/page/boot-pack.js +1 -1
- package/src/page/chart-tables.js +29 -0
- package/src/page/chart.js +411 -72
- package/src/page/checklist.js +10 -135
- package/src/page/checkpoint-proof.js +295 -0
- package/src/page/chrome.js +257 -40
- package/src/page/clipboard-shape.js +126 -0
- package/src/page/document-title-dom.js +12 -0
- package/src/page/document-title-runtime.js +15 -1
- package/src/page/document-title.js +4 -1
- package/src/page/drag.js +33 -3
- package/src/page/editor.js +207 -31
- package/src/page/emoji-data.js +162 -0
- package/src/page/export-assets.js +357 -0
- package/src/page/export-html.js +209 -0
- package/src/page/export-markdown.js +361 -0
- package/src/page/features/board.js +12 -3
- package/src/page/features/chart.js +11 -0
- package/src/page/features/checklist.js +428 -0
- package/src/page/features/dates.js +180 -0
- package/src/page/features/emoji.js +187 -0
- package/src/page/features/export-html.js +189 -0
- package/src/page/features/export-markdown.js +169 -0
- package/src/page/features/import-markdown.js +349 -0
- package/src/page/features/media.js +61 -5
- package/src/page/features/mentions.js +188 -0
- package/src/page/features/registry.js +24 -1
- package/src/page/features/table.js +375 -85
- package/src/page/features/title-cover.js +331 -0
- package/src/page/features/title-icon.js +213 -0
- package/src/page/features/toc.js +87 -0
- package/src/page/format-command.js +33 -0
- package/src/page/format-gate.js +68 -18
- package/src/page/format-host.js +71 -0
- package/src/page/format-native-commands.js +158 -0
- package/src/page/format-native.js +42 -0
- package/src/page/image-actions.js +64 -0
- package/src/page/import-markdown.js +262 -0
- package/src/page/inline.js +32 -8
- package/src/page/items.js +141 -11
- package/src/page/markdown.js +30 -2
- package/src/page/media.js +35 -119
- package/src/page/mention-picker.js +99 -0
- package/src/page/merge.js +1 -1
- package/src/page/placeholder.js +14 -1
- package/src/page/projection.js +156 -25
- package/src/page/recover.js +10 -12
- package/src/page/recovery-ui.js +464 -0
- package/src/page/rich/chart.js +63 -7
- package/src/page/save-projection.js +67 -0
- package/src/page/save-states.js +14 -4
- package/src/page/schema.js +52 -3
- package/src/page/table-paste.js +3 -1
- package/src/page/table-style.js +4 -4
- package/src/page/table.js +44 -2
- package/src/page/themes/content.css +25 -1
- package/src/page/themes/light-1.css +33 -8
- package/src/page/themes/light-1.source.css +8 -7
- package/src/page/toc-link.js +31 -0
- package/src/page/toc.js +5 -0
- package/src/page/view-hooks.js +58 -0
- package/src/page/view.js +13 -0
- package/src/page/ybind/binding.js +116 -6
- package/src/page/ybind/bridge.js +187 -20
- package/src/page/ybind/containment.js +162 -0
- package/src/page/ybind/doc.js +17 -9
- package/src/page/ybind/file-debt.js +63 -0
- package/src/page/ybind/layout.js +7 -2
- package/src/page/ybind/marks.js +2 -1
- package/src/page/ybind/moves.js +5 -4
- package/src/page/ybind/origins.js +106 -4
- package/src/page/ybind/parts.js +8 -1
- package/src/page/ybind/provider.js +125 -18
- package/src/page/ybind/recovery-proof.js +32 -9
- package/src/page/ybind/recovery-replay.js +905 -4
- package/src/page/ybind/recovery.js +174 -45
- package/src/page/ybind/table.js +67 -11
- package/src/page/ybind/walk-delta.js +258 -0
- package/src/page/ybind/walk.js +54 -29
- package/src/page/ybind/writer.js +1 -1
- package/src/plugins/ai-edit.js +13 -9
- package/src/plugins/indicator-chip.js +91 -0
- package/src/plugins/indicator.js +13 -80
- package/src/plugins/live.js +126 -15
- package/src/plugins/menu/actions.js +196 -0
- package/src/plugins/menu/people.js +279 -0
- package/src/plugins/menu/placement.js +121 -0
- package/src/plugins/menu/preferences.js +9 -0
- package/src/plugins/menu/seo.js +316 -397
- package/src/plugins/menu/style.js +171 -0
- package/src/plugins/menu/variants.js +296 -0
- package/src/plugins/menu.js +186 -183
- package/src/plugins/page-view.js +3 -2
- package/src/plugins/page.js +362 -43
- package/src/plugins/sortable.js +8 -1
- package/src/plugins/source.js +6 -0
- package/src/plugins/upgrade.js +51 -17
- package/src/plugins/upload-ui.js +12 -17
- package/src/plugins/visitor-drafts/bottom-ui.js +12 -4
- package/src/plugins/visitor-drafts/intro-style.js +7 -2
- package/src/plugins/visitor-drafts/intro.js +7 -4
- package/src/plugins/visitor-drafts/owner-chip.js +8 -12
- package/src/plugins/visitor-drafts/prompt.js +11 -9
- package/src/plugins/visitor-drafts/source.js +4 -3
- package/src/plugins/visitor-drafts/store.js +138 -9
- package/src/plugins/visitor-drafts/ui-memory.js +2 -2
- package/src/plugins/visitor-drafts-owner.js +4 -6
- package/src/plugins/visitor-drafts.js +5 -3
- package/src/recovery/store.js +1 -1
- package/src/route/route.js +4 -0
- package/src/server/checkpoint/index.js +2 -2
- package/src/server/rooms/index.js +5 -1
- package/src/server/rooms/live-data-rooms.js +302 -50
- package/src/server/rooms/page-compaction-worker.js +12 -0
- package/src/server/rooms/page-compactor.js +92 -0
- package/src/server/rooms/page-room-conformance.js +461 -0
- package/src/source/parse-steps.js +94 -0
- package/src/source/session.js +233 -57
- package/src/sync/editing-sender.js +12 -0
- package/src/sync/live-sync-edit.js +22 -2
- package/src/sync/live-sync.js +150 -23
- package/src/sync/page-relay.js +52 -0
- package/src/sync/presence-store.js +44 -4
- package/src/sync/presence.js +6 -3
- package/src/sync/saved-stream.js +28 -14
- package/src/ui/bevel-controls.js +1 -0
- package/src/ui/bevel-dialog.js +5 -2
- package/src/ui/editor-ui.js +77 -0
- package/src/ui/page-colour.js +281 -0
- package/src/ui/toast.js +16 -7
- package/src/vendor/hyper-html-api-upgrade.vendor.js +2 -2
- package/src/vendor/hyper-undo.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +9 -9
- package/src/vendor/parse5.vendor.js +2 -2
- package/src/vendor/sap-core.vendor.js +5 -5
- package/dist/m/attrs/refetch-on-save-NWPPXOCZ.js +0 -1
- package/dist/m/attrs/save-freeze-FUP4Q4X3.js +0 -1
- package/dist/m/c-2M73252O.js +0 -1
- package/dist/m/c-2SPVQSHC.js +0 -1
- package/dist/m/c-35NBGMZB.js +0 -1
- package/dist/m/c-3IQRECYS.js +0 -1
- package/dist/m/c-4B6H4G5V.js +0 -1
- package/dist/m/c-4FFHZM32.js +0 -1
- package/dist/m/c-4YSCRICO.js +0 -1
- package/dist/m/c-54RCDCBU.js +0 -1
- package/dist/m/c-5M4YEKCX.js +0 -4
- package/dist/m/c-5V3MOVJT.js +0 -1
- package/dist/m/c-66O32MV3.js +0 -28
- package/dist/m/c-6HMB24EV.js +0 -1
- package/dist/m/c-7O6AFR65.js +0 -1
- package/dist/m/c-ANQTJ3B5.js +0 -2
- package/dist/m/c-AYVOREBE.js +0 -1
- package/dist/m/c-B2ZXPOS3.js +0 -3
- package/dist/m/c-BMIZKMPV.js +0 -1
- package/dist/m/c-BTDI4Y23.js +0 -1
- package/dist/m/c-CJZSLMVQ.js +0 -1
- package/dist/m/c-DBE6ORSI.js +0 -1
- package/dist/m/c-EDHUR4MX.js +0 -1
- package/dist/m/c-EY6UOM54.js +0 -1
- package/dist/m/c-F3CKJTJS.js +0 -1
- package/dist/m/c-F7Y6FLPY.js +0 -1
- package/dist/m/c-FA4DZVTC.js +0 -1
- package/dist/m/c-G2BUZITR.js +0 -1
- package/dist/m/c-GB3JTV6E.js +0 -1
- package/dist/m/c-GBGWXTX4.js +0 -1
- package/dist/m/c-GHBCCVG4.js +0 -1
- package/dist/m/c-GJAEGGXY.js +0 -1
- package/dist/m/c-GOWBDM6J.js +0 -1
- package/dist/m/c-IB52R237.js +0 -1
- package/dist/m/c-IKWNE4R4.js +0 -1
- package/dist/m/c-IU3IYEU6.js +0 -1
- package/dist/m/c-IZ2EBRAJ.js +0 -1
- package/dist/m/c-KR4UW4KW.js +0 -1
- package/dist/m/c-KY3VJGUA.js +0 -8
- package/dist/m/c-LFPHCXY3.js +0 -1
- package/dist/m/c-LRPUO2MX.js +0 -1
- package/dist/m/c-LXENEBV7.js +0 -1
- package/dist/m/c-NMJRXKPC.js +0 -8
- package/dist/m/c-OIBUTDXI.js +0 -1
- package/dist/m/c-PPOSCAQD.js +0 -1
- package/dist/m/c-QIX5BJYN.js +0 -1
- package/dist/m/c-QQHEX6HA.js +0 -1
- package/dist/m/c-R7RMANJI.js +0 -9
- package/dist/m/c-RI4NT75Y.js +0 -1
- package/dist/m/c-RM7J7BV2.js +0 -1
- package/dist/m/c-RUJKSUQW.js +0 -1
- package/dist/m/c-SGCCC4FW.js +0 -25
- package/dist/m/c-T4VO4OCB.js +0 -1
- package/dist/m/c-TQEMVJCS.js +0 -1
- package/dist/m/c-TR6RMDQ7.js +0 -3
- package/dist/m/c-UKHTLWUW.js +0 -1
- package/dist/m/c-UNN7LWCT.js +0 -1
- package/dist/m/c-UXBMRDZB.js +0 -235
- package/dist/m/c-V7PZZRG7.js +0 -1
- package/dist/m/c-VL52ADDR.js +0 -1
- package/dist/m/c-WNYBLIOZ.js +0 -1
- package/dist/m/c-WWDBYXLK.js +0 -1
- package/dist/m/c-XNNW2CWK.js +0 -1
- package/dist/m/c-Y6USGTS5.js +0 -5
- package/dist/m/c-YG2NJYS6.js +0 -135
- package/dist/m/c-Z4VRC7GU.js +0 -7
- package/dist/m/c-Z6K52OLP.js +0 -19
- package/dist/m/c-ZDY2MDAA.js +0 -21
- package/dist/m/c-ZUCOKOHT.js +0 -1
- package/dist/m/core/admin-attrs-VGNLAUDK.js +0 -1
- package/dist/m/core/autosave-E4PEFPZ6.js +0 -1
- package/dist/m/core/conflict-notice-VQ6Y64PH.js +0 -1
- package/dist/m/core/host-permission-XN3K2IW3.js +0 -1
- package/dist/m/core/is-edit-mode-JXHDVGUL.js +0 -1
- package/dist/m/core/recovery-TBBSICE2.js +0 -1
- package/dist/m/core/save-WUCHTQBE.js +0 -1
- package/dist/m/core/save-core-FJVHT5M3.js +0 -1
- package/dist/m/core/snapshot-F3SWDZMM.js +0 -1
- package/dist/m/core/unsaved-warning-STKBHE6G.js +0 -1
- package/dist/m/events/index-23DPTUT4.js +0 -1
- package/dist/m/internals/index-WOVAP3RU.js +0 -1
- package/dist/m/lib/cache-bust-CGWT6ZJI.js +0 -1
- package/dist/m/loader-HXX6YNH5.js +0 -1
- package/dist/m/manifest-F22DEDAC.js +0 -1
- package/dist/m/manifest-edit-C3C7376B.js +0 -1
- package/dist/m/options/options-2LH76JQ3.js +0 -1
- package/dist/m/page/editor-XUTB2HB2.js +0 -189
- package/dist/m/plugins/ai-edit-ZQ4CGWIP.js +0 -6
- package/dist/m/plugins/demo-ZIBFAA3R.js +0 -1
- package/dist/m/plugins/indicator-PV4YW2ID.js +0 -1
- package/dist/m/plugins/live-NUTTXSBS.js +0 -1
- package/dist/m/plugins/menu-HCKO3DSP.js +0 -138
- package/dist/m/plugins/page-KSUH7G6J.js +0 -2
- package/dist/m/plugins/page-view-W73OW7WI.js +0 -1
- package/dist/m/plugins/people-AJJ32YCG.js +0 -1
- package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
- package/dist/m/plugins/sortable-YRMQX3ZU.js +0 -1
- package/dist/m/plugins/source-KHK5UUSK.js +0 -1
- package/dist/m/plugins/undo-OF2L3CXU.js +0 -1
- package/dist/m/plugins/upgrade-ZNYODP5Y.js +0 -1
- package/dist/m/plugins/upload-XSGNILUY.js +0 -1
- package/dist/m/plugins/visitor-drafts-XK5GW55Q.js +0 -21
- package/dist/m/plugins/visitor-drafts-owner-KSSH75EV.js +0 -1
- package/dist/m/plugins/wire-ZWVWKF76.js +0 -1
- package/dist/m/reader-core-ILWLXVMP.js +0 -68
- package/dist/m/route/route-KOCZSEGN.js +0 -1
- package/dist/m/sync/live-sync-4AUVXVJU.js +0 -1
- package/dist/m/ui/index-EVHKMKBL.js +0 -1
- package/dist/m/vendor/sap-core.vendor-A7BMUYZT.js +0 -6
- /package/dist/m/{c-FILLTJA7.js → c-QTTJYJMA.js} +0 -0
- /package/dist/m/plugins/{quickcrop-QNY3PKLD.js.LEGAL.txt → quickcrop-BQ5YYFCH.js.LEGAL.txt} +0 -0
- /package/dist/m/plugins/{richclay-RC3L2F6X.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
package/src/core/host-meta.js
CHANGED
|
@@ -149,6 +149,24 @@ export async function hostPageFormat() {
|
|
|
149
149
|
return meta.extensions.includes("page-format-v1") ? 1 : 0;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Where this host serves the page state of one accepted version, or null.
|
|
154
|
+
*
|
|
155
|
+
* Two things have to be true: the host announced `page-recovery-v1` by name, and its
|
|
156
|
+
* document block named the resource as `pageState.href`. A host that has the resource
|
|
157
|
+
* but has not announced the extension is still launching it, and a client that inferred
|
|
158
|
+
* either one from the other would retire journaled work against a pair nobody promised.
|
|
159
|
+
* Absent, empty or not a string is null, which means no conditional read and no proof
|
|
160
|
+
* from one: the records stay.
|
|
161
|
+
*
|
|
162
|
+
* @returns {Promise<?string>}
|
|
163
|
+
*/
|
|
164
|
+
export async function hostPageStateHref() {
|
|
165
|
+
const meta = await hostMeta();
|
|
166
|
+
if (!meta.extensions.includes("page-recovery-v1")) return null;
|
|
167
|
+
return meta.document?.pageState?.href || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
152
170
|
/**
|
|
153
171
|
* Whether this host keeps a version history a person can go back to.
|
|
154
172
|
*
|
|
@@ -163,6 +181,44 @@ export async function hostVersions() {
|
|
|
163
181
|
return meta.document?.versions === true;
|
|
164
182
|
}
|
|
165
183
|
|
|
184
|
+
const HOST_ACTIONS = ["codeEditor", "versions", "duplicate", "reveal", "export"];
|
|
185
|
+
|
|
186
|
+
function httpUrl(value) {
|
|
187
|
+
if (typeof value !== "string" || !value) return null;
|
|
188
|
+
try {
|
|
189
|
+
const url = new URL(value, location.href);
|
|
190
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url : null;
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* What the host offers this caller for the document, from the `document.actions` block:
|
|
198
|
+
* `{ codeEditor, versions, duplicate, reveal, export }`, each present only when the host
|
|
199
|
+
* named it. An action is a page to open (`{ href }`, any http(s) URL) or something the
|
|
200
|
+
* host does itself when the page asks (`{ post }`, a same-origin URL). A host without
|
|
201
|
+
* the block offers none of them.
|
|
202
|
+
*
|
|
203
|
+
* @returns {Promise<Record<string, {href: string} | {post: string}>>}
|
|
204
|
+
*/
|
|
205
|
+
export async function hostActions() {
|
|
206
|
+
const meta = await hostMeta();
|
|
207
|
+
const block = meta.document?.actions;
|
|
208
|
+
const actions = {};
|
|
209
|
+
if (!block || typeof block !== "object") return actions;
|
|
210
|
+
for (const name of HOST_ACTIONS) {
|
|
211
|
+
const href = httpUrl(block[name]?.href);
|
|
212
|
+
if (href) {
|
|
213
|
+
actions[name] = { href: href.href };
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const post = httpUrl(block[name]?.post);
|
|
217
|
+
if (post && post.origin === location.origin) actions[name] = { post: post.href };
|
|
218
|
+
}
|
|
219
|
+
return actions;
|
|
220
|
+
}
|
|
221
|
+
|
|
166
222
|
/** Drop the memoized answer. Tests only. */
|
|
167
223
|
export function resetHostMeta() {
|
|
168
224
|
inFlight = null;
|
package/src/core/is-edit-mode.js
CHANGED
|
@@ -83,19 +83,35 @@ const hintCanEdit = hostCanEdit || keptCanEdit;
|
|
|
83
83
|
const authoredRestriction = isDraftMode || servedStaleToken() || forcedEditMode === false ||
|
|
84
84
|
query.editmode === "false" || (isViewOnlyDocument && query.editmode !== "true");
|
|
85
85
|
let canEdit = hintCanEdit;
|
|
86
|
+
// The host's own answer as it stands, true or false, and null while it has said nothing
|
|
87
|
+
// either way. Kept beside canEdit because it is the same answer read for a different
|
|
88
|
+
// question: canEdit is what the page may do, and this is what the host has already said it
|
|
89
|
+
// will refuse.
|
|
90
|
+
let hostWritable = null;
|
|
86
91
|
|
|
87
92
|
// The host's own answer to "may this caller save here": false takes edit away, true grants
|
|
88
93
|
// it unless the document rules it out, and no answer leaves the hints in charge.
|
|
89
94
|
export function noteHostWritable(writable) {
|
|
95
|
+
hostWritable = typeof writable === "boolean" ? writable : null;
|
|
90
96
|
const next = writable === false ? false : writable === true ? hintCanEdit || !authoredRestriction : hintCanEdit;
|
|
91
97
|
if (next === canEdit) return;
|
|
92
98
|
canEdit = next;
|
|
93
99
|
if (typeof document !== "undefined") document.dispatchEvent(new Event("clay:capabilitychange"));
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
// What the host last answered about this page being writable, or null while it has said
|
|
103
|
+
// nothing. A denial is the client's own knowledge that a save will be refused, so the save
|
|
104
|
+
// lane reads it before building a request it already knows the host will not take.
|
|
105
|
+
export function learnedHostWritable() {
|
|
106
|
+
return hostWritable;
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
// Does this tab start editing? Only when something explicit asks for it.
|
|
110
|
+
// A receipt the page left on its own hide records where the tab happened to be, not a
|
|
111
|
+
// choice, so a tool page that now declares opens-in="edit" still opens editing. A switch
|
|
112
|
+
// the person made leaves an internal receipt, and that one is kept.
|
|
97
113
|
const startsEditing = receipt
|
|
98
|
-
? receipt.mode === "edit"
|
|
114
|
+
? receipt.mode === "edit" || (opensInEdit && !receipt.internal)
|
|
99
115
|
: query.editmode
|
|
100
116
|
? query.editmode === "true"
|
|
101
117
|
: forcedEditMode === true || opensInEdit;
|
package/src/core/mode-key.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { canEdit, isDraftMode, mode } from "./is-edit-mode.js";
|
|
2
2
|
import { setMode } from "./edit-mode.js";
|
|
3
3
|
import onDomReady from "../lib/dom-ready.js";
|
|
4
|
+
import { PERSIST, resolveRegionPolicy } from "../lib/region-policy.js";
|
|
4
5
|
|
|
5
6
|
const mac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
6
7
|
const chord = mac ? "⌘⇧E" : "Ctrl+Shift+E";
|
|
@@ -53,7 +54,16 @@ const NOTE_CSS = `.clay-mode-note {
|
|
|
53
54
|
opacity: .65;
|
|
54
55
|
}
|
|
55
56
|
.clay-mode-note button:hover { opacity: 1; background: rgba(23, 28, 36, .08); }
|
|
57
|
+
.clay-mode-note .clay-view-notice__edit {
|
|
58
|
+
width: auto;
|
|
59
|
+
padding: 0 10px;
|
|
60
|
+
border: 1px solid rgba(23, 28, 36, .2);
|
|
61
|
+
font-size: 13px;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
opacity: 1;
|
|
64
|
+
}
|
|
56
65
|
@media (prefers-color-scheme: dark) {
|
|
66
|
+
.clay-mode-note .clay-view-notice__edit { border-color: rgba(233, 233, 236, .22); }
|
|
57
67
|
.clay-mode-note {
|
|
58
68
|
border-color: rgba(233, 233, 236, .16);
|
|
59
69
|
color: var(--clay-ui-fg, #E9E9EC);
|
|
@@ -74,48 +84,144 @@ const NOTE_CSS = `.clay-mode-note {
|
|
|
74
84
|
}
|
|
75
85
|
}`;
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
// A note mounts in its own shadow root, so the page's CSS cannot restyle it. The helper is
|
|
88
|
+
// imported only when a note shows: this module loads on every page, a note rarely.
|
|
89
|
+
const closedNotes = new WeakSet();
|
|
90
|
+
|
|
91
|
+
function mountNote(content) {
|
|
92
|
+
import("../ui/editor-ui.js").then(({ mountEditorUI }) => {
|
|
93
|
+
if (!closedNotes.has(content)) mountEditorUI({ name: "mode-note", content, css: [NOTE_CSS] });
|
|
94
|
+
});
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
function closeNote(content) {
|
|
98
|
+
closedNotes.add(content);
|
|
99
|
+
content.getRootNode().host?.remove();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function switchTo(next) {
|
|
103
|
+
return setMode(next).then((r) => {
|
|
91
104
|
if (r.ok) return;
|
|
92
105
|
import("./mode-refusal.js").then(({ modeRefusalText }) => {
|
|
93
106
|
const text = modeRefusalText(r);
|
|
94
|
-
if (text)
|
|
107
|
+
if (!text) return;
|
|
108
|
+
// Templates that load no UI satellite still say why the switch did nothing.
|
|
109
|
+
if (typeof window.clay?.toast === "function") window.clay.toast(text, "error");
|
|
110
|
+
else if (typeof window.clay?.menu?.note === "function") window.clay.menu.note(text);
|
|
111
|
+
else showNote(text);
|
|
95
112
|
});
|
|
96
113
|
});
|
|
97
114
|
}
|
|
98
115
|
|
|
99
|
-
function
|
|
100
|
-
|
|
116
|
+
function onKey(e) {
|
|
117
|
+
if (!(canEdit || mode === "edit")) return;
|
|
118
|
+
if (!(mac ? e.metaKey : e.ctrlKey) || !e.shiftKey || e.altKey || e.code !== "KeyE" || e.isComposing) return;
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
switchTo(mode === "edit" ? "view" : "edit");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// A tab in view keeps nothing typed into it, and a page whose own script opens an
|
|
124
|
+
// editor at load (an OverType journal) accepts that typing all day. So for a person
|
|
125
|
+
// who can edit, the first edit to a field that saves in edit mode raises a notice
|
|
126
|
+
// that stays until the tab edits. Fields edit mode would unlock (inert
|
|
127
|
+
// contenteditable, viewmode:readonly, viewmode:disabled) raise it on the attempt.
|
|
128
|
+
const VIEW_NOTICE = "In view mode, so edits won't be saved.";
|
|
129
|
+
const SAVING_FIELD = ":is(input, textarea, select):is([persist], [clay-persist])";
|
|
130
|
+
const INERT_FIELD = "[editmode\\:contenteditable], [viewmode\\:readonly], [viewmode\\:disabled]";
|
|
131
|
+
|
|
132
|
+
let viewNotice = null;
|
|
133
|
+
|
|
134
|
+
function savedContent(el) {
|
|
135
|
+
if (!el.isConnected) return false;
|
|
136
|
+
const policy = resolveRegionPolicy(el);
|
|
137
|
+
return policy.persist === PERSIST.FULL && !policy.extension;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function savingField(node) {
|
|
141
|
+
const el = node?.nodeType === 1 ? node : node?.parentElement;
|
|
142
|
+
if (!el) return null;
|
|
143
|
+
const field = el.closest(SAVING_FIELD) || el.closest(INERT_FIELD) || (el.isContentEditable ? el : null);
|
|
144
|
+
return field && savedContent(field) ? field : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const liveField = (el) =>
|
|
148
|
+
el.isContentEditable || (el.matches("input, textarea, select") && !el.readOnly && !el.disabled);
|
|
149
|
+
|
|
150
|
+
const watching = () => canEdit && mode === "view";
|
|
151
|
+
|
|
152
|
+
function onViewInput(e) {
|
|
153
|
+
if (watching() && savingField(e.target)) showViewNotice();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Bubbles on window, after the page's own handlers, so a single-key shortcut the
|
|
157
|
+
// page handled (and prevented) never reads as an attempt to type.
|
|
158
|
+
function onViewKey(e) {
|
|
159
|
+
if (!watching() || e.defaultPrevented || e.isComposing || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
160
|
+
if (!(e.key.length === 1 && e.key !== " ") && e.key !== "Backspace" && e.key !== "Delete") return;
|
|
161
|
+
const target = e.target;
|
|
162
|
+
if (target?.nodeType !== 1 || liveField(target)) return;
|
|
163
|
+
if (target !== document.body && !target.closest(INERT_FIELD)) return;
|
|
164
|
+
const field = savingField(target === document.body ? document.getSelection()?.anchorNode : target);
|
|
165
|
+
if (field?.matches(INERT_FIELD)) showViewNotice();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onViewPointer(e) {
|
|
169
|
+
if (!watching()) return;
|
|
170
|
+
const field = e.target?.closest?.("[viewmode\\:disabled]");
|
|
171
|
+
if (field && savedContent(field)) showViewNotice();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function showViewNotice() {
|
|
175
|
+
if (viewNotice) return;
|
|
176
|
+
console.warn('clayjs: this tab is in view mode, so edits made here are not saved. clay.setMode("edit") switches; a page that must edit as soon as it opens declares <html opens-in="edit">.');
|
|
177
|
+
document.querySelector('clay-editor-ui[data-clay-ui="mode-note"]')?.remove();
|
|
178
|
+
viewNotice = document.createElement("div");
|
|
179
|
+
viewNotice.setAttribute("clay", "editor-ui");
|
|
180
|
+
viewNotice.setAttribute("role", "alert");
|
|
181
|
+
viewNotice.className = "clay-mode-note clay-view-notice";
|
|
182
|
+
viewNotice.innerHTML = '<span></span><button type="button" class="clay-view-notice__edit">Switch to edit</button>';
|
|
183
|
+
viewNotice.querySelector("span").textContent = VIEW_NOTICE;
|
|
184
|
+
viewNotice.querySelector("button").addEventListener("click", () => switchTo("edit"));
|
|
185
|
+
mountNote(viewNotice);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function onModeChange() {
|
|
189
|
+
if (mode === "view" || !viewNotice) return;
|
|
190
|
+
closeNote(viewNotice);
|
|
191
|
+
viewNotice = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function showNote(message) {
|
|
101
195
|
const note = document.createElement("div");
|
|
102
196
|
note.setAttribute("clay", "editor-ui");
|
|
103
197
|
note.setAttribute("role", "status");
|
|
104
198
|
note.className = "clay-mode-note";
|
|
105
199
|
note.innerHTML = `<span>This page opens in view mode. Press <kbd>${chord}</kbd> to edit.</span><button type="button" aria-label="Dismiss">×</button>`;
|
|
106
|
-
|
|
200
|
+
if (message) note.querySelector("span").textContent = message;
|
|
201
|
+
const close = () => closeNote(note);
|
|
107
202
|
note.querySelector("button").addEventListener("click", close);
|
|
108
203
|
addEventListener("keydown", (e) => { if (e.key === "Escape") close(); }, { once: true });
|
|
109
204
|
setTimeout(close, 12000);
|
|
110
|
-
|
|
205
|
+
mountNote(note);
|
|
111
206
|
}
|
|
112
207
|
|
|
113
208
|
if (!isDraftMode) {
|
|
114
209
|
document.addEventListener("keydown", onKey);
|
|
210
|
+
document.addEventListener("input", onViewInput, true);
|
|
211
|
+
document.addEventListener("pointerdown", onViewPointer, true);
|
|
212
|
+
window.addEventListener("keydown", onViewKey);
|
|
213
|
+
document.addEventListener("clay:modechange", onModeChange);
|
|
115
214
|
onDomReady(() => {
|
|
116
215
|
if (!canEdit || mode !== "view" || document.querySelector("[clay-mode-toggle]")) return;
|
|
117
|
-
|
|
118
|
-
|
|
216
|
+
// The note needs clay.storage, which the loader attaches after the core wave.
|
|
217
|
+
const whenReady = (run) => {
|
|
218
|
+
const ready = window.clay?.ready;
|
|
219
|
+
if (window.clay?.storage || typeof ready?.then !== "function") queueMicrotask(run);
|
|
220
|
+
else ready.then(run, () => {});
|
|
221
|
+
};
|
|
222
|
+
whenReady(() => {
|
|
223
|
+
// A menu loads after ready and carries its own way in.
|
|
224
|
+
if (window.clay?.menu || window.clay?.plugins?.includes?.("menu")) return;
|
|
119
225
|
const store = window.clay?.storage?.local;
|
|
120
226
|
if (!store || store.get("mode-note")) return;
|
|
121
227
|
store.set("mode-note", true);
|
package/src/core/mode-support.js
CHANGED
|
@@ -78,5 +78,10 @@ export const MODULE_SUPPORT = {
|
|
|
78
78
|
"plugins/cms.js": "export",
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
+
export const PAGE_FEATURE_SUPPORT = {
|
|
82
|
+
"page/ybind/recovery.js": { owner: "plugins/page.js", lifecycle: "child" },
|
|
83
|
+
"page/recovery-ui.js": { owner: "plugins/page.js", lifecycle: "child" },
|
|
84
|
+
};
|
|
85
|
+
|
|
81
86
|
export const supportFor = (path) =>
|
|
82
87
|
Object.prototype.hasOwnProperty.call(MODULE_SUPPORT, path) ? MODULE_SUPPORT[path] : null;
|
|
@@ -65,17 +65,30 @@ export async function runSaveBarrier({ generation = saveGeneration(), keepalive
|
|
|
65
65
|
if (!operation && !claimed) return refusal("busy");
|
|
66
66
|
const deadline = Date.now() + timeoutMs;
|
|
67
67
|
const permit = openSavePermit(generation);
|
|
68
|
+
const abort = new AbortController();
|
|
68
69
|
try {
|
|
69
70
|
for (let pass = 0; ; pass++) {
|
|
70
71
|
if (generation !== saveGeneration()) return refusal("stale-document");
|
|
71
72
|
if (Date.now() >= deadline) return refusal("timeout");
|
|
72
73
|
|
|
73
|
-
// Work that finishes on its own — a checkpoint, a queued upload — is asked to
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
// Work that finishes on its own — a checkpoint, a queued upload — is asked to, under
|
|
75
|
+
// this exit's own permit and inside its own deadline: the save such a source needs is
|
|
76
|
+
// the one write a closed session still admits. A composer waiting on its person has no
|
|
77
|
+
// flush and stays pending: that is a refusal, never a blur or a submit on their behalf.
|
|
78
|
+
const context = {
|
|
79
|
+
permit,
|
|
80
|
+
deadline,
|
|
81
|
+
keepalive,
|
|
82
|
+
signal: abort.signal,
|
|
83
|
+
timeoutMs: Math.max(0, deadline - Date.now()),
|
|
84
|
+
};
|
|
85
|
+
const sources = await bounded(flushUnsaved(context), deadline);
|
|
86
|
+
if (sources === EXPIRED) return refusal("timeout", { pending: pendingIds() });
|
|
87
|
+
if (!sources.ok) {
|
|
88
|
+
if (isSaveConflicted()) return refusal("conflict", { pending: sources.pending });
|
|
89
|
+
if (saveFateIsUnknown()) return refusal("save-unknown", { pending: sources.pending });
|
|
90
|
+
return refusal("pending-input", { pending: sources.pending });
|
|
91
|
+
}
|
|
79
92
|
if (sources.pending.length) return refusal("pending-input", { pending: sources.pending });
|
|
80
93
|
|
|
81
94
|
// Neither a conflict nor an unresolved fate is a failure a retry fixes.
|
|
@@ -106,6 +119,7 @@ export async function runSaveBarrier({ generation = saveGeneration(), keepalive
|
|
|
106
119
|
return { ok: true, state: result.state, etag: result.etag ?? null };
|
|
107
120
|
}
|
|
108
121
|
} finally {
|
|
122
|
+
abort.abort();
|
|
109
123
|
closeSavePermit(permit);
|
|
110
124
|
if (claimed) releaseDocumentOperation(claimed);
|
|
111
125
|
}
|
|
@@ -37,10 +37,10 @@ export function authoredAttr(el, name) {
|
|
|
37
37
|
// An attribute that was removed and written again lands at the end of the list, and
|
|
38
38
|
// attribute order is what a saved file compares: an untouched page would come back
|
|
39
39
|
// with `disabled` moved. Rebuilding the list in the recorded order is the only way to
|
|
40
|
-
// hand the file back exactly what it served. Runs only when something actually moved,
|
|
41
|
-
// and
|
|
42
|
-
export function restoreAuthoredOrder(el) {
|
|
43
|
-
const seen = authored.get(
|
|
40
|
+
// hand the file back exactly what it served. Runs only when something actually moved, on
|
|
41
|
+
// the way out of edit and on the save clone, whose record belongs to its live original.
|
|
42
|
+
export function restoreAuthoredOrder(el, from = el) {
|
|
43
|
+
const seen = authored.get(from);
|
|
44
44
|
if (!seen) return;
|
|
45
45
|
const current = el.getAttributeNames();
|
|
46
46
|
const desired = [
|
package/src/core/recovery.js
CHANGED
|
@@ -42,6 +42,28 @@ function pageWriterId() {
|
|
|
42
42
|
return `wr-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Each page holds a lock named for its writer for as long as it lives: a record whose
|
|
46
|
+
// writer still holds one belongs to an open tab that will save it, not to lost work.
|
|
47
|
+
const WRITER_LOCK = "clay-recovery-writer:";
|
|
48
|
+
|
|
49
|
+
function holdWriterLock(writerId) {
|
|
50
|
+
const locks = globalThis.navigator?.locks;
|
|
51
|
+
if (typeof locks?.request !== "function") return;
|
|
52
|
+
locks.request(WRITER_LOCK + writerId, () => new Promise(() => {})).catch(() => {});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function liveWriters(own) {
|
|
56
|
+
const locks = globalThis.navigator?.locks;
|
|
57
|
+
if (typeof locks?.query !== "function") return new Set();
|
|
58
|
+
try {
|
|
59
|
+
const { held = [] } = await locks.query();
|
|
60
|
+
return new Set(held.map((lock) => lock.name).filter((name) => typeof name === "string" && name.startsWith(WRITER_LOCK))
|
|
61
|
+
.map((name) => name.slice(WRITER_LOCK.length)).filter((id) => id !== own));
|
|
62
|
+
} catch {
|
|
63
|
+
return new Set();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
function recordKey(id) {
|
|
46
68
|
return Array.isArray(id) ? id.join("\u0000") : String(id);
|
|
47
69
|
}
|
|
@@ -112,6 +134,34 @@ export function noteRecoveryCapture({ captureId, html } = {}) {
|
|
|
112
134
|
for (const service of services) service.capture(captureId, html);
|
|
113
135
|
}
|
|
114
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Freeze what every service holds at this capture's boundary.
|
|
139
|
+
*
|
|
140
|
+
* A save that prepares across tasks cannot read the services after it, so the copies that
|
|
141
|
+
* could be covered are pinned here, synchronously, in the task the snapshot was taken in.
|
|
142
|
+
* `finishRecoveryCapture` then judges exactly those against the bytes the save verifies:
|
|
143
|
+
* a copy written after this capture is neither covered by it nor acknowledged by it.
|
|
144
|
+
*
|
|
145
|
+
* @param {*} captureId
|
|
146
|
+
*/
|
|
147
|
+
export function beginRecoveryCapture(captureId) {
|
|
148
|
+
if (!services.size || captureId == null) return;
|
|
149
|
+
for (const service of services) service.pinCapture(captureId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Record what this capture's final bytes cover, among the copies pinned at its boundary.
|
|
154
|
+
*
|
|
155
|
+
* Called with the verified bytes, before anything is sent. Nothing is acknowledged here.
|
|
156
|
+
*
|
|
157
|
+
* @param {*} captureId
|
|
158
|
+
* @param {string} html
|
|
159
|
+
*/
|
|
160
|
+
export function finishRecoveryCapture(captureId, html) {
|
|
161
|
+
if (!services.size || captureId == null) return;
|
|
162
|
+
for (const service of services) service.coverCapture(captureId, html);
|
|
163
|
+
}
|
|
164
|
+
|
|
115
165
|
/**
|
|
116
166
|
* Report how a capture settled.
|
|
117
167
|
*
|
|
@@ -153,10 +203,14 @@ export function createRecovery({
|
|
|
153
203
|
const channels = new Map();
|
|
154
204
|
const listeners = new Set();
|
|
155
205
|
const captures = new Map();
|
|
206
|
+
// What each preparing capture found held at its boundary, before it could read the
|
|
207
|
+
// final bytes. Cleared however the capture settles.
|
|
208
|
+
const pinned = new Map();
|
|
156
209
|
let prepared = null;
|
|
157
210
|
let identity = null;
|
|
158
211
|
let store = null;
|
|
159
212
|
let status = { ok: true, code: null, durable: false };
|
|
213
|
+
holdWriterLock(writerId);
|
|
160
214
|
|
|
161
215
|
function snapshot() {
|
|
162
216
|
return {
|
|
@@ -248,6 +302,56 @@ export function createRecovery({
|
|
|
248
302
|
return covered;
|
|
249
303
|
}
|
|
250
304
|
|
|
305
|
+
// The copies a capture taken now could cover, without reading its bytes: they do not
|
|
306
|
+
// exist yet, and by the time they do a newer copy may have replaced one of these.
|
|
307
|
+
function pinCapture(captureId) {
|
|
308
|
+
const list = [];
|
|
309
|
+
for (const entry of channels.values()) {
|
|
310
|
+
if (typeof entry.readSaved !== 'function' || !entry.held.size) continue;
|
|
311
|
+
for (const record of entry.held.values()) {
|
|
312
|
+
if (record.kind !== TEXT) continue;
|
|
313
|
+
list.push({
|
|
314
|
+
channel: entry.channel,
|
|
315
|
+
key: record.key,
|
|
316
|
+
id: record.id,
|
|
317
|
+
revision: record.revision,
|
|
318
|
+
payload: record.payload,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (list.length) pinned.set(captureId, list);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// The same coverage question `capture` asks, over the copies pinned at the capture and
|
|
326
|
+
// the bytes the save finally holds. The saved map is read once per channel, and only
|
|
327
|
+
// for a channel that had something it could cover.
|
|
328
|
+
function coverCapture(captureId, html) {
|
|
329
|
+
const list = pinned.get(captureId);
|
|
330
|
+
pinned.delete(captureId);
|
|
331
|
+
if (!list || typeof html !== 'string') return;
|
|
332
|
+
const saved = new Map();
|
|
333
|
+
const covered = new Map();
|
|
334
|
+
for (const candidate of list) {
|
|
335
|
+
if (!saved.has(candidate.channel)) {
|
|
336
|
+
const entry = channels.get(candidate.channel);
|
|
337
|
+
let read = null;
|
|
338
|
+
try {
|
|
339
|
+
read = typeof entry?.readSaved === 'function' ? entry.readSaved(html) : null;
|
|
340
|
+
} catch (_) {
|
|
341
|
+
read = null;
|
|
342
|
+
}
|
|
343
|
+
saved.set(candidate.channel, read && typeof read.get === 'function' ? read : null);
|
|
344
|
+
}
|
|
345
|
+
const read = saved.get(candidate.channel);
|
|
346
|
+
if (!read || !read.has(candidate.key)) continue;
|
|
347
|
+
if (!payloadsMatch(read.get(candidate.key), candidate.payload)) continue;
|
|
348
|
+
const coveredHere = covered.get(candidate.channel) ?? [];
|
|
349
|
+
coveredHere.push({ id: candidate.id, revision: candidate.revision });
|
|
350
|
+
covered.set(candidate.channel, coveredHere);
|
|
351
|
+
}
|
|
352
|
+
if (covered.size) captures.set(captureId, covered);
|
|
353
|
+
}
|
|
354
|
+
|
|
251
355
|
function capture(captureId, html) {
|
|
252
356
|
if (captureId == null || typeof html !== "string") return;
|
|
253
357
|
const covered = new Map();
|
|
@@ -277,6 +381,7 @@ export function createRecovery({
|
|
|
277
381
|
// acknowledgement is ordered against every put this service has already queued:
|
|
278
382
|
// a copy that reaches the database late meets the watermark it was refused by.
|
|
279
383
|
function settle(captureId, ok, etag) {
|
|
384
|
+
pinned.delete(captureId);
|
|
280
385
|
const covered = captures.get(captureId);
|
|
281
386
|
if (!covered) return Promise.resolve();
|
|
282
387
|
captures.delete(captureId);
|
|
@@ -471,7 +576,6 @@ export function createRecovery({
|
|
|
471
576
|
entry.batches += 1;
|
|
472
577
|
entry.batchBytes += record.payload.byteLength;
|
|
473
578
|
if (receipt.skipped) return { ok: true, id: record.id, revision, skipped: true };
|
|
474
|
-
planCompaction(entry);
|
|
475
579
|
return { ok: true, id: record.id, revision };
|
|
476
580
|
}
|
|
477
581
|
|
|
@@ -543,9 +647,9 @@ export function createRecovery({
|
|
|
543
647
|
* A swap that lost the head re-reads everything and tries again, so a compaction
|
|
544
648
|
* that raced an append or another compaction never deletes input it did not merge.
|
|
545
649
|
*/
|
|
546
|
-
async function swapBatches(entry, observed) {
|
|
650
|
+
async function swapBatches(entry, observed, rebase = null) {
|
|
547
651
|
for (let tries = 0; tries < 3; tries++) {
|
|
548
|
-
let capsuleId = null;
|
|
652
|
+
let capsuleId = rebase?.capsuleId ?? null;
|
|
549
653
|
const inputs = [];
|
|
550
654
|
for (const { id, revision } of observed) {
|
|
551
655
|
const read = await store.get(id);
|
|
@@ -556,7 +660,7 @@ export function createRecovery({
|
|
|
556
660
|
if (!sameId(capsuleId, record.capsuleId)) continue;
|
|
557
661
|
inputs.push(record);
|
|
558
662
|
}
|
|
559
|
-
if (!capsuleId || !inputs.length) return { ok: true, retired: [] };
|
|
663
|
+
if (!capsuleId || !(inputs.length || rebase)) return { ok: true, retired: [] };
|
|
560
664
|
const head = await store.get(capsuleId);
|
|
561
665
|
if (!head.ok || !head.record) return { ok: false, code: "missing" };
|
|
562
666
|
const capsule = head.record;
|
|
@@ -567,6 +671,8 @@ export function createRecovery({
|
|
|
567
671
|
].filter(Boolean);
|
|
568
672
|
const mergedOwned = owned.length ? copyPayload(await entry.merge(owned)) : new Uint8Array(0);
|
|
569
673
|
const mergedDependencies = dependencies.length ? copyPayload(await entry.merge(dependencies)) : new Uint8Array(0);
|
|
674
|
+
let payload = { base: capsule.payload?.base ?? new Uint8Array(0), dependencies: mergedDependencies, owned: mergedOwned };
|
|
675
|
+
if (rebase) payload = await rebase.rebuild(payload);
|
|
570
676
|
const revision = Math.max(entry.revision, capsule.revision) + 1;
|
|
571
677
|
entry.revision = revision;
|
|
572
678
|
const swapped = await store.compactBinary({
|
|
@@ -574,20 +680,11 @@ export function createRecovery({
|
|
|
574
680
|
version: entry.version,
|
|
575
681
|
capsuleId,
|
|
576
682
|
expectedRevision: capsule.revision,
|
|
577
|
-
replacement: {
|
|
578
|
-
revision,
|
|
579
|
-
updated: now?.() ?? Date.now(),
|
|
580
|
-
payload: {
|
|
581
|
-
base: capsule.payload?.base ?? new Uint8Array(0),
|
|
582
|
-
dependencies: mergedDependencies,
|
|
583
|
-
owned: mergedOwned,
|
|
584
|
-
},
|
|
585
|
-
},
|
|
683
|
+
replacement: { revision, updated: now?.() ?? Date.now(), context: rebase?.context, payload },
|
|
586
684
|
inputs: inputs.map((record) => ({ id: record.id, revision: record.revision })),
|
|
587
685
|
});
|
|
588
686
|
if (swapped.ok) {
|
|
589
|
-
entry.batches = 0;
|
|
590
|
-
entry.batchBytes = 0;
|
|
687
|
+
if (inputs.length) entry.batches = entry.batchBytes = 0;
|
|
591
688
|
return { ok: true, retired: swapped.retired, revision: swapped.revision };
|
|
592
689
|
}
|
|
593
690
|
if (swapped.code !== "stale") return { ok: false, code: swapped.code };
|
|
@@ -607,22 +704,11 @@ export function createRecovery({
|
|
|
607
704
|
return receipt.ok ? { ok: true, retired: receipt.retired } : { ok: false, code: receipt.code, retired: [] };
|
|
608
705
|
}
|
|
609
706
|
|
|
610
|
-
function
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if (!listed.ok) return;
|
|
616
|
-
const mine = listed.records.filter(
|
|
617
|
-
(record) => record.channel === entry.channel && record.kind === "binary-batch" && record.writerId === writerId
|
|
618
|
-
);
|
|
619
|
-
if (!mine.length) return;
|
|
620
|
-
await compact(entry, { records: mine.map(({ id, revision }) => ({ id, revision })) });
|
|
621
|
-
})()
|
|
622
|
-
.catch(() => {})
|
|
623
|
-
.finally(() => {
|
|
624
|
-
entry.compaction = null;
|
|
625
|
-
});
|
|
707
|
+
async function rebase(entry, { key, context, rebuild } = {}) {
|
|
708
|
+
const opened = await ensurePrepared();
|
|
709
|
+
const receipt = opened.ok ? await swapBatches(entry, [], { capsuleId: binaryOf(entry, String(key)), context, rebuild }) : opened;
|
|
710
|
+
note(receipt);
|
|
711
|
+
return receipt;
|
|
626
712
|
}
|
|
627
713
|
|
|
628
714
|
function flush(entry) {
|
|
@@ -644,8 +730,9 @@ export function createRecovery({
|
|
|
644
730
|
const result = await store.list();
|
|
645
731
|
note(result);
|
|
646
732
|
if (!result.ok) return { ok: false, code: result.code, records: [], status: snapshot() };
|
|
733
|
+
const live = await liveWriters(writerId);
|
|
647
734
|
const records = result.records
|
|
648
|
-
.filter((record) => record.channel === entry.channel)
|
|
735
|
+
.filter((record) => record.channel === entry.channel && !live.has(record.writerId))
|
|
649
736
|
.map((record) => ({ ...record, supported: record.version === entry.version }));
|
|
650
737
|
return { ok: true, records, status: snapshot() };
|
|
651
738
|
}
|
|
@@ -684,7 +771,7 @@ export function createRecovery({
|
|
|
684
771
|
return flushed;
|
|
685
772
|
}
|
|
686
773
|
|
|
687
|
-
const handle = { capture, settle };
|
|
774
|
+
const handle = { capture, settle, pinCapture, coverCapture };
|
|
688
775
|
|
|
689
776
|
function channelApi(entry) {
|
|
690
777
|
return {
|
|
@@ -698,6 +785,7 @@ export function createRecovery({
|
|
|
698
785
|
markAcknowledged: (spec) => markAcknowledged(entry, spec),
|
|
699
786
|
checkpoint: (spec) => checkpoint(entry, spec),
|
|
700
787
|
compact: (spec) => compact(entry, spec),
|
|
788
|
+
rebase: (spec) => rebase(entry, spec),
|
|
701
789
|
subscribe: (fn) => subscribe(entry, fn),
|
|
702
790
|
dispose: () => dispose(entry),
|
|
703
791
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The hook every accepted save reports to, in a module of its own so a reader can
|
|
2
|
+
// listen for accepted bytes without loading the save code.
|
|
3
|
+
const saveAcceptedHooks = [];
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Run a callback with the bytes the host just accepted.
|
|
7
|
+
*
|
|
8
|
+
* Called once per accepted save, after the response, with the exact body that was
|
|
9
|
+
* sent. Never called for a refused, failed or skipped save, and never in test mode,
|
|
10
|
+
* where nothing reached a host.
|
|
11
|
+
*
|
|
12
|
+
* @param {Function} callback - Receives the accepted HTML string
|
|
13
|
+
*/
|
|
14
|
+
export function onSaveAccepted(callback) {
|
|
15
|
+
saveAcceptedHooks.push(callback);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function notifySaveAccepted(html, captureId, artifact = null) {
|
|
19
|
+
for (const hook of saveAcceptedHooks) {
|
|
20
|
+
try {
|
|
21
|
+
// The artifact is the capture's own private model of these bytes, and it is
|
|
22
|
+
// offered only when it exists: a raw save and a capture without one keep the
|
|
23
|
+
// shape they had.
|
|
24
|
+
const accepted = artifact
|
|
25
|
+
? { captureId: captureId ?? null, artifact }
|
|
26
|
+
: { captureId: captureId ?? null };
|
|
27
|
+
hook(html, accepted);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('clayjs: onSaveAccepted hook failed:', err);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|