@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.
Files changed (177) 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 +1228 -715
  11. package/dist/m/attrs/{save-freeze-MTFVVYV5.js → save-freeze-JZHKRQLR.js} +1 -1
  12. package/dist/m/{c-TV7JHSRL.js → c-24PMUJIV.js} +1 -1
  13. package/dist/m/{c-NBNRLZXO.js → c-2AYVYEI4.js} +1 -1
  14. package/dist/m/{c-VIW5X35H.js → c-2L6O5PDS.js} +1 -1
  15. package/dist/m/{c-FNIGX4FI.js → c-2RYOGUQM.js} +1 -1
  16. package/dist/m/{c-LDUNOR57.js → c-2Z22FCFK.js} +13 -25
  17. package/dist/m/{c-DKFSOJCV.js → c-2ZVOEU4M.js} +1 -1
  18. package/dist/m/{c-BJPZCWAV.js → c-37NAONZA.js} +1 -1
  19. package/dist/m/{c-CSNE5EIF.js → c-4PO3YGDC.js} +1 -1
  20. package/dist/m/{c-UWT4CXQ4.js → c-5OJ7YF2N.js} +1 -1
  21. package/dist/m/{c-CAWNFER6.js → c-5TIZ2I64.js} +1 -1
  22. package/dist/m/{c-ZO6VL2SC.js → c-5WMXEKFH.js} +1 -1
  23. package/dist/m/{c-4TWARTLS.js → c-6RDOKLN7.js} +1 -1
  24. package/dist/m/{c-6RC66MYY.js → c-6TYGD5AB.js} +1 -1
  25. package/dist/m/c-A4IP6MBC.js +1 -0
  26. package/dist/m/{c-ZAHTFCXQ.js → c-ALDVFPWL.js} +1 -1
  27. package/dist/m/{c-LV5MABYD.js → c-AYVGD6A6.js} +1 -1
  28. package/dist/m/{c-N33TPVTL.js → c-B6O7AJYG.js} +1 -1
  29. package/dist/m/{c-P3WATEDR.js → c-BCGWJYXW.js} +1 -1
  30. package/dist/m/{c-4OGEVKR3.js → c-BGKAT2FL.js} +1 -1
  31. package/dist/m/{c-N7Y52HJH.js → c-BXYSDKAN.js} +1 -1
  32. package/dist/m/c-CRPIDGXC.js +78 -0
  33. package/dist/m/c-CUMXGHVD.js +1 -0
  34. package/dist/m/{c-Q5KAQJAG.js → c-DTMZ3ZHI.js} +1 -1
  35. package/dist/m/{c-GY4ODHO2.js → c-ELYIIYRS.js} +1 -1
  36. package/dist/m/{c-67LGZ52V.js → c-FCSZ3DC5.js} +1 -1
  37. package/dist/m/{c-NLWEZYKJ.js → c-FQGHR32O.js} +1 -1
  38. package/dist/m/{c-PCEMXY2K.js → c-G4PWUDD5.js} +1 -1
  39. package/dist/m/{c-6KBKYLPO.js → c-G6COVIZB.js} +1 -1
  40. package/dist/m/c-GBTVCJXC.js +1 -0
  41. package/dist/m/c-GC2HICFJ.js +1 -0
  42. package/dist/m/{c-TMT5TL65.js → c-HCNWJE4A.js} +2 -2
  43. package/dist/m/{c-DZBZ57EE.js → c-JCOQVZYS.js} +1 -1
  44. package/dist/m/{c-FAXQU7MQ.js → c-JGHEACGK.js} +1 -1
  45. package/dist/m/{c-PWBJVHA4.js → c-JGLX6TBK.js} +1 -1
  46. package/dist/m/c-JNRKHYWO.js +1 -0
  47. package/dist/m/{c-6UEFPHHE.js → c-KDXIJEEF.js} +1 -1
  48. package/dist/m/{c-FMCPQ4VT.js → c-L4KMT7FO.js} +2 -2
  49. package/dist/m/{c-OI2BYWVV.js → c-LCYPMULG.js} +1 -1
  50. package/dist/m/{c-4RFZROCF.js → c-LIGQYZ7M.js} +4 -4
  51. package/dist/m/{c-QHVCUTJV.js → c-LKIWEPAV.js} +1 -1
  52. package/dist/m/{c-EDSPKYMA.js → c-LUXAB2XI.js} +1 -1
  53. package/dist/m/{c-DEAFCHDO.js → c-LWIDDWAR.js} +2 -2
  54. package/dist/m/c-MPSKIU2I.js +1 -0
  55. package/dist/m/{c-6PBXRWDX.js → c-NK33MMYF.js} +1 -1
  56. package/dist/m/{c-F5AER2ID.js → c-NQLWOHLH.js} +1 -1
  57. package/dist/m/{c-L7K5EXU2.js → c-NTHUTABN.js} +1 -1
  58. package/dist/m/{c-4PHDUV3H.js → c-NW54NHKE.js} +1 -1
  59. package/dist/m/{c-3AXJDL2P.js → c-OG6GFSUX.js} +1 -1
  60. package/dist/m/{c-MTQHWQC3.js → c-OKEGUICD.js} +1 -1
  61. package/dist/m/c-OPONJ2UK.js +128 -0
  62. package/dist/m/{c-LF6HNVPU.js → c-OWQWOGYI.js} +1 -1
  63. package/dist/m/{c-FDAXZ2PM.js → c-P6Q2ZTFQ.js} +1 -1
  64. package/dist/m/c-PKCCHJ5X.js +1 -0
  65. package/dist/m/c-PWJSJEJU.js +1 -0
  66. package/dist/m/{c-Z7RJANGO.js → c-PXLTUYZC.js} +1 -1
  67. package/dist/m/c-QEN6HZCA.js +1 -0
  68. package/dist/m/{c-26KVJS6J.js → c-QGAPG7OM.js} +1 -1
  69. package/dist/m/{c-HWR6GMGK.js → c-QIR5CAKM.js} +1 -1
  70. package/dist/m/{c-QJFYVCJO.js → c-QM5LXRDT.js} +1 -1
  71. package/dist/m/{c-MRURHLKV.js → c-QUGCKMWF.js} +1 -1
  72. package/dist/m/{c-KE33W4RE.js → c-QYYUVLVG.js} +1 -1
  73. package/dist/m/c-RO73MKKR.js +1 -0
  74. package/dist/m/c-SBCBXKOZ.js +26 -0
  75. package/dist/m/{c-XDVAZDUE.js → c-SGRIYOAJ.js} +1 -1
  76. package/dist/m/c-SKF5HL4I.js +4 -0
  77. package/dist/m/{c-IWTTOC2V.js → c-SRPC3VBE.js} +1 -1
  78. package/dist/m/{c-XZJ5IK6Q.js → c-T5NK2TVY.js} +1 -1
  79. package/dist/m/{c-VFES2BP5.js → c-TANBH7FI.js} +1 -1
  80. package/dist/m/{c-H6DXQEH2.js → c-THGQQDYY.js} +1 -1
  81. package/dist/m/{c-BV6FQQXM.js → c-TSBVMWAH.js} +1 -1
  82. package/dist/m/{c-QTC6VGA4.js → c-UD5JZTDL.js} +1 -1
  83. package/dist/m/{c-ZYEJLZEY.js → c-UDGEFWNU.js} +1 -1
  84. package/dist/m/{c-ZSCBEB7I.js → c-UHUNXWKN.js} +1 -1
  85. package/dist/m/{c-FWFJUU3T.js → c-VIRFF5HG.js} +1 -1
  86. package/dist/m/{c-GPTKI7AQ.js → c-VYJ7O4RI.js} +1 -1
  87. package/dist/m/{c-TUGVINJR.js → c-W5E5P2TZ.js} +1 -1
  88. package/dist/m/{c-WWE67K5Y.js → c-W7IPVCAA.js} +1 -1
  89. package/dist/m/{c-HAVGDXAL.js → c-WJ5DRBHT.js} +1 -1
  90. package/dist/m/{c-V2J5C5MT.js → c-WQZTI5P2.js} +1 -1
  91. package/dist/m/c-X5WC4PX5.js +1 -0
  92. package/dist/m/{c-ZKELCT4T.js → c-XCFQEWW6.js} +2 -2
  93. package/dist/m/c-XLQ3BDMM.js +1 -0
  94. package/dist/m/{c-FLBUYV3P.js → c-XX75FFNL.js} +1 -1
  95. package/dist/m/{c-JHJR5BQX.js → c-YK2L436K.js} +1 -1
  96. package/dist/m/{c-MEWUGR4D.js → c-YYMZWQLR.js} +1 -1
  97. package/dist/m/core/{admin-attrs-ZG7R5V3E.js → admin-attrs-U7IJNYBG.js} +1 -1
  98. package/dist/m/core/{autosave-GI3WNJYN.js → autosave-F5YQCKVL.js} +1 -1
  99. package/dist/m/core/{conflict-notice-CEI3XY6A.js → conflict-notice-42H4ASLA.js} +1 -1
  100. package/dist/m/core/{persist-3OPWJKZS.js → persist-CJXIL7D4.js} +1 -1
  101. package/dist/m/core/{save-OJ5NSLS7.js → save-ESPSQPLV.js} +1 -1
  102. package/dist/m/core/{save-core-DX3RFVO3.js → save-core-6SQHOR6W.js} +1 -1
  103. package/dist/m/core/snapshot-FJP5IJL3.js +1 -0
  104. package/dist/m/core/{unsaved-warning-HGYH7O4H.js → unsaved-warning-HNQVTA2D.js} +1 -1
  105. package/dist/m/internals/{index-V4WC44FU.js → index-NRPNDTSA.js} +1 -1
  106. package/dist/m/loader-FBDE2UAB.js +1 -0
  107. package/dist/m/manifest-1D2F5CA5.js +1 -0
  108. package/dist/m/manifest-edit-F7CC5CFD.js +1 -0
  109. package/dist/m/manifest.json +1090 -1020
  110. package/dist/m/page/{editor-BQEPCJLG.js → editor-OSKOW6RS.js} +2 -2
  111. package/dist/m/plugins/{ai-edit-45A2J6LC.js → ai-edit-VV4QTINH.js} +1 -1
  112. package/dist/m/plugins/{cms-IFTKWF4S.js → cms-H7DTMUQX.js} +1 -1
  113. package/dist/m/plugins/{indicator-IPZJ35K6.js → indicator-WVPGMOZ7.js} +1 -1
  114. package/dist/m/plugins/{live-GPQXSXSM.js → live-DCUBKXY4.js} +1 -1
  115. package/dist/m/plugins/menu-Q4MFFQ6E.js +1 -0
  116. package/dist/m/plugins/{page-BAVLWCP6.js → page-OAWUJQEL.js} +2 -2
  117. package/dist/m/plugins/{page-view-P3UGJFDZ.js → page-view-DZ26ZDDQ.js} +1 -1
  118. package/dist/m/plugins/{people-Z4RGVQAV.js → people-5XRNRP5U.js} +1 -1
  119. package/dist/m/plugins/{quickcrop-E34PUYA3.js → quickcrop-WWF5I422.js} +2 -2
  120. package/dist/m/plugins/{richclay-S4NVJYIK.js → richclay-PGOOJ4OK.js} +2 -2
  121. package/dist/m/plugins/{sortable-WJZZBM7R.js → sortable-266HY6TR.js} +1 -1
  122. package/dist/m/plugins/{source-KVN6YNTY.js → source-6BKMK4PO.js} +1 -1
  123. package/dist/m/plugins/{upgrade-KTSICB22.js → upgrade-BNZVFHMZ.js} +1 -1
  124. package/dist/m/plugins/visitor-drafts-2AUYKAIJ.js +20 -0
  125. package/dist/m/plugins/{visitor-drafts-owner-MZP45XSG.js → visitor-drafts-owner-Z5B2OQBR.js} +1 -1
  126. package/dist/m/plugins/visitor-menu-4LQMKLPP.js +1 -0
  127. package/dist/m/plugins/{wire-SYVIBNSE.js → wire-W2EV36KZ.js} +1 -1
  128. package/dist/m/{reader-core-RUIFH32Y.js → reader-core-POFQCKRV.js} +2 -2
  129. package/dist/m/sync/{live-sync-RGQMMWK2.js → live-sync-XGUPHI3I.js} +1 -1
  130. package/dist/m/ui/{index-EZGLEM5P.js → index-VGYAKAKI.js} +1 -1
  131. package/dist/offline-manifest.json +636 -570
  132. package/docs/guide.md +36 -25
  133. package/package.json +1 -1
  134. package/src/core/save-replace.js +5 -2
  135. package/src/core/serialize-steps.js +10 -2
  136. package/src/core/snapshot.js +37 -1
  137. package/src/core/upgrade-run.js +16 -2
  138. package/src/loader-logic.js +6 -2
  139. package/src/plugins/menu/dom-items.js +45 -0
  140. package/src/plugins/menu/looks.js +32 -0
  141. package/src/plugins/menu/shell-style.js +143 -0
  142. package/src/plugins/menu/shell.js +398 -0
  143. package/src/plugins/menu/style.js +15 -116
  144. package/src/plugins/menu/variants.js +56 -10
  145. package/src/plugins/menu/visitor.js +78 -0
  146. package/src/plugins/menu.js +21 -313
  147. package/src/plugins/visitor-drafts/inert.js +1 -0
  148. package/src/plugins/visitor-drafts/intro-style.js +6 -0
  149. package/src/plugins/visitor-drafts/intro.js +77 -25
  150. package/src/plugins/visitor-drafts/owner-chip.js +18 -23
  151. package/src/plugins/visitor-drafts/prompt.js +2 -2
  152. package/src/plugins/visitor-drafts/store.js +58 -5
  153. package/src/plugins/visitor-drafts.js +1 -0
  154. package/src/plugins/visitor-menu.js +13 -0
  155. package/src/sync/live-sync.js +2 -1
  156. package/src/sync/page-relay.js +2 -2
  157. package/src/ui/page-colour.js +17 -9
  158. package/dist/m/c-5AQYL27I.js +0 -1
  159. package/dist/m/c-DP4X4JID.js +0 -1
  160. package/dist/m/c-ECY5AZMH.js +0 -1
  161. package/dist/m/c-EGTIAL6P.js +0 -1
  162. package/dist/m/c-HDWZEJHM.js +0 -183
  163. package/dist/m/c-ILGQ3UXQ.js +0 -1
  164. package/dist/m/c-JFST6QE7.js +0 -1
  165. package/dist/m/c-JH5FF4NL.js +0 -20
  166. package/dist/m/c-KV7R47FR.js +0 -1
  167. package/dist/m/c-MHI3GYKF.js +0 -1
  168. package/dist/m/c-NNNR7OXA.js +0 -3
  169. package/dist/m/c-VSPAFOKU.js +0 -1
  170. package/dist/m/core/snapshot-O7II2AGG.js +0 -1
  171. package/dist/m/loader-V6VI46HU.js +0 -1
  172. package/dist/m/manifest-83C18DED.js +0 -1
  173. package/dist/m/manifest-edit-B28C1985.js +0 -1
  174. package/dist/m/plugins/menu-UN3KRCOF.js +0 -1
  175. package/dist/m/plugins/visitor-drafts-2AWNT23K.js +0 -20
  176. /package/dist/m/plugins/{quickcrop-E34PUYA3.js.LEGAL.txt → quickcrop-WWF5I422.js.LEGAL.txt} +0 -0
  177. /package/dist/m/plugins/{richclay-S4NVJYIK.js.LEGAL.txt → richclay-PGOOJ4OK.js.LEGAL.txt} +0 -0
@@ -5,8 +5,10 @@
5
5
  // only. Keep deletes every other look, after which the page is an ordinary one-look
6
6
  // document. A choice and Keep are undo entries and saves, like the SEO panel's Done.
7
7
  import { modeRefusalText } from "../../core/mode-refusal.js";
8
+ import { OFF, labelOf, looks, nameOf, paintLook, recordLook } from "./looks.js";
9
+
10
+ export { looks };
8
11
 
9
- const OFF = "not all";
10
12
  const RECEIPT = "variants-choose";
11
13
  const KEEP_ID = "clay.variants.keep";
12
14
  const CHOOSE_LABEL = "Change the page look";
@@ -19,10 +21,13 @@ const NEXT =
19
21
  '<svg viewBox="0 0 6 10" aria-hidden="true" focusable="false"><rect width="2" height="2"/><rect x="2" y="2" width="2" height="2"/>' +
20
22
  '<rect x="4" y="4" width="2" height="2"/><rect x="2" y="6" width="2" height="2"/><rect y="8" width="2" height="2"/></svg>';
21
23
 
22
- export const looks = () => [...document.querySelectorAll("head style[clay-variant]")];
23
- const nameOf = (style) => style.getAttribute("clay-variant");
24
24
  const names = () => looks().map(nameOf);
25
25
 
26
+ const labelFor = (name) => {
27
+ const style = looks().find((candidate) => nameOf(candidate) === name);
28
+ return style ? labelOf(style) : `Variant ${name}`;
29
+ };
30
+
26
31
  // The first block without media="not all" is the saved choice. A merge or a hand edit
27
32
  // can leave none or several; this tab paints the first one either way, and the next
28
33
  // choice writes a clean state.
@@ -52,17 +57,20 @@ async function afterBaseline() {
52
57
  }
53
58
 
54
59
  function joinNames(list) {
55
- const named = list.map((name) => `Variant ${name}`);
60
+ const named = list.map(labelFor);
56
61
  return named.length < 2 ? named.join("") : `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
57
62
  }
58
63
 
59
64
  export function attachVariants({ root, capsule, addItem, removeItem, note, currentMode }) {
65
+ paintLook();
66
+
60
67
  let previewing = null;
61
68
  let pending = null;
62
69
  let copy = null;
63
70
  let pill = null;
64
71
  let drawn = null;
65
72
 
73
+ const wantsRow = Boolean(document.querySelector("[clay-menu-slot]")?.hasAttribute("clay-menu-looks"));
66
74
  const isEdit = () => currentMode() === "edit";
67
75
  const shown = () => previewing ?? savedName();
68
76
  const storage = () => window.clay && window.clay.storage && window.clay.storage.session;
@@ -102,6 +110,7 @@ export function attachVariants({ root, capsule, addItem, removeItem, note, curre
102
110
  if (!names().includes(name)) return { ok: false, code: "unknown-variant" };
103
111
  pending = previewing = null;
104
112
  writeHead(CHOOSE_LABEL, () => {
113
+ recordLook(name);
105
114
  for (const style of looks()) {
106
115
  if (nameOf(style) === name) style.removeAttribute("media");
107
116
  else if (style.getAttribute("media") !== OFF) style.setAttribute("media", OFF);
@@ -121,6 +130,9 @@ export function attachVariants({ root, capsule, addItem, removeItem, note, curre
121
130
  function keepNow(name) {
122
131
  pending = previewing = null;
123
132
  writeHead(KEEP_LABEL, () => {
133
+ // The kept block becomes an ordinary stylesheet, so `look` is what still names the
134
+ // theme: an upgrade brings the blocks back and paints this one from it.
135
+ recordLook(name);
124
136
  for (const style of looks()) {
125
137
  if (nameOf(style) === name) {
126
138
  style.removeAttribute("clay-variant");
@@ -139,7 +151,7 @@ export function attachVariants({ root, capsule, addItem, removeItem, note, curre
139
151
  const name = savedName();
140
152
  const others = names().filter((other) => other !== name);
141
153
  if (!others.length) return Promise.resolve({ ok: false, code: "unknown-variant" });
142
- const text = `Keep Variant ${name} and delete ${joinNames(others)}? The switcher goes away. Version history still has them.`;
154
+ const text = `Keep ${labelFor(name)} and delete ${joinNames(others)}? The switcher goes away. Version history still has them.`;
143
155
  return import("../../ui/dialogs.js")
144
156
  .then((m) => m.consent(text))
145
157
  .then(
@@ -220,20 +232,46 @@ export function attachVariants({ root, capsule, addItem, removeItem, note, curre
220
232
  if (all.length < 2) {
221
233
  pill?.remove();
222
234
  pill = null;
235
+ removeItem("clay.look");
236
+ removeItem("clay.look.use");
223
237
  removeItem(KEEP_ID);
224
238
  return;
225
239
  }
240
+ if (root.hasAttribute("data-embedded")) {
241
+ const name = shown();
242
+ if (wantsRow) {
243
+ addItem({
244
+ id: "clay.look",
245
+ section: "start",
246
+ stepper: {
247
+ label: "Switch look",
248
+ value: `${labelFor(name)} ${all.indexOf(name) + 1}/${all.length}`,
249
+ prev: () => step(-1),
250
+ next: () => step(1),
251
+ prevLabel: "Previous look",
252
+ nextLabel: "Next look",
253
+ },
254
+ });
255
+ } else {
256
+ removeItem("clay.look");
257
+ }
258
+ if (!isEdit() && previewing !== null) addItem({ id: "clay.look.use", section: "start", label: "Use this look", run: useShown });
259
+ else removeItem("clay.look.use");
260
+ if (isEdit()) addItem({ id: KEEP_ID, label: `Keep ${labelFor(savedName())}, remove the others…`, run: () => keep() });
261
+ else removeItem(KEEP_ID);
262
+ return;
263
+ }
226
264
  if (!pill) pill = buildPill();
227
265
  if (!pill.isConnected) root.insertBefore(pill, root.querySelector(".clay-roster") ?? capsule);
228
266
  const name = shown();
229
267
  const label = pill.querySelector(".clay-variants-name");
230
- label.textContent = `Variant ${name} `;
268
+ label.textContent = `${labelFor(name)} `;
231
269
  const count = document.createElement("small");
232
270
  count.textContent = `(${all.indexOf(name) + 1}/${all.length})`;
233
271
  label.append(count);
234
272
  pill.querySelector(".clay-variants-use").hidden = isEdit() || previewing === null;
235
273
  pill.querySelector(".clay-variants-keep").hidden = !isEdit();
236
- if (isEdit()) addItem({ id: KEEP_ID, label: `Keep Variant ${savedName()}, remove the others…`, run: () => keep() });
274
+ if (isEdit()) addItem({ id: KEEP_ID, label: `Keep ${labelFor(savedName())}, remove the others…`, run: () => keep() });
237
275
  else removeItem(KEEP_ID);
238
276
  }
239
277
 
@@ -261,22 +299,30 @@ export function attachVariants({ root, capsule, addItem, removeItem, note, curre
261
299
  refresh();
262
300
  });
263
301
 
302
+ const followRecord = () => {
303
+ if (document.documentElement.hasAttribute("look") && names().length > 1) recordLook(savedName());
304
+ };
305
+
264
306
  // A merge can rebuild a look block, which clears its CSSOM flag, or bring a peer's
265
307
  // choice or Keep; painting again settles all three.
266
308
  document.addEventListener("clay:sync-applied", () => {
267
- if (current()) refresh();
309
+ if (!current()) return;
310
+ followRecord();
311
+ refresh();
268
312
  });
269
313
 
270
314
  // An undo or redo of a choice or of Keep brings the blocks back or moves the saved one.
271
315
  // Every other entry leaves the looks alone, so only a change redraws and announces.
272
316
  document.addEventListener("clay:undo-change", () => {
273
- if (current() && state() !== drawn) refresh();
317
+ if (!current() || state() === drawn) return;
318
+ followRecord();
319
+ refresh();
274
320
  });
275
321
 
276
322
  api = window.clay.variants = {
277
323
  list: () => {
278
324
  const active = savedName();
279
- return names().map((name) => ({ name, label: `Variant ${name}`, active: name === active }));
325
+ return names().map((name) => ({ name, label: labelFor(name), active: name === active }));
280
326
  },
281
327
  get active() {
282
328
  return savedName();
@@ -0,0 +1,78 @@
1
+ import { createShell } from "./shell.js";
2
+ import { attachDomItems } from "./dom-items.js";
3
+ import { MENU_SAFETY, SHELL_BASE, SHELL_FLOATING, MENU_SKIN, MENU_TOUCH } from "./shell-style.js";
4
+ import { activeLook, labelOf, looks, nameOf, paintLook } from "./looks.js";
5
+
6
+ function attachLooks({ addItem, removeItem, slot }) {
7
+ const announce = () => {
8
+ const name = activeLook() && nameOf(activeLook());
9
+ document.dispatchEvent(new CustomEvent("clay:variant-change", { detail: { active: name, shown: name } }));
10
+ };
11
+ const draw = () => {
12
+ const all = looks();
13
+ if (all.length < 2 || !slot?.hasAttribute("clay-menu-looks")) return removeItem("clay.look");
14
+ const current = activeLook();
15
+ const by = (delta) => () => choose(nameOf(all[(all.indexOf(activeLook()) + delta + all.length) % all.length]));
16
+ addItem({
17
+ id: "clay.look",
18
+ section: "start",
19
+ stepper: {
20
+ label: "Switch look",
21
+ value: `${labelOf(current)} ${all.indexOf(current) + 1}/${all.length}`,
22
+ prev: by(-1),
23
+ next: by(1),
24
+ prevLabel: "Previous look",
25
+ nextLabel: "Next look",
26
+ },
27
+ });
28
+ };
29
+ const choose = (name) => {
30
+ if (!looks().some((style) => nameOf(style) === name)) return { ok: false, code: "unknown-variant" };
31
+ document.documentElement.setAttribute("look", name);
32
+ paintLook(name);
33
+ window.clay?.save?.soon?.();
34
+ draw();
35
+ announce();
36
+ return { ok: true };
37
+ };
38
+ if (looks().length >= 2) {
39
+ window.clay.variants = {
40
+ list: () => looks().map((style) => ({ name: nameOf(style), label: labelOf(style), active: style === activeLook() })),
41
+ get active() {
42
+ return activeLook() && nameOf(activeLook());
43
+ },
44
+ get shown() {
45
+ return activeLook() && nameOf(activeLook());
46
+ },
47
+ preview: choose,
48
+ choose,
49
+ keep: () => Promise.resolve({ ok: false, code: "not-editing" }),
50
+ };
51
+ document.addEventListener("clay:drafts-restored", () => {
52
+ draw();
53
+ announce();
54
+ });
55
+ draw();
56
+ }
57
+ }
58
+
59
+ const START_OVER = "Start over? Your changes in this browser are deleted, and the page goes back to how its owner saved it.";
60
+
61
+ function startOver() {
62
+ return import("../../ui/dialogs.js")
63
+ .then((m) => m.consent(START_OVER))
64
+ .then(() => window.clay.visitorDrafts.reset(), () => false);
65
+ }
66
+
67
+ export function start() {
68
+ if (document.querySelector('clay-editor-ui[data-clay-ui="menu"]')) return;
69
+ const shell = createShell({ sections: [null] });
70
+ const { slot, embedded, skin, addItem, removeItem, open, close, note, goo } = shell;
71
+ shell.mount(embedded ? [MENU_SAFETY, SHELL_BASE, skin ?? MENU_SKIN, MENU_TOUCH] : [MENU_SAFETY, SHELL_BASE, SHELL_FLOATING, MENU_SKIN, MENU_TOUCH]);
72
+ attachLooks({ addItem, removeItem, slot });
73
+ addItem({ id: "clay.download", label: "Download a copy", run: () => window.clay.visitorDrafts.download() });
74
+ addItem({ id: "clay.start-over", label: "Start over", run: startOver });
75
+ attachDomItems(shell);
76
+ window.clay.menu = { addItem, removeItem, open, close, note, embedded, button: goo };
77
+ document.dispatchEvent(new CustomEvent("clay:menu-ready"));
78
+ }
@@ -3,21 +3,18 @@
3
3
  // file, a sync frame, the dirty check or undo.
4
4
  import { canEdit, isDraftMode, isEditMode, mode as startMode } from "../core/is-edit-mode.js";
5
5
  import onDomReady from "../lib/dom-ready.js";
6
- import CLAYGOO from "./menu/claygoo.svg.js";
7
6
  import { modeRefusalText } from "../core/mode-refusal.js";
8
7
  import { MENU_BASE, MENU_FLOATING, MENU_SKIN, MENU_TOUCH, MENU_SURFACE } from "./menu/style.js";
9
8
  import { attachPlacement } from "./menu/placement.js";
10
9
  import { attachActions } from "./menu/actions.js";
11
10
  import { attachPeople } from "./menu/people.js";
12
11
  import { attachVariants } from "./menu/variants.js";
12
+ import { attachDomItems } from "./menu/dom-items.js";
13
13
  import { followLook } from "../ui/page-colour.js";
14
- import { mountEditorUI } from "../ui/editor-ui.js";
14
+ import { createShell, TRACK } from "./menu/shell.js";
15
+ import { MENU_SAFETY } from "./menu/shell-style.js";
15
16
 
16
17
  const SEO_ITEM = true;
17
- const SKIN_VERSION = "1";
18
- const EDGE_GAP = 8;
19
- const DISMISS_ICON = '<svg viewBox="0 0 10 10" aria-hidden="true" focusable="false"><path d="M1 0 5 4l4-4 1 1-4 4 4 4-1 1-4-4-4 4-1-1 4-4-4-4z"/></svg>';
20
- const NOTE_MS = 6000;
21
18
  const INTRO_GAP = 12;
22
19
  const GRIP =
23
20
  '<svg viewBox="0 0 6 12" aria-hidden="true" focusable="false"><rect width="2" height="2"/><rect x="4" width="2" height="2"/>' +
@@ -33,289 +30,46 @@ const SECTIONS = [
33
30
  ["clay.duplicate", "clay.download", "clay.export", "clay.reveal"],
34
31
  ["clay.versions"],
35
32
  null,
36
- ["upgrade"],
33
+ ["clay.mode", "upgrade"],
37
34
  ];
38
- const SLOTTED = new Set(SECTIONS.flat().filter(Boolean));
39
35
 
40
36
  // clay.mode is the live answer; the imported mode is the one the page booted with.
41
37
  const currentMode = () =>
42
38
  window.clay && typeof window.clay.mode === "string" ? window.clay.mode : startMode;
43
39
 
44
- // The first [clay-menu-slot] on the page holds the menu. With none, it floats in a corner.
45
- function findSlot() {
46
- const slots = document.querySelectorAll("[clay-menu-slot]");
47
- if (slots.length > 1) console.warn("[clay] This page has more than one clay-menu-slot. The menu uses the first.");
48
- return slots[0] || null;
49
- }
50
-
51
- // A skin the page wrote, in <style clay-menu-skin="1" media="not all">. The media keeps it
52
- // off the page itself. An unknown version means the default skin.
53
- function pageSkin() {
54
- const style = document.querySelector("style[clay-menu-skin]");
55
- if (!style) return null;
56
- if (style.getAttribute("clay-menu-skin") !== SKIN_VERSION) {
57
- console.warn(`[clay] This runtime reads clay-menu-skin="${SKIN_VERSION}". The menu keeps its default look.`);
58
- return null;
59
- }
60
- return style.textContent;
61
- }
62
-
63
40
  function mount() {
64
41
  if (document.querySelector('clay-editor-ui[data-clay-ui="menu"]')) return;
65
42
 
66
- const slot = findSlot();
67
- const embedded = Boolean(slot);
68
- const skin = embedded ? pageSkin() : null;
43
+ const shell = createShell({
44
+ sections: SECTIONS,
45
+ capsuleStart:
46
+ `<button type="button" class="clay-menu-grip" aria-label="Move the menu. Drag it, or use the arrow keys to move it to another corner.">${GRIP}</button>` +
47
+ `<button type="button" class="clay-menu-edit" part="edit" role="switch" aria-checked="false">` +
48
+ TRACK +
49
+ `<span class="clay-menu-label">Edit</span>` +
50
+ `</button>` +
51
+ `<span class="clay-menu-divider" aria-hidden="true"></span>`,
52
+ });
53
+ const { root, goo, slot, embedded, skin, addItem, removeItem, open, close, note: showNote, beforeOpen, focusedEl, mount: mountShell } = shell;
69
54
  const hideSwitch = embedded && slot.hasAttribute("no-edit-switch");
70
-
71
- const root = document.createElement("div");
72
- root.className = "clay-menu-dock";
73
- root.setAttribute("clay", "editor-ui");
74
- root.setAttribute("part", "dock");
75
- if (embedded) root.dataset.embedded = "";
76
- root.dataset.corner = "top-right";
77
- root.innerHTML =
78
- `<div class="clay-menu" part="capsule" clay="editor-ui">` +
79
- `<button type="button" class="clay-menu-grip" aria-label="Move the menu. Drag it, or use the arrow keys to move it to another corner.">${GRIP}</button>` +
80
- `<button type="button" class="clay-menu-edit" part="edit" role="switch" aria-checked="false">` +
81
- `<span class="clay-menu-track" aria-hidden="true"><span class="clay-menu-thumb"></span></span>` +
82
- `<span class="clay-menu-label">Edit</span>` +
83
- `</button>` +
84
- `<span class="clay-menu-divider" aria-hidden="true"></span>` +
85
- `<button type="button" class="clay-menu-goo" part="button" aria-haspopup="menu" aria-expanded="false" aria-label="Clay menu">${CLAYGOO}</button>` +
86
- `</div>` +
87
- `<div class="clay-menu-list" part="list" role="menu" aria-label="Clay menu" hidden></div>` +
88
- `<div class="clay-menu-note" part="note" role="status" hidden></div>`;
89
-
90
55
  const grip = root.querySelector(".clay-menu-grip");
91
56
  const edit = root.querySelector(".clay-menu-edit");
92
- const goo = root.querySelector(".clay-menu-goo");
93
- const list = root.querySelector(".clay-menu-list");
94
- const noteEl = root.querySelector(".clay-menu-note");
95
- const items = new Map();
96
57
 
97
58
  if (embedded) {
98
59
  grip.hidden = true;
99
- const icon = slot.querySelector("template[clay-menu-icon]");
100
- if (icon) goo.replaceChildren(icon.content.cloneNode(true));
101
- const name = slot.getAttribute("clay-menu-label");
102
- if (name) {
103
- goo.setAttribute("aria-label", name);
104
- goo.title = name;
105
- list.setAttribute("aria-label", name);
106
- }
107
60
  if (hideSwitch) {
108
61
  edit.hidden = true;
109
62
  root.querySelector(".clay-menu-divider").hidden = true;
110
63
  }
111
64
  }
112
65
 
113
- // Embedded, the list opens in the top layer, so a header's overflow cannot clip it.
114
- const topLayer = embedded && typeof list.showPopover === "function";
115
- if (topLayer) list.setAttribute("popover", "manual");
116
-
117
- function placeList() {
118
- const anchor = goo.getBoundingClientRect();
119
- const { offsetWidth: width, offsetHeight: height } = list;
120
- const left = Math.max(EDGE_GAP, Math.min(anchor.right - width, window.innerWidth - width - EDGE_GAP));
121
- const below = anchor.bottom + EDGE_GAP;
122
- const top = below + height > window.innerHeight - EDGE_GAP ? Math.max(EDGE_GAP, anchor.top - height - EDGE_GAP) : below;
123
- list.style.left = `${Math.round(left)}px`;
124
- list.style.top = `${Math.round(top)}px`;
125
- }
126
-
127
- // Focus inside the shadow root: document.activeElement only ever names the host.
128
- const focusedEl = () => root.getRootNode().activeElement || null;
129
- const isOpen = () => !list.hidden;
130
-
131
- function setExpanded(open) {
132
- list.hidden = !open;
133
- goo.setAttribute("aria-expanded", open ? "true" : "false");
134
- if (!topLayer) return;
135
- if (open) {
136
- list.showPopover();
137
- placeList();
138
- } else if (list.matches(":popover-open")) {
139
- list.hidePopover();
140
- }
141
- }
142
-
143
- const buttons = () => [...list.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])')];
144
-
145
- function focusItem(index) {
146
- const all = buttons();
147
- if (!all.length) return;
148
- all[(index + all.length) % all.length].focus();
149
- }
150
-
151
- function close() {
152
- if (!isOpen()) return;
153
- setExpanded(false);
154
- }
155
-
156
66
  let actions = null;
157
67
  let people = null;
158
-
159
- function open() {
160
- if (isOpen()) return;
68
+ beforeOpen.push(() => {
161
69
  people?.close({ restore: false });
162
70
  actions?.refresh();
163
- render();
164
- setExpanded(true);
165
- }
166
-
167
- function itemButton(item) {
168
- const link = item.href && !item.disabled;
169
- const button = document.createElement(link ? "a" : "button");
170
- if (link) {
171
- button.href = item.href;
172
- button.target = "_blank";
173
- button.rel = "noopener";
174
- } else {
175
- button.type = "button";
176
- }
177
- button.setAttribute("role", "menuitem");
178
- button.setAttribute("part", item.quiet ? "item quiet" : "item");
179
- button.dataset.menuId = item.id;
180
- if (item.badge && item.id === "upgrade") {
181
- const label = document.createElement("span");
182
- label.className = "clay-menu-upgrade";
183
- label.textContent = item.label;
184
- button.append(label);
185
- } else {
186
- button.textContent = item.label;
187
- }
188
- if (item.keys) {
189
- const kbd = document.createElement("kbd");
190
- kbd.textContent = item.keys;
191
- button.append(kbd);
192
- }
193
- if (item.disabled) button.setAttribute("aria-disabled", "true");
194
- button.addEventListener("click", () => {
195
- if (item.disabled) return;
196
- close();
197
- if (item.run) item.run({ anchor: goo });
198
- });
199
- return button;
200
- }
201
-
202
- function itemNode(item) {
203
- if (item.text) {
204
- const line = document.createElement("p");
205
- line.className = "clay-menu-version";
206
- line.setAttribute("part", "text");
207
- line.dataset.menuId = item.id;
208
- line.textContent = item.label;
209
- return line;
210
- }
211
- if (item.pair) {
212
- const pair = document.createElement("div");
213
- pair.className = "clay-menu-pair";
214
- pair.dataset.menuId = item.id;
215
- for (const part of item.pair) pair.append(itemButton(part));
216
- return pair;
217
- }
218
- if (item.dismiss) return dismissibleRow(item);
219
- return itemButton(item);
220
- }
221
-
222
- function dismissibleRow(item) {
223
- const row = document.createElement("div");
224
- row.className = "clay-menu-row";
225
- row.dataset.menuId = item.id;
226
- const dismiss = document.createElement("button");
227
- dismiss.type = "button";
228
- dismiss.setAttribute("role", "menuitem");
229
- dismiss.setAttribute("part", "dismiss");
230
- dismiss.dataset.menuId = `${item.id}.dismiss`;
231
- dismiss.setAttribute("aria-label", item.dismiss.label);
232
- dismiss.title = item.dismiss.label;
233
- dismiss.innerHTML = DISMISS_ICON;
234
- dismiss.addEventListener("click", () => {
235
- removeItem(item.id);
236
- item.dismiss.run?.();
237
- });
238
- row.append(itemButton(item), dismiss);
239
- return row;
240
- }
241
-
242
- function sections() {
243
- const inSection = (name) => [...items.values()].filter((item) => item.section === name);
244
- const extra = [...items.keys()].filter((id) => !SLOTTED.has(id) && !items.get(id).section);
245
- const fixed = SECTIONS.map((ids) => (ids || extra).map((id) => items.get(id)).filter(Boolean));
246
- return [inSection("start"), ...fixed, inSection("end")].filter((group) => group.length);
247
- }
248
-
249
- function render() {
250
- const now = focusedEl();
251
- const focused = now && list.contains(now) ? now.dataset.menuId : null;
252
- list.textContent = "";
253
- sections().forEach((group, index) => {
254
- if (index) {
255
- const sep = document.createElement("div");
256
- sep.className = "clay-menu-sep";
257
- sep.setAttribute("role", "separator");
258
- sep.setAttribute("part", "separator");
259
- list.append(sep);
260
- }
261
- for (const item of group) list.append(itemNode(item));
262
- });
263
- const dot = goo.querySelector(".clay-menu-dot");
264
- const wanted = [...items.values()].some((item) => item.badge);
265
- if (wanted && !dot) {
266
- const mark = document.createElement("span");
267
- mark.className = "clay-menu-dot";
268
- mark.setAttribute("aria-hidden", "true");
269
- goo.append(mark);
270
- } else if (!wanted && dot) {
271
- dot.remove();
272
- }
273
- if (focused) {
274
- const again = buttons().find((button) => button.dataset.menuId === focused);
275
- if (again) again.focus();
276
- else focusItem(0);
277
- }
278
- }
279
-
280
- const clean = ({ id, label, run, badge, keys, disabled, href }) => ({
281
- id,
282
- href: typeof href === "string" ? href : "",
283
- label: String(label ?? ""),
284
- run: typeof run === "function" ? run : null,
285
- badge: Boolean(badge),
286
- keys: keys ? String(keys) : "",
287
- disabled: Boolean(disabled),
288
71
  });
289
72
 
290
- function addItem({ id, label, run, badge, keys, disabled, text, pair, href, section, quiet, dismiss } = {}) {
291
- if (!id) return null;
292
- const item = clean({ id, label, run, badge, keys, disabled, href });
293
- if (text) item.text = true;
294
- if (Array.isArray(pair)) item.pair = pair.map(clean);
295
- if (section === "start" || section === "end") item.section = section;
296
- if (quiet) item.quiet = true;
297
- if (dismiss) item.dismiss = { label: String(dismiss.label || "Dismiss"), run: typeof dismiss.run === "function" ? dismiss.run : null };
298
- items.set(id, item);
299
- render();
300
- return id;
301
- }
302
-
303
- function removeItem(id) {
304
- if (items.delete(id)) render();
305
- }
306
-
307
- function showNote(text) {
308
- if (!text) return;
309
- noteEl.textContent = text;
310
- noteEl.hidden = false;
311
- clearTimeout(noteTimer);
312
- noteTimer = setTimeout(() => {
313
- noteEl.hidden = true;
314
- noteEl.textContent = "";
315
- }, NOTE_MS);
316
- }
317
-
318
- let noteTimer = 0;
319
73
  let busy = false;
320
74
 
321
75
  const applyHidden = () => {
@@ -324,7 +78,7 @@ function mount() {
324
78
 
325
79
  function setChecked() {
326
80
  edit.setAttribute("aria-checked", currentMode() === "edit" ? "true" : "false");
327
- if (hideSwitch) addItem({ id: "clay.mode", label: currentMode() === "edit" ? "View mode" : "Edit mode", section: "start", run: () => edit.click() });
81
+ if (hideSwitch) addItem({ id: "clay.mode", label: "Edit mode", toggle: true, checked: currentMode() === "edit", run: () => edit.click() });
328
82
  }
329
83
  setChecked();
330
84
 
@@ -363,50 +117,6 @@ function mount() {
363
117
  }
364
118
  });
365
119
 
366
- goo.addEventListener("click", () => {
367
- if (isOpen()) close();
368
- else open();
369
- });
370
-
371
- document.addEventListener("click", (event) => {
372
- if (isOpen() && !event.composedPath().includes(root)) close();
373
- });
374
-
375
- root.addEventListener("keydown", (event) => {
376
- if (event.key === "Escape") {
377
- if (!isOpen()) return;
378
- event.preventDefault();
379
- close();
380
- goo.focus();
381
- return;
382
- }
383
- if (event.target === goo && (event.key === "Enter" || event.key === " ")) {
384
- event.preventDefault();
385
- if (isOpen()) close();
386
- else {
387
- open();
388
- focusItem(0);
389
- }
390
- return;
391
- }
392
- if (!isOpen()) return;
393
- if (event.key === "Enter" && list.contains(event.target)) {
394
- event.preventDefault();
395
- event.target.click();
396
- return;
397
- }
398
- const all = buttons();
399
- if (!all.length) return;
400
- const at = all.indexOf(focusedEl());
401
- if (event.key === "ArrowDown") {
402
- event.preventDefault();
403
- focusItem(at < 0 ? 0 : at + 1);
404
- } else if (event.key === "ArrowUp") {
405
- event.preventDefault();
406
- focusItem(at < 0 ? all.length - 1 : at - 1);
407
- }
408
- });
409
-
410
120
  // The intro sits at the top right in normal flow, so the group moves below its
411
121
  // bottom edge while it is on the page, and back when it goes.
412
122
  const doc = document;
@@ -457,12 +167,9 @@ function mount() {
457
167
  `:root { --clay-menu-width: ${Math.ceil(rect.width)}px; --clay-menu-height: ${Math.ceil(rect.height)}px; }`;
458
168
  }
459
169
 
460
- const css = embedded ? [MENU_BASE, skin ?? MENU_SKIN, MENU_TOUCH] : [MENU_BASE, MENU_FLOATING, MENU_SKIN, MENU_TOUCH];
461
- const ui = mountEditorUI({ name: "menu", content: root, css, parent: slot || document.body, attributes: { "clay-mode-toggle": "" } });
462
- if (embedded) {
463
- window.addEventListener("resize", close);
464
- window.addEventListener("scroll", () => { if (topLayer && isOpen()) placeList(); }, { capture: true, passive: true });
465
- } else {
170
+ const css = embedded ? [MENU_SAFETY, MENU_BASE, skin ?? MENU_SKIN, MENU_TOUCH] : [MENU_SAFETY, MENU_BASE, MENU_FLOATING, MENU_SKIN, MENU_TOUCH];
171
+ const ui = mountShell(css);
172
+ if (!embedded) {
466
173
  attachPlacement({
467
174
  dock: root,
468
175
  container: ui.shadow,
@@ -487,6 +194,7 @@ function mount() {
487
194
  actions = attachActions({ addItem, note: showNote, currentMode });
488
195
  people = attachPeople({ root, capsule: root.querySelector(".clay-menu"), addItem, removeItem, closeMenu: close, note: showNote });
489
196
  attachVariants({ root, capsule: root.querySelector(".clay-menu"), addItem, removeItem, note: showNote, currentMode });
197
+ attachDomItems({ addItem, removeItem, beforeOpen });
490
198
 
491
199
  if (SEO_ITEM) {
492
200
  addItem({
@@ -27,6 +27,7 @@ export function inertVisitorDrafts() {
27
27
  editOriginal,
28
28
  backToDraft,
29
29
  downloadUrl: () => DOWNLOAD_URL,
30
+ download: () => Promise.resolve(false),
30
31
  source: null,
31
32
  };
32
33
  }
@@ -21,4 +21,10 @@ export const CLOSE_CSS = `:host { color: inherit !important; }
21
21
  .clay-intro__close:focus-visible { opacity: 1; outline: 2px solid currentColor; outline-offset: 2px; }
22
22
  .clay-intro__close svg { display: block; width: 12px; height: 12px; fill: currentColor; stroke: none; }
23
23
  @media (pointer: coarse) { .clay-intro__close { width: 44px; height: 44px; top: 4px; right: 4px; } }
24
+ .clay-intro__close[data-manual] {
25
+ width: auto; height: 36px; padding: 0 14px;
26
+ border-radius: 999px; font-size: 12px; font-weight: 500;
27
+ text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
28
+ }
29
+ @media (pointer: coarse), (max-width: 680px) { .clay-intro__close[data-manual] { height: 44px; } }
24
30
  `;