@panphora/clayjs 1.17.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 +72474 -62090
- package/dist/m/attrs/{save-freeze-MYNVXREN.js → save-freeze-LWGF6XUN.js} +1 -1
- package/dist/m/c-2GRUM77Q.js +30 -0
- package/dist/m/c-2KIEGNC4.js +1 -0
- package/dist/m/{c-AMSNPOCX.js → c-2KL3BD52.js} +1 -1
- 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-KAU6E43D.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-3XIRD4NU.js → c-5T6NZHNM.js} +1 -1
- package/dist/m/{c-74MGYWZF.js → c-5ZQV7KC4.js} +1 -1
- package/dist/m/{c-7OLLJPWP.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-Y5HBBIQ4.js → c-AMYEFA2E.js} +1 -1
- package/dist/m/c-AQYCG7QW.js +1 -0
- package/dist/m/c-ATKAOH23.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-VHM5XQHK.js → c-BIJALSRD.js} +1 -1
- package/dist/m/c-BJ7YQ5QZ.js +1 -0
- package/dist/m/{c-KIPQJX4B.js → c-BRLCTNMC.js} +1 -1
- package/dist/m/{c-VE6ZMYMC.js → c-BUAYFEV5.js} +1 -1
- package/dist/m/c-C6IPRG34.js +1 -0
- package/dist/m/{c-KU3ZFADW.js → c-CAS3NND4.js} +1 -1
- package/dist/m/{c-PS5ARVWR.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-QJ4OM3G6.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-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-TJ6WOJL3.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-WDC5GVKU.js → c-IVTRVENZ.js} +1 -1
- package/dist/m/c-JKZJIUIP.js +1 -0
- package/dist/m/{c-UDUUJPFZ.js → c-JNLX3RJB.js} +1 -1
- package/dist/m/c-K3ADYNLU.js +1 -0
- package/dist/m/c-KGHVFRR3.js +1 -0
- package/dist/m/{c-5QWQ7A5L.js → c-KJJPV77R.js} +1 -1
- package/dist/m/{c-CZUSOB7V.js → c-KNPDOMDY.js} +1 -1
- package/dist/m/{c-EZU56NQV.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-3RUU4TPX.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-5RJFTLSF.js → c-NEL7HQW3.js} +1 -1
- package/dist/m/{c-DCXOXBF3.js → c-NQJRVJ56.js} +1 -1
- 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-R3L7ZJGB.js → c-OPLUZR3A.js} +1 -1
- package/dist/m/{c-GSFIGKOP.js → c-OTMKN2OO.js} +1 -1
- package/dist/m/{c-6NC5I2TV.js → c-OYRZ7XG2.js} +1 -1
- package/dist/m/{c-EVCQ3BKA.js → c-P3OFYUMA.js} +1 -1
- 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-FRLHSY7E.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-JU3NK25H.js → c-R27DWQKX.js} +1 -1
- package/dist/m/{c-RSMOODDU.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-Q622NS2Y.js → c-S2U6NJU4.js} +1 -1
- package/dist/m/c-SPZPGD3R.js +1 -0
- package/dist/m/c-SW4YEG2D.js +8 -0
- package/dist/m/c-T637T7SN.js +1 -0
- package/dist/m/c-TJAXGIRV.js +1 -0
- package/dist/m/{c-RYUXUUBX.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-QICP2YR5.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-VILENZTD.js +1 -0
- package/dist/m/{c-HDDEEL77.js → c-VJYEVRYK.js} +1 -1
- package/dist/m/{c-SMBZG5X4.js → c-VR2BJ4AN.js} +1 -1
- 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-I2ZATQCD.js → c-X5TSXZYJ.js} +2 -2
- 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/core/{admin-attrs-QHOKPJ7W.js → admin-attrs-YY7T5FGU.js} +1 -1
- 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-FNXVEFNE.js → host-permission-6EVJGNPL.js} +1 -1
- package/dist/m/core/is-edit-mode-GCX6767E.js +1 -0
- package/dist/m/core/{persist-JFYRHMRJ.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/internals/{index-PVXGPUDA.js → index-TGLQINN3.js} +1 -1
- 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 +2111 -1573
- package/dist/m/page/{editor-NNJA4OKD.js → editor-6LRB3GE4.js} +123 -21
- 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-RM5IR6AO.js → demo-OYO5GCAO.js} +1 -1
- 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-SGPJKANK.js → people-JCHVPF7P.js} +1 -1
- package/dist/m/plugins/quickcrop-BQ5YYFCH.js +26 -0
- package/dist/m/plugins/{richclay-NRQDSWX6.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-WLYKK6TL.js → upload-NHGJXZK2.js} +1 -1
- 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-4N6GYAF6.js → wire-VBWXEXU2.js} +1 -1
- 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 +1367 -930
- package/dist/page/themes/light-1.css +8 -7
- package/dist/server/core.cjs +1440 -288
- 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/conflict-notice-ui.js +4 -7
- package/src/core/download.js +2 -2
- package/src/core/host-meta.js +38 -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 +106 -31
- 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 +170 -39
- 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/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 +89 -27
- package/src/page/boot-pack.js +1 -1
- package/src/page/chart-tables.js +21 -3
- package/src/page/chart.js +406 -63
- package/src/page/checklist.js +10 -135
- package/src/page/checkpoint-proof.js +7 -1
- package/src/page/chrome.js +250 -51
- package/src/page/clipboard-shape.js +126 -0
- package/src/page/drag.js +33 -3
- package/src/page/editor.js +136 -25
- 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/checklist.js +428 -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/registry.js +18 -0
- package/src/page/features/table.js +373 -83
- 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 +25 -1
- package/src/page/format-gate.js +68 -18
- package/src/page/format-host.js +17 -0
- package/src/page/format-native-commands.js +21 -5
- package/src/page/image-actions.js +64 -0
- package/src/page/import-markdown.js +262 -0
- package/src/page/items.js +135 -17
- package/src/page/markdown.js +6 -2
- package/src/page/media.js +35 -119
- package/src/page/placeholder.js +14 -1
- package/src/page/projection.js +98 -10
- 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 +28 -0
- package/src/page/save-states.js +13 -3
- package/src/page/schema.js +19 -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/light-1.css +8 -7
- 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 +38 -12
- package/src/page/view.js +13 -0
- package/src/page/ybind/binding.js +88 -5
- 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 +1 -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 +113 -17
- package/src/page/ybind/recovery-proof.js +32 -9
- package/src/page/ybind/recovery-replay.js +558 -15
- 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 +89 -14
- 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 +249 -44
- 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 +5 -3
- 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/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 +11 -6
- 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/c-2SPVQSHC.js +0 -1
- package/dist/m/c-336SGU5W.js +0 -1
- package/dist/m/c-3H6BZDNJ.js +0 -6
- package/dist/m/c-4DFEJ2PO.js +0 -3
- package/dist/m/c-56QRZTZH.js +0 -1
- package/dist/m/c-5J4E7O4Y.js +0 -4
- package/dist/m/c-5LKDWQTL.js +0 -19
- package/dist/m/c-5V3MOVJT.js +0 -1
- package/dist/m/c-6AZOGAPE.js +0 -1
- package/dist/m/c-6CRUUXOX.js +0 -5
- package/dist/m/c-6HMB24EV.js +0 -1
- package/dist/m/c-6KEVEQ7L.js +0 -1
- package/dist/m/c-A3E3NIMC.js +0 -1
- package/dist/m/c-BQ6LOBEE.js +0 -1
- package/dist/m/c-BTDI4Y23.js +0 -1
- package/dist/m/c-CJ3A7ZKE.js +0 -1
- package/dist/m/c-CQHPGKX2.js +0 -1
- package/dist/m/c-DFS4XZJV.js +0 -2
- package/dist/m/c-DNENWY4O.js +0 -1
- package/dist/m/c-E4LIR5X5.js +0 -1
- package/dist/m/c-FDNHHZY5.js +0 -1
- package/dist/m/c-FROJUZMV.js +0 -1
- package/dist/m/c-G2JPKCTP.js +0 -1
- package/dist/m/c-GB6TU7WV.js +0 -1
- package/dist/m/c-GFJJEBTU.js +0 -1
- package/dist/m/c-GJAEGGXY.js +0 -1
- package/dist/m/c-GOWBDM6J.js +0 -1
- package/dist/m/c-HFKUVACH.js +0 -1
- package/dist/m/c-HHDFCNHV.js +0 -1
- package/dist/m/c-IEM4NTCS.js +0 -1
- package/dist/m/c-JDCX26UP.js +0 -25
- package/dist/m/c-KGVGROZT.js +0 -1
- package/dist/m/c-KQ6TYJAP.js +0 -135
- package/dist/m/c-LFPHCXY3.js +0 -1
- package/dist/m/c-LRPUO2MX.js +0 -1
- package/dist/m/c-LWLBHLVC.js +0 -3
- package/dist/m/c-M2L47S2X.js +0 -7
- package/dist/m/c-MFP2ZWD4.js +0 -1
- package/dist/m/c-MKSQ4Y57.js +0 -1
- package/dist/m/c-NUFFJVVJ.js +0 -1
- package/dist/m/c-NUHSPT5P.js +0 -1
- package/dist/m/c-O72HVYSA.js +0 -1
- package/dist/m/c-OIBUTDXI.js +0 -1
- package/dist/m/c-OW5QIGG3.js +0 -1
- package/dist/m/c-PGRCK73F.js +0 -1
- package/dist/m/c-Q7NQD65V.js +0 -1
- package/dist/m/c-QH63LQOS.js +0 -1
- package/dist/m/c-R7IWVINQ.js +0 -1
- package/dist/m/c-RM7J7BV2.js +0 -1
- package/dist/m/c-S4ZEH46B.js +0 -9
- package/dist/m/c-SDAXDM2D.js +0 -1
- package/dist/m/c-SMGU7RT3.js +0 -235
- package/dist/m/c-SPIORRYS.js +0 -1
- package/dist/m/c-SWLRT66R.js +0 -1
- package/dist/m/c-TCJTVAFN.js +0 -1
- package/dist/m/c-TQEMVJCS.js +0 -1
- package/dist/m/c-TYLXX4NF.js +0 -21
- package/dist/m/c-U62OB4BT.js +0 -1
- package/dist/m/c-U6YWUAR3.js +0 -8
- package/dist/m/c-UDCSU5G5.js +0 -2
- package/dist/m/c-VI5RMSVY.js +0 -1
- package/dist/m/c-VRZN56XY.js +0 -1
- package/dist/m/c-W5MCHEGA.js +0 -1
- package/dist/m/c-WNYBLIOZ.js +0 -1
- package/dist/m/c-WWXIDH4O.js +0 -28
- package/dist/m/c-XCNTK76U.js +0 -1
- package/dist/m/c-XMS6DPBR.js +0 -1
- package/dist/m/c-ZUCOKOHT.js +0 -1
- package/dist/m/c-ZVPVXTJF.js +0 -4
- package/dist/m/c-ZVRKXKM7.js +0 -1
- package/dist/m/core/autosave-HRYGUWV6.js +0 -1
- package/dist/m/core/conflict-notice-6MROJN2L.js +0 -1
- package/dist/m/core/is-edit-mode-KTWA3O5N.js +0 -1
- package/dist/m/core/recovery-FLTCC2OC.js +0 -1
- package/dist/m/core/save-HQZ4S667.js +0 -1
- package/dist/m/core/save-core-KGQWFUNH.js +0 -1
- package/dist/m/core/snapshot-EGQZNT45.js +0 -1
- package/dist/m/core/unsaved-warning-IDJLD66H.js +0 -1
- package/dist/m/loader-S4I7GKXR.js +0 -1
- package/dist/m/manifest-4C419D67.js +0 -1
- package/dist/m/manifest-edit-B077F288.js +0 -1
- package/dist/m/plugins/ai-edit-QN4PMMNL.js +0 -6
- package/dist/m/plugins/indicator-URDBBOQO.js +0 -1
- package/dist/m/plugins/live-HFWD4OIH.js +0 -1
- package/dist/m/plugins/menu-3HIEQSI6.js +0 -138
- package/dist/m/plugins/page-JOKB6XPR.js +0 -2
- package/dist/m/plugins/page-view-NXOJHNW6.js +0 -1
- package/dist/m/plugins/quickcrop-QNY3PKLD.js +0 -26
- package/dist/m/plugins/sortable-GWT4PWDV.js +0 -1
- package/dist/m/plugins/source-4ZRSAIYM.js +0 -1
- package/dist/m/plugins/undo-VDA5P7FH.js +0 -1
- package/dist/m/plugins/upgrade-W7FG5VFG.js +0 -1
- package/dist/m/plugins/visitor-drafts-LIDY7NH6.js +0 -21
- package/dist/m/plugins/visitor-drafts-owner-ZXCKBZE7.js +0 -1
- package/dist/m/reader-core-DXYCURFF.js +0 -68
- package/dist/m/route/route-KOCZSEGN.js +0 -1
- package/dist/m/sync/live-sync-J7VMYTKS.js +0 -1
- package/dist/m/ui/index-3JLVI27O.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-NRQDSWX6.js.LEGAL.txt → richclay-KLGETK77.js.LEGAL.txt} +0 -0
package/docs/guide.md
CHANGED
|
@@ -4,7 +4,7 @@ This is the complete reference for building malleable documents with ClayJS: one
|
|
|
4
4
|
|
|
5
5
|
It is written for a developer or an LLM building full apps with inline editing and inline controls: kanban boards with card detail, block writers, finance trackers, CRMs, flashcards, trackers. It covers the CMS plugin only in passing ([cms: content-managed pages](https://clayjs.com/docs/guide/components/#cms-content-managed-pages)), because an app whose product is editing should not route editing through a form sidebar.
|
|
6
6
|
|
|
7
|
-
Every behavior stated here was read from ClayJS 1.
|
|
7
|
+
Every behavior stated here was read from ClayJS 1.17.0 source or executed. The four example files below are complete, tested apps that this guide quotes. A [test suite](https://github.com/panphora/clayjs/tree/main/tests/unit/guide-examples) boots each one in jsdom with the real loader and checks what it saves, how it merges a peer's edit, and what a viewer receives. Open any of them and view source, or download one and open it in HTML Clay to edit it:
|
|
8
8
|
|
|
9
9
|
| File | What it shows |
|
|
10
10
|
|---|---|
|
|
@@ -117,7 +117,7 @@ Templates are blueprints. `item-tpl` holds a new row in its active form (`conten
|
|
|
117
117
|
<script src="https://clayjs.com/v1/clay-ui.js"></script>
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
The script tags load core with `sync,sortable,undo,indicator`, `exclude=richclay` because this app has no rich text, and then the `clay-options` and `clay-ui` satellites. ([Add ClayJS to a page](https://clayjs.com/docs/guide/loading/))
|
|
120
|
+
The script tags load core with `sync,sortable,undo,indicator`, `exclude=richclay` because this app has no rich text, and then the `clay-options` and `clay-ui` satellites. ([Add ClayJS to a page](https://clayjs.com/docs/guide/loading/)) The file opens in view mode, even for its owner. Press Cmd+Shift+E (Ctrl+Shift+E elsewhere) to edit, or add `menu` to the plugin list for an Edit switch ([How the mode is decided](https://clayjs.com/docs/guide/edit-mode/#how-the-mode-is-decided)).
|
|
121
121
|
|
|
122
122
|
#### Starting the app
|
|
123
123
|
|
|
@@ -260,7 +260,7 @@ Follow these rules to keep record, chrome, and derived state separate.
|
|
|
260
260
|
2. **Chrome is authored in the file and hidden from viewers with `show-when:editmode="true"`.** This default needs no script and is in place before ClayJS loads. Use `clay="editor-ui"` only for UI a script fills or changes while the page is open, or that viewers must not receive ([Chrome](https://clayjs.com/docs/guide/architecture/#chrome)).
|
|
261
261
|
3. **Derived values are recomputed, never stored as edits.** Store the value once as the record (`bind`), and declare every other place it shows as a sapjs paint: `attr:data-state="item.state"`, `class:done="item.done"`, `text`, `show` ([sap.js: records, views and totals in markup](https://clayjs.com/docs/guide/components/#sap-js-records-views-and-totals-in-markup)). ClayJS loads the sapjs core for any page with a `[sap]` root and treats painted values as derived. Without sapjs, write derived values into `editor-ui` elements, or into `clay="no-dirty"` elements when the saved file must show them. Never copy a record into a second attribute with a script. The copy goes stale in the file, and a merge brings the stale copy back.
|
|
262
262
|
4. **Every repeated item has a unique `data-id` from birth.** Mint a fresh one on every create and every duplicate. Sync pairs elements by it.
|
|
263
|
-
5. **Per-tab state stays out of the record.** Keep which card is open, the filter, and the current view in the URL hash, `
|
|
263
|
+
5. **Per-tab state stays out of the record.** Keep which card is open, the filter, and the current view in the URL hash, `clay.storage`, or memory. Paint them through `ephemeral` attributes ([Ephemeral state: per-tab state on saved elements](https://clayjs.com/docs/guide/record/#ephemeral-state-per-tab-state-on-saved-elements)), a runtime `<style>`, or `editor-ui` elements.
|
|
264
264
|
6. **A command writes the record once, in one place, wrapped in an undo commit.** The mutation is what autosave, undo, and sync see.
|
|
265
265
|
7. **Adopt the file as it is on load.** Never rewrite the record at boot. Doing so makes the page dirty, can fall outside the save baseline, and races peers.
|
|
266
266
|
8. **Reconcile after changes you did not make.** A peer's frame, an agent's disk edit, and an undo replay change the DOM under you. Rebuild derived and chrome state on `clay:sync-applied` and on undo and redo.
|
|
@@ -308,7 +308,7 @@ Plugins add editing, sync, and UI features to `clay.js`. Most are off by default
|
|
|
308
308
|
| `live` | off | yes | `clay.live(name)`: a `clay="live"` JSON tag as a live object |
|
|
309
309
|
| `sortable` | off | no | the `sortable` attribute, `window.Sortable`, `clay:sorted` |
|
|
310
310
|
| `undo` | off | no | `clay.undo` (whole-page undo, takes Cmd+Z for the window) |
|
|
311
|
-
| `indicator` | off | no | the save status chip |
|
|
311
|
+
| `indicator` | off | no | the save status chip, drawn by a module fetched at the first input or save state |
|
|
312
312
|
| `upload` | off | no | `clay.upload` |
|
|
313
313
|
| `quickcrop` | off | yes | `clay.quickcrop` |
|
|
314
314
|
| `cms` | off | yes | `clay.cms`; implies `quickcrop` and `upload` |
|
|
@@ -318,8 +318,14 @@ Plugins add editing, sync, and UI features to `clay.js`. Most are off by default
|
|
|
318
318
|
| `demo` | off | yes | fakes saving into `localStorage` for public demos |
|
|
319
319
|
| `visitor-drafts` | off | yes | `clay.visitorDrafts`; in draft mode, the draft store, download prompt and intro area; outside it, only the owner chip, fetched when the owner edits ([Free documents for visitors](https://clayjs.com/docs/guide/free-documents/)) |
|
|
320
320
|
| `derive` | on for any page with a `[sap]` root | yes | the sapjs core: records and paints, treated as derived ([sap.js: records, views and totals in markup](https://clayjs.com/docs/guide/components/#sap-js-records-views-and-totals-in-markup)) |
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
| `page` | off | reader half only (`page-view`) | the page editor, `clay.page`; implies `live`, `sync` and `upload`, and drops `richclay` unless named |
|
|
322
|
+
| `unsent` | off | no | `unsent="<name>"`: unsent text in chrome survives a redraw and a reload, `clay.unsent` |
|
|
323
|
+
| `bind-text` | off | no | `clay.bindText`: an editor textarea outside the records, bound to one record's text |
|
|
324
|
+
| `people` | off | yes | who is editing and who wrote what |
|
|
325
|
+
| `menu` | off | yes | an Edit switch and claygoo in the top right for anyone who can edit, with the SEO settings |
|
|
326
|
+
| `upgrade` | off; added for a copy of a hyperclay.com starter template that this person can edit | yes | the template upgrade notice |
|
|
327
|
+
|
|
328
|
+
- Edit-only plugins do not load in a tab that booted in view, even when named. Their members are absent there. Test `clay.isEditMode` (or `clay.modeLifecycle.canWrite()` before a write), never `'save' in clay` or `clay.RichClay` alone, because a live exit leaves those members attached. On a `<html clay-mode-switch="live">` document, entering edit imports them while the tab still shows view, and they stay loaded after the exit ([Leaving edit](https://clayjs.com/docs/guide/edit-mode/#leaving-edit)).
|
|
323
329
|
- `exclude` wins over implications. `exclude=wire` also drops `ai-edit`.
|
|
324
330
|
- Load order is fixed regardless of how you list them.
|
|
325
331
|
- Use `exclude=richclay` when the app has no rich text. `exclude=source` saves one request and about 48 KB, at the cost of saves reprinting the whole document instead of keeping hand formatting.
|
|
@@ -328,7 +334,9 @@ Plugins add editing, sync, and UI features to `clay.js`. Most are off by default
|
|
|
328
334
|
- `derive` loads automatically when the page has a `[sap]` root at boot, and stays idle when full sapjs is on the page. `exclude=derive` opts out; `plugins=derive` loads it for a page that adds its `[sap]` root later.
|
|
329
335
|
- `visitor-drafts` loads its draft store only in draft mode. A reader or an owner editing the original gets a small module that publishes an inert `clay.visitorDrafts` and, for the owner, the owner chip. On a page with no `visitor-drafts` slot, no `api` rules tag, or a rules tag this version cannot read (not `data-rules-version="1"`, or not JSON), it warns in the console and draws nothing. [Free documents for visitors](https://clayjs.com/docs/guide/free-documents/) covers the opt in.
|
|
330
336
|
|
|
331
|
-
|
|
337
|
+
Measure what each mode you ship downloads, and keep a byte budget and a test that fails when one grows: [Keep a byte budget per boot, and fail a test when it grows](https://clayjs.com/docs/practices/#keep-a-byte-budget-per-boot-and-fail-a-test-when-it-grows).
|
|
338
|
+
|
|
339
|
+
**A good default for apps:** `?plugins=sync,sortable,undo,indicator`, plus `upload` for images or attachments, plus `exclude=richclay` when there is no rich text. A document opens in view even for its owner, so add `menu` for the Edit switch, or declare `<html opens-in="edit">` on a tool page that must edit as soon as it opens.
|
|
332
340
|
|
|
333
341
|
### 2.3 Satellites
|
|
334
342
|
|
|
@@ -348,13 +356,13 @@ Satellites are separate classic script tags. Each publishes a readiness promise
|
|
|
348
356
|
|
|
349
357
|
Satellites do not wait for `clay.ready` and work without `clay.js`, including in view mode and on a static host. In code that runs early, await the promise before using what it adds (`await clay.loaded.ui` before `clay.toast`). Inside a `clay.ready` callback, satellites have usually finished loading, so `clay.toast?.(...)` is safe and the optional call costs nothing.
|
|
350
358
|
|
|
351
|
-
`clay.standalone.js` bundles the loader, every plugin, and every satellite into one
|
|
359
|
+
`clay.standalone.js` bundles the loader, every plugin, and every satellite into one file of about 5 MB for offline use. `plugins=` still decides what runs. With it, remove the satellite tags and always `await clay.loaded.sap` or `.data` before use.
|
|
352
360
|
|
|
353
361
|
In Hyperclay Local and HTML Clay, a document needs no special loading to work offline. Keep the ordinary `https://clayjs.com/v1/` tags and the host serves them from its bundled release. Use `clay.standalone.js` for a file opened with no host at all. [Offline in a local host](https://clayjs.com/docs/guide/hosts/#offline-in-a-local-host) covers the route and what a host must do.
|
|
354
362
|
|
|
355
363
|
### 2.4 `clay.ready`
|
|
356
364
|
|
|
357
|
-
`clay.ready` is a promise that resolves with `window.clay` once boot is done and every plugin has
|
|
365
|
+
`clay.ready` is a promise that resolves with `window.clay` once boot is done and every plugin that loads at boot has started; the late plugins ([Plugins that load late](#plugins-that-load-late)) arrive after it. `clay:ready` fires on `document` at the same moment. Before it resolves, members like `clay.save` and `clay.undo` do not exist. Start app code with `clay.start(...)`: it waits for `clay.ready` and runs once per page ([Starting the app once](https://clayjs.com/docs/guide/architecture/#starting-the-app-once)).
|
|
358
366
|
|
|
359
367
|
`clay.plugins` is the frozen list of plugin names in the plan the loader resolved, late plugins included. It says what the page asked for, not what has finished loading.
|
|
360
368
|
|
|
@@ -379,6 +387,17 @@ Boot moves through `loading` (the saved article is read and validated), `joining
|
|
|
379
387
|
|
|
380
388
|
A failed or cancelled installation never emits `document:input-ready`. Blocked readiness still resolves: `clay.page.ready` settles, `clay.page.status.phase` is `blocked` with a `reason`, the saved article stays as the host served it, and no editable page appears. [When the page editor is ready](https://clayjs.com/docs/guide/edit-mode/#when-the-page-editor-is-ready) covers the reasons and what a page can offer instead.
|
|
381
389
|
|
|
390
|
+
**The two waits overlap.** The editor's code downloads while authority is prepared, so the boot waits for the longer of the two plus adoption, never for their sum. Neither branch touches the page: only the coordinator mounts, adopts, and then enables input.
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
const settled = await settleBranches(inst, [
|
|
394
|
+
branch(steps.loadEditorAssets(inst, source), inst, "editor-assets-failed", "document:assets-ready"),
|
|
395
|
+
branch(steps.prepareAuthority(inst, source), inst, "authority-failed", "document:authority-ready"),
|
|
396
|
+
]);
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The first branch to fail answers the wait at once, and the branch still on the wire is aborted instead of being left to subscribe anything. Input opens only after adoption, so the faster start creates no temporary document and no second undo history.
|
|
400
|
+
|
|
382
401
|
You can set configuration before the tag, and the loader merges into it: `window.clay = { saveToast: true }`.
|
|
383
402
|
|
|
384
403
|
### 2.5 Theming ClayJS's own UI
|
|
@@ -406,7 +425,7 @@ Your stylesheet cannot reach these elements: every declaration is inline and `!i
|
|
|
406
425
|
|
|
407
426
|
### 2.6 Plugins that load late
|
|
408
427
|
|
|
409
|
-
`source`, `sortable`, and `
|
|
428
|
+
`source`, `sortable`, `menu` and `upgrade` load after `clay:ready`, one at a time, in the order the plan resolves them. Nothing on first paint or the first keystroke waits for them. A late plugin that fails to load is logged and left absent, like any other.
|
|
410
429
|
|
|
411
430
|
Each late plugin fires `clay:plugin-ready` on `document` once its own `ready` settles, with `detail.plugin` naming it. Use this event to tune something the plugin built for itself. For example, a Kanban board waits for `sortable` and then sets the drag delay its cards need on a phone:
|
|
412
431
|
|
|
@@ -425,7 +444,7 @@ Do not read `window.Sortable` in a `clay.ready` callback: the plugin may not hav
|
|
|
425
444
|
|
|
426
445
|
### 2.7 Libraries on demand
|
|
427
446
|
|
|
428
|
-
`clay.assets.load(name)` loads a library from the same release that served `clay.js` the first time a page asks for it. The document needs no vendor URL, so a copy served by another host or opened from disk still finds it. The names are `overtype`, `marked`, and `
|
|
447
|
+
`clay.assets.load(name)` loads a library from the same release that served `clay.js` the first time a page asks for it. The document needs no vendor URL, so a copy served by another host or opened from disk still finds it. The names are `overtype`, `marked`, `highlight`, and `uimotion`, plus one grammar per `highlight/<language>` for `bash`, `css`, `javascript`, `json`, `python`, and `xml`. `clay.assets.names()` lists them.
|
|
429
448
|
|
|
430
449
|
```js
|
|
431
450
|
const { Marked } = await clay.assets.load("marked");
|
|
@@ -434,7 +453,15 @@ const { default: javascript } = await clay.assets.load("highlight/javascript");
|
|
|
434
453
|
hljs.registerLanguage("javascript", javascript);
|
|
435
454
|
```
|
|
436
455
|
|
|
437
|
-
`overtype` resolves the vendored OverType editor with its toolbar buttons. `marked` resolves the `Marked` class. `highlight` resolves the core with no grammar of its own, so a page registers only the grammars it uses. Each name loads once: a second call before the first settles shares it, a failed one is forgotten and can be tried again, and an unknown name rejects with `clay.assets: no asset named "<name>"`. The [`window.clay` reference](https://clayjs.com/docs/api/) lists what each name resolves to.
|
|
456
|
+
`overtype` resolves the vendored OverType editor with its toolbar buttons. `marked` resolves the `Marked` class. `highlight` resolves the core with no grammar of its own, so a page registers only the grammars it uses. `uimotion` resolves the engine the free apps' intros animate with: `scene(stage, options)` builds a looping scene, `mount(stage, build, options)` plays it while the stage is on screen and stops it when the stage leaves the page, `rewind(scene, keep)` returns a scene to its start, and `ease` holds the curves. Each name loads once: a second call before the first settles shares it, a failed one is forgotten and can be tried again, and an unknown name rejects with `clay.assets: no asset named "<name>"`. The [`window.clay` reference](https://clayjs.com/docs/api/) lists what each name resolves to.
|
|
457
|
+
|
|
458
|
+
### 2.8 Editor commands load on first use
|
|
459
|
+
|
|
460
|
+
The page editor's optional commands are separate modules under `src/page/features/`, listed in `COMMAND_FEATURES` with one literal import each: `mentions`, `dates`, `emoji`, `titleIcon`, `titleCover`, `exportHtml`, `importMarkdown` and `exportMarkdown`. The build splits each into its own files, and the editor fetches one the first time a person invokes that command. A plain document that never uses them fetches none: opening a page, reading it and typing in it asks for no picker, no parser and no exporter. The emoji list arrives with the first picker that needs it (`page/emoji-data.js`), and `marked` arrives with the first Markdown import.
|
|
461
|
+
|
|
462
|
+
An installed command shares the page lease's disposal. Leaving edit, or a refused exit that closes admission, disposes what the commands installed and aborts whatever was still arriving, and the next entry installs fresh against the module the browser already holds. A command asked for while its module is still on the way runs only if the edit is still open, the document revision has not moved and the selection is where it was; otherwise it refuses with `stale-target` and writes nothing. A failed fetch is reported and asked for again on the next request.
|
|
463
|
+
|
|
464
|
+
The load report and the offline manifest both carry them. `node scripts/measure-load.mjs` counts what a command fetches in its `firstUse` phase, printed as the `phase first use` row, so the editor's ready bytes never include a module nobody asked for, and `dist/offline-manifest.json` lists their files, so a host that serves a release offline still loads one on first use.
|
|
438
465
|
|
|
439
466
|
---
|
|
440
467
|
|
|
@@ -442,28 +469,266 @@ hljs.registerLanguage("javascript", javascript);
|
|
|
442
469
|
|
|
443
470
|
### 3.1 How the mode is decided
|
|
444
471
|
|
|
445
|
-
|
|
472
|
+
Three separate facts:
|
|
446
473
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
474
|
+
- `clay.canEdit` is permission: whether this person may edit the document at all.
|
|
475
|
+
- `clay.mode` is `"view"`, `"edit"` or `"draft"`, and it is a live getter.
|
|
476
|
+
- `clay.modeLifecycle.state.destination` is where edits land: `"host"`, `"draft"`, or `"demo"` when the demo plugin saves to `localStorage`.
|
|
477
|
+
|
|
478
|
+
Permission alone does not open edit. An owner on hyperclay.com opens in view, with `clay.canEdit === true`. The host's own answer about the document, `document.writable` in `/_/meta`, is the last word on `canEdit`, and a change to it fires `clay:capabilitychange`.
|
|
479
|
+
|
|
480
|
+
The mode this tab starts in is decided once, in this order:
|
|
481
|
+
|
|
482
|
+
1. **Draft mode**, on a page that opted in ([Free documents for visitors](https://clayjs.com/docs/guide/free-documents/)). The visitor gets the editing UI, and their work stays in their own browser. `?editmode=false`, a `viewonly` attribute, a stale token, `window.clayEditMode`, or a save token all turn draft mode off. Only the owner cookie plus `?editmode=true` edits the original. On a `viewonly` document or one with a stale token, `?editmode=true` is plain edit mode as usual ([The mode ladder](https://clayjs.com/docs/guide/free-documents/#the-mode-ladder)).
|
|
483
|
+
2. **The reload receipt.** A deliberate switch writes `sessionStorage` entry `clay:doc:<pathname>:mode-receipt` just before it reloads. Boot reads it once and removes it, accepts it only within 30 seconds and only for a reload of the same path, and then starts the tab in the mode it names. A tab that can edit also leaves a receipt when the page is hidden, and boot accepts it only for a browser reload, so reloading an editing tab comes back in edit. On a page that declares `<html opens-in="edit">`, that hide receipt never keeps a tab in view, so adding the attribute takes effect on the next reload; a deliberate switch to view still does. A receipt keeps a reload in the same tab, not a lasting preference.
|
|
484
|
+
3. **`?editmode=`.** Only the literal `true` asks for edit.
|
|
485
|
+
4. **`window.clayEditMode === true`** or `<html opens-in="edit">`.
|
|
486
|
+
5. **Otherwise** the tab starts in view.
|
|
487
|
+
|
|
488
|
+
Nothing else opens edit, including a page whose own script makes text editable. An editor library such as OverType or CodeMirror, a `persist` field, and `contenteditable` set by your code all accept typing in view mode, and none of that typing is saved, because the save path runs only in edit. A tool page that must be editable the moment it opens, such as a journal or a notes pad, declares `<html opens-in="edit">`. Every other document needs a way into edit: the `menu` plugin, or a control that calls `clay.setMode("edit")`.
|
|
489
|
+
|
|
490
|
+
When someone who can edit types into a document in view anyway, ClayJS shows a notice on the first edit to a field that would save in edit mode: a `persist` control, or `contenteditable` content outside `editor-ui`, `no-save` and `freeze`. A field that edit mode would unlock (`editmode:contenteditable`, `viewmode:readonly`, `viewmode:disabled`) raises it on the first attempt to type into it or press it. The notice reads "In view mode, so edits won't be saved." and stays until the tab enters edit, with a **Switch to edit** button that calls `clay.setMode("edit")`. A document without `clay-mode-switch="live"` reloads to switch, so the characters typed before the notice appeared are lost. A visitor who cannot edit never sees it, and a page with its own handled single-key shortcuts does not set it off. It also writes one warning to the console.
|
|
491
|
+
|
|
492
|
+
`<html viewonly>` in the file forces view mode. So does a response carrying only the pre-1.9.0 token `htmlclaytoken` (an out-of-date HTML Clay, which also shows an update bar).
|
|
493
|
+
|
|
494
|
+
`clay.canEdit` follows its own ladder: `?editmode=` first, then `viewonly` and a stale token, which take edit off the table, then `window.clayEditMode`, then a save token or the owner cookie.
|
|
495
|
+
|
|
496
|
+
`clay.isOwner` reflects the cookie alone. On HTML Clay, an editable page has `isOwner === false`. Use `clay.canEdit` for whether a person may edit, and `clay.mode` for whether this tab is editing.
|
|
497
|
+
|
|
498
|
+
### 3.2 Switching modes: clay.setMode
|
|
499
|
+
|
|
500
|
+
`await clay.setMode("edit")` and `await clay.setMode("view")` switch this tab. `setMode` takes one argument, and anything else throws a `TypeError`.
|
|
501
|
+
|
|
502
|
+
Three answers:
|
|
503
|
+
|
|
504
|
+
- `{ ok: true, mode, changed }`: the switch committed. `changed` is `false` when the tab was already in that mode.
|
|
505
|
+
- `{ ok: true, mode, changed: true, reloading: true }`: a reload is on its way.
|
|
506
|
+
- `{ ok: false, mode, code }`: it refused, and `mode` is still the committed one.
|
|
507
|
+
|
|
508
|
+
The refusal codes:
|
|
509
|
+
|
|
510
|
+
- `busy`: another switch to the other mode is already running.
|
|
511
|
+
- `not-allowed`: this person may not edit, or the page is in draft mode.
|
|
512
|
+
- `permission-denied`: the host answered that this caller may not write the document.
|
|
513
|
+
- `cancelled`: the page moved to another document while the host answered.
|
|
514
|
+
- `host-failed`: the workspace shell threw instead of answering.
|
|
515
|
+
- `host-refused`: the workspace shell answered without success.
|
|
516
|
+
- `save-failed`: the save before leaving edit failed or did not settle as saved.
|
|
517
|
+
- `pending-input`: text or a composition is still uncommitted in the page.
|
|
518
|
+
- `conflict`: the file has an unresolved save conflict.
|
|
519
|
+
- `save-unknown`: the outcome of the last save could not be established.
|
|
520
|
+
- `stale-document`: the file moved on while the session was being prepared.
|
|
521
|
+
- `timeout`: the 15 second deadline passed.
|
|
522
|
+
- `load-failed`: a module the entry needed failed to import.
|
|
523
|
+
- `unsupported-plugin`: a queued entry was not a participant.
|
|
453
524
|
|
|
454
|
-
|
|
525
|
+
An undeclared document saves, writes the receipt and reloads. A document with `<html clay-mode-switch="live">` and supported participants switches live. A workspace frame hands the change to its shell, whose reload owns it.
|
|
455
526
|
|
|
456
|
-
|
|
527
|
+
Draft mode always answers `not-allowed`: a draft stays local.
|
|
457
528
|
|
|
458
|
-
|
|
529
|
+
Keep the control on the committed value while a switch is pending, and show the code when it refuses:
|
|
530
|
+
|
|
531
|
+
```js
|
|
532
|
+
const toggle = document.querySelector("[clay-mode-toggle]");
|
|
533
|
+
const status = document.querySelector("[data-mode-status]");
|
|
534
|
+
let busy = false;
|
|
535
|
+
|
|
536
|
+
function paint() {
|
|
537
|
+
toggle.setAttribute("aria-checked", clay.mode === "edit" ? "true" : "false");
|
|
538
|
+
}
|
|
539
|
+
paint();
|
|
540
|
+
document.addEventListener("clay:modechange", paint);
|
|
541
|
+
|
|
542
|
+
toggle.addEventListener("click", async () => {
|
|
543
|
+
if (busy) return;
|
|
544
|
+
busy = true;
|
|
545
|
+
toggle.disabled = true;
|
|
546
|
+
status.textContent = "";
|
|
547
|
+
const result = await clay.setMode(clay.mode === "edit" ? "view" : "edit");
|
|
548
|
+
// A reload is coming: stay busy until the page goes.
|
|
549
|
+
if (result.reloading) return;
|
|
550
|
+
busy = false;
|
|
551
|
+
toggle.disabled = false;
|
|
552
|
+
if (result.ok === false) status.textContent = `Stayed in ${clay.mode}: ${result.code}`;
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Never switch by writing the root `editmode` attribute. That attribute reports the committed mode; it does not change it.
|
|
557
|
+
|
|
558
|
+
`clay.toggleEditMode()` is the one-line form. In draft mode it opens `?editmode=true`, and otherwise it calls `setMode`.
|
|
559
|
+
|
|
560
|
+
Every page answers Cmd+Shift+E (Ctrl+Shift+E elsewhere) with `setMode` for a person who can edit. A page with no switch of its own shows that person a one-time note pointing to the shortcut. Mark your own control `clay-mode-toggle` to replace the note, or load the `menu` plugin for a ready-made Edit switch in the top right ([menu](https://clayjs.com/docs/plugins/menu/)).
|
|
561
|
+
|
|
562
|
+
### 3.3 Switching without a reload: clay-mode-switch="live"
|
|
563
|
+
|
|
564
|
+
`<html clay-mode-switch="live">` is the author's promise that every edit resource can be taken back. Without it, `setMode` takes the guarded reload: it saves, writes the receipt and reloads.
|
|
565
|
+
|
|
566
|
+
A document registers participants with `clay.modeLifecycle.register(...)`, or before the loader runs through the `window.clay.modeParticipants` array, which survives the boot and is drained by the first entry.
|
|
567
|
+
|
|
568
|
+
A participant is `{ id, order, prepare }`. `order` decides prepare order and defaults to `0`. `prepare(context)` returns a lease:
|
|
569
|
+
|
|
570
|
+
- `ready`: a promise the entry awaits before it goes on, or nothing.
|
|
571
|
+
- `enable`: runs synchronously, once every preparation has succeeded.
|
|
572
|
+
- `quiesce`: stops admission without losing buffers.
|
|
573
|
+
- `resume`: takes the very same lease back after a refused exit.
|
|
574
|
+
- `dispose`: runs only after a committed exit.
|
|
575
|
+
|
|
576
|
+
`context` is `{ generation, destination, signal, represented, reader, initial, isCurrent() }`.
|
|
577
|
+
|
|
578
|
+
`clay.modeLifecycle.canWrite()` is true while this tab is in edit or draft and no transition is running. `clay.modeLifecycle.state` reports `phase` as `idle`, `entering`, `leaving` or `blocked`, beside `mode`, `destination`, `generation` and `reason`. A committed change fires `clay:modechange`, and a refusal is the returned code plus `state.reason`.
|
|
579
|
+
|
|
580
|
+
An id that starts with `clay:` is reserved for the built-in sessions.
|
|
581
|
+
|
|
582
|
+
One 15 second deadline covers the whole transition: every import a participant prepares from, the preparation itself and the commit.
|
|
583
|
+
|
|
584
|
+
`prepare` must not await `clay.ready`. The loader awaits `activateInitialMode()` before it resolves `ready`, and `activateInitialMode()` awaits every `prepare`, so a `prepare` that awaits `ready` waits on itself.
|
|
585
|
+
|
|
586
|
+
An asynchronous callback checks `context.isCurrent()` and `clay.modeLifecycle.canWrite()` before it changes a record.
|
|
587
|
+
|
|
588
|
+
A participant registered before the loader runs, with a lease that has all five members:
|
|
589
|
+
|
|
590
|
+
```js
|
|
591
|
+
window.clay.modeParticipants = [
|
|
592
|
+
{
|
|
593
|
+
id: "board",
|
|
594
|
+
order: 10,
|
|
595
|
+
prepare: async (context) => {
|
|
596
|
+
const editor = await openEditor(context.signal);
|
|
597
|
+
return {
|
|
598
|
+
ready: null,
|
|
599
|
+
enable: () => editor.adopt(),
|
|
600
|
+
quiesce: async () => editor.flush(),
|
|
601
|
+
resume: () => editor.resume(),
|
|
602
|
+
dispose: () => editor.teardown(),
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
];
|
|
607
|
+
```
|
|
459
608
|
|
|
460
|
-
### 3.
|
|
609
|
+
### 3.4 Leaving edit
|
|
461
610
|
|
|
462
|
-
|
|
611
|
+
The exit waits for the writes it is allowed to wait for, and refuses with a code when it cannot finish:
|
|
612
|
+
|
|
613
|
+
- an unsent composer or an unresolved IME composition answers `pending-input`.
|
|
614
|
+
- an unresolved conflict answers `conflict`.
|
|
615
|
+
- a save whose outcome is unknown answers `save-unknown`.
|
|
616
|
+
- a failed save answers `save-failed`.
|
|
617
|
+
- a pending upload is flushed first, and one that is still running answers `pending-input`.
|
|
618
|
+
|
|
619
|
+
A refused exit resumes the very same leases and leaves the page in edit, with `clay.modeLifecycle.state.reason` naming why. Nothing blur-submits: the exit never commits text a person did not commit.
|
|
620
|
+
|
|
621
|
+
Entering edit writes nothing. The session adopts the file it shows as its baseline (`save.adoptEntryBaseline`), so a clean enter and exit leaves the file byte for byte the same. The recorded case is `enter-leave-zero-writes` in the clayjs corpus.
|
|
622
|
+
|
|
623
|
+
### 3.5 Attributes across a mode change
|
|
624
|
+
|
|
625
|
+
The four controlled families, `editmode:contenteditable`, `editmode:onclick`, `viewmode:disabled` and `viewmode:readonly`, are handed back with their authored values after a cycle.
|
|
626
|
+
|
|
627
|
+
Frozen regions keep their saved contents.
|
|
628
|
+
|
|
629
|
+
`editmode` on `<html>` follows each commit, and it is stripped from every save.
|
|
630
|
+
|
|
631
|
+
A committed live switch drops `?editmode=` from the URL.
|
|
632
|
+
|
|
633
|
+
`opens-in` is read once at boot and never rewritten.
|
|
634
|
+
|
|
635
|
+
### 3.6 A complete example
|
|
636
|
+
|
|
637
|
+
One persisted note, a participant registered before the loader, an explicit initial edit, and a composer that holds a comment until it is sent or discarded. The participant's script goes above the loader in the same document.
|
|
638
|
+
|
|
639
|
+
```html example=mode-cycle-page
|
|
640
|
+
<!DOCTYPE html>
|
|
641
|
+
<html clay-mode-switch="live">
|
|
642
|
+
<head>
|
|
643
|
+
<meta charset="utf-8">
|
|
644
|
+
<title>Notes</title>
|
|
645
|
+
</head>
|
|
646
|
+
<body>
|
|
647
|
+
<article data-id="note">
|
|
648
|
+
<div id="content" editmode:contenteditable inert-contenteditable="plaintext-only">A note kept in the file.</div>
|
|
649
|
+
<div class="composer" clay="editor-ui">
|
|
650
|
+
<input id="comment" unsent="comment" placeholder="Add a comment">
|
|
651
|
+
<button id="send" type="button">Send</button>
|
|
652
|
+
<button id="discard" type="button">Discard</button>
|
|
653
|
+
<span id="count" aria-live="polite"></span>
|
|
654
|
+
</div>
|
|
655
|
+
</article>
|
|
656
|
+
<script src="https://clayjs.com/v1/clay.js?plugins=unsent&exclude=richclay"></script>
|
|
657
|
+
</body>
|
|
658
|
+
</html>
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
The record is a controlled element, so a save writes the inert spelling back and no reader receives an editable note. The lease hands that surface to the session and takes it back on the way out, and a write that lands in a later turn checks the session is still current and still admits writes:
|
|
662
|
+
|
|
663
|
+
```js example=mode-cycle-participant
|
|
664
|
+
window.clay = window.clay ?? {};
|
|
665
|
+
window.clay.modeParticipants = window.clay.modeParticipants ?? [];
|
|
666
|
+
|
|
667
|
+
window.clay.modeParticipants.push({
|
|
668
|
+
id: "notes",
|
|
669
|
+
order: 10,
|
|
670
|
+
prepare: async (context) => {
|
|
671
|
+
const content = document.getElementById("content");
|
|
672
|
+
const count = document.getElementById("count");
|
|
673
|
+
const editable = "plaintext-only";
|
|
674
|
+
|
|
675
|
+
const paint = () => {
|
|
676
|
+
setTimeout(() => {
|
|
677
|
+
if (!context.isCurrent() || !clay.modeLifecycle.canWrite()) return;
|
|
678
|
+
count.textContent = `${content.textContent.trim().length} characters`;
|
|
679
|
+
}, 0);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
ready: null,
|
|
684
|
+
enable() {
|
|
685
|
+
content.setAttribute("contenteditable", editable);
|
|
686
|
+
content.removeAttribute("inert-contenteditable");
|
|
687
|
+
content.addEventListener("input", paint);
|
|
688
|
+
},
|
|
689
|
+
quiesce: async () => {},
|
|
690
|
+
resume() {
|
|
691
|
+
content.setAttribute("contenteditable", editable);
|
|
692
|
+
content.removeAttribute("inert-contenteditable");
|
|
693
|
+
},
|
|
694
|
+
dispose() {
|
|
695
|
+
content.removeEventListener("input", paint);
|
|
696
|
+
content.removeAttribute("contenteditable");
|
|
697
|
+
content.setAttribute("inert-contenteditable", editable);
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
```js example=mode-cycle-submit
|
|
705
|
+
const comment = document.getElementById("comment");
|
|
706
|
+
|
|
707
|
+
document.getElementById("send").addEventListener("click", async () => {
|
|
708
|
+
const content = document.getElementById("content");
|
|
709
|
+
const line = document.createElement("p");
|
|
710
|
+
line.textContent = comment.value;
|
|
711
|
+
content.append(line);
|
|
712
|
+
clay.unsent.clear(comment);
|
|
713
|
+
comment.value = "";
|
|
714
|
+
await clay.save();
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
document.getElementById("discard").addEventListener("click", () => {
|
|
718
|
+
clay.unsent.clear(comment);
|
|
719
|
+
comment.value = "";
|
|
720
|
+
});
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Leaving edit is refused while the composer holds unsent text, and it succeeds once Send or Discard clears that text.
|
|
724
|
+
|
|
725
|
+
### 3.7 The root attributes
|
|
726
|
+
|
|
727
|
+
At load, ClayJS sets `editmode="true|false"` and `pageowner="true|false"` on `<html>` in both modes, `canedit="true"` while this person may edit (kept current on `clay:capabilitychange`), and `draftmode="true"` in draft mode. In edit mode, it also sets `savestatus` (`saving`, `saved`, `error`, `offline`, `conflict`) once the page settles, and `unsaved="true"` while there is work a save has not written. **All of them are removed on every save**, along with the host's `savetoken` and `documentetag`. The saved file's `<html>` carries none of them.
|
|
463
728
|
|
|
464
729
|
Before ClayJS runs, `editmode` is **absent**, not `"false"`. CSS keyed on `html[editmode="false"]` shows edit chrome to visitors until boot. Key on `html:not([editmode="true"])` instead.
|
|
465
730
|
|
|
466
|
-
### 3.
|
|
731
|
+
### 3.8 Showing and hiding edit controls: clay-options
|
|
467
732
|
|
|
468
733
|
Load the `clay-options.js` satellite and mark edit-only controls with `show-when:editmode="true"`:
|
|
469
734
|
|
|
@@ -493,9 +758,9 @@ Visibility attributes:
|
|
|
493
758
|
- For viewer-only content, use `hide-when:editmode="true"`, which is visible when `editmode` is absent. `show-when:editmode="false"` stays hidden until boot and then appears.
|
|
494
759
|
- The attribute that switches views is ordinary saved content. Setting `view="list"` on a saved element saves and syncs it to every collaborator. [Per-tab state](https://clayjs.com/docs/guide/architecture/#per-tab-state) covers tab-local views.
|
|
495
760
|
|
|
496
|
-
### 3.
|
|
761
|
+
### 3.9 Mode attributes: one file for editors and viewers
|
|
497
762
|
|
|
498
|
-
Four attributes make an element behave differently by mode. A **save transform** writes the inert form into the file, and **edit mode** activates it on load.
|
|
763
|
+
Four attributes make an element behave differently by mode. A **save transform** writes the inert form into the file, and **edit mode** activates it on load. A view boot never loads the activation code, so a viewer sees what the file holds.
|
|
499
764
|
|
|
500
765
|
| Attribute | In the saved file | In edit mode |
|
|
501
766
|
|---|---|---|
|
|
@@ -525,9 +790,9 @@ There is no general `editmode:<anything>`. Only these four attributes exist.
|
|
|
525
790
|
|
|
526
791
|
Use `contenteditable="plaintext-only"` for titles, labels, and cells to prevent formatting and pasted markup. Make Enter blur instead of inserting a line break, as in the skeleton's keydown handler. Use `editable` ([richclay: rich text (`editable`)](https://clayjs.com/docs/guide/components/#richclay-rich-text-editable)) only where you need rich text.
|
|
527
792
|
|
|
528
|
-
### 3.
|
|
793
|
+
### 3.10 What works in view mode
|
|
529
794
|
|
|
530
|
-
|
|
795
|
+
`window.clay` keeps `ready`, `setMode`, `mode`, `canEdit`, `modeLifecycle`, `isEditMode`, `isDraftMode`, `isOwner`, `toggleEditMode`, `Mutation`, `region`, `readData`, `writeData`, `assets`, `ephemeral`, `storage`, `host`, `trash`, `derived`, `effect`, `style`, `morph`, and `hasUnsavedChanges` (always `false`) in both modes. It also includes whatever view-mode plugins and satellites add (`clay.conflicts` and `clay.morph` with `sync`, `clay.cms`, `clay.quickcrop`, `clay.toast`, and others). There is no `save`, `getHTML`, `addDocumentTransform`, `onSnapshot`, `undo`, `RichClay`, or `upload`, and no `markDirty` unless `sync` is loaded (where it is harmless). On a template copy whose tab can edit, the `upgrade` plugin loads in view and `clay.save` holds only `replace`, so test `clay.isEditMode`, never `'save' in clay`.
|
|
531
796
|
|
|
532
797
|
- Guard edit-only APIs with optional chaining (`clay.addDocumentTransform?.(fn)`), or return early with `if (!clay.isEditMode) return`. An unguarded call throws and stops the rest of the script for viewers.
|
|
533
798
|
- Click handlers run for viewers too. Hidden buttons cannot be clicked, but guard edit actions anyway with `if (!clay.isEditMode) return`, as the examples do.
|
|
@@ -535,9 +800,9 @@ In view mode, `window.clay` keeps `ready`, `isEditMode`, `isOwner`, `toggleEditM
|
|
|
535
800
|
- With `sync` loaded, a viewer rides the saved lane and receives each accepted save, so a published page updates live ([What sync is](https://clayjs.com/docs/guide/live-sync/#what-sync-is)).
|
|
536
801
|
- Clicking inside `[trigger-save]` in view mode dispatches `clay:view-save-attempt`, so you can explain why nothing saved.
|
|
537
802
|
|
|
538
|
-
### 3.
|
|
803
|
+
### 3.11 When the page editor is ready
|
|
539
804
|
|
|
540
|
-
|
|
805
|
+
The mode this tab starts in is decided at load, but a `page` document is ready to edit later. `clay.page.ready` resolves when its editor is mounted and accepts typing. `clay.page.status` holds that boot result: `{ phase, reason, installId, at }`.
|
|
541
806
|
|
|
542
807
|
`clay.ready` does not wait for the page editor. Because `plugins/page.js` exports no `ready`, `await clay.ready` can resolve while the saved article is still on screen. Late plugins do wait, so a page that ships with them needs nothing extra.
|
|
543
808
|
|
|
@@ -557,7 +822,7 @@ if (boot?.phase !== "ready") document.documentElement.setAttribute("read-only",
|
|
|
557
822
|
|
|
558
823
|
`clay.page.install()` disposes the last installation and starts a fresh one with its own id, letting a page retry after a failure without reloading.
|
|
559
824
|
|
|
560
|
-
### 3.
|
|
825
|
+
### 3.12 The page editor on a phone
|
|
561
826
|
|
|
562
827
|
At widths up to 560 px, the page editor's toolbar keeps four controls: Bold, Italic, Link, and More. Each is at least 44 by 44 CSS px inside a 204 px bar with 4 px gaps and 8 px padding. The selection bubble is hidden so two bars never appear at once.
|
|
563
828
|
|
|
@@ -565,13 +830,13 @@ More holds the rest: Strikethrough, Code, bulleted, numbered, and checklist list
|
|
|
565
830
|
|
|
566
831
|
The bar is positioned from the visual viewport, not the layout viewport. Its top and horizontal bounds come from the visible area, so an on-screen keyboard cannot cover it and panning the visual viewport moves it with the page. The safe-area inset is read once per layout frame. Menus stay inside the visual viewport, open above the bar when there is room, and scroll internally. Desktop layouts are unchanged.
|
|
567
832
|
|
|
568
|
-
### 3.
|
|
833
|
+
### 3.13 Keyboard navigation in the page editor
|
|
569
834
|
|
|
570
835
|
Escape closes an open menu or the link popover first, returning focus to the control that opened it or to the editor.
|
|
571
836
|
|
|
572
837
|
From a block selection, Escape moves focus from the writing surface to the first toolbar control while keeping the selection, so the keyboard is never trapped in `contenteditable`. In the toolbar, Left and Right move between controls, and Escape returns to the editor. The toolbar is a single tab stop with a roving tabindex: Tab leaves it forward and Shift-Tab leaves it backward. Inside a table, Tab moves across cells and Shift-Tab moves back. The editor takes Tab for nothing else.
|
|
573
838
|
|
|
574
|
-
A table's column resize handle is a focusable vertical separator named for its column, with `aria-valuemin` 48, `aria-valuemax` 1200, and the current width in `aria-valuenow`. Left and Right preview a 10 px change (50 px with Shift), while Home and End preview the minimum and maximum. Enter commits the change, and Escape cancels it. Holding an arrow key previews across repeated keydowns. The keyup that ends the repeat, Enter, or leaving the handle commits one command, so a held arrow key records a single undo item. Double-
|
|
839
|
+
A table's column resize handle is a focusable vertical separator named for its column, with `aria-valuemin` 48, `aria-valuemax` 1200, and the current width in `aria-valuenow`. Left and Right preview a 10 px change (50 px with Shift), while Home and End preview the minimum and maximum. Enter commits the change, and Escape cancels it. Holding an arrow key previews across repeated keydowns. The keyup that ends the repeat, Enter, or leaving the handle commits one command, so a held arrow key records a single undo item. A table with no widths fills the page; the first width you set pins the other columns at their drawn widths, in the same undo step. Double-clicking a handle resets the table to the page width, and pointer dragging is unchanged. On a phone, the column's Width menu (Wider, Narrower, Fit to content, and Reset widths once the table has widths) does the same job.
|
|
575
840
|
|
|
576
841
|
---
|
|
577
842
|
|
|
@@ -583,7 +848,7 @@ A save writes the current DOM with these changes:
|
|
|
583
848
|
|
|
584
849
|
- Subtrees marked `no-save`, `no-snapshot`, or `editor-ui` are removed.
|
|
585
850
|
- `freeze` elements write the inner HTML they had when they first appeared (at load or on insertion). Their live contents are a per-tab scratchpad.
|
|
586
|
-
- Mode attributes go inert ([Mode attributes: one file for editors and viewers](https://clayjs.com/docs/guide/edit-mode/#mode-attributes-one-file-for-editors-and-viewers)). Root attributes `editmode`, `pageowner`, `savestatus`, `savetoken`, and `documentetag` are removed.
|
|
851
|
+
- Mode attributes go inert ([Mode attributes: one file for editors and viewers](https://clayjs.com/docs/guide/edit-mode/#mode-attributes-one-file-for-editors-and-viewers)). Root attributes `editmode`, `pageowner`, `canedit`, `draftmode`, `savestatus`, `unsaved`, `savetoken`, and `documentetag` are removed.
|
|
587
852
|
- `persist` controls write their live values ([`persist`: form controls](https://clayjs.com/docs/guide/record/#persist-form-controls)).
|
|
588
853
|
- Runtime residue from ClayJS editors (richclay attributes, toolbars) and browser extensions (password managers, Grammarly, Dark Reader) is stripped.
|
|
589
854
|
- Your hooks run ([Hooks](https://clayjs.com/docs/guide/saving/#hooks)).
|
|
@@ -638,6 +903,7 @@ Put `persist` on every `<input>`, `<textarea>`, and `<select>` whose value belon
|
|
|
638
903
|
```
|
|
639
904
|
|
|
640
905
|
- On `input` (text-like fields) and `change` (checkbox, radio, select), ClayJS mirrors the live value into `value`, `checked`, or `selected`. A textarea's value goes into a `data-value` attribute while typing and becomes its text content in the save.
|
|
906
|
+
- A `<textarea>` merges as one value, so do not use `<textarea persist>` for long text several people edit. Keep the text in a record element (`<pre><code>`) and bind an editor outside the records to it with `clay.bindText` ([OverType: a markdown editor](https://clayjs.com/docs/guide/components/#overtype-a-markdown-editor)).
|
|
641
907
|
- A snapshot also copies every `persist` control's live state, so a value set from script with no event is saved too.
|
|
642
908
|
- Password, hidden, and file inputs are never persisted. An authored `value` on them stays in the file.
|
|
643
909
|
- `persist` goes on the control itself, never on a wrapper.
|
|
@@ -686,6 +952,7 @@ Live sync and undo pair elements across versions by **`data-id`, then `id`**, us
|
|
|
686
952
|
- **Leave `data-id` off templates.** The command that clones a template assigns one. For a plain "add an item" button, `<button add-from="#item-tpl" add-to="#list">` handles the clone, the id, and the focus with no script.
|
|
687
953
|
- **Never stamp ids on open.** Opening an old file must not rewrite it. Add an id when an item is created or first changed.
|
|
688
954
|
- **Make ids without `crypto.randomUUID`** unless the file is only ever served over https or localhost, because it is missing in insecure contexts. The examples use `prefix + "-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6)`.
|
|
955
|
+
- **A `data-id` is also what presence names.** A record target `{ kind: "record", id }` carries it, so a stable id is what lets a peer's badge find the row again after a reload or a merge ([Presence](https://clayjs.com/docs/guide/live-sync/#presence)).
|
|
689
956
|
- **Use ids to find records in commands:** `` board.querySelector(`.card[data-id="${CSS.escape(id)}"]`) ``. Never find a record by position.
|
|
690
957
|
|
|
691
958
|
### 4.6 Assets
|
|
@@ -714,6 +981,33 @@ clay.ephemeral.toggle(card, "selected"); // add, remove, toggle(el, word, forc
|
|
|
714
981
|
|
|
715
982
|
Use `ephemeral` for state on one element. State that comes from the URL hash and spans many records (the open card, a filter) is still painted with a runtime stylesheet ([Per-tab state](https://clayjs.com/docs/guide/architecture/#per-tab-state)).
|
|
716
983
|
|
|
984
|
+
### 4.8 A template is an ordinary record with a tag
|
|
985
|
+
|
|
986
|
+
A reusable template is a record like any other, marked with a tag. It stays editable, searchable, exported, synced and in a visitor's draft, and it needs no second store and no new format. The app reads the tag off the body it already has:
|
|
987
|
+
|
|
988
|
+
```js
|
|
989
|
+
const templates = open => Entries.live().filter(a => a !== open && summary({ body: Entries.body(a), $el: a }).tags.includes('template'));
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
The tag is matched outside code spans and fences, so a template entry that shows a `#template` example does not become a template itself. Using one copies its text into the page, and the new record is written by the same save as everything else.
|
|
993
|
+
|
|
994
|
+
### 4.9 Keep a helper that reads the text beside the editor, in an `editor-ui` row
|
|
995
|
+
|
|
996
|
+
A helper that reacts to the text being written belongs in a row a script builds beside the editor, marked `clay="editor-ui"`, and refreshed on a debounce. Nothing it draws is saved, printed or synced, and no keystroke pays for it beyond the debounce:
|
|
997
|
+
|
|
998
|
+
```js
|
|
999
|
+
el = document.createElement('div');
|
|
1000
|
+
el.className = 'starters';
|
|
1001
|
+
el.setAttribute('clay', 'editor-ui');
|
|
1002
|
+
$('#writer').before(el);
|
|
1003
|
+
|
|
1004
|
+
function start(on) {
|
|
1005
|
+
on($('#writer'), 'input', () => { clearTimeout(timer); timer = setTimeout(refresh, 300); });
|
|
1006
|
+
}
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
The row is built on demand and refreshed on that debounce. Everything it draws lives in the row, so it is never saved, printed or synced.
|
|
1010
|
+
|
|
717
1011
|
---
|
|
718
1012
|
|
|
719
1013
|
## 5. Saving
|
|
@@ -722,6 +1016,8 @@ Use `ephemeral` for state on one element. State that comes from the URL hash and
|
|
|
722
1016
|
|
|
723
1017
|
In edit mode, ClayJS saves the whole document to `POST /_/save` on the page's origin (or `/_/save/<savetoken>` on HTML Clay). Cmd+S (Ctrl+S elsewhere) and clicks inside any `[trigger-save]` element call `clay.save()` on every edit-mode page, with or without autosave. A close warning appears whenever there are unsaved changes.
|
|
724
1018
|
|
|
1019
|
+
Leaving edit saves first. If that save fails, or text is still unsent, or a conflict is open, the switch is refused with a code and the page stays in edit ([Leaving edit](https://clayjs.com/docs/guide/edit-mode/#leaving-edit)).
|
|
1020
|
+
|
|
725
1021
|
#### 5.1.1 The saved title
|
|
726
1022
|
|
|
727
1023
|
ClayJS derives the saved `<title>` from the document rather than keeping the authored `<title>`. The save projection reads the captured model and takes the first top-level H1, collapsing and trimming whitespace runs. A page with no top-level H1 gets the title `Untitled`. The server checkpoint renderer runs the same function on the same model, so a room checkpoint writes the same title a client save would.
|
|
@@ -734,7 +1030,7 @@ Title changes only update the document's content. ClayJS never derives or change
|
|
|
734
1030
|
|
|
735
1031
|
`<html autosave>` present at load turns on autosave. Adding or removing it later changes nothing.
|
|
736
1032
|
|
|
737
|
-
- A watched DOM change or a `persist` input starts a 1500 ms debounce. Continuous changes save at most 10 s after the first one.
|
|
1033
|
+
- A watched DOM change or a `persist` input starts a 1500 ms debounce. Continuous changes save at most 10 s after the first one. After each save, the next automatic save waits at least 1.2 s, and at least four times as long as that save took to prepare, up to 30 s, so a page whose saves are expensive saves less often instead of saving between keystrokes.
|
|
738
1034
|
- Measured: one edit saves at about 1.5 s, 50 edits in a second save once at about 2.5 s, and an edit every 300 ms saves at about 10 s.
|
|
739
1035
|
- Template content edits never trigger autosave.
|
|
740
1036
|
- A save that fails (500 or network) is **not retried** until the next edit or the browser's `online` event. `savestatus="error"` stays up. If your app can sit idle after a failure, retry yourself:
|
|
@@ -745,12 +1041,63 @@ Title changes only update the document's content. ClayJS never derives or change
|
|
|
745
1041
|
});
|
|
746
1042
|
```
|
|
747
1043
|
|
|
1044
|
+
A save is admitted before the page is captured. The spacing is decided first, and only an admitted attempt takes a snapshot, so nothing captures the page for a save that is still only an intention:
|
|
1045
|
+
|
|
1046
|
+
```js
|
|
1047
|
+
const cooldown = Math.min(MAX_COOLDOWN_MS, Math.max(MIN_GAP_MS, COST_MULTIPLIER * costMs));
|
|
1048
|
+
const quietAt = Math.max(last, finished) + IDLE_MS;
|
|
1049
|
+
const continuousAt = Math.max(first + CONTINUOUS_MS, finished + cooldown);
|
|
1050
|
+
return Math.max(finished + MIN_GAP_MS, Math.min(quietAt, continuousAt));
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
The quiet window and the continuous ceiling are both computed from the clock, and the cooldown after a save scales with what that save cost.
|
|
1054
|
+
|
|
748
1055
|
**The load-settle window.** At load, ClayJS takes a baseline of the document. It retakes the baseline once the page is quiet for 500 ms (at most 3 s), unless the user typed or a save ran. It then sets `savestatus="saved"` and fires `clay:baseline-settled`. Anything app code writes to the record during that window becomes part of the baseline: it is not saved, not autosaved, and not counted as unsaved. It reaches the file only when a later edit triggers a save:
|
|
749
1056
|
|
|
750
1057
|
- Do not seed defaults, migrate a schema, or stamp ids at startup. Author defaults in the file.
|
|
751
1058
|
- If an app must write at startup, write after `clay:baseline-settled` (or once `<html savestatus>` exists), or follow the write with `await clay.save.force()`.
|
|
752
1059
|
|
|
753
|
-
### 5.3
|
|
1060
|
+
### 5.3 Two baselines from one capture
|
|
1061
|
+
|
|
1062
|
+
A page keeps two saved baselines from one save: the bytes a comparison uses, and the bytes the dirty check uses. Both domains are taken from one capture, because a comparison that borrowed the dirty domain would lose a batching region's edit, and a dirty baseline that borrowed the comparison domain would call a page clean while a `clay="no-trigger-autosave"` region still held new work.
|
|
1063
|
+
|
|
1064
|
+
```js
|
|
1065
|
+
const { forComparison, forDirty } = captureForComparisonAndDirty();
|
|
1066
|
+
setLastSavedBaselines(forComparison, forDirty);
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
Both baselines advance from the same capture taken before the request, never from the live DOM, so an edit made while the save was on the wire stays unsaved. A caller with one string is a page with no batching region, and passes it for both: `setLastSavedContents(val)`.
|
|
1070
|
+
|
|
1071
|
+
### 5.4 Reuse a verified parse only for the accepted capture
|
|
1072
|
+
|
|
1073
|
+
Verifying the bytes a save will send already parses them, and a second parse of the accepted bytes pairs the whole page again. Keep the parse the verification paid for, and adopt its model only when the candidate is still this document:
|
|
1074
|
+
|
|
1075
|
+
```js
|
|
1076
|
+
function withCandidate(verdict, errors, rendered, liveDocument) {
|
|
1077
|
+
if (!errors.doc) return verdict;
|
|
1078
|
+
const candidate = modelFromParsed(rendered, errors.doc, errors.parseErrors);
|
|
1079
|
+
if (checkSource(candidate, liveDocument)) return verdict;
|
|
1080
|
+
verdict.model = candidate;
|
|
1081
|
+
return verdict;
|
|
1082
|
+
}
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
`checkSource` compares what a tree comparison cannot see: the doctype and the document-level comments outside `<html>`. A model that fails it is not this document, whatever the render verified as, so the save keeps its fallback path and adopts nothing. The model is only kept for the capture the acknowledgement is about, never for newer typing.
|
|
1086
|
+
|
|
1087
|
+
### 5.5 Measure typing across several accepted saves
|
|
1088
|
+
|
|
1089
|
+
Input to view timing alone misses the work that blocks the next input from arriving. Type continuously across several accepted saves, hold each response so typing continues while the request is on the wire, and report the input delivery gaps between key events beside the input to frame times:
|
|
1090
|
+
|
|
1091
|
+
```js
|
|
1092
|
+
// node scripts/measure-save-pipeline.mjs --entries 5000 --typing-ms 60000 --pause-ms 3000 --response-delay-ms 2000
|
|
1093
|
+
inputGapsMs: stats(gaps),
|
|
1094
|
+
inputToFrameMs: stats(meter.inputs.map((input) => input.raf)),
|
|
1095
|
+
inputToViewsMs: stats(meter.inputs.map((input) => input.views)),
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
The gaps inside a save window are the ones that matter. A save that only shifts input to the next frame has still blocked the input that never arrived, and only a run across several accepted saves shows it.
|
|
1099
|
+
|
|
1100
|
+
### 5.6 Manual-save pages and batching
|
|
754
1101
|
|
|
755
1102
|
To disable autosave, such as for a long-form writer where frequent saves are unwelcome, omit `autosave`. Cmd+S, `[trigger-save]`, and the close warning still work. Save when the tab hides:
|
|
756
1103
|
|
|
@@ -768,7 +1115,7 @@ To batch saves in part of an autosaving page, wrap that region in `clay="no-trig
|
|
|
768
1115
|
|
|
769
1116
|
`clay.Mutation.pause()` and `resume()` hide DOM mutations from the mutation hub's listeners, including autosave's mutation trigger, while paused. The next save still writes those mutations. They do not pause input: `input` and `change` events on controls, including synthetic ones, still mark the page dirty and start autosave. Pair them in `try/finally`. An unbalanced pause silences mutation-driven autosave for the rest of the session.
|
|
770
1117
|
|
|
771
|
-
### 5.
|
|
1118
|
+
### 5.7 The save API
|
|
772
1119
|
|
|
773
1120
|
`clay.save()` returns `Promise<{ ok, msg, msgType, code, etag }>` and never rejects. One exception in 1.8.2: when capturing the page fails because a registered hook threw ([Hooks](https://clayjs.com/docs/guide/saving/#hooks)), the result has `msgType: "error"` and no `ok` field.
|
|
774
1121
|
|
|
@@ -783,6 +1130,7 @@ To batch saves in part of an autosaving page, wrap that region in `clay="no-trig
|
|
|
783
1130
|
|
|
784
1131
|
**Branch on `msgType`, not `ok`.** `ok: false` also means "nothing to save" or "queued behind another save". To ensure the host holds what the page holds now, use `flush`:
|
|
785
1132
|
|
|
1133
|
+
- `clay.save.soon()` saves a discrete action (a moved card, a toggled checkbox) within about 250 ms instead of after the autosave wait. Calls within a second share one save, and each resolves with that save's result. A save you asked for with Cmd+S or `soon()` that an incoming merge cancels mid preparation is captured again once, after the merge settles (150 ms of quiet, at most 2 s); an autosave cancelled the same way is not retried. Leave typing on autosave.
|
|
786
1134
|
- `clay.save.flush({ keepalive, timeoutMs })` resolves `{ state: "view" | "clean" | "saved", etag }` only when the host holds the current bytes. It waits out the settle window and any in-flight save, and retries while the page keeps changing. It rejects with `err.state` of `failed`, `conflict`, or `blocked`.
|
|
787
1135
|
- `clay.save.force()` saves without the dirty check, writing `no-dirty` and `no-watch` edits. Template content edits need no force: `save()` and the close warning see them, and only autosave does not.
|
|
788
1136
|
- `clay.save.overwrite()` re-reads the host's current version stamp and force-saves over it. This is "keep mine" after a conflict.
|
|
@@ -791,7 +1139,42 @@ To batch saves in part of an autosaving page, wrap that region in `clay="no-trig
|
|
|
791
1139
|
- `clay.registerUnsavedState({ id, isPending })` registers work held outside the DOM (such as an unapplied form) so the close warning and `flush` respect it. Call the returned `changed()` when it changes and `dispose()` when done.
|
|
792
1140
|
- `clay.markDirty()` tells the dirty gate about an edit no mutation shows, for an editor whose surface is `editor-ui` and which writes the record itself.
|
|
793
1141
|
|
|
794
|
-
### 5.
|
|
1142
|
+
### 5.8 Size limits
|
|
1143
|
+
|
|
1144
|
+
The limit comes from the host's `/_/meta` answer, `document.maxBytes`: the most bytes of uncompressed UTF-8 HTML one save body may have, inclusive. It counts only as a positive safe integer. A missing, zero, negative, fractional or non-numeric value means the limit is unknown, and `clay.save.limits()` resolves `{ maxBytes: null }`. Draft mode answers the same without asking the host at all. See [Hosts, assets and privacy](https://clayjs.com/docs/guide/hosts/#what-each-host-gives).
|
|
1145
|
+
|
|
1146
|
+
A save is measured on the exact UTF-8 bytes it is about to send, after hooks, transforms and cleanup, and refused before anything is sent when it is over. The result is `{ ok: false, code: "save-too-large", sent: false, size }`, where `size` holds `{ bytes, maxBytes, state: "over" }`. Nothing is written: no accepted listener runs, no baseline moves and no recovery copy is cleared. A host that answers 413 maps to the same code.
|
|
1147
|
+
|
|
1148
|
+
The host stays authoritative. A client that predates this check, a first save sent before discovery answers, and a limit that changed after it was read are all decided by the host's own refusal, which leaves the writing recoverable.
|
|
1149
|
+
|
|
1150
|
+
`clay.save.inspect(html)` measures bytes that are not this page, such as a file you are about to import, without capturing, sending or moving a baseline:
|
|
1151
|
+
|
|
1152
|
+
```js
|
|
1153
|
+
const size = await clay.save.inspect(candidateHTML);
|
|
1154
|
+
if (size.state === "over") return say("That is larger than this host can save.");
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
A real save that measures `near` or `over` fires `clay:save-size` and loads a small notice into the editor UI. It reports the last measurement, never a live size:
|
|
1158
|
+
|
|
1159
|
+
- Near: `This file is getting large. At the last save attempt: 1.6 MB of 2.0 MB. Download a copy, then reduce its size; saving still works.`
|
|
1160
|
+
- Over: `This host can't save a file this size. At the last save attempt: 2.1 MB of 2.0 MB. Download a copy to keep your work.`
|
|
1161
|
+
|
|
1162
|
+
**Download my copy** appears only when the measurement is over. The notice is hidden by a later save measurement below the threshold, and when the page leaves edit mode. It is loaded only when a measurement is near or over, so most pages never fetch it.
|
|
1163
|
+
|
|
1164
|
+
To make a copy of the page without some of its records, remove them from the clone with `clay.getHTML({ transform })`:
|
|
1165
|
+
|
|
1166
|
+
```js
|
|
1167
|
+
const html = clay.getHTML({
|
|
1168
|
+
transform: (root) => {
|
|
1169
|
+
for (const el of root.querySelectorAll("#entries > article.entry[data-drop]")) el.remove();
|
|
1170
|
+
},
|
|
1171
|
+
flushUndo: false
|
|
1172
|
+
});
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
The transform must be synchronous and its return value is ignored. The live page, the baselines and the network are untouched. See [Importing and exporting records](https://clayjs.com/docs/guide/architecture/#importing-and-exporting-records).
|
|
1176
|
+
|
|
1177
|
+
### 5.9 Conflicts on save (412)
|
|
795
1178
|
|
|
796
1179
|
On hosts that announce `conditional` (all three hosts in [What the host does](https://clayjs.com/docs/guide/model/#what-the-host-does)), every save sends `If-Match` with the version the tab last saw. If the file changed elsewhere and live sync has not merged that change yet, the host answers 412 and writes nothing.
|
|
797
1180
|
|
|
@@ -801,7 +1184,7 @@ On hosts that announce `conditional` (all three hosts in [What the host does](ht
|
|
|
801
1184
|
- The first save after load can go out without `If-Match` on a slow host because version discovery is not awaited. That save is last-write-wins.
|
|
802
1185
|
- If you hide the built-in notice, you must still offer overwrite and reload.
|
|
803
1186
|
|
|
804
|
-
### 5.
|
|
1187
|
+
### 5.10 Hooks
|
|
805
1188
|
|
|
806
1189
|
Hooks shape the saved bytes without touching the live page. Except for `onaftersave`, they run on a clone.
|
|
807
1190
|
|
|
@@ -841,7 +1224,7 @@ clay.onSnapshot?.((root) => {
|
|
|
841
1224
|
|
|
842
1225
|
Only strip `draggable="false"` if your own markup never authors it.
|
|
843
1226
|
|
|
844
|
-
### 5.
|
|
1227
|
+
### 5.11 Save status
|
|
845
1228
|
|
|
846
1229
|
ClayJS reflects save state on `<html>`, so you can style a status indicator with CSS alone:
|
|
847
1230
|
|
|
@@ -860,7 +1243,7 @@ html[unsaved="true"]:not([savestatus="error"]):not([savestatus="offline"]) .chip
|
|
|
860
1243
|
|
|
861
1244
|
In draft mode, nothing is saved on purpose, and `html[draftmode="true"]` indicates why the page is not marked saved.
|
|
862
1245
|
|
|
863
|
-
Put `#status` in chrome, or load the `indicator` plugin for a ready-made chip. With `clay-ui.js` loaded, save errors and offline states become persistent toasts. Without the `indicator` plugin, `clay-ui.js` also toasts "Saved" after every save; with it, set `clay.saveToast = true` to get those toasts too.
|
|
1246
|
+
Put `#status` in chrome, or load the `indicator` plugin for a ready-made chip. The chip's drawing arrives on demand, at the first input or the first save state, so it costs nothing at edit ready. With `clay-ui.js` loaded, save errors and offline states become persistent toasts. Without the `indicator` plugin, `clay-ui.js` also toasts "Saved" after every save; with it, set `clay.saveToast = true` to get those toasts too.
|
|
864
1247
|
|
|
865
1248
|
Events, all on `document`:
|
|
866
1249
|
|
|
@@ -883,7 +1266,7 @@ Events, all on `document`:
|
|
|
883
1266
|
|
|
884
1267
|
A save writes only the document's own bytes, so a downloaded HTML file links to its images rather than embedding them. When the file opens, the browser fetches uploaded images from the host's URLs. Opening a copy where those URLs do not resolve shows broken images.
|
|
885
1268
|
|
|
886
|
-
### 5.
|
|
1269
|
+
### 5.12 Recovery copies
|
|
887
1270
|
|
|
888
1271
|
`clay.recovery` keeps a local copy of edits the host has not accepted. It is not a save, and ClayJS never renders a page directly from it. After a reload, ClayJS compares the stored copies with the loaded file and asks before restoring one. A document is covered only for the channels its adapters register, so a page that registers no channel keeps no local copy.
|
|
889
1272
|
|
|
@@ -960,7 +1343,7 @@ When an incoming frame (B) meets unsaved edits in this tab (A), ClayJS merges th
|
|
|
960
1343
|
|
|
961
1344
|
More engine rules:
|
|
962
1345
|
|
|
963
|
-
- Text merges word by word inside a block, and formatting merges per character. Inline `<style>`
|
|
1346
|
+
- Text merges word by word inside a block, and formatting merges per character. Inline `<style>` text merges word by word, while a `<textarea>` merges as one value. Scripts and comments merge as whole values.
|
|
964
1347
|
- Matched live nodes are moved, not recreated, so listeners, focus, caret, and iframes survive. An element that matches nothing (re-keyed, or changed beyond recognition among similar siblings) is removed and inserted fresh, and listeners attached directly to it are gone. **Use delegated listeners** on `document` or a stable root.
|
|
965
1348
|
- A new or changed inline `<script>` in the frame runs once after the merge. Guard one-time setup with `clay.start` ([Starting the app once](https://clayjs.com/docs/guide/architecture/#starting-the-app-once)).
|
|
966
1349
|
- Window scroll is restored. Inner scroll containers are not, except the focused element's own.
|
|
@@ -1061,9 +1444,98 @@ The built-in notice reads "Another edit replaced N changes" and offers **Revert
|
|
|
1061
1444
|
|
|
1062
1445
|
### 6.7 Presence
|
|
1063
1446
|
|
|
1064
|
-
|
|
1447
|
+
`?plugins=sync` loads presence. `clay.presence` is the facade (`clayjs/src/sync/presence-store.js`), installed on `window.clay.presence` when the store loads. It has three page-facing methods:
|
|
1448
|
+
|
|
1449
|
+
```js
|
|
1450
|
+
const snapshot = clay.presence.getSnapshot(); // the same object until something changes
|
|
1451
|
+
const stop = clay.presence.subscribe(render); // the current snapshot at once, then every change
|
|
1452
|
+
clay.presence.setTarget({ kind: "record", id: "card-1", field: "title" });
|
|
1453
|
+
```
|
|
1454
|
+
|
|
1455
|
+
`subscribe(listener)` returns an unsubscribe function. It emits the current snapshot at once, even before the host answers, so a page never waits for a first frame. `setTarget(null)` clears this tab's target and publishes nobody anywhere; it does not fall back to the generic editing marker.
|
|
1456
|
+
|
|
1457
|
+
**The snapshot.** `{ supported, recordAwareness, pageAwareness, people, anonymous }`. Each person is `{ id, name, you, canEdit, editing, sessions }`, and each session is `{ id, you, n, editing }`:
|
|
1458
|
+
|
|
1459
|
+
```js
|
|
1460
|
+
{
|
|
1461
|
+
supported: true, recordAwareness: true, pageAwareness: true,
|
|
1462
|
+
people: [{
|
|
1463
|
+
id: "p_ada", name: "Ada Lovelace", you: false, canEdit: true,
|
|
1464
|
+
editing: { kind: "record", id: "card-1", field: "title" },
|
|
1465
|
+
sessions: [{ id: "s1", you: false, n: 12, editing: { kind: "page", v: 1, epoch, generation, selection } }],
|
|
1466
|
+
}],
|
|
1467
|
+
anonymous: 0,
|
|
1468
|
+
}
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
Before discovery answers, the snapshot is unsupported with no people. The host decides who appears: each recipient gets a roster filtered by its own access, and nothing in the page can widen it (`src/sync/live-sync.js`, the `presence` event).
|
|
1065
1472
|
|
|
1066
|
-
|
|
1473
|
+
**Targets.** A target is `null`, a legacy dotted path string, a record target `{ kind: "record", id, field }` with `field` optional, or a page target `{ kind: "page", v: 1, epoch, generation, selection }`. A page selection is `text` (`anchor` and `head`, each `{ block, rel }`), `blocks` (`{ ids }`), or `cell` (`{ block, row, col }`).
|
|
1474
|
+
|
|
1475
|
+
The bounds come from `src/sync/presence-target.js`:
|
|
1476
|
+
|
|
1477
|
+
- A record id of 1 to 128 printable ASCII characters.
|
|
1478
|
+
- A field of `title`, `description`, `labels`, `members`, `dates`, `checklist`, `comment`, or `attachments`.
|
|
1479
|
+
- 8 KiB per target.
|
|
1480
|
+
- 256 bytes per page id.
|
|
1481
|
+
- 1 KiB per decoded relative position.
|
|
1482
|
+
- At most 128 block ids.
|
|
1483
|
+
|
|
1484
|
+
An invalid target is refused, never repaired. Updates are coalesced to one every 250 ms, a stable target renews every 5 seconds, and it expires 15 seconds after the host receives it.
|
|
1485
|
+
|
|
1486
|
+
**A record target from a focused control.** Publish a target only while the control that writes the record has focus, and publish `null` the moment it stops: on blur, when the open record changes, when the page is hidden, when the record goes to Trash, and when the tab leaves edit mode or enters draft mode. After a blur, check again on the next microtask, so focus moving inside the same control does not flash `null`. A record with one writing control sends the fieldless shape, `{ kind: "record", id }`, where `id` is the record's `data-id`. A target never carries a title, text or date. The Journal template does this in `RecordPresence` (`templates/00-journal/journal.html`):
|
|
1487
|
+
|
|
1488
|
+
```js
|
|
1489
|
+
function reconcile() {
|
|
1490
|
+
const id = capable && clay.isEditMode && !clay.isDraftMode && !document.hidden && writerHasFocus()
|
|
1491
|
+
? openEntryId() : null;
|
|
1492
|
+
if (id === sentId) return;
|
|
1493
|
+
sentId = id;
|
|
1494
|
+
clay.presence.setTarget(id ? { kind: "record", id } : null);
|
|
1495
|
+
}
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
`capable` is the snapshot's `recordAwareness`, so a host without `record-awareness-v1` is sent nothing. Renewal and expiry belong to ClayJS: a page sets a target once and keeps no timer of its own.
|
|
1499
|
+
|
|
1500
|
+
**Showing who is on a record.** Build a map from record id to people out of each snapshot. Skip `you: true`, keep only targets with `kind: "record"` whose id is a live record on this page, and count a person once however many tabs they have. Write names with `textContent`. A slot a script creates carries `clay="no-save no-watch no-snapshot"`. A slot inside a repeated row's `<template>` carries only `clay="no-save no-watch"`: a save removes `no-snapshot` nodes from template content too, which would delete the slot from the file, and the rows sapjs renders from the template are marked `editor-ui`, which already keeps them out of snapshots. Repaint only when the names change, so a renewal announces nothing. A badge is advisory: it never blocks editing and never reaches a record.
|
|
1501
|
+
|
|
1502
|
+
**Hosts.** The client picks exactly one transport from `/_/meta` (`connect` in `presence-store.js`):
|
|
1503
|
+
|
|
1504
|
+
- hyperclay.com relays presence through the host. The target goes to `POST /_/presence`, and the roster comes back as a named `presence` event on the document's stream, with one person per account and one session per tab.
|
|
1505
|
+
- HTML Clay announces `awareness: { transport: "broadcast", scope }`. Its presence travels between tabs of the same browser on a channel named for the document scope (`src/sync/local-presence.js`). Each tab is a fresh random session, not an account, shown under the label "This computer". Two documents that share a file name never meet.
|
|
1506
|
+
- Hyperclay Local likewise shows tabs on this computer, not authenticated people.
|
|
1507
|
+
|
|
1508
|
+
In view mode the reader draws the roster too. It takes the roster from the stream it already follows, as a named `presence` event, so it opens no second connection. The roster UI is a first-use download that comes with the first presence frame, and a reader that receives no frame downloads none of it. A reader publishes no target and joins no writable room, because its lease has no write half: a target a page sets in view mode sends nothing. Its roster goes away when its stream closes.
|
|
1509
|
+
|
|
1510
|
+
Each part needs its own extension: `presence` for the roster, `presence-editing` for the generic editing marker, `record-awareness-v1` for record targets, and `page-awareness-v1` for remote carets.
|
|
1511
|
+
|
|
1512
|
+
**Runtime only.** The roster UI is marked `clay="no-save no-watch no-snapshot"` (`RUNTIME_ONLY` in `src/ui/bevel-controls.js`, applied in `src/sync/presence.js`). It never reaches a save, a snapshot, or the page's own change detection, and a roster change is not an edit. Remote carets are editor decorations, not document content ([Remote carets](https://clayjs.com/docs/plugins/page/#remote-carets)). All three tokens are needed: `no-save` alone only keeps a name out of the file, and the snapshot clone that peers receive is emitted before the save-only regions are stripped, so `no-snapshot` is what keeps a name out of that clone, while `no-watch` is what keeps a roster change from counting as an edit (`src/sync/presence.js:15-28`).
|
|
1513
|
+
|
|
1514
|
+
Presence is not for app data. For state that must be shared and is not presence, use a live store ([Live data: `clay="live"`](https://clayjs.com/docs/guide/live-sync/#live-data-clay-live)).
|
|
1515
|
+
|
|
1516
|
+
### 6.8 The page format floor
|
|
1517
|
+
|
|
1518
|
+
A document records the lowest page format a writer must understand, in document metadata outside the page value:
|
|
1519
|
+
|
|
1520
|
+
```html
|
|
1521
|
+
<meta name="clay-page-format" content="1">
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
No marker means floor 0, and a floor only ever rises: a later save cannot remove the marker or lower it. The value is protocol capability, not permission. The host still applies every access check it applied before, and the marker cannot be used to discover a private document.
|
|
1525
|
+
|
|
1526
|
+
**Every page write declares its capability.** A save and a live write carry `X-Clay-Page-Format: 1`, the format the runtime knows rather than the format this document uses, so even an ordinary document's save declares it.
|
|
1527
|
+
|
|
1528
|
+
**A host refuses a write below the floor.** Nothing is mutated. The answer is HTTP `409` with a body carrying the code `page-format-upgrade-required`, the same string as its `reason` on the save lanes, and `minimum`, the floor the document requires. That covers the ordinary save, the live data lanes, a checkpoint, a version restore and the relay, on all three hosts, and a refused live request ends that tab's stream so it receives no newer content. The stored file, its version history and its page state stay as they were, and the refused tab keeps its own unsaved work.
|
|
1529
|
+
|
|
1530
|
+
**A writer raises the floor on its first new format, through one real save.** The gate reads the floor the document already has, asks the host, and adds the marker to an otherwise equivalent capture. Activation is idempotent. It is refused while any admitted live page editor declares a capability below the floor it would set, and that check happens under the host's per-document write lock, so an editor that registers between the check and the save is refused rather than missed. A refused activation leaves ordinary writing available and every unsaved store as it was, and says **This document uses a newer page format; update the editor in the other tabs to keep writing**. The command that asked applies only if the same editor installation and document revision still exist once activation succeeds; otherwise the answer is retryable and the content is untouched.
|
|
1531
|
+
|
|
1532
|
+
**A host announces `page-format-v1` only when it can hold the floor**, meaning its enforcement and its tolerant checkpoint renderer are installed. On a host that does not announce it, a new format command is refused as `page-format-unsupported` and writes nothing.
|
|
1533
|
+
|
|
1534
|
+
**Who reads what.** ClayJS 1.16.0 and later keep floor 1 content, so those tabs are the cohort that can stay open while a floor 1 document is written. The published 1.15.x tabs cannot be changed: once a document's floor rises they are refused writes with `page-format-upgrade-required` instead of saving a version that would drop the new content, and their own unsaved work stays in the tab. A floor 0 document keeps working on all of them.
|
|
1535
|
+
|
|
1536
|
+
Opening a document that is already at floor 1 asks for nothing: only a command that writes a newer format raises a floor, and a document that never uses one never records one.
|
|
1537
|
+
|
|
1538
|
+
### 6.9 Agents
|
|
1067
1539
|
|
|
1068
1540
|
An agent edits a malleable file the way a person in another tab does:
|
|
1069
1541
|
|
|
@@ -1072,7 +1544,7 @@ An agent edits a malleable file the way a person in another tab does:
|
|
|
1072
1544
|
- **`ai-edit`** (`?plugins=ai-edit`): select text, press Cmd+J, and describe the change. The rewrite is previewed in place; Keep saves it, Revert discards it. Available where the host offers the `ai-edit` helper: HTML Clay, Hyperclay Local, and hyperclay.com (for the owner and team editors on a paid plan, billed to the document's AI credits).
|
|
1073
1545
|
- **The data API**: a host that announces `data-read`/`data-write` serves `/_/api/<file>`, a JSON read and write interface over the file defined by a rules tag ([Import and export](https://clayjs.com/docs/guide/architecture/#import-and-export)). `clay.readData()` and `clay.writeData()` call it from the page; `writeData` is for agents and other processes, and refuses while the page has unsaved edits.
|
|
1074
1546
|
|
|
1075
|
-
### 6.
|
|
1547
|
+
### 6.10 Live data: `clay="live"`
|
|
1076
1548
|
|
|
1077
1549
|
Everything above moves at save speed: a peer sees your change after your next save. A live store is for state that changes faster than that, such as a game board, a score, a cursor, or a timer. It is one JSON script that ClayJS holds as an object in memory. Page code changes it through ops, every tab of the document sees each change at once, and ClayJS writes the value back into the file at checkpoints.
|
|
1078
1550
|
|
|
@@ -1235,6 +1707,7 @@ Traps:
|
|
|
1235
1707
|
| `> ` | toggle |
|
|
1236
1708
|
| ` ``` ` | code block, when the whole paragraph is the fence |
|
|
1237
1709
|
| `---` | divider, when the whole paragraph is the dashes |
|
|
1710
|
+
| `" ` | quote, when the document's format gate allows it; otherwise the space is typed as itself |
|
|
1238
1711
|
|
|
1239
1712
|
| Typed around text | Becomes |
|
|
1240
1713
|
|---|---|
|
|
@@ -1248,7 +1721,7 @@ An opening delimiter must follow the start of the line, whitespace, or opening p
|
|
|
1248
1721
|
|
|
1249
1722
|
**Backspace immediately after a shortcut restores the typed characters**, delimiters included, through input-rule undo. It is a single history step, does not drop a peer's concurrent edit, and reapplies on redo. After another keystroke, the shortcut is plain text and Backspace deletes one character as usual.
|
|
1250
1723
|
|
|
1251
|
-
None of this loads for a reader. The input rules, menus, and title adapter are editor code, and a Document
|
|
1724
|
+
None of this loads for a reader. The input rules, menus, and title adapter are editor code, and a Document that never enters edit fetches none of them. A live entry fetches them while the tab still shows view ([switching without a reload](https://clayjs.com/docs/guide/edit-mode/#switching-without-a-reload-clay-mode-switch-live)).
|
|
1252
1725
|
|
|
1253
1726
|
### 7.2 sortable: drag to reorder
|
|
1254
1727
|
|
|
@@ -1273,6 +1746,7 @@ None of this loads for a reader. The input rules, menus, and title adapter are e
|
|
|
1273
1746
|
- `onsorted="code"` runs on drop with `this` and `evt`.
|
|
1274
1747
|
- Containers added later bind after about 200 ms.
|
|
1275
1748
|
- `window.Sortable` is SortableJS 1.15.6. `Sortable.get(el).option(name, value)` changes an instance.
|
|
1749
|
+
- `clay.sortable.move(item, toContainer, beforeItem = null)` makes the reorder a drag makes, without dragging and without waiting for SortableJS. It fires the same `clay:sorted`, records the same undo step, and answers `{ ok: true, moved }` or `{ ok: false, code }` with `not-editable`, `detached`, `item`, `group` or `anchor`. Resolve `beforeItem` by `data-id` at the moment of the move, and use it for keyboard and touch moves.
|
|
1276
1750
|
|
|
1277
1751
|
Traps:
|
|
1278
1752
|
|
|
@@ -1297,12 +1771,12 @@ Traps:
|
|
|
1297
1771
|
|
|
1298
1772
|
- `commit` must be synchronous. For async work (an upload or fetch), `await` it first, then make every DOM write in one synchronous `commit`. Pausing undo across an `await` loses the mutations made before the `await`.
|
|
1299
1773
|
- Not recorded: `editor-ui`, `no-undo`, `no-watch` regions, `richclay` regions, and every change a merge brings in.
|
|
1300
|
-
-
|
|
1774
|
+
- Every step is checked against the document as it stands before either stack moves, so a step applies whole or changes nothing. Undoing "Add card" checks that the card is still the node the step added and still holds what the step recorded; if a peer edited it, the undo is refused with `content-changed` and the card stays as the peer left it. A moved neighbour refuses with `structure-changed`, and a reused `data-id` with `duplicate-id`. A refused step stays on top and fires `clay:undo-refused`; replay it once the conflict is gone, or drop it with `clay.undo.discard(entryId)`. [undo](https://clayjs.com/docs/plugins/undo/) lists every code.
|
|
1301
1775
|
- Also exposed: `clay.undo.on("undo" | "redo" | "commit" | "clear", fn)`, `canUndo`, `canRedo`, and `history`.
|
|
1302
1776
|
|
|
1303
1777
|
Traps:
|
|
1304
1778
|
|
|
1305
|
-
- **It
|
|
1779
|
+
- **It leaves Cmd+Z to text fields.** Inside an `input`, `textarea`, `select` or `contenteditable` subtree, the browser's own undo runs. A plain control's typed value is not content until `persist` or a command writes it, so page undo has nothing to replay for it.
|
|
1306
1780
|
- History is per tab, capped at 100, and not saved.
|
|
1307
1781
|
- Undoing a delete re-inserts the original node even if a peer re-created an equivalent. Dedupe by `data-id` on `clay:sync-applied` if needed.
|
|
1308
1782
|
|
|
@@ -1335,7 +1809,7 @@ async function imageSource(file) {
|
|
|
1335
1809
|
- Events: `clay:upload-start`, `clay:upload-progress`, `clay:upload-done`, and `clay:upload-error`.
|
|
1336
1810
|
- Host limits: HTML Clay and Hyperclay Local allow 25 MB per file; hyperclay.com allows 10, 20, or 100 MB by plan.
|
|
1337
1811
|
- The URL is a host path: `/_/uploads/assets-<document>/<name>`. Write it exactly as returned. Every address of the host serves it, so it survives renaming or moving the document. Never rebuild it from parts.
|
|
1338
|
-
-
|
|
1812
|
+
- On hyperclay.com, an upload made from a document follows that document's read access once `document.upload.access` is `document`; before the account's protection migration it stays reachable by URL. To refuse private media on a host that cannot protect it, start the upload with `clay.upload.start(file, { requiredAccess: "document-or-local" })` ([Privacy](https://clayjs.com/docs/guide/hosts/#privacy)).
|
|
1339
1813
|
|
|
1340
1814
|
`clay.quickcrop(file, { aspect, maxWidth, maxHeight, type, quality })` opens a crop dialog and resolves `{ blob, dataURL, width, height }`, or `null` on cancel. It does not upload; wrap the blob as a `File` and pass it to `clay.upload`. Only one cropper opens at a time, and it sets `body.style.overflow = "hidden"` while open, which a save at that moment writes into the file.
|
|
1341
1815
|
|
|
@@ -1381,7 +1855,7 @@ For fixed content, such as code samples in a docs page, mark the block `clay="fr
|
|
|
1381
1855
|
|
|
1382
1856
|
### 7.9 OverType: a markdown editor
|
|
1383
1857
|
|
|
1384
|
-
For a notes or journal app where Markdown is the format, OverType
|
|
1858
|
+
For a notes or journal app where Markdown is the format, OverType is a textarea under a styled preview. Load it with `await clay.assets.load("overtype")`, which takes it from the release that served `clay.js` ([Libraries on demand](https://clayjs.com/docs/guide/loading/#libraries-on-demand)). **The textarea is not the record.** Keep each entry's text in a text element of the record, put the editor in a `clay="freeze"` region outside the saved records, and bind it to the open record with `clay.bindText` (`?plugins=bind-text`):
|
|
1385
1859
|
|
|
1386
1860
|
```html
|
|
1387
1861
|
<article data-id="a"><pre><code class="entry-body"># Morning
|
|
@@ -1475,7 +1949,6 @@ Rules for sapjs in a malleable file:
|
|
|
1475
1949
|
- **Give records a `data-id`**, and `data-id=""` on blueprints so `$add` and `trigger-add` mint fresh IDs. Live sync pairs elements by it.
|
|
1476
1950
|
- **Put `persist` on bound inputs inside records** so typed values reach the attributes a save captures.
|
|
1477
1951
|
- **Undo, redo, and sync merges restore `persist` controls' live values** from their attributes, so sapjs reads what the file says. Controls without `persist` are not restored; give them `persist` or re-sync them yourself.
|
|
1478
|
-
- **`viewmode:` attributes log `W03`** (unknown attribute) in sapjs, such as [Mode attributes: one file for editors and viewers](https://clayjs.com/docs/guide/edit-mode/#mode-attributes-one-file-for-editors-and-viewers)'s `viewmode:readonly`. The warning is harmless, though it makes a "no warnings" check fail.
|
|
1479
1952
|
- **Hide blueprints in your own CSS** (`[item][template] { display: none }`); sapjs's own rule arrives only with the script. Sapjs also adds `[hidden] { display: none !important }` page-wide.
|
|
1480
1953
|
- A pass is batched: after a write, call `Sap.refresh()` before reading painted output. Expressions compile with `new Function`, so a Content Security Policy without `unsafe-eval` breaks them.
|
|
1481
1954
|
|
|
@@ -1491,7 +1964,7 @@ Rules for sapjs in a malleable file:
|
|
|
1491
1964
|
|
|
1492
1965
|
It fits fixed-layout content sites such as landing pages, menus, pricing, FAQs, and team pages, where a non-developer changes copy and images. It does not fit apps where editing is the product: its form is a separate surface from the content, new list items clone the first item's attributes (including its state), and rich text inside lists is flattened in the sidebar. A file can use both if the CMS owns the header copy, the app owns `<main>`, and the rules never reach into the app's region. Give every list that can become empty a `cms-template` seed hidden by CSS, not by the `hidden` attribute (which is copied into every new item). Add `sortable` to the plugin list for drag in the sidebar.
|
|
1493
1966
|
|
|
1494
|
-
The rules tag can declare which editor opens. `data-cms-view="inline"` edits text in place and opens small popovers for everything else, while no attribute (or `"sidebar"`) opens the form in a side panel. The "Edit content" button opens whichever the page declares.
|
|
1967
|
+
The rules tag can declare which editor opens. `data-cms-view="inline"` edits text in place and opens small popovers for everything else, while no attribute (or `"sidebar"`) opens the form in a side panel. The "Edit content" button opens whichever the page declares. The CMS edits only in edit mode: while the page is in view, `clay.cms.open()` answers `{ ok: false, code: "not-allowed" }`, so a button of your own calls `clay.setMode("edit")` first.
|
|
1495
1968
|
|
|
1496
1969
|
The CMS also edits how a page looks. A rule ending in `@--name` reads and writes one CSS custom property in an element's `style` attribute. For example, `"accent": "html@--color-accent"` with `a { color: var(--color-accent) }` in your CSS gives the owner a color picker. Any `--color-*` name gets a color picker, any `--font-*` name gets a list of 40 Google Fonts, and the CMS keeps one `<link data-hcms-fonts>` loading the fonts in use. For a layout choice, put `data-hcms-component:data-layout="variant"` and `data-hcms-options:data-layout="1:cols-1 2:cols-2 3:cols-3"` on a section and key your grid CSS on `[data-layout]`: the owner gets icon tiles, and the choice writes only the attribute. Style fields on `html`, `body`, or `:root` gather in a "Page style" group at the top of the form.
|
|
1497
1970
|
|
|
@@ -1638,7 +2111,7 @@ Open on a click that is not on an editable field, handle, input, or link. Close
|
|
|
1638
2111
|
|
|
1639
2112
|
When a dialog must aggregate several records, render it into `editor-ui` and write each change back to its record by id. Call `clay.markDirty()` per edit if the record is not written immediately, and re-render it on `clay:sync-applied` without disturbing the focused field.
|
|
1640
2113
|
|
|
1641
|
-
**Half-typed text in a detail field outlives the view.** A comment box or description being edited sits in chrome (`clay="editor-ui"` or `clay="freeze"`) and carries `unsent="<name>"
|
|
2114
|
+
**Half-typed text in a detail field outlives the view.** A comment box or description being edited sits in chrome (`clay="editor-ui"` or `clay="freeze"`) and carries `unsent="<name>"`, with the `unsent` plugin loaded (`?plugins=unsent`). ClayJS keeps its value per record and name in this tab, outside the file. The record is named by the nearest `unsent-key` attribute, and by the nearest `[data-id]` when there is none, so a composer for a record that does not exist yet can name its destination (`unsent-key="capture:board"`). While a draft is held, leaving edit is refused with `pending-input`. Close the detail, take a sync redraw, or reload, and the text is back with the close warning on until it is sent.
|
|
1642
2115
|
|
|
1643
2116
|
```html
|
|
1644
2117
|
<div class="detail" clay="editor-ui">
|
|
@@ -1757,7 +2230,27 @@ Sap.batch("Import CSV", () => {
|
|
|
1757
2230
|
|
|
1758
2231
|
A rules tag with `data-rules-name="api"` gives agents and the host's data API the same shape your export uses.
|
|
1759
2232
|
|
|
1760
|
-
### 8.10
|
|
2233
|
+
### 8.10 Importing and exporting records
|
|
2234
|
+
|
|
2235
|
+
The Journal keeps every record in one container, so a backup, an import, a selected export and a print are four ways of copying or writing that same set. Each one is a named function in `journal.html`.
|
|
2236
|
+
|
|
2237
|
+
- **Back up the exact fields, blanks and Trash included.** `Backup.records` reads every record with all of its fields, and `Backup.envelope` wraps them as `{ format, version, exportedAt, entries }`. An empty body and a trashed record are still records, so a backup restores what the file holds rather than what the view shows. Markdown is not an exact data backup.
|
|
2238
|
+
- **Parse, preview and revalidate before committing.** `Backup.parse` reads a file inertly and refuses unknown fields, missing fields, duplicate ids and bad dates with a code. `Backup.plan` sorts the rows into fresh, identical and conflicting ones, `planRestore` offers `keepBoth` for a row that changed here, and `Backup.verify` reads a candidate back field for field. A refusal at any of those points changes nothing.
|
|
2239
|
+
- **Commit once, synchronously.** `confirmImport` re-reads the limit, compares the current capture with the preview's baseline, replans and re-verifies, then appends the prepared records in one synchronous step followed by one `clay.save.soon()`. There is no await between the last check and that step, so a refusal leaves the page, the route, the selection and the dirty state as they were.
|
|
2240
|
+
- **Measure the candidate against the host's limit.** `candidateWith` builds the bytes a save would send with the new records already in place, and `measureHTML` asks `clay.save.inspect` about them. An over-limit import is refused before a single record enters the document.
|
|
2241
|
+
- **Export only the bytes selected.** `selectedCopy` removes every unselected article from the copy inside `clay.getHTML({ transform })`, so the records left out are gone from the file rather than hidden by CSS. `verifySelectedCopy` reads the result back and checks the exact id set.
|
|
2242
|
+
- **Move a year out only after the copy you kept checks out.** `YearArchive.archive` downloads the year as a `.htmlclay` copy and an exact JSON backup and removes nothing; starting a download proves nothing about a second copy. **Finish moving this year** asks for the file the person kept, reads it inertly, and requires exactly the reviewed records, field for field. A different file, a changed record or a record that joined the year since the review refuses with nothing removed. Before removing, `ArchiveMoves.stage` keeps the reviewed records in the `journal.archive-moves` recovery channel and awaits its durable receipt; a browser that cannot keep it refuses and offers the copy only. The removal itself is one synchronous step after a last re-check, followed by one `clay.save.soon()`. The move is cleared only by a saved file whose bytes lack every reviewed id, so a save that never lands leaves **Archive move awaiting save** after a reload. A year whose entries link pictures kept in the journal's own files can be copied but not moved, because a copy cannot carry them yet.
|
|
2243
|
+
- **Print the canonical records.** `printSelected` flags the chosen live articles with the ephemeral `print-pick` word and print CSS narrows the printout to them, so a printout is the records themselves, not the filtered list or an open editor. The flag is never saved, and another tab's sync never carries it.
|
|
2244
|
+
|
|
2245
|
+
```js
|
|
2246
|
+
const attempt = candidateWith(added);
|
|
2247
|
+
if (!Backup.verify(attempt.html, attempt.expected).ok) return;
|
|
2248
|
+
const size = await measureHTML(attempt.html, limit);
|
|
2249
|
+
if (size.state === "over") return;
|
|
2250
|
+
store.append(fragment);
|
|
2251
|
+
```
|
|
2252
|
+
|
|
2253
|
+
### 8.11 Starting the app once
|
|
1761
2254
|
|
|
1762
2255
|
Start the app with `clay.start` so initialization runs once per page. A live-sync merge can re-insert an inline script and run it again, which doubles every listener and every chrome element. `clay.start` runs its function once after `clay.ready`, and a repeat call does nothing:
|
|
1763
2256
|
|
|
@@ -1769,7 +2262,7 @@ clay.start(() => {
|
|
|
1769
2262
|
|
|
1770
2263
|
A page with more than one app script names each one so they do not share a slot: `clay.start("board", fn)`.
|
|
1771
2264
|
|
|
1772
|
-
### 8.
|
|
2265
|
+
### 8.12 Document size and record limits
|
|
1773
2266
|
|
|
1774
2267
|
Every save captures and serializes the whole document, every change check captures it again, and every peer merges the whole document per accepted save. Measured in jsdom and scaled to a browser (estimates):
|
|
1775
2268
|
|
|
@@ -1780,9 +2273,11 @@ Every save captures and serializes the whole document, every change check captur
|
|
|
1780
2273
|
| 3,000 | 1.3 MB | ~120 ms | ~300 ms | ~190 ms |
|
|
1781
2274
|
| 6,000 | 2.6 MB | ~250 ms | ~650 ms | ~400 ms |
|
|
1782
2275
|
|
|
2276
|
+
Typing also costs one Sap pass, and a pass visits every record. Sap skips rereading unchanged record values, but it still builds fresh objects for every record, so the pass grows with the record count: at 5,000 records in jsdom, a body edit's pass takes about 27 ms ([Cost at size](https://clayjs.com/docs/plugins/sap/#cost-at-size)).
|
|
2277
|
+
|
|
1783
2278
|
Up to about 1,000 records feels instant. From 1,000 to 3,000 there is a visible hitch at each save. Past about 3,000 records or 1.5 MB, autosave stalls typing and peers lag. Hosts cap a save at 5 MB (hyperclay.com), 20 MB (Hyperclay Local), and 50 MB (HTML Clay). Design for archiving: one file per year, a hidden archive section that can move to another file, or one file per project.
|
|
1784
2279
|
|
|
1785
|
-
### 8.
|
|
2280
|
+
### 8.13 Styles a script writes
|
|
1786
2281
|
|
|
1787
2282
|
Use `clay.style` when a script owns CSS that should stay in the saved document. The saved style paints the next load before the script runs. When the script runs, it refreshes that same tag with its current CSS:
|
|
1788
2283
|
|
|
@@ -1815,16 +2310,20 @@ Use `clay="editor-ui"` instead for CSS that viewers must not receive or that rep
|
|
|
1815
2310
|
| Who edits | the local user | the local user; teammates through device sync | owner, team editors, edit-link holders |
|
|
1816
2311
|
| Live sync | tabs on this machine | tabs, plus other devices through the sync engine | every editor and viewer |
|
|
1817
2312
|
| Outside disk edits reach open tabs | yes (agents, text editors) | yes | no disk except through device sync |
|
|
1818
|
-
| Presence |
|
|
2313
|
+
| Presence (`clay.presence`) | tabs of this browser, shown as "This computer" | tabs on this computer, relayed by the app | one person per account and one session per tab, filtered per recipient |
|
|
1819
2314
|
| `by` on frames, editing marker | no | no | yes |
|
|
1820
2315
|
| Live stores ([Live data: `clay="live"`](https://clayjs.com/docs/guide/live-sync/#live-data-clay-live)) | tabs of one browser at full speed | tabs of one browser at full speed; other devices at save speed | every editor at full speed |
|
|
1821
|
-
| Page editor merge state ([Live data: `clay="live"`](https://clayjs.com/docs/guide/live-sync/#live-data-clay-live)) |
|
|
2316
|
+
| Page editor merge state ([Live data: `clay="live"`](https://clayjs.com/docs/guide/live-sync/#live-data-clay-live)) | kept beside the file: `document.pageState.href` returns the state stored with the exact bytes on disk | kept beside the file | kept beside the file |
|
|
1822
2317
|
| Wire and ai-edit | yes | yes | ai-edit only, for owners and team editors on a paid plan |
|
|
1823
2318
|
| Data API write | yes | yes | read only |
|
|
1824
2319
|
| Uploads | 25 MB, in `~/htmlclay/uploads/` | 25 MB, in the root's `uploads/` | 10, 20 or 100 MB by plan, in the account's `uploads/` |
|
|
1825
2320
|
| Upload access (`document.upload.access`) | `local` | `local` | `document` once the account's uploaded files are protected; no field before that |
|
|
1826
2321
|
| Version history | yes | 60 days, newest 20 | a version per save; a restore does not reach open editors' tabs, so reload after restoring |
|
|
1827
|
-
| Max save | 50 MB | 20 MB | 5 MB |
|
|
2322
|
+
| Max save (`document.maxBytes`) | 50 MB | 20 MB | 5 MB |
|
|
2323
|
+
|
|
2324
|
+
The `/_/meta` answer carries `document.maxBytes`: the maximum uncompressed UTF-8 HTML save body, inclusive. All three hosts send it, and ClayJS measures a save against it before sending, so a document that is over the limit is refused instead of lost ([Size limits](https://clayjs.com/docs/guide/saving/#size-limits)). A missing or unusable value means the limit is unknown, and the host's own refusal is the only answer.
|
|
2325
|
+
|
|
2326
|
+
On hyperclay.com only, the same block carries `document.maxBytesMeasures: "file"`. It means the limit is measured without the `<script merge="page">` tag that holds the document's live state. HTML Clay and Hyperclay Local send no such field.
|
|
1828
2327
|
|
|
1829
2328
|
A plain static server or a file opened from disk can show the file in view mode, but cannot save it. Use the `demo` plugin for a public demo that saves into each visitor's browser.
|
|
1830
2329
|
|
|
@@ -1855,6 +2354,7 @@ Hyperclay Local and HTML Clay each bundle one exact, published ClayJS release, c
|
|
|
1855
2354
|
- **Canonicalization on every write.** Save, data API writes, restore, export and live-sync frames turn those local addresses back into the public `https://clayjs.com/...` URLs and remove the descriptor before anything reaches disk. A saved or downloaded file never contains a `/_/clayjs/` address, so it stays portable to any host. A request with `Clay-Source: 1` receives the canonical bytes unprojected.
|
|
1856
2355
|
- **Versions.** A rolling `https://clayjs.com/v1/` URL is served from the host's bundled release. If a document pins a release the host does not bundle, or names a runtime file the bundled release lacks, every runtime URL is left public. It loads online, is not promised offline, and the response carries a `Clay-Runtime-Unavailable` header that says why. The host never substitutes another release's files into a page.
|
|
1857
2356
|
- **Older Journals.** Journals created before the release bundled its libraries load OverType and highlight.js from other CDNs, and their fonts from Google Fonts. The release maps three exact URLs to copies it ships: `https://cdn.jsdelivr.net/npm/overtype@2.6.1/dist/overtype.min.js` and `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js` to the same classic builds (which define `window.OverType` and `window.hljs`, with their licenses and pinned hashes in `compat/journal-assets.lock.json`), and the Journal's Google Fonts stylesheet URL to a stylesheet over the bundled IBM Plex files. Each match is the whole URL. Another version, a query string, another file or another host stays on the network. A Journal saved with those URLs edits offline, and a document that names anything else from those CDNs does not.
|
|
2357
|
+
- **The first switch to edit.** A document opened offline in view can switch to edit without the network. The switch reloads into an edit boot, and every file that boot loads (the mode controller, the plugins, the Document editor and their late imports) is in the bundled release. Both hosts serve that set from the release with its index digest, the first edit saves the canonical public URLs, and the reopened file is projected again. ClayJS checks it per release: a mode file missing from the release fails the closure check.
|
|
1858
2358
|
- **What stays online.** Pictures and media hosted elsewhere, AI features and collaboration with other people need the network. A picture uploaded into the local folder is a local file and works offline.
|
|
1859
2359
|
- **Checking a host.** `node scripts/verify-offline-closure.mjs` in the ClayJS repository checks a release's closure: every listed file is present with its recorded hash, and every module import resolves inside the release. It runs in three modes: `--package-root <dir>` for a built checkout, `--published-root <dir>` for an unpacked release tree and `--base-url <url>` for a running host's `/_/clayjs/<version>/` prefix. With `--base-url`, pass `--index <file>` naming the host's own bundled index. Without it, the check compares the host against the checkout's index.
|
|
1860
2360
|
|
|
@@ -1870,7 +2370,7 @@ ClayJS gives you one file per document that edits and saves itself. Here is what
|
|
|
1870
2370
|
|
|
1871
2371
|
Two plugins replace the old advice to hand-build a [block writer](https://clayjs.com/docs/guide/recipes/#block-writer) or wait for autosave:
|
|
1872
2372
|
|
|
1873
|
-
- **The [page editor](https://clayjs.com/docs/plugins/page/)** (`?plugins=page`) is a Notion-like block editor for one `article[data-page]`, backed by Yjs, where typing reaches other people as they type. It includes headings, lists, checklists, toggles, callouts, code, images, files, typed tables, boards, charts from a table, and CSV import.
|
|
2373
|
+
- **The [page editor](https://clayjs.com/docs/plugins/page/)** (`?plugins=page`) is a Notion-like block editor for one `article[data-page]`, backed by Yjs, where typing reaches other people as they type. It includes headings, toggle headings, lists, checklists, toggles, callouts, code, images, files, typed tables, boards, charts from a table, mentions of people and dates, emoji, a page icon and cover, CSV and Markdown import, and Markdown and self-contained HTML export.
|
|
1874
2374
|
- **[Live stores](https://clayjs.com/docs/plugins/live-sync/)** (`clay="live"` and `clay.live()`) move fast-changing state at network speed (25 frames per second) instead of save speed. Use [live data](https://clayjs.com/docs/guide/live-sync/#live-data-clay-live) for games, scores, and shared state.
|
|
1875
2375
|
|
|
1876
2376
|
The old limits apply elsewhere. Ordinary `editable` regions and DOM records have no CRDT and merge at save time. Rich text outside the page editor has no tables, and without `live-data`, the page editor falls back to file saves.
|
|
@@ -1879,7 +2379,7 @@ The old limits apply elsewhere. Ordinary `editable` regions and DOM records have
|
|
|
1879
2379
|
|
|
1880
2380
|
| What you are comparing to | What ClayJS gives | What is missing | What to build instead |
|
|
1881
2381
|
|---|---|---|---|
|
|
1882
|
-
| **Google Docs** | Keystroke co-editing, tables, code, callouts, and images in the [page editor](https://clayjs.com/docs/plugins/page/); word-level save merge in `editable`; recoverable conflicts; version history; `ai-edit` | Comments, suggestion mode, live cursors, pagination, footnotes, `.docx` export, tables in `editable` rich text | The [page editor](https://clayjs.com/docs/plugins/page/) or a [block writer](https://clayjs.com/docs/guide/recipes/#block-writer); comments as `<aside data-id data-anchor>`; print CSS for PDF |
|
|
2382
|
+
| **Google Docs** | Keystroke co-editing, tables, code, callouts, and images in the [page editor](https://clayjs.com/docs/plugins/page/); word-level save merge in `editable`; recoverable conflicts; version history; `ai-edit` | Comments, suggestion mode, live cursors outside the page editor, pagination, footnotes, `.docx` export, tables in `editable` rich text | The [page editor](https://clayjs.com/docs/plugins/page/) or a [block writer](https://clayjs.com/docs/guide/recipes/#block-writer); comments as `<aside data-id data-anchor>`; print CSS for PDF |
|
|
1883
2383
|
| **Google Sheets** | Persisted inputs, sap formulas over named fields, aggregates, formats, CSV, page tables and charts from a table, 10,000-row pages saving in about 400 ms | A1 references, ranges, fill-down, recalculation graph, virtualization, range selection, pivots | A page table and chart, or a [typed table](https://clayjs.com/docs/guide/recipes/#typed-table-a-spreadsheet-you-can-be-honest-about) of a few thousand rows, one file per year |
|
|
1884
2384
|
| **Google Slides** | Slides as sections, sortable, uploads, view mode as the published deck | Free positioning, guides, masters, presenter view in a second window, transitions, PPTX export | A flow-layout deck with fixed layouts per slide ([slides](https://clayjs.com/docs/guide/recipes/#slides)) |
|
|
1885
2385
|
| **Trello and Linear** | Page boards where cards have an owner and a due date; sortable columns, inline editing, checklists, attachments, undo, per-card sync | Notifications, reminders, cross-board search, mentions, integrations, boards past a few thousand cards | A page board, or the [kanban with card detail](https://clayjs.com/docs/guide/recipes/#kanban-with-card-detail) with labels, due dates, and a due-soon projection |
|
|
@@ -1891,10 +2391,11 @@ The old limits apply elsewhere. Ordinary `editable` regions and DOM records have
|
|
|
1891
2391
|
### 10.3 Limits that apply to every document
|
|
1892
2392
|
|
|
1893
2393
|
- **No comments.** ClayJS has no comment model (`ai-edit` is an AI prompt, not stored comments). Store comments as `<aside data-id data-anchor>` elements.
|
|
1894
|
-
- **
|
|
2394
|
+
- **Live cursors only in the page editor.** The page editor draws other people's carets and selections where the host announces `page-awareness-v1`. Elsewhere, `clay.presence` tells a page who is here and which record they are writing in, and the page draws its own badges ([Presence](https://clayjs.com/docs/guide/live-sync/#presence)).
|
|
1895
2395
|
- **No suggestion mode, pagination, footnotes, or `.docx` and PPTX export.** Edits apply directly without tracked changes, page breaks, footnotes, or Office exporters. Use print CSS for PDF.
|
|
1896
2396
|
- **Per-file edit access.** There are no per-row permissions. Split data for different audiences into separate files.
|
|
1897
2397
|
- **No background work in a closed file.** Nothing runs in a closed file, and a live room ends after two minutes with nobody connected. Compute due states on open.
|
|
2398
|
+
- **Large record sets cost every keystroke.** Each change runs a Sap pass that visits every record. Sap rereads only changed values, but the pass still grows with the record count, about 27 ms for a body edit at 5,000 records in jsdom. See [Document size and record limits](https://clayjs.com/docs/guide/architecture/#document-size-and-record-limits).
|
|
1898
2399
|
- **Undo is per tab.** History is capped at 100 steps per tab, is not saved, and never reverts a peer's merged edits.
|
|
1899
2400
|
|
|
1900
2401
|
> [!NOTE]
|
|
@@ -1915,12 +2416,14 @@ The old limits apply elsewhere. Ordinary `editable` regions and DOM records have
|
|
|
1915
2416
|
- `clay:sorted` logs "Moved to *Column*" in the card when it changes column.
|
|
1916
2417
|
- Templates: card, checklist item, column, chrome.
|
|
1917
2418
|
|
|
1918
|
-
Extend with labels (a settings element listing `<li data-label="bug" data-color="…">`, cards carrying `data-labels="bug ui"`), due dates (`data-due` plus a hash filter "due this week"), WIP limits (an attribute on the column, a warning in an `editor-ui` badge), and a list view (a projection or `display: contents`).
|
|
2419
|
+
Extend with labels (a settings element listing `<li data-label="bug" data-color="…">`, cards carrying `data-labels="bug ui"`, and `merge-tokens="data-labels"` on the board so two people adding labels both keep theirs, [Declared attribute merges](https://clayjs.com/docs/guide/live-sync/#declared-attribute-merges)), due dates (`data-due` plus a hash filter "due this week"), WIP limits (an attribute on the column, a warning in an `editor-ui` badge), and a list view (a projection or `display: contents`).
|
|
1919
2420
|
|
|
1920
2421
|
With sapjs, the counts and progress badges become paints, the filters an `each=` view or a frozen stylesheet, and checklists nested `items=` lists with a shared blueprint: sapjs guide section [Chrome](https://clayjs.com/docs/guide/architecture/#chrome) builds a smaller version as a complete, tested app.
|
|
1921
2422
|
|
|
1922
2423
|
### 11.2 Block writer
|
|
1923
2424
|
|
|
2425
|
+
For a Notion-like document, use the [page editor](https://clayjs.com/docs/plugins/page/) (`?plugins=page`) instead. This recipe is for a block app whose blocks are your own records.
|
|
2426
|
+
|
|
1924
2427
|
[`examples/writer.html`](https://clayjs.com/examples/writer.html). Plugins `sync,sortable,undo,upload,indicator`, satellite `clay-ui`.
|
|
1925
2428
|
|
|
1926
2429
|
**One `editable` per block**, not one big editor. The document is `#blocks[sortable="blocks"]`, a list of sibling `.block[data-id][data-type]` elements:
|
|
@@ -1984,16 +2487,16 @@ A **free document** is a published app anyone can use and keep. The visitor gets
|
|
|
1984
2487
|
|
|
1985
2488
|
### 12.1 What a visitor gets
|
|
1986
2489
|
|
|
1987
|
-
On a page that opts in ([The contract](https://clayjs.com/docs/guide/free-documents/#the-contract)), a visitor who is not the owner is in **draft mode**. Every edit-only module loads, so `editable`, `persist`, undo, sortable and the rest behave as they do for the owner, and the autosave pipeline still runs. The save lane is swapped: instead of sending the document to the host, ClayJS extracts the page's `api` JSON out of the bytes a real save would write and stores `{ "v": 1, "savedAt": "2026-10-15T00:00:00.000Z", "data": { ... } }` under `localStorage["clay:visitor-drafts:<slot>"]`. The next load applies that JSON over the file the host served, so an owner's later fix reaches a returning visitor while the visitor's own work stays. The host is never told: no save, no sync, no upload, and no `/_/meta` discovery. Two tabs on the same page share that record: a save takes what only the other tab changed, and when both changed the same thing, this tab's version is saved and the other tab's record is kept under `clay:visitor-drafts:<slot>:conflict`.
|
|
2490
|
+
On a page that opts in ([The contract](https://clayjs.com/docs/guide/free-documents/#the-contract)), a visitor who is not the owner is in **draft mode**. Every edit-only module loads, so `editable`, `persist`, undo, sortable and the rest behave as they do for the owner, and the autosave pipeline still runs. The save lane is swapped: instead of sending the document to the host, ClayJS extracts the page's `api` JSON out of the bytes a real save would write and stores `{ "v": 1, "savedAt": "2026-10-15T00:00:00.000Z", "data": { ... } }` under `localStorage["clay:visitor-drafts:<slot>@<path>"]`. The next load applies that JSON over the file the host served, so an owner's later fix reaches a returning visitor while the visitor's own work stays. The host is never told: no save, no sync, no upload, and no `/_/meta` discovery. Two tabs on the same page share that record: a save takes what only the other tab changed, and when both changed the same thing, this tab's version is saved and the other tab's record is kept under `clay:visitor-drafts:<slot>@<path>:conflict`.
|
|
1988
2491
|
|
|
1989
|
-
The owner is a visitor here too. Signed in, they get the same private draft unless the URL carries `?editmode=true`, which edits the original file and shows every visitor the change. Two things
|
|
2492
|
+
The owner is a visitor here too. Signed in, they get the same private draft unless the URL carries `?editmode=true`, which edits the original file and shows every visitor the change. Two things never put the owner in draft mode: a document opened from `file:`, where draft mode never applies, and HTML Clay, which stamps a save token on the response. On HTML Clay the owner opens the file in view and edits the original after switching to edit. Hyperclay Local is not one of them. It serves the folder over `http:` and injects no save token, so its owner is in draft mode like anyone else and adds `?editmode=true` to edit the file ([The mode ladder](https://clayjs.com/docs/guide/free-documents/#the-mode-ladder)).
|
|
1990
2493
|
|
|
1991
2494
|
### 12.2 The contract
|
|
1992
2495
|
|
|
1993
2496
|
A page that wants draft mode needs all of this. The example in [A complete example](https://clayjs.com/docs/guide/free-documents/#a-complete-example) carries every item, and its test proves the round trip:
|
|
1994
2497
|
|
|
1995
2498
|
1. An `api` rules tag naming every saved field, with a `cms-template` seed row for every list the rules describe. Hide the seed in your own CSS (`[cms-template] { display: none !important }`), never with `hidden`, which the CMS reads as data ([cms: content-managed pages](https://clayjs.com/docs/guide/components/#cms-content-managed-pages)).
|
|
1996
|
-
2. `<html visitor-drafts="<site-stem>-v1" autosave>`. The value is the slot: letters, digits, dot, underscore and hyphen, up to 100 characters. It names this app's
|
|
2499
|
+
2. `<html visitor-drafts="<site-stem>-v1" autosave>`. The value is the slot: letters, digits, dot, underscore and hyphen, up to 100 characters. It names this app's drafts. ClayJS keys each draft by the slot and the document's path, so two documents on one origin never share a visitor's draft, even with the same slot.
|
|
1997
2500
|
3. `plugins=visitor-drafts` on the loader tag, plus whatever else the app needs.
|
|
1998
2501
|
4. `persist` on every control the rules name. A typed value without `persist` is lost at save ([`persist`: form controls](https://clayjs.com/docs/guide/record/#persist-form-controls)), and in draft mode a save is the visitor's own copy.
|
|
1999
2502
|
5. `data-id` on every record and `data-id=""` on every blueprint. New rows mint their own (`trigger-add` does it, [sap.js: records, views and totals in markup](https://clayjs.com/docs/guide/components/#sap-js-records-views-and-totals-in-markup)).
|
|
@@ -2013,14 +2516,14 @@ Bump the slot (`-v2`) only when an old draft cannot fit the new rules. A draft t
|
|
|
2013
2516
|
3. `window.clayEditMode`, set by a script before boot.
|
|
2014
2517
|
4. A `savetoken` attribute the host put on `<html>`.
|
|
2015
2518
|
5. **Draft mode**, on a page that opted in ([The contract](https://clayjs.com/docs/guide/free-documents/#the-contract)).
|
|
2016
|
-
6. The `isAdminOfCurrentResource` cookie (hyperclay.com, Hyperclay Local).
|
|
2519
|
+
6. The `isAdminOfCurrentResource` cookie (hyperclay.com, Hyperclay Local). It grants permission, so `clay.canEdit` is true, and the page still opens in view: the person opens it for editing with `clay.setMode("edit")` ([How the mode is decided](https://clayjs.com/docs/guide/edit-mode/#how-the-mode-is-decided)).
|
|
2017
2520
|
|
|
2018
2521
|
The rung is where a free document lives, so the details matter:
|
|
2019
2522
|
|
|
2020
2523
|
- `?editmode=false` is view mode, and nothing below it changes that.
|
|
2021
2524
|
- `?editmode=true` edits the original **only with the owner cookie**, and only on a page that draft mode would otherwise claim. A query parameter on its own never grants writes to the host: an anonymous visitor with `?editmode=true` stays in draft mode.
|
|
2022
2525
|
- The owner cookie alone is not enough either. The signed in owner gets the same draft as everyone else until they open the page with `?editmode=true`, and the owner chip ([The download prompt and the intro area](https://clayjs.com/docs/guide/free-documents/#the-download-prompt-and-the-intro-area)) is their way there.
|
|
2023
|
-
- `viewonly`, a stale token, `window.clayEditMode` or a `savetoken` the host stamped on `<html>` all turn draft mode off. The first two leave it in view mode
|
|
2526
|
+
- `viewonly`, a stale token, `window.clayEditMode` or a `savetoken` the host stamped on `<html>` all turn draft mode off. The first two leave it in view mode. `window.clayEditMode = true` starts the load in edit mode against the host, and a `savetoken` grants permission, so the page opens in view and `clay.setMode("edit")` edits the original. The host still decides who may write ([How the mode is decided](https://clayjs.com/docs/guide/edit-mode/#how-the-mode-is-decided)). On a `viewonly` document or one with a stale token, `?editmode=true` is not draft mode either: it is the same edit mode as before, saving to a host that refuses the write.
|
|
2024
2527
|
- Draft mode needs all four of the plugin name in `plugins=`, a valid slot, an `http:` or `https:` page and an `api` rules tag. That tag also has to carry `data-rules-version="1"` and hold JSON that parses; a newer version or unreadable JSON leaves the page out of draft mode with one console warning naming the problem. On `file:` it never applies, which is exactly why the download is a real file.
|
|
2025
2528
|
- `<html draftmode="true">` says which mode this load is in, beside `editmode` and `pageowner` ([The root attributes](https://clayjs.com/docs/guide/edit-mode/#the-root-attributes)), and is removed from every save.
|
|
2026
2529
|
|
|
@@ -2028,7 +2531,7 @@ The rung is where a free document lives, so the details matter:
|
|
|
2028
2531
|
|
|
2029
2532
|
`clay.isDraftMode` is true, and everything else follows from it:
|
|
2030
2533
|
|
|
2031
|
-
- **The plugins that talk to the host are dropped**, even when the page names them: `sync`, `live`, `upload`, `quickcrop`, `cms`, `people`, `wire`, `ai-edit`, `demo` and `source`. `clay.upload`, `clay.cms`, `clay.conflicts` and `clay.source` are absent, and code that guards them (`clay.upload?.(...)`) behaves as it does for a viewer. The `[refetch-on-save]` attribute is dropped too, since it would fetch its element from the host again after every save.
|
|
2534
|
+
- **The plugins that talk to the host are dropped**, even when the page names them: `sync`, `live`, `page`, `upload`, `quickcrop`, `cms`, `people`, `menu`, `upgrade`, `wire`, `ai-edit`, `demo` and `source`. `clay.upload`, `clay.cms`, `clay.conflicts` and `clay.source` are absent, and code that guards them (`clay.upload?.(...)`) behaves as it does for a viewer. The `[refetch-on-save]` attribute is dropped too, since it would fetch its element from the host again after every save.
|
|
2032
2535
|
- **The save lane is the browser.** `clay.save()` and autosave both run, `savestatus` moves as usual, and the save answers `msgType: "success"` with `msg: "Saved in this browser"`. A storage failure answers the 507 path with `code: "draft-storage"` and fires `clay:drafts-store-failed`; the next good write clears it.
|
|
2033
2536
|
- **No request reaches the host.** The download link goes to the host's download page, which holds the email sign-up. The prompt's own stylesheet is a Google Fonts link.
|
|
2034
2537
|
- **`clay.readData()` and `clay.writeData()` refuse.** Both reject with `code: "DataUnavailableInDraft"`: the saved file belongs to the owner, and this visitor's work is in their browser, so there is nothing for them to read or write there. A page that calls them in draft mode gets that refusal instead of a request ([Import and export](https://clayjs.com/docs/guide/architecture/#import-and-export)).
|
|
@@ -2038,7 +2541,7 @@ The rung is where a free document lives, so the details matter:
|
|
|
2038
2541
|
|---|---|
|
|
2039
2542
|
| `active` | `true` when the plugin found a slot and the rules tag, `false` otherwise |
|
|
2040
2543
|
| `slot` | the slot from `<html visitor-drafts="...">` |
|
|
2041
|
-
| `key` | the `localStorage` key, `clay:visitor-drafts:<slot>` |
|
|
2544
|
+
| `key` | the `localStorage` key, `clay:visitor-drafts:<slot>@<path>`, where `<path>` is the document's path up to its `.html` or `.htmlclay` file (`/` for the root and for a host that serves one document at every path) |
|
|
2042
2545
|
| `hasDraft()` | is there a stored draft for this slot |
|
|
2043
2546
|
| `restored()` | `{ restored, hadDraft, parked }` for this load, or `null` when the plugin is not active |
|
|
2044
2547
|
| `saveNow()` | write the page to the slot now; `true` when it landed, `false` when it did not |
|
|
@@ -2057,16 +2560,24 @@ The restore runs while ClayJS boots, before `clay.ready` resolves, and applies t
|
|
|
2057
2560
|
|
|
2058
2561
|
Code that drew before `clay.ready` has two ways to hear about the restore: listen for `clay:drafts-restored` on `document`, or call `clay.visitorDrafts.onRestore(fn)`, which runs `fn` at once when the restore already happened. Either way the detail is `{ restored, hadDraft, parked }`.
|
|
2059
2562
|
|
|
2060
|
-
A draft that no longer fits the page is never overwritten. When the stored JSON is not `{ v, data }` (garbage, a bare array, a missing `data`), the raw value is moved to `localStorage["clay:visitor-drafts:<slot>:unreadable:<ISO timestamp>"]`, the slot is cleared, and the page loads as authored with `parked: true` in the detail. When the write is refused only for keys the rules do not know, those keys are dropped and the rest is applied, so adding a field to the rules never costs a visitor their work.
|
|
2563
|
+
A draft that no longer fits the page is never overwritten. When the stored JSON is not `{ v, data }` (garbage, a bare array, a missing `data`), the raw value is moved to `localStorage["clay:visitor-drafts:<slot>@<path>:unreadable:<ISO timestamp>"]`, the slot is cleared, and the page loads as authored with `parked: true` in the detail. When the write is refused only for keys the rules do not know, those keys are dropped and the rest is applied, so adding a field to the rules never costs a visitor their work.
|
|
2564
|
+
|
|
2565
|
+
**Drafts saved by an older ClayJS.** A draft stored under the slot alone is copied into the document's key on its first load, with its intro and prompt memory, and the old key is left as it is. Code outside ClayJS that reads a draft should read `clay.visitorDrafts.key`; a script that must run before `clay.js` uses the same path rule.
|
|
2566
|
+
|
|
2567
|
+
### 12.6 Two tabs
|
|
2061
2568
|
|
|
2062
|
-
|
|
2569
|
+
Each save merges what the other tab saved. Fields merge one by one, and lists of rows with ids merge row by row: a row added or removed in one tab is kept or removed, and rows changed in different tabs both keep their change. When both tabs changed the same field, reordered a list, or moved one row to different places, this tab's value is saved and the other tab's whole record is kept under `<key>:conflict`.
|
|
2570
|
+
|
|
2571
|
+
### 12.7 The download prompt and the intro area
|
|
2063
2572
|
|
|
2064
2573
|
Both are runtime chrome: `clay="editor-ui"` and `sap-ignore`, built by the plugin on every load. Neither is in the file, neither is saved or synced, and neither is read by sapjs.
|
|
2065
2574
|
|
|
2066
|
-
Both remember themselves per slot in `localStorage["clay:visitor-drafts:<slot>:ui"]`, as `{ "v": 1, "prompt": { "shown": ..., "dismissed": ..., "uses": ..., "downloaded": ... }, "intro": { "done": null | "closed" | "scrolled" }, "src": ... }`. Clearing that key shows the intro and the prompt again.
|
|
2575
|
+
Both remember themselves per slot and path in `localStorage["clay:visitor-drafts:<slot>@<path>:ui"]`, as `{ "v": 1, "prompt": { "shown": ..., "dismissed": ..., "uses": ..., "downloaded": ... }, "intro": { "done": null | "closed" | "scrolled" }, "src": ... }`. Clearing that key shows the intro and the prompt again.
|
|
2067
2576
|
|
|
2068
2577
|
**The intro** is authored per app as a `<template visitor-intro>`. The template holds the author's whole strip: its markup and any `<style>` it needs. The plugin clones it into a section at the very top of the page body, before the app's own content, and adds a quiet close (`x`) in the corner. The intro leaves for good when the visitor closes it or scrolls it past the top of the viewport; the ui memory records `"closed"` or `"scrolled"` as the reason.
|
|
2069
2578
|
|
|
2579
|
+
A `<script type="module">` inside the template runs when the intro mounts. Animate it with `clay.assets.load('uimotion')`, never an engine copied into the file, so a visitor the intro skips loads no motion code.
|
|
2580
|
+
|
|
2070
2581
|
```html
|
|
2071
2582
|
<template visitor-intro>
|
|
2072
2583
|
<p>One paragraph on what the app does and who it is for.</p>
|
|
@@ -2087,13 +2598,34 @@ An element inside the template with `[visitor-intro-start]` scrolls to the app,
|
|
|
2087
2598
|
|
|
2088
2599
|
**The owner chip** shows only to the signed in owner, at the bottom left: it says whether this load is their private draft ("Edit the original") or the original ("Back to my draft") and switches with `clay.visitorDrafts.editOriginal()` and `backToDraft()`.
|
|
2089
2600
|
|
|
2090
|
-
### 12.
|
|
2601
|
+
### 12.8 Migrating an app's own storage
|
|
2091
2602
|
|
|
2092
2603
|
**Not supported yet:** the plugin reads no migration hook, so an app that kept its earlier data under its own `localStorage` key has to move those values into the page from its own code, on the first load after the switch.
|
|
2093
2604
|
|
|
2094
|
-
|
|
2605
|
+
**Change a key only after every reader reads both.** A reader that spells the key by hand keeps reading the old one, and an old key stops changing the moment ClayJS writes the new one. The download page and the calculator's row reset both spell the draft key themselves, so both learned to read the new key and the old one before the key changed:
|
|
2606
|
+
|
|
2607
|
+
```js
|
|
2608
|
+
function readDrafts(win, slot, path) {
|
|
2609
|
+
if (!slot) return null;
|
|
2610
|
+
try {
|
|
2611
|
+
var legacy = 'clay:visitor-drafts:' + slot;
|
|
2612
|
+
var key = legacy + '@' + draftPath(path);
|
|
2613
|
+
var raw = win.localStorage.getItem(key);
|
|
2614
|
+
if (raw == null && win.localStorage.getItem(key + ':legacy') == null) raw = win.localStorage.getItem(legacy);
|
|
2615
|
+
var stored = raw ? JSON.parse(raw) : null;
|
|
2616
|
+
var data = stored && stored.data;
|
|
2617
|
+
return data && typeof data === 'object' && !Array.isArray(data) && Object.keys(data).length ? data : null;
|
|
2618
|
+
} catch (e) {
|
|
2619
|
+
return null;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
```
|
|
2623
|
+
|
|
2624
|
+
The `:legacy` marker is what stops a reset from coming back: once ClayJS has copied the old draft, a reader that still saw the old key would restore work the visitor asked to clear. A reader that can run before `clay.js` uses the same path rule, so it finds the same key ClayJS writes.
|
|
2625
|
+
|
|
2626
|
+
### 12.9 A complete example
|
|
2095
2627
|
|
|
2096
|
-
[`examples/free-reading-list.html`](https://clayjs.com/examples/free-reading-list.html). Plugins `visitor-drafts,indicator&exclude=richclay`, satellites `clay-options` and `sap.js`. A reading list: a title, an author and a status select per book, one scalar goal for the year, and sapjs counting the books that are read. It is the shortest file that carries every item of the contract, and `tests/unit/guide-examples/free-document.test.js` runs it end to end: edit a title, add a book, check the slot, then load the file again with that storage and check the books and the counts come back. The record is authored in the saved form,
|
|
2628
|
+
[`examples/free-reading-list.html`](https://clayjs.com/examples/free-reading-list.html). Plugins `visitor-drafts,indicator&exclude=richclay`, satellites `clay-options` and `sap.js`. A reading list: a title, an author and a status select per book, one scalar goal for the year, and sapjs counting the books that are read. It is the shortest file that carries every item of the contract, and `tests/unit/guide-examples/free-document.test.js` runs it end to end: edit a title, add a book, check the slot, then load the file again with that storage and check the books and the counts come back. The record is authored in the saved form, with its `viewmode:` attributes. The `reading` option of a status select carries no `selected`: it is the first option, which is the default, and the row that is `read` marks it.
|
|
2097
2629
|
|
|
2098
2630
|
```html
|
|
2099
2631
|
<!DOCTYPE html>
|
|
@@ -2183,26 +2715,41 @@ An element inside the template with `[visitor-intro-start]` scrolls to the app,
|
|
|
2183
2715
|
1. Loads with `clay.js` as a classic script at the end of `<body>`, every tag on the same version prefix.
|
|
2184
2716
|
2. `<html autosave>` unless it is a deliberate manual-save writer.
|
|
2185
2717
|
3. `sync` is in the plugin list, and `exclude=richclay` if there is no rich text.
|
|
2186
|
-
4. The
|
|
2187
|
-
5.
|
|
2188
|
-
6.
|
|
2189
|
-
7. Every
|
|
2190
|
-
8.
|
|
2191
|
-
9.
|
|
2192
|
-
10.
|
|
2193
|
-
11.
|
|
2194
|
-
12.
|
|
2195
|
-
13.
|
|
2196
|
-
14.
|
|
2197
|
-
15.
|
|
2198
|
-
16.
|
|
2199
|
-
17.
|
|
2200
|
-
18.
|
|
2201
|
-
19.
|
|
2202
|
-
20.
|
|
2203
|
-
21.
|
|
2204
|
-
|
|
2205
|
-
|
|
2718
|
+
4. The owner has a way into edit: the `menu` plugin, or a control of your own that calls `clay.setMode()` and carries `clay-mode-toggle`. Without either, ClayJS shows a one-time note pointing to Cmd+Shift+E (Ctrl+Shift+E elsewhere).
|
|
2719
|
+
5. The fallback rule `html:not([editmode="true"]) [show-when\:editmode="true"] { display: none }` is in the stylesheet, and every edit-only control has `show-when:editmode="true"`.
|
|
2720
|
+
6. Records are authored in the saved form (`inert-contenteditable`, `disabled`, `readonly`); templates in the active form.
|
|
2721
|
+
7. Every repeated record has a unique `data-id`; templates carry none; every create and duplicate mints a fresh one.
|
|
2722
|
+
8. Every control whose value matters has `persist`; no filter or search box does.
|
|
2723
|
+
9. All chrome is cloned from a `<template>` into `clay="editor-ui"` after `clay.ready`; nothing in the file is chrome.
|
|
2724
|
+
10. Tab-local state is in the hash, `clay.storage.local` or `clay.storage.session`, or memory, painted through a runtime style; opening, filtering and searching leave `clay.hasUnsavedChanges()` false.
|
|
2725
|
+
11. Derived values are sapjs paints, in `editor-ui`, or `no-dirty` when the file must show them; every hand-written derived write is guarded, and no script copies a record into a second attribute.
|
|
2726
|
+
12. Commands are wrapped in `clay.undo.commit`; derived views are drawn in `clay.effect`, with no effect warnings in the console.
|
|
2727
|
+
13. Views whose record a peer deleted are closed, in the effect or a `clay:sync-applied` handler.
|
|
2728
|
+
14. The app script starts with `clay.start`; all listeners are delegated.
|
|
2729
|
+
15. Nothing writes the record at boot.
|
|
2730
|
+
16. Edit-only calls are guarded for view mode, and edit actions check `clay.isEditMode`.
|
|
2731
|
+
17. `clay.confirm` calls have a rejection handler.
|
|
2732
|
+
18. Save failures are visible (indicator, `savestatus` CSS or clay-ui).
|
|
2733
|
+
19. Library residue (Sortable, third-party editors) is stripped in a snapshot hook.
|
|
2734
|
+
20. Images use `clay.upload`, embedding only on `unsupported`.
|
|
2735
|
+
21. The file opens in view mode with JavaScript disabled and shows correct content and no edit controls.
|
|
2736
|
+
22. With sap: `Sap.status().ok`, the live root matches the file after load (no computed value rewritten), no `[sap-error]`, rows with your own ids.
|
|
2737
|
+
23. A tool page whose script opens an editor at load (a journal, a notes pad) declares `<html opens-in="edit">`. Without it the page opens in view, accepts typing and saves none of it.
|
|
2738
|
+
|
|
2739
|
+
### 13.2 Testing a document that changes modes
|
|
2740
|
+
|
|
2741
|
+
Each check names the clayjs test that proves the library side.
|
|
2742
|
+
|
|
2743
|
+
- **It opens in view by default.** `tests/unit/mode-decision.test.js` pins the table, including an owner cookie alone opening in view with permission kept.
|
|
2744
|
+
- **An explicit edit enters edit.** The same file covers `?editmode=true`, `window.clayEditMode = true` and `<html opens-in="edit">`, and `tests/unit/mode-transition.test.js` covers an explicit initial edit preparing the session a live first entry does.
|
|
2745
|
+
- **A draft document stays a draft.** `tests/unit/mode-draft.test.js` shows a declared draft refusing both directions.
|
|
2746
|
+
- **A refused exit can be retried.** `tests/unit/mode-lifecycle.test.js` covers a quiesce that refuses and resumes the same leases, and `tests/unit/mode-transition.test.js` covers the next entry starting a fresh session.
|
|
2747
|
+
- **Pending work refuses the exit.** `tests/unit/mode-transition.test.js` covers a registered pending source, and `tests/unit/mode-save-barrier.test.js` covers the barrier's codes.
|
|
2748
|
+
- **A clean enter and exit writes nothing.** `tests/unit/mode-transition.test.js` asserts it, and the corpus case `enter-leave-zero-writes` records the same run.
|
|
2749
|
+
|
|
2750
|
+
`tests/mode-counterexamples/` is a set of recorded cases. Each one boots a fixture in jsdom through the real loader, applies a schedule with the transport, the chunk imports and the clock supplied by the harness, and judges the result from what the run left behind. It is not a browser run: no phone, no keyboard and no real network appears in it.
|
|
2751
|
+
|
|
2752
|
+
### 13.3 Trap index
|
|
2206
2753
|
|
|
2207
2754
|
| Trap | Section |
|
|
2208
2755
|
|---|---|
|
|
@@ -2213,6 +2760,10 @@ An element inside the template with `[visitor-intro-start]` scrolls to the app,
|
|
|
2213
2760
|
| Records authored with live `contenteditable` stay editable for viewers until the first save | [Mode attributes: one file for editors and viewers](https://clayjs.com/docs/guide/edit-mode/#mode-attributes-one-file-for-editors-and-viewers) |
|
|
2214
2761
|
| Templates in the inert form give clones that cannot be edited until reload | [Mode attributes: one file for editors and viewers](https://clayjs.com/docs/guide/edit-mode/#mode-attributes-one-file-for-editors-and-viewers) |
|
|
2215
2762
|
| Unguarded edit-only API calls throw in view mode | [What works in view mode](https://clayjs.com/docs/guide/edit-mode/#what-works-in-view-mode) |
|
|
2763
|
+
| A page whose script opens an editor at load accepts typing in view mode and saves nothing | [How the mode is decided](https://clayjs.com/docs/guide/edit-mode/#how-the-mode-is-decided) |
|
|
2764
|
+
| Reading `clay.mode` once at boot and keeping it | [How the mode is decided](https://clayjs.com/docs/guide/edit-mode/#how-the-mode-is-decided) |
|
|
2765
|
+
| Using `clay.canEdit` as the guard for writing; use `clay.modeLifecycle.canWrite()` | [Switching without a reload: clay-mode-switch="live"](https://clayjs.com/docs/guide/edit-mode/#switching-without-a-reload-clay-mode-switch-live) |
|
|
2766
|
+
| Switching modes by writing the root `editmode` attribute | [Switching modes: clay.setMode](https://clayjs.com/docs/guide/edit-mode/#switching-modes-clay-setmode) |
|
|
2216
2767
|
| `hidden`, `.selected`, `<details open>` and library inline styles save; use `ephemeral` | [What a save writes](https://clayjs.com/docs/guide/record/#what-a-save-writes), [Ephemeral state: per-tab state on saved elements](https://clayjs.com/docs/guide/record/#ephemeral-state-per-tab-state-on-saved-elements) |
|
|
2217
2768
|
| `no-save` chrome still reaches peers; use `editor-ui` | [Regions: the `clay` attribute](https://clayjs.com/docs/guide/record/#regions-the-clay-attribute) |
|
|
2218
2769
|
| Edits in `no-watch` or `no-dirty` are invisible to `save()` and the close warning | [Regions: the `clay` attribute](https://clayjs.com/docs/guide/record/#regions-the-clay-attribute) |
|
|
@@ -2243,12 +2794,13 @@ An element inside the template with `[visitor-intro-start]` scrolls to the app,
|
|
|
2243
2794
|
| A peer's frame re-runs inline scripts | [What a merge does](https://clayjs.com/docs/guide/live-sync/#what-a-merge-does), [Starting the app once](https://clayjs.com/docs/guide/architecture/#starting-the-app-once) |
|
|
2244
2795
|
| A `no-dirty` region replaced by a peer raises a false conflict notice (1.8.2) | [Regions and JSON under sync](https://clayjs.com/docs/guide/live-sync/#regions-and-json-under-sync) |
|
|
2245
2796
|
| Same-key JSON edits under `merge`: incoming wins, yours goes to the notice | [Regions and JSON under sync](https://clayjs.com/docs/guide/live-sync/#regions-and-json-under-sync) |
|
|
2797
|
+
| Two tabs changing one shared `data-*` attribute (labels, a counter) conflict unless a container declares `merge-tokens` or `merge-max` | [Declared attribute merges](https://clayjs.com/docs/guide/live-sync/#declared-attribute-merges) |
|
|
2246
2798
|
| Rewriting the record at boot makes incoming frames hold | [Focus, typing and long-running editors](https://clayjs.com/docs/guide/live-sync/#focus-typing-and-long-running-editors) |
|
|
2247
2799
|
| Parsed JSON goes stale after a merge | [Reconciling in app code](https://clayjs.com/docs/guide/live-sync/#reconciling-in-app-code) |
|
|
2248
2800
|
| A script that copies a record into a styling attribute goes stale in the file, and a merge brings the stale copy back | [Rules for DOM state](https://clayjs.com/docs/guide/model/#rules-for-dom-state), [sap.js: records, views and totals in markup](https://clayjs.com/docs/guide/components/#sap-js-records-views-and-totals-in-markup) |
|
|
2249
2801
|
| An `api` rule that reads a painted value can read a stale one until a tab repaints and saves | [sap.js: records, views and totals in markup](https://clayjs.com/docs/guide/components/#sap-js-records-views-and-totals-in-markup) |
|
|
2250
2802
|
| Full sapjs markup under the sapjs core halts with `E50` | [sap.js: records, views and totals in markup](https://clayjs.com/docs/guide/components/#sap-js-records-views-and-totals-in-markup) |
|
|
2251
|
-
|
|
|
2803
|
+
| Presence is runtime only: the host filters the roster, and a caret it cannot place is not drawn | [Presence](https://clayjs.com/docs/guide/live-sync/#presence) |
|
|
2252
2804
|
| `data-richclay` card mode deletes images on load | [richclay: rich text (`editable`)](https://clayjs.com/docs/guide/components/#richclay-rich-text-editable) |
|
|
2253
2805
|
| The richclay toolbar is unusable inside `showModal()` and popovers | [richclay: rich text (`editable`)](https://clayjs.com/docs/guide/components/#richclay-rich-text-editable) |
|
|
2254
2806
|
| Rich text has its own undo; page undo never records it | [richclay: rich text (`editable`)](https://clayjs.com/docs/guide/components/#richclay-rich-text-editable), [undo: whole-page undo](https://clayjs.com/docs/guide/components/#undo-whole-page-undo) |
|
|
@@ -2258,8 +2810,8 @@ An element inside the template with `[visitor-intro-start]` scrolls to the app,
|
|
|
2258
2810
|
| Sortable handle mode is fixed at bind time | [sortable: drag to reorder](https://clayjs.com/docs/guide/components/#sortable-drag-to-reorder) |
|
|
2259
2811
|
| `onsorting` cannot veto a move | [sortable: drag to reorder](https://clayjs.com/docs/guide/components/#sortable-drag-to-reorder) |
|
|
2260
2812
|
| Sortable residue saves | [sortable: drag to reorder](https://clayjs.com/docs/guide/components/#sortable-drag-to-reorder), [Hooks](https://clayjs.com/docs/guide/saving/#hooks) |
|
|
2261
|
-
|
|
|
2262
|
-
|
|
|
2813
|
+
| A plain input's typed value is not content until `persist` or a command writes it, so page undo has nothing to replay for it | [undo: whole-page undo](https://clayjs.com/docs/guide/components/#undo-whole-page-undo) |
|
|
2814
|
+
| A refused undo or redo stays on top of the stack until it is replayed or discarded | [undo: whole-page undo](https://clayjs.com/docs/guide/components/#undo-whole-page-undo) |
|
|
2263
2815
|
| Pausing undo across an `await` loses the earlier mutations | [undo: whole-page undo](https://clayjs.com/docs/guide/components/#undo-whole-page-undo) |
|
|
2264
2816
|
| Sap reads live control values: undo without re-syncing inputs saves the undone number | [`persist`: form controls](https://clayjs.com/docs/guide/record/#persist-form-controls), [sap.js: records, views and totals in markup](https://clayjs.com/docs/guide/components/#sap-js-records-views-and-totals-in-markup) |
|
|
2265
2817
|
| `clay.upload` is not a picker; embed only on `unsupported` | [upload and quickcrop](https://clayjs.com/docs/guide/components/#upload-and-quickcrop) |
|