@panphora/clayjs 1.22.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 +1228 -715
- package/dist/m/attrs/{save-freeze-MTFVVYV5.js → save-freeze-JZHKRQLR.js} +1 -1
- package/dist/m/{c-TV7JHSRL.js → c-24PMUJIV.js} +1 -1
- package/dist/m/{c-NBNRLZXO.js → c-2AYVYEI4.js} +1 -1
- package/dist/m/{c-VIW5X35H.js → c-2L6O5PDS.js} +1 -1
- package/dist/m/{c-FNIGX4FI.js → c-2RYOGUQM.js} +1 -1
- package/dist/m/{c-LDUNOR57.js → c-2Z22FCFK.js} +13 -25
- package/dist/m/{c-DKFSOJCV.js → c-2ZVOEU4M.js} +1 -1
- package/dist/m/{c-BJPZCWAV.js → c-37NAONZA.js} +1 -1
- package/dist/m/{c-CSNE5EIF.js → c-4PO3YGDC.js} +1 -1
- package/dist/m/{c-UWT4CXQ4.js → c-5OJ7YF2N.js} +1 -1
- package/dist/m/{c-CAWNFER6.js → c-5TIZ2I64.js} +1 -1
- package/dist/m/{c-ZO6VL2SC.js → c-5WMXEKFH.js} +1 -1
- package/dist/m/{c-4TWARTLS.js → c-6RDOKLN7.js} +1 -1
- package/dist/m/{c-6RC66MYY.js → c-6TYGD5AB.js} +1 -1
- package/dist/m/c-A4IP6MBC.js +1 -0
- package/dist/m/{c-ZAHTFCXQ.js → c-ALDVFPWL.js} +1 -1
- package/dist/m/{c-LV5MABYD.js → c-AYVGD6A6.js} +1 -1
- package/dist/m/{c-N33TPVTL.js → c-B6O7AJYG.js} +1 -1
- package/dist/m/{c-P3WATEDR.js → c-BCGWJYXW.js} +1 -1
- package/dist/m/{c-4OGEVKR3.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-Q5KAQJAG.js → c-DTMZ3ZHI.js} +1 -1
- package/dist/m/{c-GY4ODHO2.js → c-ELYIIYRS.js} +1 -1
- package/dist/m/{c-67LGZ52V.js → c-FCSZ3DC5.js} +1 -1
- package/dist/m/{c-NLWEZYKJ.js → c-FQGHR32O.js} +1 -1
- package/dist/m/{c-PCEMXY2K.js → c-G4PWUDD5.js} +1 -1
- package/dist/m/{c-6KBKYLPO.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-TMT5TL65.js → c-HCNWJE4A.js} +2 -2
- package/dist/m/{c-DZBZ57EE.js → c-JCOQVZYS.js} +1 -1
- package/dist/m/{c-FAXQU7MQ.js → c-JGHEACGK.js} +1 -1
- package/dist/m/{c-PWBJVHA4.js → c-JGLX6TBK.js} +1 -1
- package/dist/m/c-JNRKHYWO.js +1 -0
- package/dist/m/{c-6UEFPHHE.js → c-KDXIJEEF.js} +1 -1
- package/dist/m/{c-FMCPQ4VT.js → c-L4KMT7FO.js} +2 -2
- package/dist/m/{c-OI2BYWVV.js → c-LCYPMULG.js} +1 -1
- package/dist/m/{c-4RFZROCF.js → c-LIGQYZ7M.js} +4 -4
- package/dist/m/{c-QHVCUTJV.js → c-LKIWEPAV.js} +1 -1
- package/dist/m/{c-EDSPKYMA.js → c-LUXAB2XI.js} +1 -1
- package/dist/m/{c-DEAFCHDO.js → c-LWIDDWAR.js} +2 -2
- package/dist/m/c-MPSKIU2I.js +1 -0
- package/dist/m/{c-6PBXRWDX.js → c-NK33MMYF.js} +1 -1
- package/dist/m/{c-F5AER2ID.js → c-NQLWOHLH.js} +1 -1
- package/dist/m/{c-L7K5EXU2.js → c-NTHUTABN.js} +1 -1
- package/dist/m/{c-4PHDUV3H.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-LF6HNVPU.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-Z7RJANGO.js → c-PXLTUYZC.js} +1 -1
- package/dist/m/c-QEN6HZCA.js +1 -0
- package/dist/m/{c-26KVJS6J.js → c-QGAPG7OM.js} +1 -1
- package/dist/m/{c-HWR6GMGK.js → c-QIR5CAKM.js} +1 -1
- package/dist/m/{c-QJFYVCJO.js → c-QM5LXRDT.js} +1 -1
- package/dist/m/{c-MRURHLKV.js → c-QUGCKMWF.js} +1 -1
- package/dist/m/{c-KE33W4RE.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-XDVAZDUE.js → c-SGRIYOAJ.js} +1 -1
- package/dist/m/c-SKF5HL4I.js +4 -0
- package/dist/m/{c-IWTTOC2V.js → c-SRPC3VBE.js} +1 -1
- package/dist/m/{c-XZJ5IK6Q.js → c-T5NK2TVY.js} +1 -1
- package/dist/m/{c-VFES2BP5.js → c-TANBH7FI.js} +1 -1
- package/dist/m/{c-H6DXQEH2.js → c-THGQQDYY.js} +1 -1
- package/dist/m/{c-BV6FQQXM.js → c-TSBVMWAH.js} +1 -1
- package/dist/m/{c-QTC6VGA4.js → c-UD5JZTDL.js} +1 -1
- package/dist/m/{c-ZYEJLZEY.js → c-UDGEFWNU.js} +1 -1
- package/dist/m/{c-ZSCBEB7I.js → c-UHUNXWKN.js} +1 -1
- package/dist/m/{c-FWFJUU3T.js → c-VIRFF5HG.js} +1 -1
- package/dist/m/{c-GPTKI7AQ.js → c-VYJ7O4RI.js} +1 -1
- package/dist/m/{c-TUGVINJR.js → c-W5E5P2TZ.js} +1 -1
- package/dist/m/{c-WWE67K5Y.js → c-W7IPVCAA.js} +1 -1
- package/dist/m/{c-HAVGDXAL.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-ZKELCT4T.js → c-XCFQEWW6.js} +2 -2
- package/dist/m/c-XLQ3BDMM.js +1 -0
- package/dist/m/{c-FLBUYV3P.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-GI3WNJYN.js → autosave-F5YQCKVL.js} +1 -1
- package/dist/m/core/{conflict-notice-CEI3XY6A.js → conflict-notice-42H4ASLA.js} +1 -1
- package/dist/m/core/{persist-3OPWJKZS.js → persist-CJXIL7D4.js} +1 -1
- package/dist/m/core/{save-OJ5NSLS7.js → save-ESPSQPLV.js} +1 -1
- package/dist/m/core/{save-core-DX3RFVO3.js → save-core-6SQHOR6W.js} +1 -1
- package/dist/m/core/snapshot-FJP5IJL3.js +1 -0
- package/dist/m/core/{unsaved-warning-HGYH7O4H.js → unsaved-warning-HNQVTA2D.js} +1 -1
- package/dist/m/internals/{index-V4WC44FU.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 +1090 -1020
- package/dist/m/page/{editor-BQEPCJLG.js → editor-OSKOW6RS.js} +2 -2
- package/dist/m/plugins/{ai-edit-45A2J6LC.js → ai-edit-VV4QTINH.js} +1 -1
- package/dist/m/plugins/{cms-IFTKWF4S.js → cms-H7DTMUQX.js} +1 -1
- package/dist/m/plugins/{indicator-IPZJ35K6.js → indicator-WVPGMOZ7.js} +1 -1
- package/dist/m/plugins/{live-GPQXSXSM.js → live-DCUBKXY4.js} +1 -1
- package/dist/m/plugins/menu-Q4MFFQ6E.js +1 -0
- package/dist/m/plugins/{page-BAVLWCP6.js → page-OAWUJQEL.js} +2 -2
- package/dist/m/plugins/{page-view-P3UGJFDZ.js → page-view-DZ26ZDDQ.js} +1 -1
- package/dist/m/plugins/{people-Z4RGVQAV.js → people-5XRNRP5U.js} +1 -1
- package/dist/m/plugins/{quickcrop-E34PUYA3.js → quickcrop-WWF5I422.js} +2 -2
- package/dist/m/plugins/{richclay-S4NVJYIK.js → richclay-PGOOJ4OK.js} +2 -2
- package/dist/m/plugins/{sortable-WJZZBM7R.js → sortable-266HY6TR.js} +1 -1
- package/dist/m/plugins/{source-KVN6YNTY.js → source-6BKMK4PO.js} +1 -1
- package/dist/m/plugins/{upgrade-KTSICB22.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-SYVIBNSE.js → wire-W2EV36KZ.js} +1 -1
- package/dist/m/{reader-core-RUIFH32Y.js → reader-core-POFQCKRV.js} +2 -2
- package/dist/m/sync/{live-sync-RGQMMWK2.js → live-sync-XGUPHI3I.js} +1 -1
- package/dist/m/ui/{index-EZGLEM5P.js → index-VGYAKAKI.js} +1 -1
- package/dist/offline-manifest.json +636 -570
- package/docs/guide.md +36 -25
- package/package.json +1 -1
- 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 +6 -2
- 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/page-colour.js +17 -9
- package/dist/m/c-5AQYL27I.js +0 -1
- package/dist/m/c-DP4X4JID.js +0 -1
- package/dist/m/c-ECY5AZMH.js +0 -1
- package/dist/m/c-EGTIAL6P.js +0 -1
- package/dist/m/c-HDWZEJHM.js +0 -183
- package/dist/m/c-ILGQ3UXQ.js +0 -1
- package/dist/m/c-JFST6QE7.js +0 -1
- package/dist/m/c-JH5FF4NL.js +0 -20
- package/dist/m/c-KV7R47FR.js +0 -1
- package/dist/m/c-MHI3GYKF.js +0 -1
- package/dist/m/c-NNNR7OXA.js +0 -3
- package/dist/m/c-VSPAFOKU.js +0 -1
- package/dist/m/core/snapshot-O7II2AGG.js +0 -1
- package/dist/m/loader-V6VI46HU.js +0 -1
- package/dist/m/manifest-83C18DED.js +0 -1
- package/dist/m/manifest-edit-B28C1985.js +0 -1
- package/dist/m/plugins/menu-UN3KRCOF.js +0 -1
- package/dist/m/plugins/visitor-drafts-2AWNT23K.js +0 -20
- /package/dist/m/plugins/{quickcrop-E34PUYA3.js.LEGAL.txt → quickcrop-WWF5I422.js.LEGAL.txt} +0 -0
- /package/dist/m/plugins/{richclay-S4NVJYIK.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
|
|
|
@@ -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
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
|
|
@@ -277,7 +281,7 @@ export function resolveModules(params, isEditMode, { isDraftMode = false, hasSap
|
|
|
277
281
|
for (const name of PLUGIN_ORDER) {
|
|
278
282
|
if (!enabled.has(name)) continue;
|
|
279
283
|
const spec = PLUGIN_PATHS[name];
|
|
280
|
-
if (spec.editOnly && !isEditMode) continue;
|
|
284
|
+
if ((spec.editOnly && !isEditMode) || (spec.draft && !isDraftMode)) continue;
|
|
281
285
|
(LATE.has(name) ? late : plugins).push(spec.ownerPath && !isDraftMode ? spec.ownerPath : spec.path);
|
|
282
286
|
}
|
|
283
287
|
return { core, plugins, late };
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export const MENU_SAFETY = `@layer clay-menu-safety {
|
|
2
|
+
.clay-menu-dock [hidden], .clay-menu-spots[hidden], .clay-menu-dock[hidden] { display: none !important; }
|
|
3
|
+
.clay-menu :focus-visible:not(.clay-menu-edit) { outline: 2px solid var(--bevel-brass) !important; outline-offset: -2px !important; }
|
|
4
|
+
@media (pointer: coarse), (max-width: 680px) {
|
|
5
|
+
.clay-menu-goo, .clay-menu-list [part~="dismiss"], .clay-menu-list [part~="step"], .clay-menu-list [role^="menuitem"] { min-width: 44px !important; min-height: 44px !important; }
|
|
6
|
+
}
|
|
7
|
+
@media (prefers-reduced-motion: reduce) {
|
|
8
|
+
.clay-menu-dock *, .clay-menu-dock *::before, .clay-menu-dock *::after { transition: none !important; animation: none !important; }
|
|
9
|
+
}
|
|
10
|
+
@media (forced-colors: active) {
|
|
11
|
+
.clay-menu-list { border: 1px solid CanvasText !important; }
|
|
12
|
+
.clay-menu :focus-visible:not(.clay-menu-edit) { outline-color: Highlight !important; }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export const SHELL_A = `.clay-menu-dock, .clay-menu-spots, .clay-seo {
|
|
18
|
+
--bevel-font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
19
|
+
--bevel-font-serif: Newsreader, Georgia, "Times New Roman", serif;
|
|
20
|
+
--bevel-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
21
|
+
--bevel-ground: light-dark(#FDF8F0, #0B0C13);
|
|
22
|
+
--bevel-surface: light-dark(#FFFCF6, #1A1D2C);
|
|
23
|
+
--bevel-sunk: light-dark(#F4ECDF, #22263A);
|
|
24
|
+
--bevel-ink: light-dark(#2B241B, #ECEAF2);
|
|
25
|
+
--bevel-ink-2: light-dark(#4A4033, #C9CDE0);
|
|
26
|
+
--bevel-muted: light-dark(#6B5942, #A3A9C2);
|
|
27
|
+
--bevel-faint: light-dark(#9A8A70, #6A7090);
|
|
28
|
+
--bevel-line: light-dark(#E8DCC6, #2C3149);
|
|
29
|
+
--bevel-line-2: light-dark(#D8C8AF, #3A4058);
|
|
30
|
+
--bevel-line-3: light-dark(#C4B192, #4A536F);
|
|
31
|
+
--bevel-brass: light-dark(#8F6310, #D9A445);
|
|
32
|
+
--bevel-brass-soft: light-dark(#F0E4CC, #3A2F17);
|
|
33
|
+
--bevel-ox: light-dark(#A83A2C, #FF6B79);
|
|
34
|
+
--bevel-ox-soft: light-dark(#F6DED6, #3A2226);
|
|
35
|
+
--bevel-teal: light-dark(#256056, #4E9C8C);
|
|
36
|
+
--bevel-teal-soft: light-dark(#DCEBE7, #17332E);
|
|
37
|
+
--bevel-face: light-dark(#F1E7D4, #2A2F48);
|
|
38
|
+
--bevel-edge-hi: light-dark(#FFF9ED, #454C6C);
|
|
39
|
+
--bevel-edge-lo: light-dark(#D6C3A5, #171B2C);
|
|
40
|
+
--bevel-face-in: light-dark(#E3D0B3, #20253B);
|
|
41
|
+
--bevel-edge-in: light-dark(#B89F7B, #12152A);
|
|
42
|
+
--bevel-edge-in-hi: light-dark(#F0E1C9, #4D587A);
|
|
43
|
+
--bevel-knob: light-dark(#FCF8F1, #5A6286);
|
|
44
|
+
--bevel-knob-hi: light-dark(#FFF9ED, #7A83A8);
|
|
45
|
+
--clay-shadow: 0 1px 2px light-dark(rgb(43 36 27 / .07), rgb(0 0 0 / .35)), 0 10px 28px -10px light-dark(rgb(43 36 27 / .32), rgb(0 0 0 / .7));
|
|
46
|
+
--clay-sheet-shadow: 0 16px 30px -22px light-dark(rgb(43 36 27 / .45), rgb(0 0 0 / .85));
|
|
47
|
+
color-scheme: inherit;
|
|
48
|
+
font-family: var(--bevel-font-sans);
|
|
49
|
+
color: var(--bevel-ink);
|
|
50
|
+
-webkit-text-size-adjust: 100%;
|
|
51
|
+
text-size-adjust: 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.clay-menu-dock { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 1.4; }
|
|
55
|
+
.clay-menu-dock[data-embedded] { position: relative; display: inline-flex; }
|
|
56
|
+
.clay-menu-dock[hidden], .clay-menu-spots[hidden], .clay-menu-dock [hidden], .clay-seo [hidden] { display: none !important; }
|
|
57
|
+
|
|
58
|
+
.clay-menu { box-sizing: border-box; display: inline-flex; align-items: center; gap: 2px; user-select: none; -webkit-user-select: none; }
|
|
59
|
+
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
export const SHELL_B = `.clay-menu-goo { position: relative; display: inline-grid; place-items: center; box-sizing: border-box; margin: 0; padding: 0; cursor: pointer; }
|
|
63
|
+
.clay-menu-goo svg { display: block; }
|
|
64
|
+
.clay-menu-dot { position: absolute; top: -1px; right: -2px; width: 8px; height: 8px; border-radius: 50%; background: var(--bevel-brass); box-shadow: 0 0 0 2px var(--bevel-surface); }
|
|
65
|
+
|
|
66
|
+
.clay-menu :focus-visible, .clay-roster:focus-visible { outline: 2px solid var(--bevel-brass); outline-offset: 2px; }
|
|
67
|
+
.clay-menu-edit:focus-visible { outline: none; }
|
|
68
|
+
.clay-menu-edit:focus-visible .clay-menu-track { outline: 2px solid var(--bevel-brass); outline-offset: 2px; }
|
|
69
|
+
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
export const SHELL_C = `/* Popovers: the menu, the people panel, the status note. Below the dock, above it in a bottom corner. */
|
|
73
|
+
.clay-menu-list, .clay-people, .clay-menu-note { position: absolute; z-index: 1; top: calc(100% + 8px); right: 0; box-sizing: border-box; margin: 0; color: var(--bevel-ink); text-align: left; }
|
|
74
|
+
.clay-people, .clay-menu-note { border: 1px solid var(--bevel-line-2); background: var(--bevel-surface); box-shadow: var(--clay-shadow); }
|
|
75
|
+
|
|
76
|
+
.clay-menu-list { width: max-content; max-width: calc(100vw - 24px); max-height: calc(100dvh - 72px); overflow-y: auto; overscroll-behavior: contain; }
|
|
77
|
+
.clay-menu-list[popover] { position: fixed; inset: auto; margin: 0; }
|
|
78
|
+
.clay-menu-list [role^="menuitem"] { box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; margin: 0; border: 0; background: none; text-align: left; text-decoration: none; cursor: pointer; }
|
|
79
|
+
.clay-menu-list [role^="menuitem"]:hover:not([aria-disabled="true"]), .clay-menu-list [role^="menuitem"]:focus-visible { outline: none; background: var(--bevel-sunk); }
|
|
80
|
+
.clay-menu-list [role^="menuitem"][aria-disabled="true"] { cursor: default; }
|
|
81
|
+
.clay-menu-pair { display: flex; gap: 12px; padding: 3px 9px; }
|
|
82
|
+
.clay-menu-list .clay-menu-pair [role="menuitem"] { flex: none; width: auto; justify-content: flex-start; gap: 9px; }
|
|
83
|
+
.clay-menu-list kbd { flex: none; white-space: nowrap; }
|
|
84
|
+
.clay-menu-row { display: flex; align-items: center; gap: 2px; }
|
|
85
|
+
.clay-menu-list .clay-menu-row > [part~="item"] { flex: 1; width: auto; min-width: 0; }
|
|
86
|
+
.clay-menu-list .clay-menu-row > [part~="dismiss"] { flex: none; width: 28px; justify-content: center; padding: 0; }
|
|
87
|
+
.clay-menu-row > [part~="dismiss"] svg { display: block; width: 10px; height: 10px; fill: currentColor; }
|
|
88
|
+
.clay-menu-upgrade { display: inline-flex; align-items: center; gap: 8px; }
|
|
89
|
+
.clay-menu-upgrade::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--bevel-brass); }
|
|
90
|
+
.clay-menu-toggle .clay-menu-track { flex: none; }
|
|
91
|
+
.clay-menu-version { box-sizing: border-box; margin: 0; padding: 7px 9px; }
|
|
92
|
+
|
|
93
|
+
.clay-menu-note { z-index: 2; width: max-content; max-width: 264px; padding: 8px 12px; font-size: 13px; line-height: 1.45; }
|
|
94
|
+
|
|
95
|
+
.clay-menu-stepper { display: flex; align-items: center; gap: 2px; padding-left: 9px; font: 400 13px/1.35 var(--bevel-font-sans); color: var(--bevel-ink); }
|
|
96
|
+
.clay-menu-stepper-label { flex: 1; margin-right: 12px; white-space: nowrap; }
|
|
97
|
+
.clay-menu-stepper-value { min-width: 5.5em; text-align: center; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
98
|
+
.clay-menu-list .clay-menu-stepper [role="menuitem"] { flex: none; display: inline-grid; place-items: center; box-sizing: border-box; width: 36px; min-height: 36px; padding: 0; }
|
|
99
|
+
.clay-menu-stepper svg { display: block; width: 16px; height: 16px; fill: currentColor; }
|
|
100
|
+
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
export const SHELL_BASE = SHELL_A + SHELL_B + SHELL_C;
|
|
104
|
+
|
|
105
|
+
export const SHELL_FLOATING = `/* The dock: the roster (when a page opts in) and the capsule, fixed in one corner. */
|
|
106
|
+
.clay-menu-dock { position: fixed; z-index: 2147482000; top: max(12px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-right)); }
|
|
107
|
+
.clay-menu-dock[data-corner$="left"] { right: auto; left: max(12px, env(safe-area-inset-left)); flex-direction: row-reverse; }
|
|
108
|
+
.clay-menu-dock[data-corner^="bottom"] { top: auto; bottom: max(12px, env(safe-area-inset-bottom)); }
|
|
109
|
+
.clay-menu-dock[data-corner$="left"] :is(.clay-menu-list, .clay-people, .clay-menu-note) { right: auto; left: 0; }
|
|
110
|
+
.clay-menu-dock[data-corner^="bottom"] :is(.clay-menu-list, .clay-people, .clay-menu-note) { top: auto; bottom: calc(100% + 8px); }
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
export const MENU_SKIN = `.clay-menu { height: 34px; padding: 0 4px 0 2px; border: 1px solid var(--bevel-line-3); background: var(--bevel-surface); box-shadow: var(--clay-shadow); }
|
|
114
|
+
.clay-menu-goo { width: 28px; height: 28px; min-height: 0; border-radius: 0; border: 2px solid transparent; background: none; color: var(--bevel-ink); }
|
|
115
|
+
.clay-menu-goo::before { content: ""; position: absolute; inset: -8px -8px -8px -4px; }
|
|
116
|
+
.clay-menu-goo svg { width: 18px; height: auto; fill: currentColor; }
|
|
117
|
+
.clay-menu-goo:hover svg { translate: 0 -1px; }
|
|
118
|
+
.clay-menu-goo:active, .clay-menu-goo[aria-expanded="true"] { background: var(--bevel-face-in); border-color: var(--bevel-edge-in) var(--bevel-edge-in-hi) var(--bevel-edge-in-hi) var(--bevel-edge-in); }
|
|
119
|
+
.clay-menu-goo:active svg, .clay-menu-goo[aria-expanded="true"] svg { translate: 1px 1px; }
|
|
120
|
+
.clay-menu-list { min-width: 284px; padding: 6px; border: 1px solid var(--bevel-line-2); background: var(--bevel-surface); box-shadow: var(--clay-shadow); }
|
|
121
|
+
.clay-menu-list [role^="menuitem"] { min-height: 32px; padding: 5px 9px; font: 400 13px/1.35 var(--bevel-font-sans); color: var(--bevel-ink); }
|
|
122
|
+
.clay-menu-list [role^="menuitem"][aria-disabled="true"] { color: var(--bevel-faint); }
|
|
123
|
+
.clay-menu-list [role^="menuitem"][aria-disabled="true"] kbd { opacity: .6; }
|
|
124
|
+
.clay-menu-list .clay-menu-pair [role="menuitem"] { min-height: 30px; padding: 4px 6px 4px 9px; border: 1px solid var(--bevel-line); }
|
|
125
|
+
.clay-menu-list kbd { font: 500 11px/1 var(--bevel-font-mono); padding: 3px 5px; border: 1px solid var(--bevel-line-2); background: var(--bevel-sunk); color: var(--bevel-muted); }
|
|
126
|
+
.clay-menu-list [part~="quiet"] { font-size: 12px; color: var(--bevel-muted); }
|
|
127
|
+
.clay-menu-sep { height: 1px; margin: 4px 2px; background: var(--bevel-line); }
|
|
128
|
+
.clay-menu-version { font: 400 11.5px/1.4 var(--bevel-font-mono); color: var(--bevel-muted); }
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
// Last, so no skin can shrink a touch target or bring motion back.
|
|
132
|
+
export const MENU_TOUCH = `@media (pointer: coarse), (max-width: 680px) {
|
|
133
|
+
.clay-menu-list [role^="menuitem"] { min-height: 44px; font-size: 15px; }
|
|
134
|
+
.clay-menu-list .clay-menu-pair [role="menuitem"] { min-height: 44px; }
|
|
135
|
+
.clay-menu-list .clay-menu-stepper [role="menuitem"] { width: 44px; min-height: 44px; }
|
|
136
|
+
:is(.clay-menu-dock, .clay-seo) :is(input, textarea, select) { font-size: 16px; }
|
|
137
|
+
.clay-seo-input, .clay-people-input { min-height: 40px; }
|
|
138
|
+
.clay-menu-btn { min-height: 40px; }
|
|
139
|
+
}
|
|
140
|
+
@media (prefers-reduced-motion: reduce) {
|
|
141
|
+
.clay-menu-track, .clay-menu-thumb { transition: none; }
|
|
142
|
+
}
|
|
143
|
+
`;
|