@panphora/clayjs 1.21.0 → 1.23.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 +8871 -8629
- package/dist/m/attrs/{save-freeze-MTFVVYV5.js → save-freeze-JZHKRQLR.js} +1 -1
- package/dist/m/{c-OABVRYOT.js → c-24PMUJIV.js} +1 -1
- package/dist/m/{c-EOCZ2YVL.js → c-2AYVYEI4.js} +1 -1
- package/dist/m/{c-UXEVJ63W.js → c-2L6O5PDS.js} +1 -1
- package/dist/m/{c-VBLFAQD2.js → c-2RYOGUQM.js} +1 -1
- package/dist/m/{c-LDUNOR57.js → c-2Z22FCFK.js} +13 -25
- package/dist/m/{c-NLZ5AN4L.js → c-2ZVOEU4M.js} +1 -1
- package/dist/m/{c-BJPZCWAV.js → c-37NAONZA.js} +1 -1
- package/dist/m/{c-ZE46QDIH.js → c-3TLG3OLG.js} +1 -1
- package/dist/m/{c-CSNE5EIF.js → c-4PO3YGDC.js} +1 -1
- package/dist/m/{c-OJYM642Q.js → c-5FKRZNJV.js} +1 -1
- package/dist/m/{c-SO63UGPQ.js → c-5OJ7YF2N.js} +1 -1
- package/dist/m/{c-CAWNFER6.js → c-5TIZ2I64.js} +1 -1
- package/dist/m/{c-7SXYQCN4.js → c-5WMXEKFH.js} +1 -1
- package/dist/m/{c-XQSXKIKM.js → c-6RDOKLN7.js} +1 -1
- package/dist/m/{c-BRLCTNMC.js → c-6TYGD5AB.js} +1 -1
- package/dist/m/c-A4IP6MBC.js +1 -0
- package/dist/m/{c-I2IFKJX2.js → c-ALDVFPWL.js} +1 -1
- package/dist/m/{c-LV5MABYD.js → c-AYVGD6A6.js} +1 -1
- package/dist/m/{c-LQU7TXIH.js → c-B6O7AJYG.js} +1 -1
- package/dist/m/{c-SBSMJIPC.js → c-BCGWJYXW.js} +1 -1
- package/dist/m/{c-RT3C74GY.js → c-BGKAT2FL.js} +1 -1
- package/dist/m/{c-N7Y52HJH.js → c-BXYSDKAN.js} +1 -1
- package/dist/m/c-CRPIDGXC.js +78 -0
- package/dist/m/c-CUMXGHVD.js +1 -0
- package/dist/m/{c-67ILSNPR.js → c-DTMZ3ZHI.js} +1 -1
- package/dist/m/{c-EZ7B5AZT.js → c-ELYIIYRS.js} +1 -1
- package/dist/m/{c-VAK7SG37.js → c-EUGJVHDD.js} +1 -1
- package/dist/m/{c-67LGZ52V.js → c-FCSZ3DC5.js} +1 -1
- package/dist/m/{c-JUE7B4OX.js → c-FQGHR32O.js} +1 -1
- package/dist/m/{c-ZJLU56DV.js → c-G4PWUDD5.js} +1 -1
- package/dist/m/{c-CJFDLD43.js → c-G6COVIZB.js} +1 -1
- package/dist/m/c-GBTVCJXC.js +1 -0
- package/dist/m/c-GC2HICFJ.js +1 -0
- package/dist/m/{c-44B5ODN7.js → c-HCNWJE4A.js} +2 -2
- package/dist/m/{c-BJ7YQ5QZ.js → c-IHUWUKIU.js} +1 -1
- package/dist/m/{c-LIBE4HPD.js → c-JCOQVZYS.js} +1 -1
- package/dist/m/{c-RG6XUZFA.js → c-JGHEACGK.js} +1 -1
- package/dist/m/{c-3GJ4XARY.js → c-JGLX6TBK.js} +1 -1
- package/dist/m/c-JNRKHYWO.js +1 -0
- package/dist/m/{c-PIVIYLTY.js → c-KDXIJEEF.js} +1 -1
- package/dist/m/{c-NFR673TL.js → c-L4KMT7FO.js} +2 -2
- package/dist/m/{c-E4IBSAQM.js → c-LCYPMULG.js} +1 -1
- package/dist/m/{c-SJPD46ZO.js → c-LIGQYZ7M.js} +4 -4
- package/dist/m/{c-U5X57T4L.js → c-LKIWEPAV.js} +1 -1
- package/dist/m/{c-EDSPKYMA.js → c-LUXAB2XI.js} +1 -1
- package/dist/m/{c-YSB6JKV2.js → c-LWIDDWAR.js} +2 -2
- package/dist/m/c-MPSKIU2I.js +1 -0
- package/dist/m/{c-DHXPDLW6.js → c-NK33MMYF.js} +1 -1
- package/dist/m/{c-GPGYKER7.js → c-NQLWOHLH.js} +1 -1
- package/dist/m/{c-LPUJXRXW.js → c-NTHUTABN.js} +1 -1
- package/dist/m/{c-Y33LUUIQ.js → c-NW54NHKE.js} +1 -1
- package/dist/m/{c-3AXJDL2P.js → c-OG6GFSUX.js} +1 -1
- package/dist/m/{c-MTQHWQC3.js → c-OKEGUICD.js} +1 -1
- package/dist/m/c-OPONJ2UK.js +128 -0
- package/dist/m/{c-3YH6JSIL.js → c-OWQWOGYI.js} +1 -1
- package/dist/m/{c-FDAXZ2PM.js → c-P6Q2ZTFQ.js} +1 -1
- package/dist/m/c-PKCCHJ5X.js +1 -0
- package/dist/m/c-PWJSJEJU.js +1 -0
- package/dist/m/{c-2ZZBC45O.js → c-PXLTUYZC.js} +1 -1
- package/dist/m/c-QEN6HZCA.js +1 -0
- package/dist/m/{c-JOH753EJ.js → c-QGAPG7OM.js} +1 -1
- package/dist/m/{c-W23KVGUO.js → c-QIR5CAKM.js} +1 -1
- package/dist/m/{c-GKEATME6.js → c-QM5LXRDT.js} +1 -1
- package/dist/m/c-QUGCKMWF.js +1 -0
- package/dist/m/{c-FNVQAACK.js → c-QYYUVLVG.js} +1 -1
- package/dist/m/c-RO73MKKR.js +1 -0
- package/dist/m/c-SBCBXKOZ.js +26 -0
- package/dist/m/{c-J3BXFTZS.js → c-SGRIYOAJ.js} +1 -1
- package/dist/m/c-SKF5HL4I.js +4 -0
- package/dist/m/{c-W3K23S72.js → c-SRPC3VBE.js} +1 -1
- package/dist/m/{c-K76AUL4I.js → c-T5NK2TVY.js} +1 -1
- package/dist/m/{c-EFD6QNTE.js → c-TANBH7FI.js} +1 -1
- package/dist/m/{c-5734MF2P.js → c-THGQQDYY.js} +1 -1
- package/dist/m/{c-YQUJ74R3.js → c-TSBVMWAH.js} +1 -1
- package/dist/m/{c-VY6XRBOW.js → c-UD5JZTDL.js} +1 -1
- package/dist/m/{c-M4FSPNQ2.js → c-UDGEFWNU.js} +1 -1
- package/dist/m/{c-ZSCBEB7I.js → c-UHUNXWKN.js} +1 -1
- package/dist/m/{c-BTLFLLML.js → c-VIRFF5HG.js} +1 -1
- package/dist/m/{c-2VB555FG.js → c-VYJ7O4RI.js} +1 -1
- package/dist/m/{c-TUGVINJR.js → c-W5E5P2TZ.js} +1 -1
- package/dist/m/{c-EOCKLMKX.js → c-W7IPVCAA.js} +1 -1
- package/dist/m/{c-5FEU5MMY.js → c-WJ5DRBHT.js} +1 -1
- package/dist/m/{c-V2J5C5MT.js → c-WQZTI5P2.js} +1 -1
- package/dist/m/c-X5WC4PX5.js +1 -0
- package/dist/m/{c-GVPNDIKJ.js → c-XCFQEWW6.js} +2 -2
- package/dist/m/c-XLQ3BDMM.js +1 -0
- package/dist/m/{c-XD5MOROJ.js → c-XX75FFNL.js} +1 -1
- package/dist/m/{c-JHJR5BQX.js → c-YK2L436K.js} +1 -1
- package/dist/m/{c-MEWUGR4D.js → c-YYMZWQLR.js} +1 -1
- package/dist/m/core/{admin-attrs-ZG7R5V3E.js → admin-attrs-U7IJNYBG.js} +1 -1
- package/dist/m/core/{autosave-VPORYJSE.js → autosave-F5YQCKVL.js} +1 -1
- package/dist/m/core/{conflict-notice-JMAZLVRX.js → conflict-notice-42H4ASLA.js} +1 -1
- package/dist/m/core/{persist-3OPWJKZS.js → persist-CJXIL7D4.js} +1 -1
- package/dist/m/core/{save-AOJBDSOK.js → save-ESPSQPLV.js} +1 -1
- package/dist/m/core/{save-core-IOCMXO5L.js → save-core-6SQHOR6W.js} +1 -1
- package/dist/m/core/snapshot-FJP5IJL3.js +1 -0
- package/dist/m/core/{unsaved-warning-U5VUJFOE.js → unsaved-warning-HNQVTA2D.js} +1 -1
- package/dist/m/internals/{index-HDRANDWE.js → index-NRPNDTSA.js} +1 -1
- package/dist/m/loader-FBDE2UAB.js +1 -0
- package/dist/m/manifest-1D2F5CA5.js +1 -0
- package/dist/m/manifest-edit-F7CC5CFD.js +1 -0
- package/dist/m/manifest.json +1139 -1069
- package/dist/m/page/{editor-EY2WAAGB.js → editor-OSKOW6RS.js} +2 -2
- package/dist/m/plugins/ai-edit-VV4QTINH.js +6 -0
- package/dist/m/plugins/cms-H7DTMUQX.js +2235 -0
- package/dist/m/plugins/{indicator-4VDXWYTD.js → indicator-WVPGMOZ7.js} +1 -1
- package/dist/m/plugins/{live-5EZN2EYP.js → live-DCUBKXY4.js} +1 -1
- package/dist/m/plugins/menu-Q4MFFQ6E.js +1 -0
- package/dist/m/plugins/{page-LVW5DWA4.js → page-OAWUJQEL.js} +2 -2
- package/dist/m/plugins/{page-view-5KQ76IJB.js → page-view-DZ26ZDDQ.js} +1 -1
- package/dist/m/plugins/{people-C7TVBBMR.js → people-5XRNRP5U.js} +1 -1
- package/dist/m/plugins/{quickcrop-BQ5YYFCH.js → quickcrop-WWF5I422.js} +2 -2
- package/dist/m/plugins/{richclay-KLGETK77.js → richclay-PGOOJ4OK.js} +2 -2
- package/dist/m/plugins/{sortable-WJZZBM7R.js → sortable-266HY6TR.js} +1 -1
- package/dist/m/plugins/{source-CCEUM3OR.js → source-6BKMK4PO.js} +1 -1
- package/dist/m/plugins/{upgrade-NW556WJD.js → upgrade-BNZVFHMZ.js} +1 -1
- package/dist/m/plugins/visitor-drafts-2AUYKAIJ.js +20 -0
- package/dist/m/plugins/{visitor-drafts-owner-MZP45XSG.js → visitor-drafts-owner-Z5B2OQBR.js} +1 -1
- package/dist/m/plugins/visitor-menu-4LQMKLPP.js +1 -0
- package/dist/m/plugins/{wire-VRYPY6EJ.js → wire-W2EV36KZ.js} +1 -1
- package/dist/m/{reader-core-2GXY7SHY.js → reader-core-POFQCKRV.js} +2 -2
- package/dist/m/sync/{live-sync-SKPMFYOO.js → live-sync-XGUPHI3I.js} +1 -1
- package/dist/m/ui/{index-QNIPDHXQ.js → index-VGYAKAKI.js} +1 -1
- package/dist/offline-manifest.json +666 -606
- package/docs/guide.md +38 -27
- package/package.json +2 -2
- package/src/core/save-replace.js +5 -2
- package/src/core/serialize-steps.js +10 -2
- package/src/core/snapshot.js +37 -1
- package/src/core/upgrade-run.js +16 -2
- package/src/loader-logic.js +10 -3
- package/src/plugins/ai-edit.js +6 -17
- package/src/plugins/cms.js +3 -6
- package/src/plugins/menu/dom-items.js +45 -0
- package/src/plugins/menu/looks.js +32 -0
- package/src/plugins/menu/shell-style.js +143 -0
- package/src/plugins/menu/shell.js +398 -0
- package/src/plugins/menu/style.js +15 -116
- package/src/plugins/menu/variants.js +56 -10
- package/src/plugins/menu/visitor.js +78 -0
- package/src/plugins/menu.js +21 -313
- package/src/plugins/visitor-drafts/inert.js +1 -0
- package/src/plugins/visitor-drafts/intro-style.js +6 -0
- package/src/plugins/visitor-drafts/intro.js +77 -25
- package/src/plugins/visitor-drafts/owner-chip.js +18 -23
- package/src/plugins/visitor-drafts/prompt.js +2 -2
- package/src/plugins/visitor-drafts/store.js +58 -5
- package/src/plugins/visitor-drafts.js +1 -0
- package/src/plugins/visitor-menu.js +13 -0
- package/src/sync/live-sync.js +2 -1
- package/src/sync/page-relay.js +2 -2
- package/src/ui/bevel.js +1 -1
- package/src/ui/page-colour.js +17 -9
- package/src/ui/skins/cms.js +1 -1
- package/src/ui/skins/quickcrop.js +1 -1
- package/src/ui/skins/richclay.js +1 -1
- package/src/vendor/hypercms.vendor.js +16 -69
- package/dist/m/c-AL4RBJF2.js +0 -1
- package/dist/m/c-DDDOKPI6.js +0 -3
- package/dist/m/c-EGTIAL6P.js +0 -1
- package/dist/m/c-FSWRGBVQ.js +0 -1
- package/dist/m/c-HDWZEJHM.js +0 -183
- package/dist/m/c-HMYWIHFG.js +0 -1
- package/dist/m/c-JH5FF4NL.js +0 -20
- package/dist/m/c-M6FNQCTQ.js +0 -1
- package/dist/m/c-MHI3GYKF.js +0 -1
- package/dist/m/c-MIL4B2JQ.js +0 -1
- package/dist/m/c-S5IEIZCN.js +0 -1
- package/dist/m/c-VSPAFOKU.js +0 -1
- package/dist/m/c-WFMRX5RZ.js +0 -1
- package/dist/m/core/snapshot-O7II2AGG.js +0 -1
- package/dist/m/loader-ZVPK4ZN3.js +0 -1
- package/dist/m/manifest-5498AFD3.js +0 -1
- package/dist/m/manifest-edit-62A5A00F.js +0 -1
- package/dist/m/plugins/ai-edit-J5VLSW5I.js +0 -6
- package/dist/m/plugins/cms-LRAOZHT7.js +0 -2293
- package/dist/m/plugins/menu-DM2JHPSR.js +0 -1
- package/dist/m/plugins/visitor-drafts-2AWNT23K.js +0 -20
- package/src/ui/skins/cms-toggle.js +0 -15
- /package/dist/m/plugins/{quickcrop-BQ5YYFCH.js.LEGAL.txt → quickcrop-WWF5I422.js.LEGAL.txt} +0 -0
- /package/dist/m/plugins/{richclay-KLGETK77.js.LEGAL.txt → richclay-PGOOJ4OK.js.LEGAL.txt} +0 -0
package/docs/guide.md
CHANGED
|
@@ -315,7 +315,7 @@ Plugins add editing, sync, and UI features to `clay.js`. Most are off by default
|
|
|
315
315
|
| `sync` | off | yes | live sync, `clay.conflicts`, `clay.morph`, presence and the editing marker |
|
|
316
316
|
| `live` | off | yes | `clay.live(name)`: a `clay="live"` JSON tag as a live object |
|
|
317
317
|
| `sortable` | off | no | the `sortable` attribute, `window.Sortable`, `clay:sorted` |
|
|
318
|
-
| `undo` | off | no | `clay.undo` (whole-page undo
|
|
318
|
+
| `undo` | off | no | `clay.undo` (whole-page undo over DOM mutations; leaves Cmd+Z to text fields) |
|
|
319
319
|
| `indicator` | off | no | the save status chip, drawn by a module fetched at the first input or save state |
|
|
320
320
|
| `upload` | off | no | `clay.upload` |
|
|
321
321
|
| `quickcrop` | off | yes | `clay.quickcrop` |
|
|
@@ -326,6 +326,7 @@ Plugins add editing, sync, and UI features to `clay.js`. Most are off by default
|
|
|
326
326
|
| `ai-edit` | off | no | select text, ask an agent to rewrite it; implies `wire` |
|
|
327
327
|
| `demo` | off | yes | fakes saving into `localStorage` for public demos |
|
|
328
328
|
| `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/)) |
|
|
329
|
+
| `visitor-menu` | off | no (draft mode only) | `clay.menu` and `clay.variants` in draft mode: **Download a copy**, [DOM menu items](https://clayjs.com/docs/plugins/menu/#menu-items-from-the-page) and [looks](https://clayjs.com/docs/plugins/menu/#looks), with no owner or host actions ([The visitor menu](https://clayjs.com/docs/plugins/menu/#the-visitor-menu)) |
|
|
329
330
|
| `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)) |
|
|
330
331
|
| `page` | off | reader half only (`page-view`) | the page editor, `clay.page`; implies `live`, `sync` and `upload`, and drops `richclay` unless named |
|
|
331
332
|
| `unsent` | off | no | `unsent="<name>"`: unsent text in chrome survives a redraw and a reload, `clay.unsent` |
|
|
@@ -337,15 +338,20 @@ Plugins add editing, sync, and UI features to `clay.js`. Most are off by default
|
|
|
337
338
|
- 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)).
|
|
338
339
|
- `exclude` wins over implications. `exclude=wire` also drops `ai-edit`.
|
|
339
340
|
- Load order is fixed regardless of how you list them.
|
|
340
|
-
- Use `exclude=richclay` when the
|
|
341
|
-
- Nothing implies `undo`, on purpose: name it when the
|
|
341
|
+
- Use `exclude=richclay` when the document 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.
|
|
342
|
+
- Nothing implies `undo`, on purpose: name it only when the document has discrete DOM commands that should undo as whole-page steps. It leaves Cmd+Z to text fields ([undo: whole-page undo](https://clayjs.com/docs/guide/components/#undo-whole-page-undo)).
|
|
342
343
|
- In view mode, `page` loads only its reader half and nothing it implies. A Document on `plugins=page` resolves to `page-view`, with no live data service, no generic `sync`, and no `upload`. Name `sync` as well (`plugins=page,sync`) when the reader should take its saved files from sync's lane instead of its own.
|
|
343
344
|
- `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.
|
|
344
345
|
- `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.
|
|
346
|
+
- `visitor-menu` loads only in draft mode (`draftOnly` and `editOnly`). A reader in view mode and an owner editing the original skip it and download zero bytes ([The visitor menu](https://clayjs.com/docs/plugins/menu/#the-visitor-menu)).
|
|
345
347
|
|
|
346
348
|
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).
|
|
347
349
|
|
|
348
|
-
**
|
|
350
|
+
**Defaults by document kind:**
|
|
351
|
+
|
|
352
|
+
- **Owned document:** `?plugins=sync,indicator,menu&exclude=richclay`. It gives a plain-text document live sync, a save status chip, and an Edit switch (`source` is on by default in edit mode, and `derive` turns on when `[sap]` is present). A document opens in view even for its owner, which is why `menu` is in the default; declare `<html opens-in="edit">` on a tool document that must edit as soon as it opens.
|
|
353
|
+
- **Free document:** `?plugins=visitor-drafts,visitor-menu,menu&exclude=richclay`. In draft mode, ClayJS drops `menu`, `sync`, `upload`, `people`, `source`, and the other host plugins while `visitor-drafts` and `visitor-menu` run; when the owner edits the original, `visitor-menu` is skipped and `menu` and `source` run; for a view-mode reader, neither menu renders.
|
|
354
|
+
- **Add by task on either kind:** `sortable` for `[sortable]` lists, `upload` for attachments, `people` for collaborator names, `bind-text` for a bound `<textarea>`, `unsent` for `[unsent]` inputs, `indicator` when a free document has no `savestatus` UI of its own, `sync` when the owner of a free document co-edits across tabs, and `undo` only when the document runs discrete DOM commands (adding, removing, or moving records) that should undo as whole-page steps. Leave `undo` off when editing happens inside text inputs, textareas, or `contenteditable`, which already use the browser's native undo; otherwise `undo` also groups idle DOM mutations into page steps on Cmd+Z outside the field. For formatted rich text, drop `exclude=richclay` and use `editable`.
|
|
349
355
|
|
|
350
356
|
### 2.3 Satellites
|
|
351
357
|
|
|
@@ -1979,7 +1985,7 @@ Rules for sapjs in a malleable file:
|
|
|
1979
1985
|
|
|
1980
1986
|
### 7.12 cms: content-managed pages
|
|
1981
1987
|
|
|
1982
|
-
`?plugins=cms` turns a page into a content-managed page. A JSON rules tag maps field names to selectors, and
|
|
1988
|
+
`?plugins=cms` turns a page into a content-managed page. It brings ClayJS's menu plugin automatically; the menu is a small dock in a corner of the page, shown only to someone who can edit. A JSON rules tag maps field names to selectors, and **Open the CMS (Edit content)** in the menu opens a sidebar form (or edits text in place) that writes straight into the DOM.
|
|
1983
1989
|
|
|
1984
1990
|
```html
|
|
1985
1991
|
<script data-rules-name="cms" data-rules-version="1" type="application/json">
|
|
@@ -1989,7 +1995,7 @@ Rules for sapjs in a malleable file:
|
|
|
1989
1995
|
|
|
1990
1996
|
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.
|
|
1991
1997
|
|
|
1992
|
-
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.
|
|
1998
|
+
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. **Open the CMS (Edit content)** in the menu 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.
|
|
1993
1999
|
|
|
1994
2000
|
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.
|
|
1995
2001
|
|
|
@@ -2518,17 +2524,17 @@ The owner is a visitor here too. Signed in, they get the same private draft unle
|
|
|
2518
2524
|
|
|
2519
2525
|
### 12.2 The contract
|
|
2520
2526
|
|
|
2521
|
-
A
|
|
2527
|
+
A document 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 the data contract, and its test proves the round trip:
|
|
2522
2528
|
|
|
2523
2529
|
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)).
|
|
2524
|
-
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
|
|
2525
|
-
3. `plugins=visitor-drafts` on the loader tag
|
|
2530
|
+
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 document'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.
|
|
2531
|
+
3. `plugins=visitor-drafts,visitor-menu` on the loader tag (plus `menu` when the owner also edits the hosted or local file, `exclude=richclay` when there is no rich text, and whatever else the document needs; see [The visitor menu](https://clayjs.com/docs/plugins/menu/#the-visitor-menu)).
|
|
2526
2532
|
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.
|
|
2527
2533
|
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)).
|
|
2528
|
-
6. `<meta name="application-name" content="...">`. The prompt, the intro and the download name the
|
|
2529
|
-
7.
|
|
2530
|
-
8. Optional: a `<template visitor-intro>` ([The download prompt and the intro area](https://clayjs.com/docs/guide/free-documents/#the-download-prompt-and-the-intro-area)). No template, no intro; the prompt still shows.
|
|
2531
|
-
9. A round trip test in your own repo: write a draft, load the
|
|
2534
|
+
6. `<meta name="application-name" content="...">`. The prompt, the intro and the download name the document from it, falling back to the title before the first `:` or `|`.
|
|
2535
|
+
7. An appreciation or feedback link in the document, not in the plugin: a `mailto:` `<a clay-menu-item data-id="...">` element with its `cms-template` seed row in a hidden container outside `clay="freeze"`, covered by the `api` rules tag ([Menu items from the page](https://clayjs.com/docs/plugins/menu/#menu-items-from-the-page)).
|
|
2536
|
+
8. Optional: a `<template visitor-intro>` or `<template visitor-intro="manual">` ([The download prompt and the intro area](https://clayjs.com/docs/guide/free-documents/#the-download-prompt-and-the-intro-area)). No template, no intro; the prompt still shows.
|
|
2537
|
+
9. A round trip test in your own repo: write a draft, load the document again with that storage, and check that the values and the computed counts come back.
|
|
2532
2538
|
|
|
2533
2539
|
Bump the slot (`-v2`) only when an old draft cannot fit the new rules. A draft that no longer fits is parked, not thrown away ([Restoring and redrawing](https://clayjs.com/docs/guide/free-documents/#restoring-and-redrawing)).
|
|
2534
2540
|
|
|
@@ -2556,10 +2562,11 @@ The rung is where a free document lives, so the details matter:
|
|
|
2556
2562
|
|
|
2557
2563
|
`clay.isDraftMode` is true, and everything else follows from it:
|
|
2558
2564
|
|
|
2559
|
-
- **The plugins that talk to the host are dropped**, even when the
|
|
2565
|
+
- **The plugins that talk to the host are dropped**, even when the document 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.
|
|
2566
|
+
- **`menu` stays dropped for visitors, and `visitor-menu` runs instead.** `menu` carries the Edit switch, SEO settings, presence, look deletion (`keep()`), and `/_/meta` capability checks, all of which belong to the owner. When the loader tag names `visitor-menu`, draft mode loads `visitor-menu` in its place and mounts `clay.menu` with **Download a copy**, [DOM menu items](https://clayjs.com/docs/plugins/menu/#menu-items-from-the-page), and [looks](https://clayjs.com/docs/plugins/menu/#looks), with no owner code and no host requests ([The visitor menu](https://clayjs.com/docs/plugins/menu/#the-visitor-menu)).
|
|
2560
2567
|
- **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.
|
|
2561
2568
|
- **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.
|
|
2562
|
-
- **`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
|
|
2569
|
+
- **`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 document 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)).
|
|
2563
2570
|
- **`clay.visitorDrafts`** is the plugin's whole surface:
|
|
2564
2571
|
|
|
2565
2572
|
| Member | What it is |
|
|
@@ -2569,12 +2576,13 @@ The rung is where a free document lives, so the details matter:
|
|
|
2569
2576
|
| `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) |
|
|
2570
2577
|
| `hasDraft()` | is there a stored draft for this slot |
|
|
2571
2578
|
| `restored()` | `{ restored, hadDraft, parked }` for this load, or `null` when the plugin is not active |
|
|
2572
|
-
| `saveNow()` | write the
|
|
2579
|
+
| `saveNow()` | write the document to the slot now; `true` when it landed, `false` when it did not |
|
|
2573
2580
|
| `reset()` | delete this slot and reload the page; nothing outside draft mode |
|
|
2574
2581
|
| `onRestore(fn)` | call `fn(detail)` at once when the restore already ran, otherwise on `clay:drafts-restored` |
|
|
2575
2582
|
| `editOriginal()` | navigate to this page with `?editmode=true` |
|
|
2576
2583
|
| `backToDraft()` | navigate back with `editmode` removed |
|
|
2577
2584
|
| `downloadUrl()` | the download link the prompt is using, with `src` |
|
|
2585
|
+
| `download()` | flush the draft store and trigger the prompt's download |
|
|
2578
2586
|
| `source` | the `?src=` this slot remembers, or `null` ([The download prompt and the intro area](https://clayjs.com/docs/guide/free-documents/#the-download-prompt-and-the-intro-area)) |
|
|
2579
2587
|
|
|
2580
2588
|
- **Three events** on `document`: `clay:drafts-restored` with `{ restored, hadDraft, parked }`, `clay:drafts-saved` with `{ data }`, and `clay:drafts-store-failed` with `{ error }`.
|
|
@@ -2599,13 +2607,16 @@ Both are runtime chrome: `clay="editor-ui"` and `sap-ignore`, built by the plugi
|
|
|
2599
2607
|
|
|
2600
2608
|
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.
|
|
2601
2609
|
|
|
2602
|
-
**The intro** is authored per
|
|
2610
|
+
**The intro** is authored per document as `<template visitor-intro>` (the default) or `<template visitor-intro="manual">`. The template holds the author's whole strip: its markup and any `<style>` it needs. The plugin clones it into `<section class="clay-intro">` at the very top of `document.body`, before the document's own content.
|
|
2611
|
+
|
|
2612
|
+
- **Default (`<template visitor-intro>`):** adds a quiet `x` button (`Hide this introduction`) in the corner and dismisses for good when the visitor clicks `x` or scrolls the strip past the top of the viewport. UI memory records `"closed"` or `"scrolled"`, and no menu item is added.
|
|
2613
|
+
- **Manual (`<template visitor-intro="manual">`):** adds a **Hide intro** button in the corner instead of `x`, and never collapses or dismisses on scroll. Clicking **Hide intro** (or `[visitor-intro-dismiss]`) records `"closed"` in UI memory, removes the section, focuses the document, and adds **Show intro** (`id: "clay.intro"`) to `clay.menu` (waiting for `clay:menu-ready` when the menu mounts later, and also adding **Show intro** on loads where the intro starts hidden because a draft already exists or `done` is `"closed"`). Choosing **Show intro** clears `intro.done`, prepends the same `<section>` back onto `document.body` without re-cloning the template, removes **Show intro** from the menu, and scrolls to the top.
|
|
2603
2614
|
|
|
2604
|
-
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.
|
|
2615
|
+
A `<script type="module">` inside the template runs when the intro first 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.
|
|
2605
2616
|
|
|
2606
2617
|
```html
|
|
2607
|
-
<template visitor-intro>
|
|
2608
|
-
<p>One paragraph on what the
|
|
2618
|
+
<template visitor-intro="manual">
|
|
2619
|
+
<p>One paragraph on what the document does and who it is for.</p>
|
|
2609
2620
|
</template>
|
|
2610
2621
|
```
|
|
2611
2622
|
|
|
@@ -2615,11 +2626,11 @@ When there is a release video, add it as the first element of the template, and
|
|
|
2615
2626
|
<video src="/_/uploads/assets/kanban/release.mp4" poster="/_/uploads/assets/kanban/release-poster.jpg" controls playsinline preload="metadata"></video>
|
|
2616
2627
|
```
|
|
2617
2628
|
|
|
2618
|
-
An element inside the template with `[visitor-intro-start]` scrolls to the
|
|
2629
|
+
An element inside the template with `[visitor-intro-start]` scrolls smoothly to the document below the intro, and one with `[visitor-intro-dismiss]` closes the intro the same way the corner button does. A deep link (a URL with a `#...`) skips the intro on load so a shared link opens where it points, an existing draft skips it too, and `?intro=1` forces it open.
|
|
2619
2630
|
|
|
2620
|
-
**The prompt** needs nothing from the
|
|
2631
|
+
**The prompt** needs nothing from the document but its name: a small tab at the bottom right ("This app is free to keep") that opens into a card holding the download link and "Don't show again", with a minimize button in its corner. The tab's X and the card's minimize button leave a small "Keep" button with the download icon that reopens the download card. That choice survives reload. It opens itself once after the visitor has touched three fields. "Don't show again" stops those reminders for that slot but keeps the icon, including for previously dismissed notices. A storage failure opens it in its error state, and the next good save clears it. In that error state the download link does not go to the host, whose download page would read the stored copy that just failed to be written: the plugin builds the file from the live page in the browser instead, named after the page file with `.htmlclay` at the end.
|
|
2621
2632
|
|
|
2622
|
-
**The download link** is `/_/download<path>?as=htmlclay`, with the
|
|
2633
|
+
**The download link** is `/_/download<path>?as=htmlclay`, with the document's path (`/_/download?as=htmlclay` for `/`) and `&src=<src>` when this slot has one. `clay.visitorDrafts.downloadUrl()` returns it. **`?src=` is first touch**: the first valid value the page sees for this slot is kept and rides into the download link and every signup. Valid values match `/^[a-z0-9][a-z0-9-]{0,39}$/`.
|
|
2623
2634
|
|
|
2624
2635
|
**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()`.
|
|
2625
2636
|
|
|
@@ -2739,7 +2750,7 @@ The `:legacy` marker is what stops a reset from coming back: once ClayJS has cop
|
|
|
2739
2750
|
|
|
2740
2751
|
1. Loads with `clay.js` as a classic script at the end of `<body>`, every tag on the same version prefix.
|
|
2741
2752
|
2. `<html autosave>` unless it is a deliberate manual-save writer.
|
|
2742
|
-
3. `sync` is in
|
|
2753
|
+
3. On an owned collaborative document, `sync` is in `plugins=`; on a [free document](https://clayjs.com/docs/guide/free-documents/), `visitor-drafts,visitor-menu,menu` is in `plugins=`. Add `exclude=richclay` if there is no rich text, and add task plugins (`sortable`, `upload`, `undo`, `indicator`) only when the document uses them.
|
|
2743
2754
|
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).
|
|
2744
2755
|
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"`.
|
|
2745
2756
|
6. Records are authored in the saved form (`inert-contenteditable`, `disabled`, `readonly`); templates in the active form.
|
|
@@ -2748,9 +2759,9 @@ The `:legacy` marker is what stops a reset from coming back: once ClayJS has cop
|
|
|
2748
2759
|
9. All chrome is cloned from a `<template>` into `clay="editor-ui"` after `clay.ready`; nothing in the file is chrome.
|
|
2749
2760
|
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.
|
|
2750
2761
|
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.
|
|
2751
|
-
12.
|
|
2762
|
+
12. When `undo` is loaded, commands are wrapped in `clay.undo?.commit(label, fn)` (falling back to `fn()` when `undo` is absent); derived views are drawn in `clay.effect`, with no effect warnings in the console.
|
|
2752
2763
|
13. Views whose record a peer deleted are closed, in the effect or a `clay:sync-applied` handler.
|
|
2753
|
-
14. The
|
|
2764
|
+
14. The document script starts with `clay.start`; all listeners are delegated.
|
|
2754
2765
|
15. Nothing writes the record at boot.
|
|
2755
2766
|
16. Edit-only calls are guarded for view mode, and edit actions check `clay.isEditMode`.
|
|
2756
2767
|
17. `clay.confirm` calls have a rejection handler.
|
|
@@ -2759,7 +2770,7 @@ The `:legacy` marker is what stops a reset from coming back: once ClayJS has cop
|
|
|
2759
2770
|
20. Images use `clay.upload`, embedding only on `unsupported`.
|
|
2760
2771
|
21. The file opens in view mode with JavaScript disabled and shows correct content and no edit controls.
|
|
2761
2772
|
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.
|
|
2762
|
-
23. A tool
|
|
2773
|
+
23. A tool document 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.
|
|
2763
2774
|
|
|
2764
2775
|
### 13.2 Testing a document that changes modes
|
|
2765
2776
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panphora/clayjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "clayjs: malleable documents. Save lifecycle for self-saving HTML.",
|
|
6
6
|
"license": "MIT-0",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"@fontsource-variable/work-sans": "5.3.0",
|
|
97
97
|
"@fontsource/ibm-plex-mono": "5.3.0",
|
|
98
98
|
"@fontsource/ibm-plex-sans": "5.3.0",
|
|
99
|
-
"@panphora/bevel": "0.
|
|
99
|
+
"@panphora/bevel": "0.3.2",
|
|
100
100
|
"@web/test-runner": "^0.20.2",
|
|
101
101
|
"@web/test-runner-commands": "^0.9.0",
|
|
102
102
|
"@web/test-runner-playwright": "^0.11.1",
|
package/src/core/save-replace.js
CHANGED
|
@@ -24,12 +24,15 @@ function failed() {
|
|
|
24
24
|
*
|
|
25
25
|
* @param {string} html - the whole document, as it should be stored
|
|
26
26
|
* @param {Object} [options]
|
|
27
|
-
* @param {?string} [options.etag] - the stamp to replace,
|
|
27
|
+
* @param {?string} [options.etag] - the stamp to replace. Left out, it is this tab's
|
|
28
|
+
* stamp at the moment of writing; given, even as null, it is exactly that stamp, so a
|
|
29
|
+
* caller that captured the document earlier never writes against a later version.
|
|
28
30
|
* @returns {Promise<{ok: true, etag: ?string}|{ok: false, code: "conflict"|"forbidden"|"unsupported"|"failed"}>}
|
|
29
31
|
*/
|
|
30
32
|
export async function replace(html, { etag } = {}) {
|
|
31
33
|
if (typeof html !== "string" || html === "") return failed();
|
|
32
34
|
|
|
35
|
+
const pinned = etag !== undefined;
|
|
33
36
|
const given = typeof etag === "string" && etag ? etag : null;
|
|
34
37
|
// Discovery is what says whether the capability exists, and it is also where a
|
|
35
38
|
// tab that has never saved learns the stamp it is about to replace.
|
|
@@ -38,7 +41,7 @@ export async function replace(html, { etag } = {}) {
|
|
|
38
41
|
|
|
39
42
|
// With no stamp the write would be unconditional, which is exactly what this lane
|
|
40
43
|
// promises never to send. A reload gives the tab the stamp it is missing.
|
|
41
|
-
const stamp = given
|
|
44
|
+
const stamp = pinned ? given : lastSeenEtag();
|
|
42
45
|
if (!stamp) return { ok: false, code: "conflict" };
|
|
43
46
|
const controller = new AbortController();
|
|
44
47
|
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
@@ -103,7 +103,15 @@ export function* serializeForSyncSteps(clone) {
|
|
|
103
103
|
const shell = bareRoot.outerHTML;
|
|
104
104
|
const endTag = `</${bareRoot.localName}>`;
|
|
105
105
|
const out = ["<!DOCTYPE html>", shell.slice(0, shell.length - endTag.length)];
|
|
106
|
-
|
|
106
|
+
const blocks = noscriptBlocks(clone);
|
|
107
|
+
const raw = blocks.length ? swapNoscriptMarkers(blocks) : [];
|
|
108
|
+
try {
|
|
109
|
+
yield* serializeChildrenSteps(clone, clonePreventingOnclone(clone, false), out, { bytes: 0 });
|
|
110
|
+
} finally {
|
|
111
|
+
for (const [el, kids] of blocks) el.replaceChildren(...kids);
|
|
112
|
+
}
|
|
107
113
|
out.push(endTag);
|
|
108
|
-
|
|
114
|
+
let html = out.join('');
|
|
115
|
+
for (const [marker, text] of raw) html = html.split(marker).join(text);
|
|
116
|
+
return html;
|
|
109
117
|
}
|
package/src/core/snapshot.js
CHANGED
|
@@ -294,6 +294,30 @@ export function noscriptBlocks(clone) {
|
|
|
294
294
|
return blocks;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Each <noscript> of a document parsed with scripting off, given back the one text node a
|
|
299
|
+
* scripting parser keeps. DOMParser parses with scripting off, so a received <noscript>
|
|
300
|
+
* holds its markup as elements and its escaped text decoded: merged as elements, a fallback
|
|
301
|
+
* stylesheet would go live, and written raw, decoded text would become markup. Its
|
|
302
|
+
* innerHTML in that document is the source again, escaping included.
|
|
303
|
+
*/
|
|
304
|
+
export function noscriptsAsText(doc) {
|
|
305
|
+
const found = [];
|
|
306
|
+
const collect = (node) => {
|
|
307
|
+
for (const child of node.childNodes) {
|
|
308
|
+
if (child.nodeType !== 1) continue;
|
|
309
|
+
if (child.localName === 'noscript' && child.namespaceURI === XHTML_NS) {
|
|
310
|
+
if (child.firstChild) found.push(child);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
collect(child.localName === 'template' && child.content ? child.content : child);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
collect(doc.documentElement ?? doc);
|
|
317
|
+
for (const el of found) el.replaceChildren(el.ownerDocument.createTextNode(el.innerHTML));
|
|
318
|
+
return doc;
|
|
319
|
+
}
|
|
320
|
+
|
|
297
321
|
/** Swap each block's text for a marker, and answer the text each marker stands for. */
|
|
298
322
|
export function swapNoscriptMarkers(blocks) {
|
|
299
323
|
const nonce = Math.random().toString(36).slice(2);
|
|
@@ -941,11 +965,23 @@ export function serializeForSync(clone) {
|
|
|
941
965
|
// truncate the broadcast.
|
|
942
966
|
const shell = bareRoot.outerHTML;
|
|
943
967
|
const endTag = `</${bareRoot.localName}>`;
|
|
968
|
+
// A <noscript>'s text is written raw, as serializeClone writes it: escaped, a head
|
|
969
|
+
// <noscript> parses as text on the receiving side, which closes the head and puts
|
|
970
|
+
// the fallback markup at the top of <body>, and that tab then saves it.
|
|
971
|
+
const blocks = noscriptBlocks(clone);
|
|
972
|
+
const raw = blocks.length ? swapNoscriptMarkers(blocks) : [];
|
|
973
|
+
let inner;
|
|
974
|
+
try {
|
|
975
|
+
inner = clone.innerHTML;
|
|
976
|
+
} finally {
|
|
977
|
+
for (const [el, kids] of blocks) el.replaceChildren(...kids);
|
|
978
|
+
}
|
|
979
|
+
for (const [marker, text] of raw) inner = inner.split(marker).join(text);
|
|
944
980
|
// The doctype, so this artifact is a complete document like every other one this
|
|
945
981
|
// module produces. Spec section 2 asks for that, and it costs nothing on the wire:
|
|
946
982
|
// a receiver parses the string and morphs documentElement against documentElement,
|
|
947
983
|
// so the prologue is consumed by the parser and never reaches the morph.
|
|
948
|
-
return "<!DOCTYPE html>" + shell.slice(0, shell.length - endTag.length) +
|
|
984
|
+
return "<!DOCTYPE html>" + shell.slice(0, shell.length - endTag.length) + inner + endTag;
|
|
949
985
|
}
|
|
950
986
|
|
|
951
987
|
/** A private deep copy of a capture's clone, for work that outlives the capture event. */
|
package/src/core/upgrade-run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { replace as replaceDocument } from "./save-replace.js";
|
|
2
|
-
import { lastSeenEtag } from "./etag.js";
|
|
2
|
+
import { lastSeenEtag, seedEtag } from "./etag.js";
|
|
3
3
|
import { claimDocumentOperation, releaseDocumentOperation } from "./save-core.js";
|
|
4
4
|
import { isNewerVersion } from "./version.js";
|
|
5
5
|
import { upgrade, readMeta } from "../vendor/hyper-html-api-upgrade.vendor.js";
|
|
@@ -281,6 +281,16 @@ async function applyUnderSlot(update, { doc, loc, fetchFn, clay, reload, replace
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// The stamp is read before the capture, so it can only be older than the bytes the
|
|
285
|
+
// upgrade starts from: a save that lands while the capture runs or the release
|
|
286
|
+
// downloads makes the write below fail instead of being overwritten by it.
|
|
287
|
+
// A tab that has never saved learns its stamp from discovery here, before the capture,
|
|
288
|
+
// not inside the write after the download, when it would describe a later version.
|
|
289
|
+
if (etag === undefined && !lastSeenEtag()) {
|
|
290
|
+
try { await seedEtag(); } catch {}
|
|
291
|
+
}
|
|
292
|
+
const stamp = etag !== undefined ? etag : lastSeenEtag();
|
|
293
|
+
|
|
284
294
|
// An editing tab's live article holds the editor, not the page: the upgrade starts from
|
|
285
295
|
// the document as a save writes it.
|
|
286
296
|
let source = doc;
|
|
@@ -290,6 +300,10 @@ async function applyUnderSlot(update, { doc, loc, fetchFn, clay, reload, replace
|
|
|
290
300
|
} catch {
|
|
291
301
|
return { ok: false, code: "failed", message: SAVE_FAILED };
|
|
292
302
|
}
|
|
303
|
+
} else {
|
|
304
|
+
// A view-mode tab's live document can change under the download (a disk frame from
|
|
305
|
+
// another tab), so it is copied now, at the same moment as the stamp.
|
|
306
|
+
source = new DOMParser().parseFromString("<!DOCTYPE html>\n" + doc.documentElement.outerHTML, "text/html");
|
|
293
307
|
}
|
|
294
308
|
const prepared = await prepare(update, { doc: source, loc, fetchFn });
|
|
295
309
|
if (!prepared.ok) return prepared;
|
|
@@ -301,7 +315,7 @@ async function applyUnderSlot(update, { doc, loc, fetchFn, clay, reload, replace
|
|
|
301
315
|
: replaceDocument;
|
|
302
316
|
let result;
|
|
303
317
|
try {
|
|
304
|
-
result = await save(prepared.html, { etag:
|
|
318
|
+
result = await save(prepared.html, { etag: stamp });
|
|
305
319
|
} catch {
|
|
306
320
|
return { ok: false, code: "failed", message: SAVE_FAILED };
|
|
307
321
|
}
|
package/src/loader-logic.js
CHANGED
|
@@ -82,6 +82,9 @@ export const PLUGIN_PATHS = {
|
|
|
82
82
|
// owner editing the original, gets the owner module: the chip when it applies and
|
|
83
83
|
// the inert `clay.visitorDrafts`, without the drafts UI's 24 KB.
|
|
84
84
|
"visitor-drafts": { path: "plugins/visitor-drafts.js", ownerPath: "plugins/visitor-drafts-owner.js", editOnly: false, default: false },
|
|
85
|
+
// `draft: true` loads it only in a visitor's draft. The key is short on purpose: every
|
|
86
|
+
// reader downloads this file, and the reader's Brotli ceiling counts it.
|
|
87
|
+
"visitor-menu": { path: "plugins/visitor-menu.js", editOnly: true, draft: true },
|
|
85
88
|
// An editor textarea outside the saved records, bound to one record's text
|
|
86
89
|
// element. Edit mode only: the page that asks for it is the page whose owner
|
|
87
90
|
// writes in it.
|
|
@@ -147,6 +150,7 @@ export const MODULES = {
|
|
|
147
150
|
"plugins/demo.js": () => import("./plugins/demo.js"),
|
|
148
151
|
"plugins/visitor-drafts.js": () => import("./plugins/visitor-drafts.js"),
|
|
149
152
|
"plugins/visitor-drafts-owner.js": () => import("./plugins/visitor-drafts-owner.js"),
|
|
153
|
+
"plugins/visitor-menu.js": () => import("./plugins/visitor-menu.js"),
|
|
150
154
|
"plugins/bind-text.js": () => import("./plugins/bind-text.js"),
|
|
151
155
|
"plugins/source.js": () => import("./plugins/source.js"),
|
|
152
156
|
"plugins/derive.js": () => import("./plugins/derive.js"),
|
|
@@ -155,7 +159,7 @@ export const MODULES = {
|
|
|
155
159
|
|
|
156
160
|
// `source` is last on purpose: its install captures a save clone, so it wants every
|
|
157
161
|
// plugin that registers a document transform to have registered it first.
|
|
158
|
-
const PLUGIN_ORDER = ["visitor-drafts", "richclay", "indicator", "sortable", "undo", "unsent", "quickcrop", "upload", "people", "menu", "upgrade", "cms", "page-view", "sync", "live", "page", "message", "like", "wire", "ai-edit", "demo", "derive", "bind-text", "source"];
|
|
162
|
+
const PLUGIN_ORDER = ["visitor-drafts", "visitor-menu", "richclay", "indicator", "sortable", "undo", "unsent", "quickcrop", "upload", "people", "menu", "upgrade", "cms", "page-view", "sync", "live", "page", "message", "like", "wire", "ai-edit", "demo", "derive", "bind-text", "source"];
|
|
159
163
|
|
|
160
164
|
// A plugin that cannot do its whole job alone. hypercms reads the cropper through
|
|
161
165
|
// a capability lookup (`clay.quickcrop`) and silently uploads the raw file when it
|
|
@@ -182,7 +186,10 @@ const PLUGIN_ORDER = ["visitor-drafts", "richclay", "indicator", "sortable", "un
|
|
|
182
186
|
// image and file blocks send through `clay.upload`: without the plugin every drop
|
|
183
187
|
// ends in "File uploads are not available", while with it a host that stores no
|
|
184
188
|
// files answers `unsupported` and the editor offers to embed the file instead.
|
|
185
|
-
|
|
189
|
+
// The menu is how a person opens the CMS: hypercms adds no button of its own. It loads
|
|
190
|
+
// in view too, because that is where a person starts, and it shows itself only to
|
|
191
|
+
// someone who can edit. A page with its own control uses exclude=menu.
|
|
192
|
+
const IMPLIES = { cms: ["quickcrop", "upload", "menu"], "ai-edit": ["wire"], page: ["live", "sync", "upload"] };
|
|
186
193
|
|
|
187
194
|
// A Document's editor owns its article's rich text, so richclay (for [editable]) has
|
|
188
195
|
// nothing to do there; plugins=page,richclay still loads it.
|
|
@@ -274,7 +281,7 @@ export function resolveModules(params, isEditMode, { isDraftMode = false, hasSap
|
|
|
274
281
|
for (const name of PLUGIN_ORDER) {
|
|
275
282
|
if (!enabled.has(name)) continue;
|
|
276
283
|
const spec = PLUGIN_PATHS[name];
|
|
277
|
-
if (spec.editOnly && !isEditMode) continue;
|
|
284
|
+
if ((spec.editOnly && !isEditMode) || (spec.draft && !isDraftMode)) continue;
|
|
278
285
|
(LATE.has(name) ? late : plugins).push(spec.ownerPath && !isDraftMode ? spec.ownerPath : spec.path);
|
|
279
286
|
}
|
|
280
287
|
return { core, plugins, late };
|
package/src/plugins/ai-edit.js
CHANGED
|
@@ -670,17 +670,13 @@ function buildChrome() {
|
|
|
670
670
|
|
|
671
671
|
window.addEventListener('scroll', () => { positionChrome(); hideChip(); }, { passive: true, signal });
|
|
672
672
|
window.addEventListener('resize', () => { placeBubble(); if (panel && !panel.hidden) fitTextarea(); positionChrome(); }, { passive: true, signal });
|
|
673
|
-
//
|
|
673
|
+
// A docked CMS sidebar shifts the page under this chrome.
|
|
674
674
|
const replace = () => { placeBubble(); positionChrome(); };
|
|
675
675
|
document.addEventListener('hcms:open', () => { replace(); setTimeout(replace, 350); }, { signal });
|
|
676
676
|
document.addEventListener('hcms:close', () => { replace(); setTimeout(replace, 350); }, { signal });
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
if (records.some((r) => touched(r.addedNodes) || touched(r.removedNodes))) replace();
|
|
681
|
-
});
|
|
682
|
-
cmsToggle.observe(document.body, { childList: true });
|
|
683
|
-
observers.push(cmsToggle);
|
|
677
|
+
const bodyChildren = new MutationObserver(() => syncBubble());
|
|
678
|
+
bodyChildren.observe(document.body, { childList: true });
|
|
679
|
+
observers.push(bodyChildren);
|
|
684
680
|
|
|
685
681
|
// A target removed from the page ends its edit, so the save hold and the paused
|
|
686
682
|
// observers never outlive it. A running bar is no exception: it is torn down with
|
|
@@ -748,17 +744,10 @@ function refreshHelperState() {
|
|
|
748
744
|
}).catch(() => {});
|
|
749
745
|
}
|
|
750
746
|
|
|
751
|
-
// The bubble sits in the bottom-right corner
|
|
752
|
-
// toggle when the page has one, bottoms aligned, so the two never overlap.
|
|
747
|
+
// The bubble sits in the bottom-right corner.
|
|
753
748
|
function placeBubble() {
|
|
754
749
|
if (!docBubble) return;
|
|
755
|
-
|
|
756
|
-
const rect = host && host.getBoundingClientRect();
|
|
757
|
-
const beside = !!rect && rect.width > 0 && rect.height > 0;
|
|
758
|
-
docBubble.pin({
|
|
759
|
-
right: beside ? `${Math.round((document.documentElement.clientWidth || window.innerWidth) - rect.left + 8)}px` : '16px',
|
|
760
|
-
bottom: beside ? `${Math.round((document.documentElement.clientHeight || window.innerHeight) - rect.bottom)}px` : '16px',
|
|
761
|
-
});
|
|
750
|
+
docBubble.pin({ right: '16px', bottom: '16px' });
|
|
762
751
|
paintLook(docBubble);
|
|
763
752
|
}
|
|
764
753
|
|
package/src/plugins/cms.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
// The CMS shell,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The CMS shell, inline layer and ghost rows, with their --mirk-* tokens pinned to Bevel
|
|
2
|
+
// and the Bevel system mono face. The CMS keeps its own stylesheet and its own light/dark
|
|
3
|
+
// switching.
|
|
4
4
|
import { installSkin } from "../ui/vendor-skin.js";
|
|
5
5
|
import { CSS } from "../ui/skins/cms.js";
|
|
6
|
-
import { CSS as TOGGLE_CSS, ROOTS as TOGGLE_ROOTS } from "../ui/skins/cms-toggle.js";
|
|
7
6
|
import { cms as hypercms } from "../vendor/hypercms.vendor.js";
|
|
8
7
|
|
|
9
8
|
export { cms, default } from "../vendor/hypercms.vendor.js";
|
|
10
9
|
|
|
11
10
|
installSkin("cms", CSS);
|
|
12
|
-
// The toggle takes the page's scheme, so Bevel's face and ink resolve the way the page reads.
|
|
13
|
-
installSkin("cms-toggle", TOGGLE_CSS, { roots: TOGGLE_ROOTS });
|
|
14
11
|
|
|
15
12
|
const fault = (message, code) => Object.assign(new Error(`clay:cms: ${message}`), { code });
|
|
16
13
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const SELECTOR = "[clay-menu-item]:not([cms-template])";
|
|
2
|
+
|
|
3
|
+
const canSave = () => window.clay?.mode === "edit" || window.clay?.mode === "draft";
|
|
4
|
+
|
|
5
|
+
async function removeAndSave(el) {
|
|
6
|
+
await window.clay?.ready;
|
|
7
|
+
if (!canSave() || !el.isConnected) return false;
|
|
8
|
+
const undo = window.clay?.undo;
|
|
9
|
+
if (undo && typeof undo.commit === "function") undo.commit("Remove this item", () => el.remove());
|
|
10
|
+
else el.remove();
|
|
11
|
+
window.clay?.save?.soon?.();
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function attachDomItems({ addItem, removeItem, beforeOpen }) {
|
|
16
|
+
const shown = new Set();
|
|
17
|
+
function sync() {
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const el of document.querySelectorAll(SELECTOR)) {
|
|
20
|
+
const key = el.getAttribute("data-id") || el.id;
|
|
21
|
+
if (!key) {
|
|
22
|
+
console.warn("[clay] A clay-menu-item needs a data-id or an id.", el);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const id = `dom.${key}`;
|
|
26
|
+
seen.add(id);
|
|
27
|
+
const dismiss = el.getAttribute("clay-menu-dismiss");
|
|
28
|
+
addItem({
|
|
29
|
+
id,
|
|
30
|
+
label: el.textContent.trim(),
|
|
31
|
+
href: el.getAttribute("href") || "",
|
|
32
|
+
section: el.getAttribute("clay-menu-section") || undefined,
|
|
33
|
+
quiet: el.hasAttribute("clay-menu-quiet"),
|
|
34
|
+
run: el.hasAttribute("href") ? null : () => el.click(),
|
|
35
|
+
dismiss: dismiss && canSave() ? { label: dismiss, run: () => removeAndSave(el) } : undefined,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
for (const id of shown) if (!seen.has(id)) removeItem(id);
|
|
39
|
+
shown.clear();
|
|
40
|
+
for (const id of seen) shown.add(id);
|
|
41
|
+
}
|
|
42
|
+
beforeOpen.push(sync);
|
|
43
|
+
for (const name of ["clay:drafts-restored", "clay:sync-applied", "clay:modechange"]) document.addEventListener(name, sync);
|
|
44
|
+
sync();
|
|
45
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const OFF = "not all";
|
|
2
|
+
export const looks = () => [...document.querySelectorAll("head style[clay-variant]")];
|
|
3
|
+
export const nameOf = (style) => style.getAttribute("clay-variant");
|
|
4
|
+
export const labelOf = (style) => style.getAttribute("clay-variant-label") || `Variant ${nameOf(style)}`;
|
|
5
|
+
|
|
6
|
+
export function wantedLook() {
|
|
7
|
+
const name = document.documentElement.getAttribute("look");
|
|
8
|
+
return name && looks().some((style) => nameOf(style) === name) ? name : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function activeLook() {
|
|
12
|
+
const all = looks();
|
|
13
|
+
const wanted = wantedLook();
|
|
14
|
+
return all.find((style) => nameOf(style) === wanted) || all.find((style) => style.getAttribute("media") !== OFF) || all[0] || null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function paintLook(name = wantedLook()) {
|
|
18
|
+
document.querySelector("style[clay-look-hint]")?.remove();
|
|
19
|
+
if (!name) return;
|
|
20
|
+
for (const style of looks()) {
|
|
21
|
+
if (nameOf(style) === name) style.removeAttribute("media");
|
|
22
|
+
else if (style.getAttribute("media") !== OFF) style.setAttribute("media", OFF);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const carriesLook = () => /"html@look"/.test(document.querySelector('script[data-rules-name~="api"]')?.textContent ?? "");
|
|
27
|
+
|
|
28
|
+
export function recordLook(name) {
|
|
29
|
+
if (!carriesLook()) return;
|
|
30
|
+
if (name) document.documentElement.setAttribute("look", name);
|
|
31
|
+
else document.documentElement.removeAttribute("look");
|
|
32
|
+
}
|