@panphora/clayjs 1.24.1 → 1.25.1
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 +6060 -4662
- package/dist/m/attrs/{refetch-on-save-DPGJJMGN.js → refetch-on-save-5CBFUJUF.js} +1 -1
- package/dist/m/attrs/save-freeze-PUYX32AK.js +1 -0
- package/dist/m/{c-HIX5H5L7.js → c-2A2ATCIH.js} +1 -1
- package/dist/m/{c-BOMQIQ3V.js → c-2EFMJYGJ.js} +1 -1
- package/dist/m/{c-MP6S4AZU.js → c-2GB34XL4.js} +2 -2
- package/dist/m/c-2TJCFPX4.js +4 -0
- package/dist/m/c-33SS5A4Y.js +1 -0
- package/dist/m/{c-PCNJETUA.js → c-34J5HC7N.js} +1 -1
- package/dist/m/{c-AT7GLQGV.js → c-3AAQJAKJ.js} +1 -1
- package/dist/m/c-3F3AADKG.js +1 -0
- package/dist/m/{c-HL6BLJNA.js → c-3WX5FABT.js} +1 -1
- package/dist/m/c-3XNTQQEJ.js +1 -0
- package/dist/m/{c-ZRGEN6QQ.js → c-3Y234AUB.js} +2 -2
- package/dist/m/{c-SUXHUL7U.js → c-42RLKGEY.js} +1 -1
- package/dist/m/{c-2BLVA6IN.js → c-44WITKWQ.js} +1 -1
- package/dist/m/c-47M4QHCK.js +1 -0
- package/dist/m/{c-UOUUI265.js → c-4M2T676V.js} +1 -1
- package/dist/m/{c-SXGEWPFC.js → c-4NAYDOWH.js} +1 -1
- package/dist/m/{c-OCYG4NBQ.js → c-4OZZ7ENT.js} +2 -2
- package/dist/m/{c-UOOIF2AL.js → c-5IIEOPIV.js} +1 -1
- package/dist/m/{c-YDW4VCHP.js → c-755ZOHOM.js} +1 -1
- package/dist/m/c-7Q4XYDF4.js +1 -0
- package/dist/m/c-7RHOMAMR.js +1 -0
- package/dist/m/{c-7V6MGMX2.js → c-7Z2625LX.js} +1 -1
- package/dist/m/{c-CTGRDT6E.js → c-A3OKSYRI.js} +1 -1
- package/dist/m/{c-GNI7WHKF.js → c-A5IYL7JI.js} +1 -1
- package/dist/m/c-A5LASUEZ.js +1 -0
- package/dist/m/c-AQVQKFKB.js +1 -0
- package/dist/m/{c-FLEIU6CG.js → c-ASGHHLFX.js} +1 -1
- package/dist/m/c-ASMLERFA.js +7 -0
- package/dist/m/{c-LUXAB2XI.js → c-AZM34SPO.js} +1 -1
- package/dist/m/{c-ZFO5RW6G.js → c-BHLYQGTA.js} +1 -1
- package/dist/m/{c-C5AKJVRH.js → c-BJ2RRQBQ.js} +1 -1
- package/dist/m/{c-FVPNX2GO.js → c-C27EVUQ6.js} +1 -1
- package/dist/m/{c-DFKBGSN2.js → c-C5UGNOC2.js} +1 -1
- package/dist/m/c-C6DXZIXD.js +1 -0
- package/dist/m/{c-EDE45NM7.js → c-C7SUMNMI.js} +1 -1
- package/dist/m/{c-STRI73GQ.js → c-CL5LSATX.js} +1 -1
- package/dist/m/c-CPRGR2II.js +1 -0
- package/dist/m/{c-NFQFV7OP.js → c-D2OPWOWO.js} +1 -1
- package/dist/m/{c-ZKKAF4UT.js → c-D4N4UJCQ.js} +2 -2
- package/dist/m/{c-6LEYVQJK.js → c-DAUQ6JZ3.js} +1 -1
- package/dist/m/{c-ZGCO4J3H.js → c-DDCYVLGG.js} +1 -1
- package/dist/m/{c-KQKKIVLW.js → c-DVMDYRAE.js} +1 -1
- package/dist/m/{c-OG6GFSUX.js → c-DYTBSJBI.js} +1 -1
- package/dist/m/{c-P2HKMJAW.js → c-EAV7A6BT.js} +1 -1
- package/dist/m/{c-X75G43SZ.js → c-EEWPNUM2.js} +1 -1
- package/dist/m/c-EHAR4RJJ.js +3 -0
- package/dist/m/c-EIDZHQHZ.js +1 -0
- package/dist/m/c-ELXMSEN5.js +1 -0
- package/dist/m/{c-QQFHEODI.js → c-EQ5NBOGA.js} +1 -1
- package/dist/m/c-FC5RUFPR.js +1 -0
- package/dist/m/{c-ND3Q2G2I.js → c-GFASNR55.js} +1 -1
- package/dist/m/{c-A4L7APIU.js → c-GNFDX6SU.js} +1 -1
- package/dist/m/{c-LATNQ6Q5.js → c-H3M6UDNV.js} +1 -1
- package/dist/m/{c-YLHTD3KX.js → c-H4M2T4A6.js} +1 -1
- package/dist/m/{c-DDS23EY6.js → c-HXGNH6GY.js} +1 -1
- package/dist/m/{c-PDBNCJCT.js → c-HYNM5U6B.js} +1 -1
- package/dist/m/c-J2YYLEVM.js +1 -0
- package/dist/m/c-J2ZW4HMR.js +1 -0
- package/dist/m/{c-WCG6AKA7.js → c-JC4RZW4E.js} +1 -1
- package/dist/m/c-K2ZHQMPF.js +1 -0
- package/dist/m/{c-6IJEVE6B.js → c-KU3PX7LL.js} +1 -1
- package/dist/m/{c-YYMZWQLR.js → c-L6KVXPBO.js} +1 -1
- package/dist/m/{c-CV4T65JE.js → c-LUBZU4AZ.js} +4 -4
- package/dist/m/{c-5FXEKU4Y.js → c-LYNLY3MR.js} +1 -1
- package/dist/m/{c-NQAPWM7O.js → c-MHTO3QAD.js} +1 -1
- package/dist/m/{c-HDVH3MVC.js → c-MKAUC6DB.js} +1 -1
- package/dist/m/c-MLMJUNRV.js +1 -0
- package/dist/m/c-MMEVDN6D.js +1 -0
- package/dist/m/c-MOP5KBWZ.js +1 -0
- package/dist/m/{c-LA2BW7UY.js → c-MZCOGNZL.js} +1 -1
- package/dist/m/{c-XW5TIYYB.js → c-N4GEJMXW.js} +1 -1
- package/dist/m/{c-RS77ODLY.js → c-NL4I4KNG.js} +1 -1
- package/dist/m/{c-2PVGJFI5.js → c-NW4ZFKCY.js} +1 -1
- package/dist/m/{c-PYG2NGWU.js → c-O3DPFZCE.js} +1 -1
- package/dist/m/c-O4YIDULY.js +1 -0
- package/dist/m/{c-LTSPHVAK.js → c-OBMZASYQ.js} +1 -1
- package/dist/m/c-OZOACVUD.js +1 -0
- package/dist/m/{c-YK2L436K.js → c-OZVK3TFI.js} +1 -1
- package/dist/m/c-P73XPAOA.js +1 -0
- package/dist/m/c-PP3CUF3U.js +1 -0
- package/dist/m/{c-B7GOFGQR.js → c-Q2CB57UG.js} +1 -1
- package/dist/m/{c-TWCXBWS3.js → c-QCDVDZIR.js} +1 -1
- package/dist/m/{c-LSVN3OLG.js → c-QL4NYM2L.js} +1 -1
- package/dist/m/c-RES2S5Q3.js +1 -0
- package/dist/m/c-RFGELYEU.js +2 -0
- package/dist/m/c-RGFPLQGV.js +1 -0
- package/dist/m/c-SMLAJASY.js +1 -0
- package/dist/m/{c-MQKSKQHG.js → c-STAPQ2X6.js} +1 -1
- package/dist/m/c-SYVF6ZMF.js +1 -0
- package/dist/m/{c-SE3SIY7Y.js → c-T5IWJOMW.js} +1 -1
- package/dist/m/{c-DD26VXAP.js → c-T7LOTEWO.js} +1 -1
- package/dist/m/{c-47IBJGPQ.js → c-TWB5ZQU4.js} +1 -1
- package/dist/m/c-U35T5NAY.js +1 -0
- package/dist/m/c-U4TWEIKZ.js +1 -0
- package/dist/m/c-UW6MMZ4V.js +1 -0
- package/dist/m/{c-JWDCQL6J.js → c-V5L6HWVL.js} +1 -1
- package/dist/m/c-VGG3UR7I.js +1 -0
- package/dist/m/{c-CZHTXGJS.js → c-VO6RSFCO.js} +1 -1
- package/dist/m/c-VU4WEWPH.js +4 -0
- package/dist/m/c-W7LWCJHD.js +1 -0
- package/dist/m/c-WOJ2RTV4.js +1 -0
- package/dist/m/{c-T3P3AXEW.js → c-WOWGGNCJ.js} +1 -1
- package/dist/m/{c-6RGEQNVV.js → c-XCOQNISP.js} +1 -1
- package/dist/m/{c-SGGGCIVD.js → c-XS7G6U3Z.js} +1 -1
- package/dist/m/{c-4PO3YGDC.js → c-Y3J5LDKB.js} +1 -1
- package/dist/m/c-Y3MPVURA.js +1 -0
- package/dist/m/c-Y6HH7LTM.js +1 -0
- package/dist/m/{c-RO73MKKR.js → c-YRDKVEGR.js} +1 -1
- package/dist/m/{c-FEC6LGFF.js → c-YZGWSCMT.js} +1 -1
- package/dist/m/{c-7CM4GMTS.js → c-ZJ7726FO.js} +1 -1
- package/dist/m/{c-SKMIR5HQ.js → c-ZPI7M5U5.js} +1 -1
- package/dist/m/core/{admin-attrs-ONRWVXPK.js → admin-attrs-43JBA6JB.js} +1 -1
- package/dist/m/core/{autosave-TBKAAAFB.js → autosave-HD7Y7UNV.js} +1 -1
- package/dist/m/core/{conflict-notice-FOEE6I7E.js → conflict-notice-Y2NTMDGJ.js} +1 -1
- package/dist/m/core/draft-mode-M5XNYBMY.js +1 -0
- package/dist/m/core/{host-permission-X4BENZEU.js → host-permission-5BYP24GC.js} +1 -1
- package/dist/m/core/{is-edit-mode-FFHCQ5DR.js → is-edit-mode-73OXKK5H.js} +1 -1
- package/dist/m/core/{persist-6ILUR34S.js → persist-MX3VYVBN.js} +1 -1
- package/dist/m/core/{recovery-APWADU4K.js → recovery-VZF7RJZF.js} +1 -1
- package/dist/m/core/{save-6HT2LABE.js → save-JZ7R4ET2.js} +1 -1
- package/dist/m/core/{save-core-EVMNZQ4V.js → save-core-I4N6S4ED.js} +1 -1
- package/dist/m/core/{site-edits-G4BOZMV4.js → site-edits-TIJDPG6H.js} +1 -1
- package/dist/m/core/{snapshot-FJP5IJL3.js → snapshot-N5MKDGNI.js} +1 -1
- package/dist/m/core/unsaved-warning-BP6ZY3PG.js +1 -0
- package/dist/m/events/{index-B6TGDYMG.js → index-YKQIBZE5.js} +1 -1
- package/dist/m/internals/index-3LAVREAU.js +1 -0
- package/dist/m/lib/{cache-bust-NH7VBWHO.js → cache-bust-BL4KUQAJ.js} +1 -1
- package/dist/m/{loader-GLLVDUZA.js → loader-7ORQVRCK.js} +1 -1
- package/dist/m/manifest-A27A4278.js +1 -0
- package/dist/m/manifest-edit-E8A61EB4.js +1 -0
- package/dist/m/manifest.json +1608 -1498
- package/dist/m/options/{options-55LQHSJ4.js → options-LKIZ7YTG.js} +1 -1
- package/dist/m/page/{editor-KAZDJNND.js → editor-HS2CAV3W.js} +5 -5
- package/dist/m/plugins/{ai-edit-LABECCSN.js → ai-edit-WDV2GLDU.js} +2 -2
- package/dist/m/plugins/{cms-T2HLI7JG.js → cms-4KET6RZT.js} +9 -9
- package/dist/m/plugins/{demo-T7B4RIKL.js → demo-LMCMBILC.js} +1 -1
- package/dist/m/plugins/{derive-6LPJOCAV.js → derive-R4USHZ7P.js} +1 -1
- package/dist/m/plugins/{indicator-J4ABFARX.js → indicator-OLPZ4WFI.js} +1 -1
- package/dist/m/plugins/{live-PGBIEMGB.js → live-CO4VJS46.js} +1 -1
- package/dist/m/plugins/{menu-YZL3X2AJ.js → menu-ESEFQARM.js} +1 -1
- package/dist/m/plugins/{page-WOCNX2IN.js → page-L3VKIQU2.js} +2 -2
- package/dist/m/plugins/{page-view-SJODOOGE.js → page-view-6KNTXABH.js} +1 -1
- package/dist/m/plugins/{people-C4TCWFOQ.js → people-EBWE337I.js} +1 -1
- package/dist/m/plugins/quickcrop-VNRXGRPC.js +26 -0
- package/dist/m/plugins/{richclay-Y2NV44NA.js → richclay-V6WKJTUF.js} +17 -17
- package/dist/m/plugins/sortable-BAG3MSWS.js +1 -0
- package/dist/m/plugins/{source-YK7MO23W.js → source-OYHDXKTB.js} +1 -1
- package/dist/m/plugins/undo-WUK2NLBA.js +1 -0
- package/dist/m/plugins/{upgrade-NNTJBSK2.js → upgrade-SZQJYEQQ.js} +1 -1
- package/dist/m/plugins/{upload-ZKKJV76X.js → upload-NZET3DNQ.js} +1 -1
- package/dist/m/plugins/visitor-drafts-ZE6PCAFP.js +20 -0
- package/dist/m/plugins/{visitor-drafts-owner-LHUHFU3L.js → visitor-drafts-owner-TLECTYWS.js} +1 -1
- package/dist/m/plugins/{visitor-menu-FY4KM2SW.js → visitor-menu-5XY57JQA.js} +1 -1
- package/dist/m/plugins/wire-S5ATC4QI.js +1 -0
- package/dist/m/reader-core-AHOAP5DH.js +77 -0
- package/dist/m/sync/live-sync-DX4K5GDB.js +1 -0
- package/dist/m/ui/index-G7ZJXNMG.js +1 -0
- package/dist/m/vendor/sap-core.vendor-5TWOJYVM.js +7 -0
- package/dist/offline-manifest.json +844 -784
- package/dist/server/core.cjs +4245 -2614
- package/dist/server/markdown.cjs +1 -0
- package/dist/server/markdown.mjs +7 -0
- package/docs/guide.md +27 -37
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +6 -5
- package/package.json +6 -2
- package/packed-contract.json +3 -0
- package/src/attrs/save-freeze.js +1 -1
- package/src/core/admin-contenteditable.js +1 -1
- package/src/core/admin-inputs.js +1 -1
- package/src/core/admin-onclick.js +1 -1
- package/src/core/admin-resources.js +5 -2
- package/src/core/conflict-notice-ui.js +59 -34
- package/src/core/draft-mode.js +5 -0
- package/src/core/save-core.js +10 -10
- package/src/core/save.js +10 -10
- package/src/core/snapshot.js +56 -43
- package/src/core/stale-host-notice.js +20 -7
- package/src/lib/dirty-gate.js +19 -0
- package/src/page/editor.js +10 -4
- package/src/page/import-markdown.js +22 -8
- package/src/page/save-projection.js +1 -1
- package/src/plugins/menu/visitor.js +33 -1
- package/src/plugins/quickcrop.js +128 -69
- package/src/plugins/sortable.js +25 -9
- package/src/plugins/visitor-drafts/commit.js +50 -3
- package/src/plugins/visitor-drafts/idb.js +8 -2
- package/src/plugins/visitor-drafts/recovery-menu.js +118 -0
- package/src/plugins/visitor-drafts/recovery.js +64 -0
- package/src/plugins/visitor-drafts/session.js +61 -13
- package/src/plugins/visitor-drafts/store.js +44 -8
- package/src/plugins/visitor-drafts.js +34 -5
- package/src/server/markdown/index.js +23 -0
- package/src/sync/conflicts.js +3 -1
- package/src/sync/data-guard-notice.js +137 -18
- package/src/sync/live-sync.js +1 -1
- package/src/sync/wire-profile.js +9 -4
- package/src/ui/bevel-controls.js +4 -1
- package/src/ui/dialogs.js +11 -7
- package/src/ui/modal.js +9 -7
- package/src/ui/toast.js +67 -32
- package/src/vendor/hyper-html-api-data.vendor.js +1 -1
- 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 +7 -7
- package/src/vendor/richclay.vendor.js +16 -16
- package/src/vendor/sap-core.vendor.js +6 -5
- package/src/vendor/sap-ownership.vendor.js +181 -11
- package/tool-generation/context.md +69 -0
- package/tool-generation/system.md +2 -2
- package/dist/m/attrs/save-freeze-JZHKRQLR.js +0 -1
- package/dist/m/c-277QP5ET.js +0 -1
- package/dist/m/c-3UW5EJ77.js +0 -1
- package/dist/m/c-42ZNV7LI.js +0 -7
- package/dist/m/c-6Q6RGFBF.js +0 -1
- package/dist/m/c-APGHGDZM.js +0 -1
- package/dist/m/c-BGN7RCTJ.js +0 -1
- package/dist/m/c-CJ7QYEWI.js +0 -1
- package/dist/m/c-CVSWLX3X.js +0 -1
- package/dist/m/c-DXPOFLEJ.js +0 -1
- package/dist/m/c-EILLN2H2.js +0 -1
- package/dist/m/c-EODTTNVA.js +0 -1
- package/dist/m/c-ESK4S67I.js +0 -1
- package/dist/m/c-FDWR3FL7.js +0 -1
- package/dist/m/c-FYDKYW6E.js +0 -1
- package/dist/m/c-HKSLOVU4.js +0 -1
- package/dist/m/c-HTLYXCUH.js +0 -1
- package/dist/m/c-IUL7PU4P.js +0 -1
- package/dist/m/c-JQF2EUCG.js +0 -1
- package/dist/m/c-MFOSMAVM.js +0 -3
- package/dist/m/c-MQYEDF3Q.js +0 -1
- package/dist/m/c-MR75YPRT.js +0 -1
- package/dist/m/c-NHXOBD6A.js +0 -1
- package/dist/m/c-NVU3DORS.js +0 -1
- package/dist/m/c-OYHI67B6.js +0 -1
- package/dist/m/c-PWJSJEJU.js +0 -1
- package/dist/m/c-QENKN7QI.js +0 -1
- package/dist/m/c-R23UHMXG.js +0 -1
- package/dist/m/c-SGH3UTF3.js +0 -1
- package/dist/m/c-SXNSSHPP.js +0 -1
- package/dist/m/c-TFLRT5JX.js +0 -1
- package/dist/m/c-U4NTUVDZ.js +0 -1
- package/dist/m/c-V2U3F4QX.js +0 -1
- package/dist/m/c-VG5XIN4T.js +0 -1
- package/dist/m/c-VHSMPITY.js +0 -1
- package/dist/m/c-Z4G2UK6T.js +0 -4
- package/dist/m/core/draft-mode-4MCL3LYI.js +0 -1
- package/dist/m/core/unsaved-warning-ITZDIZ2I.js +0 -1
- package/dist/m/internals/index-HS3CQDFW.js +0 -1
- package/dist/m/manifest-50FF1C45.js +0 -1
- package/dist/m/manifest-edit-BE1A621D.js +0 -1
- package/dist/m/plugins/quickcrop-X5MRIBAP.js +0 -26
- package/dist/m/plugins/sortable-XCHQG65Z.js +0 -1
- package/dist/m/plugins/undo-VLRBQNPN.js +0 -1
- package/dist/m/plugins/visitor-drafts-RTZNONHT.js +0 -20
- package/dist/m/plugins/wire-M2NR354P.js +0 -1
- package/dist/m/reader-core-T2S3JC4E.js +0 -77
- package/dist/m/sync/live-sync-Z64YREWQ.js +0 -1
- package/dist/m/ui/index-MGTWOW2C.js +0 -1
- package/dist/m/vendor/sap-core.vendor-QAEPMETX.js +0 -6
- /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
- /package/dist/m/plugins/{quickcrop-X5MRIBAP.js.LEGAL.txt → quickcrop-VNRXGRPC.js.LEGAL.txt} +0 -0
- /package/dist/m/plugins/{richclay-Y2NV44NA.js.LEGAL.txt → richclay-V6WKJTUF.js.LEGAL.txt} +0 -0
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { saveToken } from "../core/host-attrs.js";
|
|
2
2
|
import { storage } from "../lib/doc-storage.js";
|
|
3
|
+
import { isSaveInProgress } from "../core/save-core.js";
|
|
3
4
|
|
|
4
|
-
const TITLE = "
|
|
5
|
+
const TITLE = "Some saved data was removed";
|
|
6
|
+
const TITLES = {
|
|
7
|
+
recent: "A change from outside this page removed something you typed",
|
|
8
|
+
"all-typed": "A change from outside this page removed everything you typed",
|
|
9
|
+
emptied: "A change from outside this page emptied this page's data",
|
|
10
|
+
unreadable: "A change from outside this page left its data unreadable",
|
|
11
|
+
};
|
|
12
|
+
const TYPED_SHOWN = 3;
|
|
13
|
+
const HOUR_MS = 3600000;
|
|
5
14
|
const DONE_KEY = "data-guard-done";
|
|
6
15
|
const READ_TIMEOUT_MS = 6000;
|
|
7
16
|
const AFTER_SAVE_MS = 1500;
|
|
8
17
|
const MIN_READ_GAP_MS = 5000;
|
|
9
18
|
const VISIBLE_GAP_MS = 30000;
|
|
19
|
+
const EXTERNAL_WINDOW_MS = 10000;
|
|
20
|
+
const SETTLE_CAP_MS = 6000;
|
|
21
|
+
const SAVE_UNSETTLED_EVENTS = ["clay:save-error", "clay:save-offline", "clay:save-conflict"];
|
|
10
22
|
const UNREACHED = "Could not reach the server. Try again.";
|
|
11
23
|
const REFUSED = "That did not go through. Try again.";
|
|
12
24
|
const SAVE_FIRST = "Save your changes first, then try again.";
|
|
@@ -20,7 +32,15 @@ const isPending = (event) => Boolean(event) && typeof event.id === "string" && e
|
|
|
20
32
|
const allowed = (current, event, choice, flag) => flag === true && !current.refused.has(`${event.id}:${choice}`);
|
|
21
33
|
const canRestore = (current, event) => allowed(current, event, "restore", event.restorable);
|
|
22
34
|
const canRevert = (current, event) => allowed(current, event, "revert", event.canRevert);
|
|
35
|
+
const canPutBack = (current, event) => allowed(current, event, "put-back", event.canPutBack);
|
|
36
|
+
const typed = (event) => (Array.isArray(event.typed) ? event.typed : []);
|
|
37
|
+
const reasoned = (event) => Boolean(event.reason);
|
|
38
|
+
const titleFor = (event) => TITLES[event.reason] ?? TITLE;
|
|
39
|
+
const typedLoss = (event) => event.reason === "recent" || event.reason === "all-typed";
|
|
40
|
+
const toneFor = (event) => (typedLoss(event) ? "info" : "warning");
|
|
23
41
|
const credentials = () => (saveToken() ? "omit" : "same-origin");
|
|
42
|
+
const settling = (current) =>
|
|
43
|
+
isSaveInProgress() || (current.lastExternal > 0 && Date.now() - current.lastExternal < EXTERNAL_WINDOW_MS);
|
|
24
44
|
const headers = (extra = {}) => ({ "Document-URL": location.href, ...extra });
|
|
25
45
|
|
|
26
46
|
function removeServerBar() {
|
|
@@ -34,7 +54,7 @@ function say(message, type) {
|
|
|
34
54
|
}, () => {});
|
|
35
55
|
}
|
|
36
56
|
|
|
37
|
-
function
|
|
57
|
+
function sourceOf(event) {
|
|
38
58
|
const summary = event.lossSummary || {};
|
|
39
59
|
const source = summary.provenance === "external" ? "A change from outside this page" : "A save from this page";
|
|
40
60
|
const loss = summary.removedItems > 0
|
|
@@ -43,12 +63,54 @@ function describe(current, event) {
|
|
|
43
63
|
const row = Array.isArray(event.preview) ? event.preview[0] : null;
|
|
44
64
|
const parts = [`${source} ${loss}.`];
|
|
45
65
|
if (row) parts.push(`${row.key}: ${row.yours} before, ${row.now} now.`);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
return parts;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function labelFor(path) {
|
|
70
|
+
const parts = String(path ?? "").split(".").filter((part) => part && !/^\[[^\]]*\]$/.test(part));
|
|
71
|
+
const last = (parts[parts.length - 1] ?? "").replace(/\[[^\]]*\]/g, "").trim();
|
|
72
|
+
return last.replace(/[_-]+/g, " ") || "value";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function typedLine(event) {
|
|
76
|
+
const rows = typed(event);
|
|
77
|
+
if (!rows.length) return "";
|
|
78
|
+
const shown = rows.slice(0, TYPED_SHOWN).map((row) => `${labelFor(row.path)}: "${row.yours}"`);
|
|
79
|
+
const more = rows.length - shown.length;
|
|
80
|
+
return `${shown.join("; ")}${more > 0 ? ` and ${more} more.` : "."}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function typedWhen(event) {
|
|
84
|
+
if (event.reason === "all-typed") return "";
|
|
85
|
+
const row = typed(event)[0];
|
|
86
|
+
const at = typeof row?.at === "number" ? row.at : Date.parse(row?.at);
|
|
87
|
+
if (!Number.isFinite(at)) return "";
|
|
88
|
+
const ago = Date.now() - at;
|
|
89
|
+
if (ago < 0 || ago >= HOUR_MS) return "";
|
|
90
|
+
const minutes = Math.max(1, Math.floor(ago / 60000));
|
|
91
|
+
return `You typed ${typed(event).length === 1 ? "it" : "them"} ${plural(minutes, "minute")} ago.`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function detailFor(current, event) {
|
|
95
|
+
if (!reasoned(event)) {
|
|
96
|
+
const parts = sourceOf(event);
|
|
97
|
+
if (canRestore(current, event)) {
|
|
98
|
+
if (event.droppedAdditions > 0) parts.push(`Restoring removes ${plural(event.droppedAdditions, "item")} added or changed since.`);
|
|
99
|
+
} else if (canRevert(current, event)) {
|
|
100
|
+
parts.push("This data no longer fits the page, but the whole change can be undone.");
|
|
101
|
+
} else {
|
|
102
|
+
parts.push("It cannot be restored from here.");
|
|
103
|
+
}
|
|
104
|
+
if (canRevert(current, event)) parts.push("Undo the change also removes edits made since.");
|
|
105
|
+
return parts.join(" ");
|
|
106
|
+
}
|
|
107
|
+
const parts = [];
|
|
108
|
+
if (typedLoss(event)) {
|
|
109
|
+
parts.push(typedLine(event) || sourceOf(event).join(" "));
|
|
110
|
+
const when = typedWhen(event);
|
|
111
|
+
if (when) parts.push(when);
|
|
50
112
|
} else {
|
|
51
|
-
parts.push("
|
|
113
|
+
parts.push(sourceOf(event).join(" "));
|
|
52
114
|
}
|
|
53
115
|
if (canRevert(current, event)) parts.push("Undo the change also removes edits made since.");
|
|
54
116
|
return parts.join(" ");
|
|
@@ -56,14 +118,26 @@ function describe(current, event) {
|
|
|
56
118
|
|
|
57
119
|
function actionsFor(current, event) {
|
|
58
120
|
const actions = [];
|
|
59
|
-
if (
|
|
121
|
+
if (!reasoned(event)) {
|
|
122
|
+
if (canRestore(current, event)) actions.push({ label: "Restore", onClick: () => choose(current, event, "restore") });
|
|
123
|
+
if (canRevert(current, event)) actions.push({ label: "Undo the change", onClick: () => choose(current, event, "revert") });
|
|
124
|
+
actions.push({ label: "Keep as is", onClick: () => choose(current, event, "dismiss") });
|
|
125
|
+
return actions;
|
|
126
|
+
}
|
|
127
|
+
if (canPutBack(current, event)) actions.push({ label: "Put it back", onClick: () => choose(current, event, "put-back") });
|
|
60
128
|
if (canRevert(current, event)) actions.push({ label: "Undo the change", onClick: () => choose(current, event, "revert") });
|
|
61
|
-
actions.push({ label: "Keep
|
|
129
|
+
actions.push({ label: "Keep the change", onClick: () => choose(current, event, "dismiss") });
|
|
62
130
|
return actions;
|
|
63
131
|
}
|
|
64
132
|
|
|
65
133
|
async function draw(current, event) {
|
|
66
|
-
const
|
|
134
|
+
const title = titleFor(event);
|
|
135
|
+
const detail = detailFor(current, event);
|
|
136
|
+
const key = [
|
|
137
|
+
event.id, title, event.reason ?? "", JSON.stringify(typed(event)),
|
|
138
|
+
canRestore(current, event), canPutBack(current, event), canRevert(current, event),
|
|
139
|
+
event.lastWriteAt, detail,
|
|
140
|
+
].join("|");
|
|
67
141
|
if (current.drawn === key) return true;
|
|
68
142
|
current.drawn = key;
|
|
69
143
|
current.eventId = event.id;
|
|
@@ -77,15 +151,20 @@ async function draw(current, event) {
|
|
|
77
151
|
return false;
|
|
78
152
|
}
|
|
79
153
|
if (session !== current || current.drawn !== key) return false;
|
|
80
|
-
|
|
81
|
-
|
|
154
|
+
const previous = current.title;
|
|
155
|
+
current.title = title;
|
|
156
|
+
if (previous && previous !== title) ui.dismissPersistent(previous, { now: true });
|
|
157
|
+
ui.dismissPersistent(title, { now: true });
|
|
158
|
+
ui.toastPersistent(title, toneFor(event), { detail, actions: actionsFor(current, event) });
|
|
82
159
|
return true;
|
|
83
160
|
}
|
|
84
161
|
|
|
85
162
|
function clearNotice(current) {
|
|
163
|
+
const title = current.title;
|
|
86
164
|
current.drawn = null;
|
|
87
165
|
current.eventId = null;
|
|
88
|
-
|
|
166
|
+
current.title = null;
|
|
167
|
+
if (title) ui?.dismissPersistent(title, { now: true });
|
|
89
168
|
}
|
|
90
169
|
|
|
91
170
|
async function read(current) {
|
|
@@ -113,7 +192,11 @@ async function read(current) {
|
|
|
113
192
|
if (session !== current || turn !== current.turn) return;
|
|
114
193
|
if (!body || typeof body !== "object" || !("event" in body)) return;
|
|
115
194
|
if (isPending(body.event)) {
|
|
116
|
-
if (current.resolved.has(body.event.id)) return;
|
|
195
|
+
if (current.resolved.has(body.event.id) || current.waiting === body.event.id) return;
|
|
196
|
+
if (!current.waited.has(body.event.id) && settling(current)) {
|
|
197
|
+
waitForSettle(current, body.event.id);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
117
200
|
if (await draw(current, body.event)) removeServerBar();
|
|
118
201
|
return;
|
|
119
202
|
}
|
|
@@ -150,7 +233,7 @@ function settle(current, event, choice, status, body) {
|
|
|
150
233
|
} else if (status === 422) {
|
|
151
234
|
current.refused.add(`${event.id}:${choice}`);
|
|
152
235
|
if (current.eventId === event.id && !current.resolved.has(event.id)) draw(current, event);
|
|
153
|
-
say(choice === "restore" ? "That data no longer fits this page, so it could not be restored." : "That earlier version is no longer available.", "warning");
|
|
236
|
+
say(choice === "restore" || choice === "put-back" ? "That data no longer fits this page, so it could not be restored." : "That earlier version is no longer available.", "warning");
|
|
154
237
|
} else if (status === 401 || status === 403) {
|
|
155
238
|
say(RELOAD_FIRST, "error");
|
|
156
239
|
} else {
|
|
@@ -202,17 +285,44 @@ function onNotification(current, e) {
|
|
|
202
285
|
if (typeof data?.id !== "string") return;
|
|
203
286
|
current.turn++;
|
|
204
287
|
current.resolved.add(data.id);
|
|
288
|
+
if (current.waiting === data.id) stopWaiting(current);
|
|
205
289
|
if (current.eventId === data.id) clearNotice(current);
|
|
206
290
|
return;
|
|
207
291
|
}
|
|
208
292
|
if (action === "raised" && isPending(data) && !current.resolved.has(data.id)) {
|
|
209
293
|
current.turn++;
|
|
294
|
+
if (current.waiting === data.id) return;
|
|
295
|
+
if (!current.waited.has(data.id) && settling(current)) {
|
|
296
|
+
waitForSettle(current, data.id);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
210
299
|
draw(current, data).then((drawn) => { if (drawn) removeServerBar(); }, () => {});
|
|
211
300
|
}
|
|
212
301
|
}
|
|
213
302
|
|
|
214
303
|
const readAt = (current) => Math.max(current.lastSave + AFTER_SAVE_MS, current.lastRead + MIN_READ_GAP_MS);
|
|
215
304
|
|
|
305
|
+
function stopWaiting(current) {
|
|
306
|
+
if (!current.waiting) return false;
|
|
307
|
+
current.waiting = null;
|
|
308
|
+
clearTimeout(current.waitTimer);
|
|
309
|
+
current.waitTimer = null;
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function endWait(current, { now = false } = {}) {
|
|
314
|
+
if (!stopWaiting(current) || session !== current) return;
|
|
315
|
+
if (now) read(current);
|
|
316
|
+
else armRead(current);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function waitForSettle(current, id) {
|
|
320
|
+
current.waited.add(id);
|
|
321
|
+
current.waiting = id;
|
|
322
|
+
clearTimeout(current.waitTimer);
|
|
323
|
+
current.waitTimer = setTimeout(() => endWait(current, { now: true }), SETTLE_CAP_MS);
|
|
324
|
+
}
|
|
325
|
+
|
|
216
326
|
function armRead(current) {
|
|
217
327
|
if (current.timer) return;
|
|
218
328
|
current.timer = setTimeout(() => {
|
|
@@ -235,22 +345,30 @@ export function start({ href, reload = () => window.location.reload() }) {
|
|
|
235
345
|
const listeners = new AbortController();
|
|
236
346
|
const current = {
|
|
237
347
|
url: url.href, reload, listeners, turn: 0, lastRead: 0, lastSave: 0, timer: null,
|
|
238
|
-
|
|
348
|
+
lastExternal: 0, waiting: null, waitTimer: null, waited: new Set(),
|
|
349
|
+
busy: false, drawn: null, eventId: null, title: null, resolved: new Set(), refused: new Set(),
|
|
239
350
|
};
|
|
240
351
|
session = current;
|
|
241
352
|
const { signal } = listeners;
|
|
242
353
|
document.addEventListener("clay:notification", (e) => onNotification(current, e), { signal });
|
|
354
|
+
document.addEventListener("clay:sync-applied", (e) => {
|
|
355
|
+
if (e.detail?.source === "disk") current.lastExternal = Date.now();
|
|
356
|
+
}, { signal });
|
|
243
357
|
document.addEventListener("clay:save-saved", () => {
|
|
244
358
|
current.lastSave = Date.now();
|
|
245
|
-
|
|
359
|
+
if (current.waiting) endWait(current);
|
|
360
|
+
else armRead(current);
|
|
246
361
|
}, { signal });
|
|
362
|
+
for (const name of SAVE_UNSETTLED_EVENTS) {
|
|
363
|
+
document.addEventListener(name, () => endWait(current, { now: true }), { signal });
|
|
364
|
+
}
|
|
247
365
|
document.addEventListener("visibilitychange", () => {
|
|
248
366
|
if (document.visibilityState === "visible" && Date.now() - current.lastRead >= VISIBLE_GAP_MS) read(current);
|
|
249
367
|
}, { signal });
|
|
250
368
|
const done = storage.session.get(DONE_KEY);
|
|
251
369
|
if (done) {
|
|
252
370
|
storage.session.remove(DONE_KEY);
|
|
253
|
-
say(done === "restore" ? "Your saved data is back." : "The earlier version is back.", "success");
|
|
371
|
+
say(done === "restore" || done === "put-back" ? "Your saved data is back." : "The earlier version is back.", "success");
|
|
254
372
|
}
|
|
255
373
|
read(current);
|
|
256
374
|
}
|
|
@@ -261,5 +379,6 @@ export function stop() {
|
|
|
261
379
|
session = null;
|
|
262
380
|
current.listeners.abort();
|
|
263
381
|
clearTimeout(current.timer);
|
|
382
|
+
clearTimeout(current.waitTimer);
|
|
264
383
|
clearNotice(current);
|
|
265
384
|
}
|
package/src/sync/live-sync.js
CHANGED
|
@@ -933,7 +933,7 @@ class LiveSync {
|
|
|
933
933
|
// Whichever wire discovery selected. start() does not call connect() until the
|
|
934
934
|
// profile is known, so this is never null on the normal path.
|
|
935
935
|
const profile = this._profile || WIRE_PROFILES.legacy;
|
|
936
|
-
const path = profile.streamPath(window.location.href, this.lane, this.resumeId, this.clientId);
|
|
936
|
+
const path = profile.streamPath(window.location.href, this.lane, this.resumeId, this.clientId, PAGE_FORMAT);
|
|
937
937
|
// A frame scheduled while the page showed waits for an animation frame, which a
|
|
938
938
|
// page hidden since then never runs. Move it to the hidden schedule.
|
|
939
939
|
if (!this._hiddenHandler) {
|
package/src/sync/wire-profile.js
CHANGED
|
@@ -25,14 +25,19 @@
|
|
|
25
25
|
|
|
26
26
|
import { hostMeta } from '../core/host-meta.js';
|
|
27
27
|
|
|
28
|
+
// A live tab declares the page format it can read on its own stream, because a tab whose
|
|
29
|
+
// store boots staged never sends /_/sync/data, and the host would otherwise count it as an
|
|
30
|
+
// older editor and refuse every save that raises the page floor.
|
|
31
|
+
const formatParam = (format) => (format == null ? '' : `&blockeditor-format=${format}`);
|
|
32
|
+
|
|
28
33
|
export const WIRE_PROFILES = {
|
|
29
34
|
spec: {
|
|
30
35
|
name: 'spec',
|
|
31
36
|
relayPath: '/_/sync',
|
|
32
37
|
documentHeader: 'Document-URL',
|
|
33
38
|
snapshotKey: 'snapshot',
|
|
34
|
-
streamPath: (href, lane, resumeId, clientId) =>
|
|
35
|
-
`/_/sync?document-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}`,
|
|
39
|
+
streamPath: (href, lane, resumeId, clientId, format) =>
|
|
40
|
+
`/_/sync?document-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}${formatParam(format)}`,
|
|
36
41
|
},
|
|
37
42
|
// What every published clayjs speaks, and what hyperclayjs and the inline script in
|
|
38
43
|
// every Collection dashboard hardcode. Hosts keep these addresses permanently.
|
|
@@ -41,8 +46,8 @@ export const WIRE_PROFILES = {
|
|
|
41
46
|
relayPath: '/_/live-sync/save',
|
|
42
47
|
documentHeader: 'Page-URL',
|
|
43
48
|
snapshotKey: 'html',
|
|
44
|
-
streamPath: (href, lane, resumeId, clientId) =>
|
|
45
|
-
`/_/live-sync/stream?page-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}`,
|
|
49
|
+
streamPath: (href, lane, resumeId, clientId, format) =>
|
|
50
|
+
`/_/live-sync/stream?page-url=${encodeURIComponent(href)}&lane=${lane}&resume-id=${resumeId}&client-id=${encodeURIComponent(clientId)}${formatParam(format)}`,
|
|
46
51
|
},
|
|
47
52
|
};
|
|
48
53
|
|
package/src/ui/bevel-controls.js
CHANGED
|
@@ -186,10 +186,13 @@ export function bevelButton(label, { variant = 'default', small = false, onClick
|
|
|
186
186
|
return b;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
export function bevelIconButton(svg, { label, onClick = null }) {
|
|
189
|
+
export function bevelIconButton(svg, { label, onClick = null, theme = null, parts = [], labelParts = [] }) {
|
|
190
190
|
const b = bevelButton('', {
|
|
191
191
|
small: true,
|
|
192
192
|
onClick,
|
|
193
|
+
theme,
|
|
194
|
+
parts,
|
|
195
|
+
labelParts,
|
|
193
196
|
extra: ['width:26px', 'height:26px', 'padding:0', 'display:inline-grid', 'place-items:center'],
|
|
194
197
|
labelExtra: ['display:inline-grid', 'width:12px', 'height:12px'],
|
|
195
198
|
});
|
package/src/ui/dialogs.js
CHANGED
|
@@ -6,9 +6,10 @@ import { bevelBox, bevelButton, bevelInput, bevelText } from "./bevel-controls.j
|
|
|
6
6
|
import { TOKENS, FONT_SANS, FONT_MONO } from "./bevel.js";
|
|
7
7
|
import { resolveTheme } from "./theme.js";
|
|
8
8
|
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// it is ClayJS's to style. The prompt is the dialog's title, as in the
|
|
9
|
+
// Prompts, titles, paragraphs and labels are text (or a node placed as it is), so a
|
|
10
|
+
// record or a visitor cannot run markup. extraContent is markup (or a node). Only the
|
|
11
|
+
// frame around it is ClayJS's to style. The prompt is the dialog's title, as in the
|
|
12
|
+
// dashboard.
|
|
12
13
|
function createModal(promptText, yesCallback, extraContent = "", includeInput = false, defaultValue = "", yesLabel = "OK") {
|
|
13
14
|
const theme = resolveTheme();
|
|
14
15
|
const content = bevelBox("div", ["display:flex", "flex-direction:column", "gap:14px", `font:14.5px/1.55 ${FONT_SANS}`, `color:${TOKENS["ink-2"]}`], { theme, parts: ["dialog.content"], role: "text" });
|
|
@@ -19,7 +20,8 @@ function createModal(promptText, yesCallback, extraContent = "", includeInput =
|
|
|
19
20
|
input.required = true;
|
|
20
21
|
content.append(input);
|
|
21
22
|
}
|
|
22
|
-
content.
|
|
23
|
+
if (extraContent instanceof Node) content.append(extraContent);
|
|
24
|
+
else if (extraContent) content.insertAdjacentHTML("beforeend", extraContent);
|
|
23
25
|
|
|
24
26
|
themodal.title = promptText;
|
|
25
27
|
themodal.html = includeInput || extraContent ? content : "";
|
|
@@ -102,7 +104,8 @@ export function tell(promptText, ...content) {
|
|
|
102
104
|
const box = bevelBox("div", ["display:flex", "flex-direction:column", "gap:12px"], { theme, parts: ["dialog.content"] });
|
|
103
105
|
for (const c of content) {
|
|
104
106
|
const paragraph = bevelText("div", [`font:14.5px/1.55 ${FONT_SANS}`, `color:${TOKENS["ink-2"]}`, "overflow-wrap:anywhere"], undefined, { theme });
|
|
105
|
-
paragraph.
|
|
107
|
+
if (c instanceof Node) paragraph.append(c);
|
|
108
|
+
else paragraph.textContent = c;
|
|
106
109
|
box.append(paragraph);
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -148,7 +151,7 @@ export function snippet(title, content, extraContent = '') {
|
|
|
148
151
|
`background:${TOKENS.sunk}`, `border:1px solid ${TOKENS["line-2"]}`,
|
|
149
152
|
], { theme, parts: ["dialog.well"], role: "well" });
|
|
150
153
|
const pre = bevelText("pre", ["display:block", "margin:0", "white-space:nowrap", `font:13px/1.6 ${FONT_MONO}`], undefined, { theme, parts: ["dialog.code"], role: "code" });
|
|
151
|
-
pre.
|
|
154
|
+
pre.textContent = content;
|
|
152
155
|
well.append(pre);
|
|
153
156
|
const copy = bevelButton("Copy", {
|
|
154
157
|
small: true,
|
|
@@ -160,7 +163,8 @@ export function snippet(title, content, extraContent = '') {
|
|
|
160
163
|
},
|
|
161
164
|
});
|
|
162
165
|
box.append(well, copy);
|
|
163
|
-
|
|
166
|
+
if (extraContent instanceof Node) box.append(extraContent);
|
|
167
|
+
else if (extraContent) box.insertAdjacentHTML("beforeend", extraContent);
|
|
164
168
|
|
|
165
169
|
themodal.title = title;
|
|
166
170
|
themodal.html = box;
|
package/src/ui/modal.js
CHANGED
|
@@ -18,12 +18,12 @@ export const SURFACE_THEME = Symbol("clay.surfaceTheme");
|
|
|
18
18
|
- user confirms
|
|
19
19
|
- everything about the modal resets
|
|
20
20
|
|
|
21
|
-
themodal.html = content; //
|
|
22
|
-
themodal.yes = content;
|
|
21
|
+
themodal.html = content; // the body: markup, or a node placed as it is
|
|
22
|
+
themodal.yes = content; // text, or a node placed as it is
|
|
23
23
|
themodal.no = content;
|
|
24
24
|
|
|
25
25
|
themodal.closeHtml = "x"; // any non-empty value shows the corner close
|
|
26
|
-
themodal.title = content; //
|
|
26
|
+
themodal.title = content; // text or a node: a header with the title (optional)
|
|
27
27
|
themodal.width = "440px"; // the panel's width (default 600px)
|
|
28
28
|
|
|
29
29
|
themodal.disableFocus = true;
|
|
@@ -367,12 +367,14 @@ const themodal = (() => {
|
|
|
367
367
|
modalYesElem.type = 'submit';
|
|
368
368
|
modalButtonsElem.append(modalNoElem, modalYesElem);
|
|
369
369
|
|
|
370
|
-
//
|
|
371
|
-
|
|
370
|
+
// A node is placed as it is. A string is text, except in the body, which is
|
|
371
|
+
// the one slot that takes markup.
|
|
372
|
+
const place = (target, value, markup = false) => {
|
|
372
373
|
if (value instanceof Node) target.append(value);
|
|
373
|
-
else target.innerHTML = value;
|
|
374
|
+
else if (markup) target.innerHTML = value;
|
|
375
|
+
else target.textContent = value;
|
|
374
376
|
};
|
|
375
|
-
place(modalContentElem, html);
|
|
377
|
+
place(modalContentElem, html, true);
|
|
376
378
|
place(modalYesElem.firstChild, yes);
|
|
377
379
|
place(modalNoElem.firstChild, no);
|
|
378
380
|
if (title) place(shell.heading, title);
|
package/src/ui/toast.js
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
// The Hyperclay dashboard's toast (Bevel's React toast with the dashboard's overrides),
|
|
5
5
|
// drawn inline: a 3px tone edge, a 16px Bevel glyph, 13.5px semibold text, a quiet close.
|
|
6
|
-
import { set } from "../lib/hostile-css.js";
|
|
7
|
-
import { bevelBox, bevelSurface, bevelText, bevelButton, protectIcon, paintLook } from "./bevel-controls.js";
|
|
6
|
+
import { set, style } from "../lib/hostile-css.js";
|
|
7
|
+
import { bevelBox, bevelSurface, bevelText, bevelButton, protectIcon, paintLook, restyleBox } from "./bevel-controls.js";
|
|
8
8
|
import { TOKENS, RULES, GLYPHS } from "./bevel.js";
|
|
9
|
+
import { resolveTheme } from "./theme.js";
|
|
10
|
+
import { themed, tokenRulesFor, partRules, markParts } from "./theme-parts.js";
|
|
9
11
|
import { mountEditorUI } from "./editor-ui.js";
|
|
10
12
|
|
|
11
13
|
const TONES = {
|
|
@@ -34,48 +36,49 @@ function motion() {
|
|
|
34
36
|
// body, and a detached stack would swallow every toast after it. The stack sits in its own
|
|
35
37
|
// shadow root: the inline !important rules already beat the page's stylesheets, and the
|
|
36
38
|
// root also keeps out what they cannot, such as a page's ::before content.
|
|
37
|
-
function stack() {
|
|
39
|
+
function stack(theme) {
|
|
38
40
|
const existing = document.querySelector('clay-editor-ui[data-clay-ui="toasts"]')?.shadowRoot.querySelector("[data-clay-toasts]");
|
|
39
41
|
if (existing) return existing;
|
|
40
42
|
const box = bevelBox("div", [
|
|
41
43
|
"position:fixed", "top:18px", "right:18px", "z-index:9999",
|
|
42
44
|
"display:flex", "flex-direction:column", "align-items:flex-end", "gap:10px",
|
|
43
45
|
"width:min(330px, calc(100vw - 36px))", "pointer-events:none",
|
|
44
|
-
]);
|
|
46
|
+
], { theme, parts: ["toast.stack"] });
|
|
45
47
|
box.setAttribute("data-clay-toasts", "");
|
|
46
48
|
box.setAttribute("aria-live", "polite");
|
|
47
49
|
mountEditorUI({ name: "toasts", content: box });
|
|
48
50
|
return box;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
function build(message, type, { detail = "", actions = [] } = {}) {
|
|
53
|
+
function build(message, type, { detail = "", actions = [] } = {}, theme) {
|
|
52
54
|
const tone = TONES[type];
|
|
53
55
|
const el = bevelSurface("div", [
|
|
54
56
|
"display:flex", "align-items:flex-start", ...RULES.toast, "width:100%",
|
|
55
57
|
`border-inline-start:3px solid ${tone.edge}`,
|
|
56
58
|
"cursor:pointer", "pointer-events:auto",
|
|
57
59
|
...HIDDEN, `transition:${motion()}`,
|
|
58
|
-
]);
|
|
60
|
+
], { theme, parts: ["toast.panel"] });
|
|
61
|
+
if (themed(theme)) style(el, [`border-inline-start-color:${tone.edge}`, ...partRules(theme, ["toast.panel"], {})]);
|
|
59
62
|
el.setAttribute("data-clay-toast", type);
|
|
60
|
-
const body = bevelBox("div", ["flex:1 1 auto", "min-width:0", "display:flex", "flex-direction:column", "gap:8px"]);
|
|
61
|
-
const head = bevelBox("div", ["display:flex", "align-items:flex-start", "gap:8px"]);
|
|
62
|
-
const glyph = bevelText("span", ["display:inline-grid", "flex:none", "padding-top:1px", `color:${tone.edge}`]);
|
|
63
|
+
const body = bevelBox("div", ["flex:1 1 auto", "min-width:0", "display:flex", "flex-direction:column", "gap:8px"], { theme });
|
|
64
|
+
const head = bevelBox("div", ["display:flex", "align-items:flex-start", "gap:8px"], { theme });
|
|
65
|
+
const glyph = bevelText("span", ["display:inline-grid", "flex:none", "padding-top:1px", `color:${tone.edge}`], undefined, { theme, parts: ["toast.icon"], role: null });
|
|
63
66
|
glyph.innerHTML = tone.icon;
|
|
64
67
|
protectIcon(glyph.firstElementChild, 16);
|
|
65
68
|
// A message can carry a filename, and a filename is whatever whoever wrote the
|
|
66
69
|
// file chose. It goes in as text so markup in a name can never become markup here.
|
|
67
|
-
const text = bevelText("span", ["flex:1 1 auto", "min-width:120px", ...RULES.toastTitle, "overflow-wrap:anywhere"], message);
|
|
70
|
+
const text = bevelText("span", ["flex:1 1 auto", "min-width:120px", ...RULES.toastTitle, "overflow-wrap:anywhere"], message, { theme, parts: ["toast.text"] });
|
|
68
71
|
head.append(glyph, text);
|
|
69
72
|
body.append(head);
|
|
70
73
|
// Detail and actions are the same untrusted text as the title: textContent for
|
|
71
74
|
// both, so a reason string can never become markup in the toast either.
|
|
72
75
|
if (detail) {
|
|
73
|
-
body.append(bevelText("span", ["display:block", `padding-inline-start:${TEXT_INSET}px`, `color:${TOKENS.muted}`, "overflow-wrap:anywhere"], detail));
|
|
76
|
+
body.append(bevelText("span", ["display:block", `padding-inline-start:${TEXT_INSET}px`, `color:${TOKENS.muted}`, "overflow-wrap:anywhere"], detail, { theme, parts: ["toast.text"], role: "mutedText" }));
|
|
74
77
|
}
|
|
75
78
|
if (actions.length) {
|
|
76
|
-
const row = bevelBox("div", ["display:flex", "flex-wrap:wrap", "gap:8px", `padding-inline-start:${TEXT_INSET - BUTTON_LABEL_INSET}px`]);
|
|
79
|
+
const row = bevelBox("div", ["display:flex", "flex-wrap:wrap", "gap:8px", `padding-inline-start:${TEXT_INSET - BUTTON_LABEL_INSET}px`], { theme });
|
|
77
80
|
for (const { label, onClick } of actions) {
|
|
78
|
-
row.append(bevelButton(label, { small: true, variant: "quiet", onClick, extra: ["padding:4px 8px"] }));
|
|
81
|
+
row.append(bevelButton(label, { small: true, variant: "quiet", onClick, extra: ["padding:4px 8px"], theme }));
|
|
79
82
|
}
|
|
80
83
|
body.append(row);
|
|
81
84
|
}
|
|
@@ -84,31 +87,61 @@ function build(message, type, { detail = "", actions = [] } = {}) {
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
// The dashboard's toast close: a bare 24px square in the muted ink, the sunk ground on hover.
|
|
87
|
-
function closeButton(onClick) {
|
|
88
|
-
const
|
|
90
|
+
function closeButton(onClick, theme) {
|
|
91
|
+
const base = [
|
|
89
92
|
"flex:none", "display:inline-grid", "place-items:center", ...RULES.toastClose,
|
|
90
93
|
"cursor:pointer",
|
|
91
|
-
]
|
|
94
|
+
];
|
|
95
|
+
const b = bevelBox("button", base);
|
|
92
96
|
b.type = "button";
|
|
93
97
|
b.setAttribute("aria-label", "Dismiss");
|
|
94
98
|
b.title = "Dismiss";
|
|
95
99
|
b.innerHTML = CLOSE_ICON;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
if (!themed(theme)) {
|
|
101
|
+
protectIcon(b.firstElementChild, 14);
|
|
102
|
+
const paint = (lit) => {
|
|
103
|
+
set(b, "color", lit ? TOKENS.ink : TOKENS.muted);
|
|
104
|
+
set(b, "background", lit ? TOKENS.sunk : "transparent");
|
|
105
|
+
};
|
|
106
|
+
b.addEventListener("pointerenter", () => paint(true));
|
|
107
|
+
b.addEventListener("pointerleave", () => paint(false));
|
|
108
|
+
b.addEventListener("focus", () => { paint(true); set(b, "outline", `2px solid ${TOKENS.brass}`); set(b, "outline-offset", "1px"); });
|
|
109
|
+
b.addEventListener("blur", () => { paint(false); set(b, "outline", "none"); });
|
|
110
|
+
b.addEventListener("click", onClick);
|
|
111
|
+
return b;
|
|
112
|
+
}
|
|
113
|
+
const names = ["close", "toast.close"];
|
|
114
|
+
const icon = ["icon", "toast.closeIcon"];
|
|
115
|
+
markParts(b, theme, names);
|
|
116
|
+
markParts(b.firstElementChild, theme, icon);
|
|
117
|
+
const flags = { hover: false, focus: false, pressed: false };
|
|
118
|
+
const paint = () => {
|
|
119
|
+
const act = { hover: flags.hover, active: flags.pressed, focus: flags.focus };
|
|
120
|
+
const svg = b.firstElementChild;
|
|
121
|
+
svg.style.cssText = "";
|
|
122
|
+
protectIcon(svg, 14);
|
|
123
|
+
style(svg, partRules(theme, icon, act));
|
|
124
|
+
restyleBox(b, [...base, ...tokenRulesFor(theme, "close", act), ...partRules(theme, names, act)]);
|
|
100
125
|
};
|
|
101
|
-
b.addEventListener("pointerenter", () => paint(
|
|
102
|
-
b.addEventListener("pointerleave", () => paint(
|
|
103
|
-
b.addEventListener("
|
|
104
|
-
|
|
126
|
+
b.addEventListener("pointerenter", () => { flags.hover = true; paint(); });
|
|
127
|
+
b.addEventListener("pointerleave", () => { flags.hover = false; flags.pressed = false; paint(); });
|
|
128
|
+
b.addEventListener("pointerdown", (e) => {
|
|
129
|
+
if (e.button !== undefined && e.button !== 0) return;
|
|
130
|
+
flags.pressed = true;
|
|
131
|
+
paint();
|
|
132
|
+
});
|
|
133
|
+
b.addEventListener("pointerup", () => { flags.pressed = false; paint(); });
|
|
134
|
+
b.addEventListener("focus", () => { flags.focus = true; paint(); });
|
|
135
|
+
b.addEventListener("blur", () => { flags.focus = false; flags.pressed = false; paint(); });
|
|
105
136
|
b.addEventListener("click", onClick);
|
|
137
|
+
paint();
|
|
106
138
|
return b;
|
|
107
139
|
}
|
|
108
140
|
|
|
109
|
-
function reveal(el) {
|
|
110
|
-
stack().append(el);
|
|
111
|
-
|
|
141
|
+
function reveal(el, theme) {
|
|
142
|
+
stack(theme).append(el);
|
|
143
|
+
if (themed(theme)) set(el, "color-scheme", theme.scheme === "light dark" ? "light" : theme.scheme);
|
|
144
|
+
else paintLook(el, { background: TOKENS.surface });
|
|
112
145
|
setTimeout(() => {
|
|
113
146
|
set(el, "opacity", "1");
|
|
114
147
|
set(el, "transform", "none");
|
|
@@ -125,9 +158,10 @@ function dismiss(el, after) {
|
|
|
125
158
|
}
|
|
126
159
|
|
|
127
160
|
function toast(message, messageType = "success") {
|
|
128
|
-
const
|
|
161
|
+
const theme = resolveTheme();
|
|
162
|
+
const el = build(message, TONES[messageType] ? messageType : "success", {}, theme);
|
|
129
163
|
el.addEventListener("click", () => dismiss(el));
|
|
130
|
-
reveal(el);
|
|
164
|
+
reveal(el, theme);
|
|
131
165
|
setTimeout(() => dismiss(el), 6600);
|
|
132
166
|
}
|
|
133
167
|
|
|
@@ -142,17 +176,18 @@ function toastPersistent(message, messageType = "warning", { detail = "", action
|
|
|
142
176
|
activePersistentToasts.delete(message);
|
|
143
177
|
}
|
|
144
178
|
|
|
145
|
-
const
|
|
179
|
+
const theme = resolveTheme();
|
|
180
|
+
const el = build(message, TONES[messageType] ? messageType : "warning", { detail, actions }, theme);
|
|
146
181
|
set(el, "cursor", "default");
|
|
147
182
|
const close = closeButton((e) => {
|
|
148
183
|
e.stopPropagation();
|
|
149
184
|
// A newer toast with the same message may have taken this entry since.
|
|
150
185
|
dismiss(el, () => { if (activePersistentToasts.get(message) === el) activePersistentToasts.delete(message); });
|
|
151
|
-
});
|
|
186
|
+
}, theme);
|
|
152
187
|
el.append(close);
|
|
153
188
|
|
|
154
189
|
activePersistentToasts.set(message, el);
|
|
155
|
-
reveal(el);
|
|
190
|
+
reveal(el, theme);
|
|
156
191
|
}
|
|
157
192
|
|
|
158
193
|
// Takes down the persistent toast showing this message, if one is up. `now` takes the
|