@panphora/clayjs 1.19.0 → 1.20.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.
Files changed (176) hide show
  1. package/dist/all.js +1 -1
  2. package/dist/clay-dom.js +1 -1
  3. package/dist/clay-events.js +1 -1
  4. package/dist/clay-internals.js +1 -1
  5. package/dist/clay-options.js +1 -1
  6. package/dist/clay-router.js +1 -1
  7. package/dist/clay-ui.js +1 -1
  8. package/dist/clay-utils.js +1 -1
  9. package/dist/clay.js +1 -1
  10. package/dist/clay.standalone.js +8186 -8076
  11. package/dist/m/attrs/{refetch-on-save-ZUE7PSCI.js → refetch-on-save-DPGJJMGN.js} +1 -1
  12. package/dist/m/attrs/save-freeze-MTFVVYV5.js +1 -0
  13. package/dist/m/{c-DHUCEMXE.js → c-2VB555FG.js} +1 -1
  14. package/dist/m/{c-KGHVFRR3.js → c-3AXJDL2P.js} +1 -1
  15. package/dist/m/{c-FOOE4GML.js → c-3GJ4XARY.js} +1 -1
  16. package/dist/m/c-3TWCKFOP.js +1 -0
  17. package/dist/m/{c-LPCZOYTX.js → c-3YH6JSIL.js} +1 -1
  18. package/dist/m/{c-AIWN4QN4.js → c-44B5ODN7.js} +2 -2
  19. package/dist/m/{c-S2U6NJU4.js → c-62HZW5DH.js} +1 -1
  20. package/dist/m/{c-ZGDYHKG3.js → c-67ILSNPR.js} +1 -1
  21. package/dist/m/c-67LGZ52V.js +20 -0
  22. package/dist/m/{c-ZRUQ5FSU.js → c-7SXYQCN4.js} +1 -1
  23. package/dist/m/{c-LACAAQFB.js → c-AL4RBJF2.js} +1 -1
  24. package/dist/m/{c-L42OLXWM.js → c-ANNFZFLN.js} +1 -1
  25. package/dist/m/{c-QO4HNGCQ.js → c-B4VYNXTQ.js} +1 -1
  26. package/dist/m/{c-7PRJCA3E.js → c-BJPZCWAV.js} +1 -1
  27. package/dist/m/{c-V52FE5FI.js → c-BTLFLLML.js} +1 -1
  28. package/dist/m/c-C4IIQZGJ.js +1 -0
  29. package/dist/m/{c-5T6NZHNM.js → c-CAWNFER6.js} +1 -1
  30. package/dist/m/c-CSNE5EIF.js +1 -0
  31. package/dist/m/{c-DVYGFTSK.js → c-DDDOKPI6.js} +2 -2
  32. package/dist/m/{c-2RARVIAK.js → c-DHXPDLW6.js} +1 -1
  33. package/dist/m/{c-WCXLG3MV.js → c-E4IBSAQM.js} +1 -1
  34. package/dist/m/{c-IVTRVENZ.js → c-EDSPKYMA.js} +1 -1
  35. package/dist/m/{c-2Y4DOHSY.js → c-EFD6QNTE.js} +1 -1
  36. package/dist/m/{c-7MG5RU63.js → c-EGTIAL6P.js} +1 -1
  37. package/dist/m/{c-FCNAO2MI.js → c-EOCKLMKX.js} +1 -1
  38. package/dist/m/{c-UXFUJEZO.js → c-EOCZ2YVL.js} +1 -1
  39. package/dist/m/{c-K72SY2TL.js → c-EZ7B5AZT.js} +1 -1
  40. package/dist/m/{c-UPD33ZPE.js → c-FDAXZ2PM.js} +1 -1
  41. package/dist/m/{c-P3CK66NT.js → c-FNVQAACK.js} +1 -1
  42. package/dist/m/c-FSWRGBVQ.js +1 -0
  43. package/dist/m/c-FTWGYI5E.js +1 -0
  44. package/dist/m/{c-WKNRJX3C.js → c-GKEATME6.js} +1 -1
  45. package/dist/m/{c-QINHDETP.js → c-GPGYKER7.js} +1 -1
  46. package/dist/m/{c-FWKHM4E7.js → c-GSRLXYWL.js} +1 -1
  47. package/dist/m/{c-B7J4K2AE.js → c-GVPNDIKJ.js} +2 -2
  48. package/dist/m/{c-FTQKI7SP.js → c-I2IFKJX2.js} +1 -1
  49. package/dist/m/c-ICMAOOGK.js +1 -0
  50. package/dist/m/{c-HUISJXHF.js → c-J3BXFTZS.js} +1 -1
  51. package/dist/m/{c-BIJALSRD.js → c-JDZDF2QM.js} +1 -1
  52. package/dist/m/{c-UDHIVLYH.js → c-JH5FF4NL.js} +1 -1
  53. package/dist/m/{c-OYRZ7XG2.js → c-JHJR5BQX.js} +1 -1
  54. package/dist/m/{c-V44KWI2L.js → c-JOH753EJ.js} +2 -2
  55. package/dist/m/{c-P3OFYUMA.js → c-JUE7B4OX.js} +1 -1
  56. package/dist/m/c-K76AUL4I.js +7 -0
  57. package/dist/m/{c-ZWIOJGMT.js → c-LA2BW7UY.js} +1 -1
  58. package/dist/m/{c-ICJYHABF.js → c-LDUNOR57.js} +1 -1
  59. package/dist/m/{c-XSZKP646.js → c-LIBE4HPD.js} +1 -1
  60. package/dist/m/c-LPUJXRXW.js +1 -0
  61. package/dist/m/{c-JK3PHE7J.js → c-LQU7TXIH.js} +1 -1
  62. package/dist/m/{c-5ZQV7KC4.js → c-LV5MABYD.js} +1 -1
  63. package/dist/m/{c-GWNC3UGL.js → c-M4FSPNQ2.js} +1 -1
  64. package/dist/m/{c-2KIEGNC4.js → c-MEWUGR4D.js} +1 -1
  65. package/dist/m/c-MIL4B2JQ.js +1 -0
  66. package/dist/m/{c-IVMD3FEM.js → c-N7Y52HJH.js} +1 -1
  67. package/dist/m/{c-O4JMKN6U.js → c-NFR673TL.js} +2 -2
  68. package/dist/m/{c-ZKNBMNN3.js → c-NLZ5AN4L.js} +1 -1
  69. package/dist/m/{c-RRZJWFLE.js → c-OABVRYOT.js} +1 -1
  70. package/dist/m/c-PIVIYLTY.js +1 -0
  71. package/dist/m/{c-L7JL5TL3.js → c-QY7F6EN6.js} +1 -1
  72. package/dist/m/{c-UY4QX42B.js → c-RG6XUZFA.js} +1 -1
  73. package/dist/m/c-RHMPKZZ7.js +1 -0
  74. package/dist/m/{c-TJAXGIRV.js → c-RQRMPZXD.js} +1 -1
  75. package/dist/m/{c-GY422N3Q.js → c-RS77ODLY.js} +1 -1
  76. package/dist/m/{c-BAUBNYXK.js → c-RT3C74GY.js} +1 -1
  77. package/dist/m/{c-CAS3NND4.js → c-S5IEIZCN.js} +1 -1
  78. package/dist/m/{c-HXPHZN7Q.js → c-SBSMJIPC.js} +1 -1
  79. package/dist/m/c-SFKI6OLM.js +2 -0
  80. package/dist/m/{c-4FJTBE6I.js → c-SJPD46ZO.js} +4 -4
  81. package/dist/m/{c-JCP3XDSJ.js → c-SO63UGPQ.js} +1 -1
  82. package/dist/m/{c-PLP4JKFC.js → c-TUGVINJR.js} +1 -1
  83. package/dist/m/{c-Q26MME6N.js → c-TWCXBWS3.js} +1 -1
  84. package/dist/m/{c-GNDL3TKB.js → c-U5X57T4L.js} +1 -1
  85. package/dist/m/{c-ZS4US2XV.js → c-UXEVJ63W.js} +1 -1
  86. package/dist/m/{c-KJJPV77R.js → c-V2J5C5MT.js} +1 -1
  87. package/dist/m/{c-7PYNAULW.js → c-VBLFAQD2.js} +1 -1
  88. package/dist/m/{c-ERQSNR6K.js → c-VQCEOGKH.js} +2 -2
  89. package/dist/m/{c-YVZ2L7CE.js → c-VSPAFOKU.js} +1 -1
  90. package/dist/m/{c-L6FLIUXP.js → c-VY6XRBOW.js} +1 -1
  91. package/dist/m/{c-2KL3BD52.js → c-W23KVGUO.js} +1 -1
  92. package/dist/m/{c-AXGY2WTB.js → c-W3K23S72.js} +1 -1
  93. package/dist/m/c-WFMRX5RZ.js +1 -0
  94. package/dist/m/{c-BWFVSBDF.js → c-WN3MXOLT.js} +1 -1
  95. package/dist/m/{c-MIKZEG5I.js → c-XD5MOROJ.js} +1 -1
  96. package/dist/m/{c-CQQDL6O4.js → c-XQSXKIKM.js} +1 -1
  97. package/dist/m/{c-I4MQHOEY.js → c-Y33LUUIQ.js} +1 -1
  98. package/dist/m/{c-JDARJDOT.js → c-YQUJ74R3.js} +1 -1
  99. package/dist/m/{c-F6G2OGYG.js → c-YSB6JKV2.js} +2 -2
  100. package/dist/m/{c-WQWIRWO3.js → c-Z32V4MHK.js} +1 -1
  101. package/dist/m/{c-BHZYSHW4.js → c-ZJLU56DV.js} +1 -1
  102. package/dist/m/{c-DKLS74XB.js → c-ZQXNHZZT.js} +2 -2
  103. package/dist/m/{c-TNEM3RNM.js → c-ZSCBEB7I.js} +1 -1
  104. package/dist/m/core/{admin-attrs-YY7T5FGU.js → admin-attrs-ZG7R5V3E.js} +1 -1
  105. package/dist/m/core/{autosave-RVJPYLRT.js → autosave-VPORYJSE.js} +1 -1
  106. package/dist/m/core/{conflict-notice-2WGT52LH.js → conflict-notice-JMAZLVRX.js} +1 -1
  107. package/dist/m/core/{persist-UKXV5KLA.js → persist-3OPWJKZS.js} +1 -1
  108. package/dist/m/core/{recovery-ZOUOJVIX.js → recovery-DRFZRJED.js} +1 -1
  109. package/dist/m/core/{save-A5JXV6ME.js → save-AOJBDSOK.js} +1 -1
  110. package/dist/m/core/{save-core-MKRPSEEO.js → save-core-IOCMXO5L.js} +1 -1
  111. package/dist/m/core/{snapshot-NU3YM6GR.js → snapshot-O7II2AGG.js} +1 -1
  112. package/dist/m/core/unsaved-warning-U5VUJFOE.js +1 -0
  113. package/dist/m/events/{index-RN2JNGBV.js → index-B6TGDYMG.js} +1 -1
  114. package/dist/m/internals/{index-TGLQINN3.js → index-HDRANDWE.js} +1 -1
  115. package/dist/m/lib/{cache-bust-MUMFXZK6.js → cache-bust-NH7VBWHO.js} +1 -1
  116. package/dist/m/lib/{region-policy-SB2AWVG5.js → region-policy-KWBPROWF.js} +1 -1
  117. package/dist/m/{loader-34YPFKOH.js → loader-DZZLUBVB.js} +1 -1
  118. package/dist/m/manifest-3AEF5191.js +1 -0
  119. package/dist/m/manifest-edit-29A26F22.js +1 -0
  120. package/dist/m/manifest.json +1202 -1158
  121. package/dist/m/options/{options-EJMS3K24.js → options-55LQHSJ4.js} +1 -1
  122. package/dist/m/page/{editor-WRQGHDSG.js → editor-EY2WAAGB.js} +2 -2
  123. package/dist/m/plugins/{ai-edit-X2L2KIEP.js → ai-edit-J5VLSW5I.js} +1 -1
  124. package/dist/m/plugins/{demo-OYO5GCAO.js → demo-42BSYQ7T.js} +1 -1
  125. package/dist/m/plugins/{live-7C36XEI2.js → live-5EZN2EYP.js} +1 -1
  126. package/dist/m/plugins/menu-KGWKT6CS.js +1 -0
  127. package/dist/m/plugins/{page-BSW6MGDO.js → page-LVW5DWA4.js} +2 -2
  128. package/dist/m/plugins/{page-view-WFZVTPQK.js → page-view-5KQ76IJB.js} +1 -1
  129. package/dist/m/plugins/{sortable-4S6SQC3D.js → sortable-WJZZBM7R.js} +1 -1
  130. package/dist/m/plugins/{source-TJXOIOYY.js → source-CCEUM3OR.js} +1 -1
  131. package/dist/m/plugins/{undo-NLPGCJQY.js → undo-3JEYK5QY.js} +1 -1
  132. package/dist/m/plugins/{upgrade-HSUNDNJD.js → upgrade-NW556WJD.js} +1 -1
  133. package/dist/m/plugins/{upload-NHGJXZK2.js → upload-TTCQRIM5.js} +1 -1
  134. package/dist/m/plugins/{visitor-drafts-BW43DGY3.js → visitor-drafts-2AWNT23K.js} +2 -2
  135. package/dist/m/plugins/{visitor-drafts-owner-7ODQG7JX.js → visitor-drafts-owner-MZP45XSG.js} +1 -1
  136. package/dist/m/plugins/{wire-6OSIL3I7.js → wire-VRYPY6EJ.js} +1 -1
  137. package/dist/m/{reader-core-PLZYLYLT.js → reader-core-2GXY7SHY.js} +2 -2
  138. package/dist/m/sync/{live-sync-UXNI6ZF6.js → live-sync-SKPMFYOO.js} +1 -1
  139. package/dist/m/ui/{index-IVWN2QC4.js → index-QNIPDHXQ.js} +1 -1
  140. package/dist/offline-manifest.json +634 -610
  141. package/docs/guide.md +24 -8
  142. package/package.json +3 -2
  143. package/src/core/conflict-download.js +8 -7
  144. package/src/core/conflict-notice-ui.js +4 -4
  145. package/src/core/copy-content.js +103 -0
  146. package/src/core/download.js +14 -2
  147. package/src/core/save-size-notice.js +2 -2
  148. package/src/core/upgrade-run.js +5 -1
  149. package/src/lib/region-policy.js +1 -1
  150. package/src/page/features/export-html.js +4 -4
  151. package/src/page/features/export-markdown.js +2 -2
  152. package/src/page/recovery-ui.js +2 -2
  153. package/src/page/save-states.js +4 -4
  154. package/src/plugins/menu/actions.js +1 -1
  155. package/src/plugins/page.js +4 -4
  156. package/src/plugins/visitor-drafts/prompt.js +12 -12
  157. package/src/plugins/wire.js +3 -1
  158. package/dist/m/attrs/save-freeze-LWGF6XUN.js +0 -1
  159. package/dist/m/c-DCFSEJZG.js +0 -1
  160. package/dist/m/c-EXEDYU4H.js +0 -1
  161. package/dist/m/c-GRXHHFPO.js +0 -1
  162. package/dist/m/c-HT2DKBV2.js +0 -1
  163. package/dist/m/c-MCOOHCSG.js +0 -1
  164. package/dist/m/c-MMXKAX2W.js +0 -1
  165. package/dist/m/c-VE7RKNKZ.js +0 -7
  166. package/dist/m/c-VR2BJ4AN.js +0 -1
  167. package/dist/m/c-WGW764WX.js +0 -1
  168. package/dist/m/c-WNTYKCDG.js +0 -1
  169. package/dist/m/c-YBSS55KU.js +0 -20
  170. package/dist/m/core/unsaved-warning-4ZPEV7BF.js +0 -1
  171. package/dist/m/manifest-0D166F4F.js +0 -1
  172. package/dist/m/manifest-edit-3877153E.js +0 -1
  173. package/dist/m/plugins/menu-MFC4DI5E.js +0 -1
  174. /package/dist/m/{c-QTTJYJMA.js → c-FILLTJA7.js} +0 -0
  175. /package/dist/m/{c-5JLFROUB.js → c-X5LNHET5.js} +0 -0
  176. /package/dist/m/plugins/{people-JCHVPF7P.js → people-C7TVBBMR.js} +0 -0
package/docs/guide.md CHANGED
@@ -4,7 +4,7 @@ This is the complete reference for building malleable documents with ClayJS: one
4
4
 
5
5
  It is written for a developer or an LLM building full apps with inline editing and inline controls: kanban boards with card detail, block writers, finance trackers, CRMs, flashcards, trackers. It covers the CMS plugin only in passing ([cms: content-managed pages](https://clayjs.com/docs/guide/components/#cms-content-managed-pages)), because an app whose product is editing should not route editing through a form sidebar.
6
6
 
7
- Every behavior stated here was read from ClayJS 1.17.0 source or executed. The four example files below are complete, tested apps that this guide quotes. A [test suite](https://github.com/panphora/clayjs/tree/main/tests/unit/guide-examples) boots each one in jsdom with the real loader and checks what it saves, how it merges a peer's edit, and what a viewer receives. Open any of them and view source, or download one and open it in HTML Clay to edit it:
7
+ Every behavior stated here was read from ClayJS 1.17.0 source or executed. The four example files below are complete, tested apps that this guide quotes. A [test suite](https://github.com/hyperspacesystems/clayjs/tree/main/tests/unit/guide-examples) boots each one in jsdom with the real loader and checks what it saves, how it merges a peer's edit, and what a viewer receives. Open any of them and view source, or download one and open it in HTML Clay to edit it:
8
8
 
9
9
  | File | What it shows |
10
10
  |---|---|
@@ -892,7 +892,23 @@ Traps in this table:
892
892
  - **Fill a scratchpad after `clay.ready`, and insert new ones empty.** The contents are captured when the element first appears. Content written before boot, or present in a cloned element when inserted, becomes its saved contents. Empty the scratchpads in a copied record before inserting it.
893
893
  - **Create `editor-ui` roots at runtime from an unmarked `<template>` on every load.** An `editor-ui` root disappears from the saved file, so it cannot be authored in the file. Do not mark the blueprint inside the `<template>` as `editor-ui`, or the first save empties the template and leaves no chrome to clone on the next load. Leave the blueprint unmarked and set `clay="editor-ui"` on each clone as you insert it.
894
894
 
895
- ### 4.3 `persist`: form controls
895
+ ### 4.3 Copy policy: `no-copy` and `no-copy-unless:paid-forms`
896
+
897
+ Save and sync tokens govern the working document; copy-policy tokens on `clay` govern what travels into an explicit copy:
898
+
899
+ ```html
900
+ <script src="/analytics.js" defer clay="no-copy"></script>
901
+ <header class="intro" clay="no-copy">…</header>
902
+ <form action="/_/message" method="post" clay="no-copy-unless:paid-forms">…</form>
903
+ ```
904
+
905
+ - **Normal saves keep the original.** Ordinary saves, live sync, versions, and automatic recovery retain `no-copy` and conditional regions.
906
+ - **Explicit copies strip `no-copy` content, even for the owner.** Copies, duplicates, forks, HTML downloads, and ZIP exports remove `clay="no-copy"` elements (including inside `<template>` contents). User-requested recovery downloads filter visible HTML and embedded earlier HTML pages while preserving recovery records and edits.
907
+ - **Authored loaders vs runtime scripts.** An authored loader marked `clay="no-copy"` stays in original saves and disappears from copies. Any SDK `<script>` added at runtime uses `clay="no-save no-snapshot no-watch no-copy"` so it is not persisted. ClayJS core knows only generic copy policy, not any analytics provider.
908
+ - **Conditional paid forms.** `clay="no-copy-unless:paid-forms"` aliases legacy `data-paid-only="forms"` (still supported). Hosted copy/create checks the destination's existing `paid-forms` (`forms`) entitlement; offline downloads have no destination entitlement and strip optional conditional content. Reusable templates retain conditional regions for creation-time filtering, while unconditional `no-copy` regions are removed before owned template outputs. Exact case-sensitive matching fails closed, so an unknown conditional spelling cannot disguise a required paid feature.
909
+ - **Supporting hosts and raw file limits.** Enforced in ClayJS client downloads and recovery exports, Hyperclay hosted copy/create/download flows, and Hyperclay Local / HTML Clay copies and exports. Finder, `cp`, and browser Save Page As bypass app hooks and copy raw file bytes unchanged.
910
+
911
+ ### 4.4 `persist`: form controls
896
912
 
897
913
  Put `persist` on every `<input>`, `<textarea>`, and `<select>` whose value belongs to the record. A control's typed value is a property, not an attribute, and serialization only reads attributes. Without `persist`, a typed value is lost on save and the file keeps the authored `value`.
898
914
 
@@ -913,7 +929,7 @@ Put `persist` on every `<input>`, `<textarea>`, and `<select>` whose value belon
913
929
 
914
930
  **After undo, redo, or a merge, ClayJS puts each control's display back in step with its attributes.** A control the user has touched shows its live property, not its attribute. A merge and the undo plugin copy `checked`, `value`, and `selected` back onto the property (undo does this for `persist` controls outside `no-undo` regions). A merge leaves the focused text field alone, while undo restores it too.
915
931
 
916
- ### 4.4 Where to keep data that is not visible text
932
+ ### 4.5 Where to keep data that is not visible text
917
933
 
918
934
  In order of preference:
919
935
 
@@ -935,7 +951,7 @@ In order of preference:
935
951
  4. **A live JSON script (`clay="live"`)** for state that changes faster than a save: game boards, scores, cursors ([Live data: `clay="live"`](https://clayjs.com/docs/guide/live-sync/#live-data-clay-live)).
936
952
  5. **`<template>` content: never for mutable data.** It is saved, but edits there never trigger autosave.
937
953
 
938
- ### 4.5 Element identity: `data-id` and `id`
954
+ ### 4.6 Element identity: `data-id` and `id`
939
955
 
940
956
  Live sync and undo pair elements across versions by **`data-id`, then `id`**, used only when the value is unique on its side and the tag names match. Without an id, elements pair by content, which works for distinct text and fails for identical new items.
941
957
 
@@ -956,11 +972,11 @@ Live sync and undo pair elements across versions by **`data-id`, then `id`**, us
956
972
  - **A `data-id` is also what presence names.** A record target `{ kind: "record", id }` carries it, so a stable id is what lets a peer's badge find the row again after a reload or a merge ([Presence](https://clayjs.com/docs/guide/live-sync/#presence)).
957
973
  - **Use ids to find records in commands:** `` board.querySelector(`.card[data-id="${CSS.escape(id)}"]`) ``. Never find a record by position.
958
974
 
959
- ### 4.6 Assets
975
+ ### 4.7 Assets
960
976
 
961
977
  ClayJS saves an image through its `src`. Upload the file and write the returned URL ([upload and quickcrop](https://clayjs.com/docs/guide/components/#upload-and-quickcrop)). Every host returns a host path such as `/_/uploads/assets-<name>/photo-ab12.png`, which keeps working on that host when the document is renamed or moved. A document opened straight from disk shows those images broken; Export as zip takes a document with its files. Where the host cannot store files, embed a `data:` URL. Data URLs work anywhere, but every save carries the bytes, so keep embedded images small.
962
978
 
963
- ### 4.7 Ephemeral state: per-tab state on saved elements
979
+ ### 4.8 Ephemeral state: per-tab state on saved elements
964
980
 
965
981
  Use `ephemeral` for per-tab state on a saved element, such as a selected card, a row being dragged, or an open section. Toggling a normal class or attribute is an edit like any other: it marks the page unsaved, starts an autosave, adds an undo step, reaches every peer, and lands in the file. Mark it `ephemeral` instead:
966
982
 
@@ -982,7 +998,7 @@ clay.ephemeral.toggle(card, "selected"); // add, remove, toggle(el, word, forc
982
998
 
983
999
  Use `ephemeral` for state on one element. State that comes from the URL hash and spans many records (the open card, a filter) is still painted with a runtime stylesheet ([Per-tab state](https://clayjs.com/docs/guide/architecture/#per-tab-state)).
984
1000
 
985
- ### 4.8 A template is an ordinary record with a tag
1001
+ ### 4.9 A template is an ordinary record with a tag
986
1002
 
987
1003
  A reusable template is a record like any other, marked with a tag. It stays editable, searchable, exported, synced and in a visitor's draft, and it needs no second store and no new format. The app reads the tag off the body it already has:
988
1004
 
@@ -992,7 +1008,7 @@ const templates = open => Entries.live().filter(a => a !== open && summary({ bod
992
1008
 
993
1009
  The tag is matched outside code spans and fences, so a template entry that shows a `#template` example does not become a template itself. Using one copies its text into the page, and the new record is written by the same save as everything else.
994
1010
 
995
- ### 4.9 Keep a helper that reads the text beside the editor, in an `editor-ui` row
1011
+ ### 4.10 Keep a helper that reads the text beside the editor, in an `editor-ui` row
996
1012
 
997
1013
  A helper that reacts to the text being written belongs in a row a script builds beside the editor, marked `clay="editor-ui"`, and refreshed on a debounce. Nothing it draws is saved, printed or synced, and no keystroke pays for it beyond the debounce:
998
1014
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panphora/clayjs",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "type": "module",
5
5
  "description": "clayjs: malleable documents. Save lifecycle for self-saving HTML.",
6
6
  "license": "MIT-0",
@@ -72,6 +72,7 @@
72
72
  "build:standalone": "node build.js --standalone",
73
73
  "prepack": "node build.js --standalone",
74
74
  "test:standalone": "node build.js --standalone && web-test-runner --config tests/browser/wtr.config.mjs",
75
+ "browser:install": "node scripts/chrome-for-testing/browser-runtime.cjs install",
75
76
  "prepublishOnly": "testbox run",
76
77
  "dev": "node tests/stub-server.mjs",
77
78
  "build": "npm run build:bevel && npm run build:skins && node build.js",
@@ -111,7 +112,7 @@
111
112
  "marked": "18.0.14",
112
113
  "overtype": "2.6.1",
113
114
  "parse5": "^7.3.0",
114
- "playwright": "1.62.1",
115
+ "playwright": "1.64.1",
115
116
  "postcss": "^8.5.28",
116
117
  "prosemirror-commands": "1.7.2",
117
118
  "prosemirror-dropcursor": "1.8.4",
@@ -1,6 +1,7 @@
1
1
  import { serializeRecoveryClone, captureRecoveryPage } from "./snapshot.js";
2
2
  import { rowOf } from "./conflict-presentation.js";
3
3
  import { documentName, downloadHtml } from "./download.js";
4
+ import { stripCopyContent } from "./copy-content.js";
4
5
 
5
6
  export { documentName, downloadHtml };
6
7
 
@@ -31,12 +32,12 @@ export function buildRecovery(records, { refused = false } = {}) {
31
32
  if (apply && !seen.has(apply.id)) { seen.add(apply.id); applies.push(apply); }
32
33
  }
33
34
  applies.sort((a, b) => (a.ticket || 0) - (b.ticket || 0));
34
- const pageOf = (a) => serializeRecoveryClone(a.recoveryRoot ?? a.root, { prepared: a.domain === "save" });
35
+ const pageOf = (a) => stripCopyContent(serializeRecoveryClone(a.recoveryRoot ?? a.root, { prepared: a.domain === "save" }));
35
36
 
36
37
  let html;
37
38
  let older;
38
39
  if (refused || !applies.length) {
39
- html = captureRecoveryPage();
40
+ html = stripCopyContent(captureRecoveryPage());
40
41
  older = applies;
41
42
  } else {
42
43
  html = pageOf(applies[applies.length - 1]);
@@ -66,9 +67,9 @@ export function buildRecovery(records, { refused = false } = {}) {
66
67
  // The review panel's own copy: the page a reload kept, when there is one, with every
67
68
  // stored row it drew listed beside it as JSON. The rows are already plain, so nothing
68
69
  // here reads the ledger the tab no longer has.
69
- export function downloadStored({ html = null, edits = [] } = {}) {
70
+ export async function downloadStored({ html = null, edits = [] } = {}) {
70
71
  try {
71
- const page = typeof html === "string" ? html : captureRecoveryPage();
72
+ const page = stripCopyContent(typeof html === "string" ? html : captureRecoveryPage());
72
73
  const data = {
73
74
  format: "clay-lost-edits",
74
75
  version: 1,
@@ -78,17 +79,17 @@ export function downloadStored({ html = null, edits = [] } = {}) {
78
79
  edits: Array.isArray(edits) ? edits : [],
79
80
  earlierPages: [],
80
81
  };
81
- return downloadHtml({ html: embed(page, data), name: `${documentName()}-my-copy-${stamp()}.html` });
82
+ return await downloadHtml({ html: embed(page, data), name: `${documentName()}-my-copy-${stamp()}.html` });
82
83
  } catch (err) {
83
84
  console.error("clayjs: could not build the download", err);
84
85
  return false;
85
86
  }
86
87
  }
87
88
 
88
- export function downloadRecovery(records, { refused = false } = {}) {
89
+ export async function downloadRecovery(records, { refused = false } = {}) {
89
90
  try {
90
91
  const { html, name } = buildRecovery(records, { refused });
91
- return downloadHtml({ html, name });
92
+ return await downloadHtml({ html, name });
92
93
  } catch (err) {
93
94
  console.error("clayjs: could not build the download", err);
94
95
  return false;
@@ -707,12 +707,12 @@ async function onRevert() {
707
707
  let downloadedTimer = null;
708
708
 
709
709
  // Download my copy. Nothing here acknowledges or releases a record.
710
- function onDownload() {
710
+ async function onDownload() {
711
711
  const l = ledger();
712
712
  // A refusal ends in a reload that takes every record with it, so its copy carries
713
713
  // all of them; a replaced notice covers the rows it drew.
714
714
  const records = refused ? (l?.list() ?? []) : renderedIds.map((id) => l?.get(id)).filter(Boolean);
715
- const ok = downloadRecovery(records, { refused: !!refused });
715
+ const ok = await downloadRecovery(records, { refused: !!refused });
716
716
  downloadFailed = !ok;
717
717
  downloaded = ok;
718
718
  clearTimeout(downloadedTimer);
@@ -801,14 +801,14 @@ async function onCopyStored(entry) {
801
801
 
802
802
  // Download my copy, from the stored root: the page as this tab had it, with the
803
803
  // losing text in it. Nothing here acknowledges or releases a record.
804
- function onDownloadRecovered() {
804
+ async function onDownloadRecovered() {
805
805
  const record = recovered.find((item) => item.root);
806
806
  const edits = [];
807
807
  for (const item of recovered) {
808
808
  for (const entry of item.conflicts) edits.push({ id: entry.id, kind: entry.kind, name: storedName(entry), yours: entry.yours, now: entry.now, sentence: entry.sentence });
809
809
  }
810
810
  for (const entry of recoveredUndo) edits.push({ id: entry.id, kind: entry.kind, name: entry.name, yours: entry.yours, now: entry.now, sentence: entry.sentence });
811
- const ok = downloadStored({ html: record?.root ?? null, edits });
811
+ const ok = await downloadStored({ html: record?.root ?? null, edits });
812
812
  downloadFailed = !ok;
813
813
  downloaded = ok;
814
814
  clearTimeout(downloadedTimer);
@@ -0,0 +1,103 @@
1
+ // Copy-only content: marked regions that stay in the file, in normal saves, in snapshots
2
+ // and in recovery storage, and are removed only from the bytes of a copy someone asks for.
3
+ // The removal is a splice on the original string using parse5's source offsets, so every
4
+ // byte outside a removed range survives, templates and implicit end tags included.
5
+
6
+ import { parse } from "../vendor/parse5.vendor.js";
7
+
8
+ const CLAY = "clay";
9
+ const PAID_ONLY = "data-paid-only";
10
+ const NO_COPY = "no-copy";
11
+ const CONDITIONAL_FORMS = "no-copy-unless:paid-forms";
12
+ const CLAY_TOKENS = /[\t\n\f\r ]+/;
13
+
14
+ function attributeOf(node, name) {
15
+ return node.attrs?.find((attr) => attr.name === name)?.value ?? null;
16
+ }
17
+
18
+ function childrenOf(node) {
19
+ return node.content ? [...(node.childNodes || []), node.content] : (node.childNodes || []);
20
+ }
21
+
22
+ function eachElement(node, visit) {
23
+ visit(node);
24
+ for (const child of childrenOf(node)) eachElement(child, visit);
25
+ }
26
+
27
+ function clayTokensOf(node) {
28
+ return (attributeOf(node, CLAY) ?? "").split(CLAY_TOKENS);
29
+ }
30
+
31
+ // The marker is compared to a feature key exactly: no trim, no lowercase, so a typo, an
32
+ // empty value or `FORMS` strips nothing and the content stays, which is the direction
33
+ // that fails closed. `clay="no-copy-unless:paid-forms"` is the approved spelling of the
34
+ // same condition.
35
+ function paidMarkersOf(node) {
36
+ const markers = [];
37
+ const value = attributeOf(node, PAID_ONLY);
38
+ if (value !== null) markers.push(value);
39
+ if (clayTokensOf(node).includes(CONDITIONAL_FORMS)) markers.push("forms");
40
+ return [...new Set(markers)];
41
+ }
42
+
43
+ function lastDescendantEnd(node) {
44
+ let end = null;
45
+ eachElement(node, (entry) => {
46
+ const entryEnd = entry.sourceCodeLocation?.endOffset;
47
+ if (typeof entryEnd === "number" && entryEnd >= 0 && (end === null || entryEnd > end)) end = entryEnd;
48
+ });
49
+ return end;
50
+ }
51
+
52
+ function endOffsetFor(node) {
53
+ const location = node.sourceCodeLocation;
54
+ if (typeof location?.endOffset === "number" && location.endOffset >= 0) return location.endOffset;
55
+ const fallback = lastDescendantEnd(node) ?? location?.startTag?.endOffset ?? null;
56
+ if (fallback === null) return null;
57
+ console.warn(`clayjs: <${node.tagName}> has no end offset; removing through ${fallback}`);
58
+ return fallback;
59
+ }
60
+
61
+ function markedRanges(html, features) {
62
+ const ranges = [];
63
+ eachElement(parse(html, { sourceCodeLocationInfo: true }), (node) => {
64
+ const markers = paidMarkersOf(node);
65
+ const unconditional = clayTokensOf(node).includes(NO_COPY);
66
+ const conditional = features === null ? markers.length > 0 : markers.some((marker) => features.includes(marker));
67
+ if (!unconditional && !conditional) return;
68
+ const start = node.sourceCodeLocation?.startOffset;
69
+ const end = typeof start === "number" ? endOffsetFor(node) : null;
70
+ if (typeof start === "number" && start >= 0 && end !== null && end > start) ranges.push({ start, end });
71
+ });
72
+ return ranges.filter((range, index) => !ranges.some((other, otherIndex) =>
73
+ otherIndex !== index && other.start <= range.start && range.end <= other.end));
74
+ }
75
+
76
+ function leadingRunStart(html, start) {
77
+ const newline = html.lastIndexOf("\n", start - 1);
78
+ if (newline < 0 || /\S/.test(html.slice(newline + 1, start))) return start;
79
+ return html[newline - 1] === "\r" ? newline - 1 : newline;
80
+ }
81
+
82
+ function spliceRanges(html, ranges) {
83
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
84
+ let stripped = html;
85
+ let limit = html.length;
86
+ for (let index = sorted.length - 1; index >= 0; index -= 1) {
87
+ const { start, end } = sorted[index];
88
+ if (end > limit) continue;
89
+ const from = leadingRunStart(html, start);
90
+ if (from >= end) continue;
91
+ stripped = stripped.slice(0, from) + stripped.slice(end);
92
+ limit = from;
93
+ }
94
+ return stripped;
95
+ }
96
+
97
+ // `features` decides the conditional regions: null (the default, and the offline
98
+ // destination) strips every one of them, while a list strips only the missing features
99
+ // it names. Unconditional `no-copy` content always goes.
100
+ export function stripCopyContent(html, features = null) {
101
+ if (typeof html !== "string" || !/clay|data-paid-only/i.test(html)) return html;
102
+ return spliceRanges(html, markedRanges(html, features));
103
+ }
@@ -1,6 +1,13 @@
1
1
  // Saving a page as a file the browser downloads. Kept apart from the conflict copy
2
2
  // builders so an editor that only offers "download this page" loads none of them.
3
3
 
4
+ const HTML_TYPES = ["text/html", "application/xhtml+xml"];
5
+
6
+ function isHtml(type) {
7
+ if (typeof type !== "string") return false;
8
+ return HTML_TYPES.includes(type.split(";")[0].trim().toLowerCase());
9
+ }
10
+
4
11
  export function documentName(pathname = location.pathname) {
5
12
  const last = pathname.split("/").filter(Boolean).pop();
6
13
  if (!last) return pathname.endsWith("/") || pathname === "" ? "index" : "page";
@@ -9,10 +16,15 @@ export function documentName(pathname = location.pathname) {
9
16
  return name.replace(/\.(html?|htmlclay)$/i, "") || "page";
10
17
  }
11
18
 
12
- export function downloadHtml({ html, name, type = "text/html" }) {
19
+ export async function downloadHtml({ html, name, type = "text/html" }) {
13
20
  let url = null;
14
21
  try {
15
- url = URL.createObjectURL(new Blob([html], { type }));
22
+ let contents = html;
23
+ if (isHtml(type) && typeof html === "string" && /clay|data-paid-only/i.test(html)) {
24
+ const { stripCopyContent } = await import("./copy-content.js");
25
+ contents = stripCopyContent(html);
26
+ }
27
+ url = URL.createObjectURL(new Blob([contents], { type }));
16
28
  const a = document.createElement("a");
17
29
  a.setAttribute("clay", "no-save no-watch no-snapshot");
18
30
  a.href = url;
@@ -57,8 +57,8 @@ function place() {
57
57
  // The safe copy the conflict notice offers, for the same reason: a file the person can
58
58
  // open has to exist outside a host that will not store these bytes. It already reports
59
59
  // its own failure.
60
- function download() {
61
- downloadStored({});
60
+ async function download() {
61
+ await downloadStored({});
62
62
  }
63
63
 
64
64
  function build() {
@@ -348,7 +348,11 @@ export async function pageBytes(clay, { loc = location, fetchFn = (url, init) =>
348
348
  export async function downloadCopy({ clay, doc = document, loc = location, fetchFn } = {}) {
349
349
  if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return false;
350
350
  try {
351
- const bytes = await pageBytes(clay, { loc, fetchFn });
351
+ let bytes = await pageBytes(clay, { loc, fetchFn });
352
+ if (typeof bytes === "string" && /clay|data-paid-only/i.test(bytes)) {
353
+ const { stripCopyContent } = await import("./copy-content.js");
354
+ bytes = stripCopyContent(bytes);
355
+ }
352
356
  const url = URL.createObjectURL(new Blob([bytes], { type: "text/html" }));
353
357
  const a = doc.createElement("a");
354
358
  a.setAttribute("clay", "editor-ui");
@@ -54,7 +54,7 @@ export const REGION_ATTRS = ['no-save', 'no-trigger-autosave', 'no-dirty', 'no-u
54
54
  // Every canonical token spellable inside the space-separated `clay` attribute.
55
55
  // Exported: this list used to exist as a private CLAY_TOKENS that nothing read,
56
56
  // while callers hardcoded their own copies.
57
- export const TOKENS = [...POLICY_TOKENS];
57
+ export const TOKENS = [...POLICY_TOKENS, 'no-copy', 'no-copy-unless:paid-forms'];
58
58
 
59
59
  // True when a region marker is present, whether spelled as a `clay` token
60
60
  // (whitespace-token semantics, matching [clay~=token]) or a legacy bare attribute.
@@ -112,12 +112,12 @@ export function createFeature({ view = null, download = downloadHtml, fetch: fet
112
112
  }
113
113
 
114
114
  function decide(s) {
115
- listen(s, s.panel, "click", (event) => {
115
+ listen(s, s.panel, "click", async (event) => {
116
116
  const b = event.target.closest?.("[data-act]");
117
117
  if (!b) return;
118
118
  event.preventDefault();
119
119
  if (b.dataset.act === "close") close();
120
- else if (b.dataset.act === "download") save(s);
120
+ else if (b.dataset.act === "download") await save(s);
121
121
  else if (b.dataset.act === "retry") run().catch((error) => console.error("clayjs: the reading copy could not be made", error));
122
122
  });
123
123
  listen(s, doc, "keydown", (event) => {
@@ -130,9 +130,9 @@ export function createFeature({ view = null, download = downloadHtml, fetch: fet
130
130
 
131
131
  // The one place a reading copy becomes a file. The download is handed the bytes that
132
132
  // were shown, never a second render, so what a person saw is what they get.
133
- function save(s) {
133
+ async function save(s) {
134
134
  if (!s.ready || disposed) return { ok: false, code: "not-editing" };
135
- const saved = download({ html: s.ready.html, name: s.ready.name });
135
+ const saved = await download({ html: s.ready.html, name: s.ready.name });
136
136
  close();
137
137
  return { ok: saved === true, downloaded: saved === true };
138
138
  }
@@ -83,10 +83,10 @@ export function createFeature({ view = null, download = downloadHtml } = {}) {
83
83
  s.panel.replaceChildren(...parts);
84
84
  }
85
85
 
86
- function save() {
86
+ async function save() {
87
87
  const s = session;
88
88
  if (!s) return { ok: false, code: "no-export" };
89
- if (download({ html: s.report.text, name: s.name, type: "text/markdown" })) {
89
+ if (await download({ html: s.report.text, name: s.name, type: "text/markdown" })) {
90
90
  close();
91
91
  view.focus?.();
92
92
  return { ok: true, downloaded: true };
@@ -228,12 +228,12 @@ export function createRecoveryReview({
228
228
  button.setAttribute("aria-expanded", "false");
229
229
  };
230
230
 
231
- const download = () => {
231
+ const download = async () => {
232
232
  const edits = [];
233
233
  for (const row of rows) edits.push({ id: row.id, kind: "recovered-page", name: row.message, yours: row.text, now: "", sentence: row.message });
234
234
  const composing = "Text typed while composing never reached the page.";
235
235
  for (const row of journaled) edits.push({ id: row.id, kind: "composing", name: "Typed text", yours: row.text, now: "", sentence: composing });
236
- if (!downloadStored({ html: null, edits })) failure = "That download could not be made.";
236
+ if (!(await downloadStored({ html: null, edits }))) failure = "That download could not be made.";
237
237
  draw();
238
238
  };
239
239
 
@@ -67,14 +67,14 @@ function withoutPageTag(html) {
67
67
  // One complete, openable file: the capture the save would have sent, with the
68
68
  // page projection already applied by the live store's snapshot listener, and
69
69
  // without the page editor's merge state, which no saved file or download carries.
70
- function downloadPageCopy() {
70
+ async function downloadPageCopy() {
71
71
  const { forSave } = captureForSaveAndComparison({ emitForSync: false });
72
- return downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
72
+ return await downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
73
73
  }
74
74
 
75
75
  // A copy of the page captured earlier (captureForSaveAndComparison().forSave), as a download.
76
- function downloadSnapshot(forSave) {
77
- return downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
76
+ async function downloadSnapshot(forSave) {
77
+ return await downloadHtml({ html: withoutPageTag(forSave), name: `${documentName()}.html` });
78
78
  }
79
79
 
80
80
  // The article's own plain text, never the editor's chrome: the capture carries
@@ -146,7 +146,7 @@ export function attachActions({ addItem, note, currentMode }) {
146
146
  if (typeof html === "string" && /^https?:$/.test(location.protocol)) {
147
147
  html = html.replace(/(^|[^\w./:-])\/_\/uploads\//g, `$1${location.origin}/_/uploads/`);
148
148
  }
149
- if (typeof html !== "string" || !downloadHtml({ html, name: `${documentName()}.html` })) {
149
+ if (typeof html !== "string" || !(await downloadHtml({ html, name: `${documentName()}.html` }))) {
150
150
  note("Couldn't build the download. Try again.");
151
151
  }
152
152
  },
@@ -233,17 +233,17 @@ function stopEditing(inst) {
233
233
 
234
234
  // The tab's own page, when the store no longer holds it (another page replaced it, or the
235
235
  // room refused it): the saved document with the article rendered from the tab's document.
236
- function downloadTabCopy(inst) {
237
- if (!inst.tabValue) return downloadPageCopy();
236
+ async function downloadTabCopy(inst) {
237
+ if (!inst.tabValue) return await downloadPageCopy();
238
238
  const { forSave } = captureForSaveAndComparison({ emitForSync: false });
239
239
  const doc = new DOMParser().parseFromString(forSave, "text/html");
240
240
  const articles = doc.querySelectorAll(ARTICLE_SEL);
241
- if (articles.length !== 1) return downloadPageCopy();
241
+ if (articles.length !== 1) return await downloadPageCopy();
242
242
  const rendered = pageCode.renderFromTags([JSON.stringify(inst.tabValue)]);
243
243
  articles[0].innerHTML = rendered.html;
244
244
  if (rendered.rev == null) articles[0].removeAttribute("data-rev");
245
245
  else articles[0].setAttribute("data-rev", rendered.rev);
246
- return downloadSnapshot(`<!DOCTYPE html>\n${doc.documentElement.outerHTML}`);
246
+ return await downloadSnapshot(`<!DOCTYPE html>\n${doc.documentElement.outerHTML}`);
247
247
  }
248
248
 
249
249
  function stopWithNotice(inst, text, { reload = true } = {}) {
@@ -103,22 +103,19 @@ export async function mountPrompt({ memory, appName, link }) {
103
103
  });
104
104
  }
105
105
 
106
- function downloadCurrent() {
106
+ async function downloadCurrent() {
107
107
  const getHTML = window.clay && window.clay.getHTML;
108
108
  if (typeof getHTML !== "function" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return false;
109
109
  const segment = location.pathname.split("/").filter(Boolean).pop() || "index.html";
110
- const objectUrl = URL.createObjectURL(new Blob([getHTML()], { type: "text/html" }));
111
- const a = document.createElement("a");
112
- a.href = objectUrl;
113
- a.download = segment.replace(/\.html$/, ".htmlclay");
114
- document.body.appendChild(a);
115
- a.click();
116
- a.remove();
117
- setTimeout(() => URL.revokeObjectURL?.(objectUrl), 1000);
118
- return true;
110
+ try {
111
+ const { downloadHtml } = await import("../../core/download.js");
112
+ return await downloadHtml({ html: getHTML(), name: segment.replace(/\.html$/, ".htmlclay") });
113
+ } catch {
114
+ return false;
115
+ }
119
116
  }
120
117
 
121
- root.addEventListener("click", (e) => {
118
+ root.addEventListener("click", async (e) => {
122
119
  const t = e.target.closest("[data-hcp]");
123
120
  if (t) {
124
121
  const act = t.getAttribute("data-hcp");
@@ -133,7 +130,10 @@ export async function mountPrompt({ memory, appName, link }) {
133
130
  if (e.target.closest("[data-hcp-link]")) {
134
131
  if (root.getAttribute("data-hcp-reason") === "error") {
135
132
  e.preventDefault();
136
- downloadCurrent();
133
+ if (!(await downloadCurrent())) {
134
+ root.querySelector(".hcp-body [data-hcp-for='error']").textContent = "The download could not be made. Keep this page open and try Download free again.";
135
+ return;
136
+ }
137
137
  }
138
138
  mem.downloaded = true; mem.shown = true; memory.save();
139
139
  }
@@ -788,7 +788,9 @@ function send(payload, opts = {}) {
788
788
  // Accepted by the router and taken by nobody. Reporting this as an error
789
789
  // rather than a pending request is the difference between a UI that says
790
790
  // "start an agent" and one that spins forever.
791
- finish(rec, "error", "no agent is attached to this file");
791
+ finish(rec, "error", rec.helper
792
+ ? `The local program "${rec.helper}" is not connected to this file.`
793
+ : "No program is connected to this file.");
792
794
  }
793
795
  };
794
796
 
@@ -1 +0,0 @@
1
- import{a}from"../c-OYRZ7XG2.js";import"../c-Q26MME6N.js";import"../c-ZWIOJGMT.js";import"../c-IJUK2GPW.js";import"../c-7MG5RU63.js";import"../c-XQXZAO7N.js";import"../c-S7LK4KUE.js";import"../c-VG5XIN4T.js";import"../c-2YYDMLNZ.js";import"../c-HA6INT6J.js";import"../c-DCFSEJZG.js";import"../c-WRBKEJD4.js";import"../c-VHNSBD2M.js";export{a as default};
@@ -1 +0,0 @@
1
- import{b as m,c as g,d as c,e as f,f as v}from"./c-WRBKEJD4.js";var w=['[src^="chrome-extension://"]','[href^="chrome-extension://"]','[src^="moz-extension://"]','[href^="moz-extension://"]','[src^="safari-web-extension://"]','[href^="safari-web-extension://"]','[id="1p-menu-live-region"]',"com-1password-button","com-1password-menu","com-1password-notification","grammarly-extension","grammarly-desktop-integration","lt-div","lt-card","lt-toolbar","lt-highlighter","[data-lastpass-root]","[data-lastpass-icon-root]","[data-dashlane-shadowhost]","[data-klarna-extension]",'[class*="klarna-extension"]',".skiptranslate","#goog-gt-tt",".goog-te-banner-frame",".goog-te-balloon-frame","style.darkreader","style[data-darkreader-mode]","style[data-darkreader-scheme]","#loom-companion-mv3","loom-container","[ext-id]"],N=["data-grammarly","data-lt","data-1p","data-com-onepassword","data-lastpass","data-lp","data-kwimpala","data-bitwarden","data-bw","data-dashlane","data-np","data-protonpass","data-proton-pass","data-keeper","data-rf","data-roboform","data-honey","data-rakuten","data-capital-one","data-wikibuy","data-klarna","data-darkreader","data-evernote","data-pocket","data-pin"],u=w.join(","),_=new RegExp("^(?:"+N.join("|")+")(?:-|$)");function X(e){if(!(!e||!e.querySelectorAll))try{for(let a of e.querySelectorAll(u))a.remove();let t=a=>{let r=a.attributes;if(!r)return;let n=null;for(let s=0;s<r.length;s++){let o=r[s].name;_.test(o.toLowerCase())&&(n||(n=[])).push(o)}if(n)for(let s of n)a.removeAttribute(s)};t(e);for(let a of e.querySelectorAll("*"))t(a)}catch(t){typeof console<"u"&&console.warn("[clay] stripExtensionNoise skipped:",t)}}var l={FULL:"full",FROZEN:"frozen",NONE:"none"},k=["no-save","no-trigger-autosave","no-dirty","no-undo","no-watch","no-data","freeze","editor-ui","live"],A=[...m],i=g;function z(e,t){let a=new Set((e.getAttribute("clay")||"").trim().split(/\s+/).filter(Boolean));a.has(t)||(a.add(t),e.setAttribute("clay",Array.from(a).join(" ")))}var T=`${f("save")}, [save-remove]`,h='[clay~="freeze"], [freeze], [save-freeze]',y='[clay~="no-save"], [clay~="no-trigger-autosave"], [clay~="no-dirty"], [clay~="freeze"], [clay~="no-watch"], [no-save], [save-remove], [no-trigger-autosave], [no-dirty], [save-ignore], [freeze], [save-freeze], [no-watch], [mutations-ignore], [clay~="live"]',x='[clay~="no-trigger-autosave"], [no-trigger-autosave]',j='[clay~="live"]',R='[clay~="no-dirty"], [no-dirty], [save-ignore]',S='[clay~="no-save"], [clay~="no-dirty"], [clay~="freeze"], [clay~="no-watch"], [no-save], [save-remove], [no-dirty], [save-ignore], [freeze], [save-freeze], [no-watch], [mutations-ignore], [clay~="live"]',p=`${f("snapshot")}, [snapshot-remove]`,O=f("data");function I(e){let t=E(e);return!!(t&&t.closest&&t.closest(p))}function b(e){return v(e,"data")}var d={full:0,frozen:1,none:2},C=["full","frozen","none"];function E(e){return e&&e.nodeType!==1?e.parentElement:e}function L(e){let t=E(e);if(t&&t.closest&&t.closest(u))return{watched:!1,autosaveTriggered:!1,dirtyTracked:!1,undoable:!1,persist:l.FULL,extension:!0};let a=!0,r=!0,n=!1,s=!1,o=0;for(;t&&t.nodeType===1;)t.hasAttribute&&(c(t,"no-watch")&&(a=!1),i(t,"no-trigger-autosave")&&(n=!0),i(t,"no-dirty")&&(n=!0,s=!0),i(t,"live")&&(n=!0,s=!0,r=!1),c(t,"no-undo")&&(r=!1),c(t,"no-save")&&(o=Math.max(o,d.none)),i(t,"freeze")&&(o=Math.max(o,d.frozen)),i(t,"mutations-ignore")&&(a=!1),i(t,"save-remove")&&(o=Math.max(o,d.none),r=!1),i(t,"save-ignore")&&(n=!0,s=!0,r=!1),i(t,"save-freeze")&&(o=Math.max(o,d.frozen),r=!1)),t=t.parentElement;return a||(n=!0,r=!1,s=!0),o>0&&(n=!0,s=!0),{watched:a,autosaveTriggered:!n,dirtyTracked:!s,undoable:r,persist:C[o],extension:!1}}function P(e){let t=E(e);if(t&&t.closest&&t.closest(u))return!0;for(;t&&t.nodeType===1;){if(t.hasAttribute&&(c(t,"no-watch")||i(t,"mutations-ignore")))return!0;t=t.parentElement}return!1}function F(e,t){return{watched:e.watched&&t.watched,autosaveTriggered:e.autosaveTriggered&&t.autosaveTriggered,dirtyTracked:e.dirtyTracked&&t.dirtyTracked,undoable:e.undoable&&t.undoable,persist:d[e.persist]>=d[t.persist]?e.persist:t.persist,extension:e.extension||t.extension}}var D={"no-watch":e=>!e.watched,"mutations-ignore":e=>!e.watched,"no-save":e=>e.persist===l.NONE,"save-remove":e=>e.persist===l.NONE,freeze:e=>e.persist===l.FROZEN,"save-freeze":e=>e.persist===l.FROZEN,"no-trigger-autosave":e=>!e.autosaveTriggered,"no-dirty":e=>!e.dirtyTracked,"save-ignore":e=>!e.dirtyTracked,"no-undo":e=>!e.undoable};function M(e,t,a){if(e.extension)return!0;if(a&&a.length)return a.some(r=>D[r]?.(e)||!1);switch(t){case"observed":return!e.watched;case"autosave":return!e.autosaveTriggered;case"dirty":return!e.dirtyTracked;case"undo":return!e.undoable;default:return!e.watched||!e.autosaveTriggered||e.persist!==l.FULL}}var K={resolveRegionPolicy:L,isInert:P,isSnapshotRemoved:I,isDataExcluded:b,skipForPolicy:M,strictestPolicy:F,addRegionToken:z,PERSIST:l,TOKENS:A,REGION_ATTRS:k,STRIP_FROM_SAVE:T,STRIP_FROM_COMPARISON:y,STRIP_FROM_DIRTY_CHECK:S,NO_TRIGGER_AUTOSAVE_SELECTOR:x,NO_DIRTY_SELECTOR:R,SNAPSHOT_REMOVE_SELECTOR:p,NO_DATA_SELECTOR:O,FREEZE_SELECTOR:h,selectors:{stripFromSave:T,stripFromComparison:y,stripFromDirtyCheck:S,noTriggerAutosave:x,noDirty:R,snapshotRemove:p,noData:O,freeze:h}},q=K;export{u as a,_ as b,X as c,l as d,k as e,A as f,z as g,T as h,h as i,y as j,x as k,j as l,R as m,S as n,p as o,O as p,I as q,b as r,L as s,P as t,F as u,M as v,K as w,q as x};
@@ -1 +0,0 @@
1
- var o={table:{types:["table"],views:["table","table_cell"],module:"page/features/table.js",load:()=>import("./c-JK3PHE7J.js")},board:{types:["board"],module:"page/features/board.js",load:()=>import("./c-ZRUQ5FSU.js")},chart:{types:["chart"],views:["chart"],module:"page/features/chart.js",load:()=>import("./c-L6FLIUXP.js")},checklist:{types:[],views:[],match:e=>e.type.name==="list_item"&&e.attrs.kind==="check",module:"page/features/checklist.js",load:()=>import("./c-HXPHZN7Q.js")},code:{types:["code_block"],module:"page/features/code.js",load:()=>import("./c-RRZJWFLE.js")},toc:{types:["toc"],module:"page/features/toc.js",load:()=>import("./c-GWNC3UGL.js")},titleIcon:{types:[],views:["heading"],match:e=>e.attrs.pageIcon!=null||e.attrs.pageCoverSrc!=null,module:"page/features/title-icon.js",load:()=>import("./c-O4JMKN6U.js")},media:{types:["image","file"],module:"page/features/media.js",load:()=>import("./c-DHUCEMXE.js")},tool:{types:["tool"],module:"page/features/tool.js",boot:["page/tools.js","page/tool-data.js"],load:()=>import("./c-WKNRJX3C.js")}},r=e=>Object.keys(o).find(t=>o[t].types.includes(e))??null,s=e=>{for(let t of Object.keys(o)){let a=o[t];if(a.match?a.match(e):a.types.includes(e.type.name))return t}return null},l={mentions:{module:"page/features/mentions.js",load:()=>import("./c-MIKZEG5I.js")},dates:{module:"page/features/dates.js",load:()=>import("./c-FOOE4GML.js")},emoji:{module:"page/features/emoji.js",load:()=>import("./c-UXFUJEZO.js")},titleIcon:{module:"page/features/title-icon.js",load:()=>import("./c-O4JMKN6U.js")},titleCover:{module:"page/features/title-cover.js",load:()=>import("./c-QINHDETP.js")},exportHtml:{module:"page/features/export-html.js",load:()=>import("./c-DKLS74XB.js")},importMarkdown:{module:"page/features/import-markdown.js",load:()=>import("./c-AXGY2WTB.js")},exportMarkdown:{module:"page/features/export-markdown.js",load:()=>import("./c-ERQSNR6K.js")}};export{o as a,r as b,s as c,l as d};
@@ -1 +0,0 @@
1
- import{a,b,c}from"./c-UY4QX42B.js";import"./c-L7JL5TL3.js";import"./c-BJ7YQ5QZ.js";import"./c-4JZDZFF6.js";import"./c-VAK7SG37.js";import"./c-MHI3GYKF.js";import"./c-NUHP2XDB.js";import"./c-DCFSEJZG.js";import"./c-WRBKEJD4.js";import"./c-VHNSBD2M.js";export{c as default,b as dismissPersistent,a as toastPersistent};
@@ -1 +0,0 @@
1
- import{a as d}from"./c-WNTYKCDG.js";import"./c-PZ4XEPUP.js";import{g as w,l as v,n as x,q as s}from"./c-BJ7YQ5QZ.js";import{a as p}from"./c-4JZDZFF6.js";import{e as f}from"./c-VAK7SG37.js";import{b as m}from"./c-MHI3GYKF.js";import"./c-MCOOHCSG.js";import{b as c}from"./c-NUHP2XDB.js";import{f as l}from"./c-3O7OBDCM.js";import"./c-BPF2NSPV.js";import"./c-P7KNDG34.js";import"./c-CYWLPPUS.js";import"./c-N7JPUPGC.js";import"./c-7MG5RU63.js";import"./c-XQXZAO7N.js";import"./c-S7LK4KUE.js";import"./c-VG5XIN4T.js";import"./c-2YYDMLNZ.js";import"./c-HA6INT6J.js";import"./c-DCFSEJZG.js";import"./c-WRBKEJD4.js";import"./c-VHNSBD2M.js";var $="Download a copy, then reduce its size; saving still works.",b="Download a copy to keep your work.",t=null,a=null,i=null,r=null;function h(e){return typeof e!="number"||!Number.isFinite(e)||e<0?"an unknown size":e>=1024*1024?`${(e/(1024*1024)).toFixed(1)} MB`:e>=1024?`${Math.max(1,Math.round(e/1024))} KB`:`${e} bytes`}function z({bytes:e,maxBytes:n,state:S}){let u=`At the last save attempt: ${h(e)} of ${h(n)}.`;return S==="over"?`This host can't save a file this size. ${u} ${b}`:`This file is getting large. ${u} ${$}`}function o(){if(!t)return;let e=window.visualViewport,n=e?Math.max(0,window.innerHeight-e.height-e.offsetTop):0;c(t,"bottom",`calc(${16+n}px + env(safe-area-inset-bottom,0px))`)}function y(){d({})}function A(){t=v("div",["position:fixed","left:50%","transform:translateX(-50%)","z-index:2147483000","display:flex","align-items:center","gap:10px 12px","width:max-content","max-width:calc(100vw - 24px)","flex-wrap:wrap","padding:8px 8px 8px 14px","text-align:left",`color-scheme:${p()}`]),t.setAttribute("clay","editor-ui"),t.setAttribute("data-clay-save-size",""),t.setAttribute("role","status"),a=x("span",[],""),i=w("Download my copy",{small:!0,onClick:y}),s(i,!1,"inline-flex"),t.append(a,i),document.body.appendChild(t),o(),window.visualViewport?.addEventListener("resize",o),window.visualViewport?.addEventListener("scroll",o)}function M(e){t||A(),a.textContent=z(e),s(i,e.state==="over","inline-flex"),o(),m(t,{background:f.surface})}function E(){t&&(window.visualViewport?.removeEventListener("resize",o),window.visualViewport?.removeEventListener("scroll",o),t.remove(),t=null,a=null,i=null)}function g(e){let n=e?.detail;n&&(n.state==="near"||n.state==="over"?M(n):E())}function k(){l||L()}function N(e){l&&(r||=new AbortController,document.addEventListener("clay:save-size",g,{signal:r.signal}),document.addEventListener("clay:modechange",k,{signal:r.signal}),e?.type==="clay:save-size"&&g(e))}function L(){r?.abort(),r=null,E()}export{N as start,L as stop};
@@ -1 +0,0 @@
1
- function r(t=location.pathname){let o=t.split("/").filter(Boolean).pop();if(!o)return t.endsWith("/")||t===""?"index":"page";let l=o;try{l=decodeURIComponent(o)}catch{}return l.replace(/\.(html?|htmlclay)$/i,"")||"page"}function c({html:t,name:o,type:l="text/html"}){let n=null;try{n=URL.createObjectURL(new Blob([t],{type:l}));let e=document.createElement("a");return e.setAttribute("clay","no-save no-watch no-snapshot"),e.href=n,e.download=o,e.style.setProperty("display","none","important"),document.body.appendChild(e),e.click(),e.remove(),setTimeout(()=>URL.revokeObjectURL(n),4e4),!0}catch(e){return console.error("clayjs: could not build the download",e),n&&URL.revokeObjectURL(n),!1}}export{r as a,c as b};
@@ -1 +0,0 @@
1
- import{a as b,b as c,c as d,d as e,e as f}from"./c-FCNAO2MI.js";import{g as a}from"./c-4FJTBE6I.js";import"./c-EXKHPOVC.js";import"./c-L5VFATT6.js";import"./c-CQQDL6O4.js";import"./c-ZGDYHKG3.js";import"./c-W4JCVLAB.js";import"./c-7K4SCQB7.js";import"./c-WMRJKQ2V.js";import"./c-AIWN4QN4.js";import"./c-QIXT3Q7C.js";import"./c-WCXLG3MV.js";import"./c-57XDY6HC.js";import"./c-I2SCCUEW.js";import"./c-VHNSBD2M.js";export{e as ChartView,d as chartFromSlash,a as chartTables,b as insertChartAfterTable,f as refreshCharts,c as tableLabel};