@panphora/clayjs 1.21.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) 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 +8871 -8629
  11. package/dist/m/attrs/{save-freeze-MTFVVYV5.js → save-freeze-JZHKRQLR.js} +1 -1
  12. package/dist/m/{c-OABVRYOT.js → c-24PMUJIV.js} +1 -1
  13. package/dist/m/{c-EOCZ2YVL.js → c-2AYVYEI4.js} +1 -1
  14. package/dist/m/{c-UXEVJ63W.js → c-2L6O5PDS.js} +1 -1
  15. package/dist/m/{c-VBLFAQD2.js → c-2RYOGUQM.js} +1 -1
  16. package/dist/m/{c-LDUNOR57.js → c-2Z22FCFK.js} +13 -25
  17. package/dist/m/{c-NLZ5AN4L.js → c-2ZVOEU4M.js} +1 -1
  18. package/dist/m/{c-BJPZCWAV.js → c-37NAONZA.js} +1 -1
  19. package/dist/m/{c-ZE46QDIH.js → c-3TLG3OLG.js} +1 -1
  20. package/dist/m/{c-CSNE5EIF.js → c-4PO3YGDC.js} +1 -1
  21. package/dist/m/{c-OJYM642Q.js → c-5FKRZNJV.js} +1 -1
  22. package/dist/m/{c-SO63UGPQ.js → c-5OJ7YF2N.js} +1 -1
  23. package/dist/m/{c-CAWNFER6.js → c-5TIZ2I64.js} +1 -1
  24. package/dist/m/{c-7SXYQCN4.js → c-5WMXEKFH.js} +1 -1
  25. package/dist/m/{c-XQSXKIKM.js → c-6RDOKLN7.js} +1 -1
  26. package/dist/m/{c-BRLCTNMC.js → c-6TYGD5AB.js} +1 -1
  27. package/dist/m/c-A4IP6MBC.js +1 -0
  28. package/dist/m/{c-I2IFKJX2.js → c-ALDVFPWL.js} +1 -1
  29. package/dist/m/{c-LV5MABYD.js → c-AYVGD6A6.js} +1 -1
  30. package/dist/m/{c-LQU7TXIH.js → c-B6O7AJYG.js} +1 -1
  31. package/dist/m/{c-SBSMJIPC.js → c-BCGWJYXW.js} +1 -1
  32. package/dist/m/{c-RT3C74GY.js → c-BGKAT2FL.js} +1 -1
  33. package/dist/m/{c-N7Y52HJH.js → c-BXYSDKAN.js} +1 -1
  34. package/dist/m/c-CRPIDGXC.js +78 -0
  35. package/dist/m/c-CUMXGHVD.js +1 -0
  36. package/dist/m/{c-67ILSNPR.js → c-DTMZ3ZHI.js} +1 -1
  37. package/dist/m/{c-EZ7B5AZT.js → c-ELYIIYRS.js} +1 -1
  38. package/dist/m/{c-VAK7SG37.js → c-EUGJVHDD.js} +1 -1
  39. package/dist/m/{c-67LGZ52V.js → c-FCSZ3DC5.js} +1 -1
  40. package/dist/m/{c-JUE7B4OX.js → c-FQGHR32O.js} +1 -1
  41. package/dist/m/{c-ZJLU56DV.js → c-G4PWUDD5.js} +1 -1
  42. package/dist/m/{c-CJFDLD43.js → c-G6COVIZB.js} +1 -1
  43. package/dist/m/c-GBTVCJXC.js +1 -0
  44. package/dist/m/c-GC2HICFJ.js +1 -0
  45. package/dist/m/{c-44B5ODN7.js → c-HCNWJE4A.js} +2 -2
  46. package/dist/m/{c-BJ7YQ5QZ.js → c-IHUWUKIU.js} +1 -1
  47. package/dist/m/{c-LIBE4HPD.js → c-JCOQVZYS.js} +1 -1
  48. package/dist/m/{c-RG6XUZFA.js → c-JGHEACGK.js} +1 -1
  49. package/dist/m/{c-3GJ4XARY.js → c-JGLX6TBK.js} +1 -1
  50. package/dist/m/c-JNRKHYWO.js +1 -0
  51. package/dist/m/{c-PIVIYLTY.js → c-KDXIJEEF.js} +1 -1
  52. package/dist/m/{c-NFR673TL.js → c-L4KMT7FO.js} +2 -2
  53. package/dist/m/{c-E4IBSAQM.js → c-LCYPMULG.js} +1 -1
  54. package/dist/m/{c-SJPD46ZO.js → c-LIGQYZ7M.js} +4 -4
  55. package/dist/m/{c-U5X57T4L.js → c-LKIWEPAV.js} +1 -1
  56. package/dist/m/{c-EDSPKYMA.js → c-LUXAB2XI.js} +1 -1
  57. package/dist/m/{c-YSB6JKV2.js → c-LWIDDWAR.js} +2 -2
  58. package/dist/m/c-MPSKIU2I.js +1 -0
  59. package/dist/m/{c-DHXPDLW6.js → c-NK33MMYF.js} +1 -1
  60. package/dist/m/{c-GPGYKER7.js → c-NQLWOHLH.js} +1 -1
  61. package/dist/m/{c-LPUJXRXW.js → c-NTHUTABN.js} +1 -1
  62. package/dist/m/{c-Y33LUUIQ.js → c-NW54NHKE.js} +1 -1
  63. package/dist/m/{c-3AXJDL2P.js → c-OG6GFSUX.js} +1 -1
  64. package/dist/m/{c-MTQHWQC3.js → c-OKEGUICD.js} +1 -1
  65. package/dist/m/c-OPONJ2UK.js +128 -0
  66. package/dist/m/{c-3YH6JSIL.js → c-OWQWOGYI.js} +1 -1
  67. package/dist/m/{c-FDAXZ2PM.js → c-P6Q2ZTFQ.js} +1 -1
  68. package/dist/m/c-PKCCHJ5X.js +1 -0
  69. package/dist/m/c-PWJSJEJU.js +1 -0
  70. package/dist/m/{c-2ZZBC45O.js → c-PXLTUYZC.js} +1 -1
  71. package/dist/m/c-QEN6HZCA.js +1 -0
  72. package/dist/m/{c-JOH753EJ.js → c-QGAPG7OM.js} +1 -1
  73. package/dist/m/{c-W23KVGUO.js → c-QIR5CAKM.js} +1 -1
  74. package/dist/m/{c-GKEATME6.js → c-QM5LXRDT.js} +1 -1
  75. package/dist/m/c-QUGCKMWF.js +1 -0
  76. package/dist/m/{c-FNVQAACK.js → c-QYYUVLVG.js} +1 -1
  77. package/dist/m/c-RO73MKKR.js +1 -0
  78. package/dist/m/c-SBCBXKOZ.js +26 -0
  79. package/dist/m/{c-J3BXFTZS.js → c-SGRIYOAJ.js} +1 -1
  80. package/dist/m/c-SKF5HL4I.js +4 -0
  81. package/dist/m/{c-W3K23S72.js → c-SRPC3VBE.js} +1 -1
  82. package/dist/m/{c-K76AUL4I.js → c-T5NK2TVY.js} +1 -1
  83. package/dist/m/{c-EFD6QNTE.js → c-TANBH7FI.js} +1 -1
  84. package/dist/m/{c-5734MF2P.js → c-THGQQDYY.js} +1 -1
  85. package/dist/m/{c-YQUJ74R3.js → c-TSBVMWAH.js} +1 -1
  86. package/dist/m/{c-VY6XRBOW.js → c-UD5JZTDL.js} +1 -1
  87. package/dist/m/{c-M4FSPNQ2.js → c-UDGEFWNU.js} +1 -1
  88. package/dist/m/{c-ZSCBEB7I.js → c-UHUNXWKN.js} +1 -1
  89. package/dist/m/{c-BTLFLLML.js → c-VIRFF5HG.js} +1 -1
  90. package/dist/m/{c-2VB555FG.js → c-VYJ7O4RI.js} +1 -1
  91. package/dist/m/{c-TUGVINJR.js → c-W5E5P2TZ.js} +1 -1
  92. package/dist/m/{c-EOCKLMKX.js → c-W7IPVCAA.js} +1 -1
  93. package/dist/m/{c-5FEU5MMY.js → c-WJ5DRBHT.js} +1 -1
  94. package/dist/m/{c-V2J5C5MT.js → c-WQZTI5P2.js} +1 -1
  95. package/dist/m/c-X5WC4PX5.js +1 -0
  96. package/dist/m/{c-GVPNDIKJ.js → c-XCFQEWW6.js} +2 -2
  97. package/dist/m/c-XLQ3BDMM.js +1 -0
  98. package/dist/m/{c-XD5MOROJ.js → c-XX75FFNL.js} +1 -1
  99. package/dist/m/{c-JHJR5BQX.js → c-YK2L436K.js} +1 -1
  100. package/dist/m/{c-MEWUGR4D.js → c-YYMZWQLR.js} +1 -1
  101. package/dist/m/core/{admin-attrs-ZG7R5V3E.js → admin-attrs-U7IJNYBG.js} +1 -1
  102. package/dist/m/core/{autosave-VPORYJSE.js → autosave-F5YQCKVL.js} +1 -1
  103. package/dist/m/core/{conflict-notice-JMAZLVRX.js → conflict-notice-42H4ASLA.js} +1 -1
  104. package/dist/m/core/{persist-3OPWJKZS.js → persist-CJXIL7D4.js} +1 -1
  105. package/dist/m/core/{save-AOJBDSOK.js → save-ESPSQPLV.js} +1 -1
  106. package/dist/m/core/{save-core-IOCMXO5L.js → save-core-6SQHOR6W.js} +1 -1
  107. package/dist/m/core/snapshot-FJP5IJL3.js +1 -0
  108. package/dist/m/core/{unsaved-warning-U5VUJFOE.js → unsaved-warning-HNQVTA2D.js} +1 -1
  109. package/dist/m/internals/{index-HDRANDWE.js → index-NRPNDTSA.js} +1 -1
  110. package/dist/m/loader-FBDE2UAB.js +1 -0
  111. package/dist/m/manifest-1D2F5CA5.js +1 -0
  112. package/dist/m/manifest-edit-F7CC5CFD.js +1 -0
  113. package/dist/m/manifest.json +1139 -1069
  114. package/dist/m/page/{editor-EY2WAAGB.js → editor-OSKOW6RS.js} +2 -2
  115. package/dist/m/plugins/ai-edit-VV4QTINH.js +6 -0
  116. package/dist/m/plugins/cms-H7DTMUQX.js +2235 -0
  117. package/dist/m/plugins/{indicator-4VDXWYTD.js → indicator-WVPGMOZ7.js} +1 -1
  118. package/dist/m/plugins/{live-5EZN2EYP.js → live-DCUBKXY4.js} +1 -1
  119. package/dist/m/plugins/menu-Q4MFFQ6E.js +1 -0
  120. package/dist/m/plugins/{page-LVW5DWA4.js → page-OAWUJQEL.js} +2 -2
  121. package/dist/m/plugins/{page-view-5KQ76IJB.js → page-view-DZ26ZDDQ.js} +1 -1
  122. package/dist/m/plugins/{people-C7TVBBMR.js → people-5XRNRP5U.js} +1 -1
  123. package/dist/m/plugins/{quickcrop-BQ5YYFCH.js → quickcrop-WWF5I422.js} +2 -2
  124. package/dist/m/plugins/{richclay-KLGETK77.js → richclay-PGOOJ4OK.js} +2 -2
  125. package/dist/m/plugins/{sortable-WJZZBM7R.js → sortable-266HY6TR.js} +1 -1
  126. package/dist/m/plugins/{source-CCEUM3OR.js → source-6BKMK4PO.js} +1 -1
  127. package/dist/m/plugins/{upgrade-NW556WJD.js → upgrade-BNZVFHMZ.js} +1 -1
  128. package/dist/m/plugins/visitor-drafts-2AUYKAIJ.js +20 -0
  129. package/dist/m/plugins/{visitor-drafts-owner-MZP45XSG.js → visitor-drafts-owner-Z5B2OQBR.js} +1 -1
  130. package/dist/m/plugins/visitor-menu-4LQMKLPP.js +1 -0
  131. package/dist/m/plugins/{wire-VRYPY6EJ.js → wire-W2EV36KZ.js} +1 -1
  132. package/dist/m/{reader-core-2GXY7SHY.js → reader-core-POFQCKRV.js} +2 -2
  133. package/dist/m/sync/{live-sync-SKPMFYOO.js → live-sync-XGUPHI3I.js} +1 -1
  134. package/dist/m/ui/{index-QNIPDHXQ.js → index-VGYAKAKI.js} +1 -1
  135. package/dist/offline-manifest.json +666 -606
  136. package/docs/guide.md +38 -27
  137. package/package.json +2 -2
  138. package/src/core/save-replace.js +5 -2
  139. package/src/core/serialize-steps.js +10 -2
  140. package/src/core/snapshot.js +37 -1
  141. package/src/core/upgrade-run.js +16 -2
  142. package/src/loader-logic.js +10 -3
  143. package/src/plugins/ai-edit.js +6 -17
  144. package/src/plugins/cms.js +3 -6
  145. package/src/plugins/menu/dom-items.js +45 -0
  146. package/src/plugins/menu/looks.js +32 -0
  147. package/src/plugins/menu/shell-style.js +143 -0
  148. package/src/plugins/menu/shell.js +398 -0
  149. package/src/plugins/menu/style.js +15 -116
  150. package/src/plugins/menu/variants.js +56 -10
  151. package/src/plugins/menu/visitor.js +78 -0
  152. package/src/plugins/menu.js +21 -313
  153. package/src/plugins/visitor-drafts/inert.js +1 -0
  154. package/src/plugins/visitor-drafts/intro-style.js +6 -0
  155. package/src/plugins/visitor-drafts/intro.js +77 -25
  156. package/src/plugins/visitor-drafts/owner-chip.js +18 -23
  157. package/src/plugins/visitor-drafts/prompt.js +2 -2
  158. package/src/plugins/visitor-drafts/store.js +58 -5
  159. package/src/plugins/visitor-drafts.js +1 -0
  160. package/src/plugins/visitor-menu.js +13 -0
  161. package/src/sync/live-sync.js +2 -1
  162. package/src/sync/page-relay.js +2 -2
  163. package/src/ui/bevel.js +1 -1
  164. package/src/ui/page-colour.js +17 -9
  165. package/src/ui/skins/cms.js +1 -1
  166. package/src/ui/skins/quickcrop.js +1 -1
  167. package/src/ui/skins/richclay.js +1 -1
  168. package/src/vendor/hypercms.vendor.js +16 -69
  169. package/dist/m/c-AL4RBJF2.js +0 -1
  170. package/dist/m/c-DDDOKPI6.js +0 -3
  171. package/dist/m/c-EGTIAL6P.js +0 -1
  172. package/dist/m/c-FSWRGBVQ.js +0 -1
  173. package/dist/m/c-HDWZEJHM.js +0 -183
  174. package/dist/m/c-HMYWIHFG.js +0 -1
  175. package/dist/m/c-JH5FF4NL.js +0 -20
  176. package/dist/m/c-M6FNQCTQ.js +0 -1
  177. package/dist/m/c-MHI3GYKF.js +0 -1
  178. package/dist/m/c-MIL4B2JQ.js +0 -1
  179. package/dist/m/c-S5IEIZCN.js +0 -1
  180. package/dist/m/c-VSPAFOKU.js +0 -1
  181. package/dist/m/c-WFMRX5RZ.js +0 -1
  182. package/dist/m/core/snapshot-O7II2AGG.js +0 -1
  183. package/dist/m/loader-ZVPK4ZN3.js +0 -1
  184. package/dist/m/manifest-5498AFD3.js +0 -1
  185. package/dist/m/manifest-edit-62A5A00F.js +0 -1
  186. package/dist/m/plugins/ai-edit-J5VLSW5I.js +0 -6
  187. package/dist/m/plugins/cms-LRAOZHT7.js +0 -2293
  188. package/dist/m/plugins/menu-DM2JHPSR.js +0 -1
  189. package/dist/m/plugins/visitor-drafts-2AWNT23K.js +0 -20
  190. package/src/ui/skins/cms-toggle.js +0 -15
  191. /package/dist/m/plugins/{quickcrop-BQ5YYFCH.js.LEGAL.txt → quickcrop-WWF5I422.js.LEGAL.txt} +0 -0
  192. /package/dist/m/plugins/{richclay-KLGETK77.js.LEGAL.txt → richclay-PGOOJ4OK.js.LEGAL.txt} +0 -0
@@ -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
  `;
@@ -5,6 +5,8 @@ import { bootPhase } from "../../core/boot-phase.js";
5
5
 
6
6
  export const COPY = {
7
7
  close: "Hide this introduction",
8
+ hide: "Hide intro",
9
+ show: "Show intro",
8
10
  label: (appName) => `About ${appName}`,
9
11
  };
10
12
 
@@ -43,17 +45,7 @@ function focusApp() {
43
45
  else document.body.focus({ preventScroll: true });
44
46
  }
45
47
 
46
- export function mountIntro({ memory, template, appName, hadDraft }) {
47
- if (!template || !template.content.textContent.trim() && !template.content.querySelector("img, svg, video, canvas")) return null;
48
- if (!forced()) {
49
- if (memory.get().intro.done || hadDraft) return null;
50
- if (bootPhase.servedHash) return null;
51
- if (window.matchMedia && window.matchMedia("print").matches) return null;
52
- }
53
- return mount({ memory, template, appName });
54
- }
55
-
56
- function mount({ memory, template, appName }) {
48
+ function buildSection({ template, appName, labelText, onClose }) {
57
49
  stylesOnce();
58
50
 
59
51
  const section = document.createElement("section");
@@ -66,11 +58,48 @@ function mount({ memory, template, appName }) {
66
58
  const close = document.createElement("button");
67
59
  close.type = "button";
68
60
  close.className = "clay-intro__close";
69
- close.setAttribute("aria-label", COPY.close);
70
- close.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true"><path d="M1.4 0 6 4.6 10.6 0 12 1.4 7.4 6 12 10.6 10.6 12 6 7.4 1.4 12 0 10.6 4.6 6 0 1.4z"/></svg>';
71
- close.addEventListener("click", () => dismiss("closed"));
61
+ if (labelText) {
62
+ close.textContent = labelText;
63
+ close.setAttribute("data-manual", "");
64
+ } else {
65
+ close.setAttribute("aria-label", COPY.close);
66
+ close.innerHTML = '<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true"><path d="M1.4 0 6 4.6 10.6 0 12 1.4 7.4 6 12 10.6 10.6 12 6 7.4 1.4 12 0 10.6 4.6 6 0 1.4z"/></svg>';
67
+ }
68
+ close.addEventListener("click", () => onClose());
72
69
  mountEditorUI({ name: "intro-close", content: close, css: [CLOSE_CSS], parent: section });
73
70
 
71
+ section.addEventListener("click", (event) => {
72
+ if (event.target.closest("[visitor-intro-dismiss]")) {
73
+ onClose();
74
+ return;
75
+ }
76
+ if (event.target.closest("[visitor-intro-start]")) {
77
+ event.preventDefault();
78
+ window.scrollTo({ top: window.scrollY + section.getBoundingClientRect().bottom, behavior: "smooth" });
79
+ }
80
+ });
81
+
82
+ return section;
83
+ }
84
+
85
+ export function mountIntro({ memory, template, appName, hadDraft }) {
86
+ if (!template || !template.content.textContent.trim() && !template.content.querySelector("img, svg, video, canvas")) return null;
87
+ if (template.getAttribute("visitor-intro") === "manual") {
88
+ const printing = window.matchMedia && window.matchMedia("print").matches;
89
+ const open = forced() || !(memory.get().intro.done || hadDraft || bootPhase.servedHash || printing);
90
+ return mountManual({ memory, template, appName, open });
91
+ }
92
+ if (!forced()) {
93
+ if (memory.get().intro.done || hadDraft) return null;
94
+ if (bootPhase.servedHash) return null;
95
+ if (window.matchMedia && window.matchMedia("print").matches) return null;
96
+ }
97
+ return mount({ memory, template, appName });
98
+ }
99
+
100
+ function mount({ memory, template, appName }) {
101
+ const section = buildSection({ template, appName, onClose: () => dismiss("closed") });
102
+
74
103
  let gone = false;
75
104
  let frame = 0;
76
105
 
@@ -102,20 +131,43 @@ function mount({ memory, template, appName }) {
102
131
  });
103
132
  }
104
133
 
105
- section.addEventListener("click", (event) => {
106
- if (event.target.closest("[visitor-intro-dismiss]")) {
107
- dismiss("closed");
108
- return;
109
- }
110
- if (event.target.closest("[visitor-intro-start]")) {
111
- event.preventDefault();
112
- window.scrollTo({ top: window.scrollY + section.getBoundingClientRect().bottom, behavior: "smooth" });
113
- }
114
- });
115
-
116
134
  document.body.prepend(section);
117
135
  announceBottomUi();
118
136
  addEventListener("scroll", onScroll, { passive: true });
119
137
 
120
138
  return section;
121
139
  }
140
+
141
+ function mountManual({ memory, template, appName, open }) {
142
+ let section = null;
143
+ const showRow = (wanted) => {
144
+ const menu = window.clay?.menu;
145
+ if (!menu) return;
146
+ if (wanted) menu.addItem({ id: "clay.intro", label: COPY.show, run: show });
147
+ else menu.removeItem("clay.intro");
148
+ };
149
+ function attach() {
150
+ section ??= buildSection({ template, appName, labelText: COPY.hide, onClose: hide });
151
+ document.body.prepend(section);
152
+ announceBottomUi();
153
+ showRow(false);
154
+ }
155
+ function hide() {
156
+ memory.get().intro.done = "closed";
157
+ memory.save();
158
+ section.remove();
159
+ announceBottomUi();
160
+ showRow(true);
161
+ focusApp();
162
+ }
163
+ function show() {
164
+ memory.get().intro.done = null;
165
+ memory.save();
166
+ attach();
167
+ window.scrollTo(0, 0);
168
+ }
169
+ document.addEventListener("clay:menu-ready", () => showRow(!section?.isConnected));
170
+ if (open) attach();
171
+ else showRow(true);
172
+ return section;
173
+ }
@@ -1,6 +1,11 @@
1
1
  import { isDraftMode } from "../../core/is-edit-mode.js";
2
2
  import { BOTTOM_UI_CHANGED, announceBottomUi, bottomBarHeight, promptPanel } from "./bottom-ui.js";
3
3
  import { mountEditorUI } from "../../ui/editor-ui.js";
4
+ import { followLook } from "../../ui/page-colour.js";
5
+
6
+ // The notice sits on the page for as long as the owner edits, so it takes most of the
7
+ // page's colour instead of Bevel's: a navy card on a brown page reads as a foreign object.
8
+ const OWNER_LEAN = 0.8;
4
9
 
5
10
  export const COPY = {
6
11
  draft: "You're seeing a private draft, like a visitor.",
@@ -11,29 +16,17 @@ export const COPY = {
11
16
  };
12
17
 
13
18
  const CHIP_CSS = `.clay-owner {
14
- --hcp-ground: #FDF8F0;
15
- --hcp-surface: #FFFCF6;
16
- --hcp-sunk: #F4ECDF;
17
- --hcp-ink: #2B241B;
18
- --hcp-ink-2: #4A4033;
19
- --hcp-muted: #6B5942;
20
- --hcp-line: #E8DCC6;
21
- --hcp-line-2: #D8C8AF;
22
- --hcp-brass: #8F6310;
23
- --hcp-shadow: 0 1px 2px rgba(23, 28, 36, .06), 0 14px 34px -14px rgba(43, 36, 27, .32);
19
+ --hcp-surface: light-dark(#FFFCF6, #11131E);
20
+ --hcp-ground: var(--bevel-surface, var(--hcp-surface));
21
+ --hcp-ink: light-dark(#2B241B, #ECEAF2);
22
+ --hcp-ink-2: light-dark(#4A4033, #C9CDE0);
23
+ --hcp-muted: light-dark(#6B5942, #8A90AB);
24
+ --hcp-line-2: color-mix(in srgb, var(--hcp-ink) 22%, var(--hcp-ground));
25
+ --hcp-brass: light-dark(#8F6310, #D9A445);
26
+ --hcp-shadow: 0 1px 2px light-dark(rgba(23, 28, 36, .06), rgba(0, 0, 0, .4)), 0 14px 34px -14px light-dark(rgba(43, 36, 27, .32), rgba(0, 0, 0, .7));
24
27
  --hcp-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
25
28
  --hcp-lift: 0px;
26
- color-scheme: light;
27
- }
28
- @media (prefers-color-scheme: dark) {
29
- .clay-owner {
30
- --hcp-surface: #11131E; --hcp-sunk: #1A1D2C;
31
- --hcp-ink: #ECEAF2; --hcp-ink-2: #C9CDE0; --hcp-muted: #8A90AB;
32
- --hcp-line: #262B42; --hcp-line-2: #353B52;
33
- --hcp-brass: #D9A445;
34
- --hcp-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 16px 38px -14px rgba(0, 0, 0, .7);
35
- color-scheme: dark;
36
- }
29
+ color-scheme: light dark;
37
30
  }
38
31
  .clay-owner, .clay-owner *, .clay-owner *::before, .clay-owner *::after {
39
32
  box-sizing: border-box; margin: 0; padding: 0; border: 0;
@@ -46,8 +39,8 @@ const CHIP_CSS = `.clay-owner {
46
39
  max-width: min(460px, calc(100vw - 24px));
47
40
  padding: 8px 8px 8px 12px;
48
41
  font-family: var(--hcp-sans); font-size: 13px; font-weight: 400; line-height: 1.45;
49
- color: var(--hcp-ink-2); text-align: left; background: var(--hcp-surface);
50
- border: 1px solid var(--hcp-line-2); border-left: 3px solid var(--hcp-brass);
42
+ color: var(--hcp-ink-2); text-align: left; background: var(--hcp-ground);
43
+ border: 1px solid var(--hcp-line-2);
51
44
  box-shadow: var(--hcp-shadow);
52
45
  -webkit-font-smoothing: antialiased;
53
46
  }
@@ -151,6 +144,7 @@ export function mountOwnerChip() {
151
144
  }
152
145
 
153
146
  function liftOff() {
147
+ unfollow?.();
154
148
  removeEventListener("scroll", lift);
155
149
  removeEventListener("resize", lift);
156
150
  document.removeEventListener(BOTTOM_UI_CHANGED, lift);
@@ -168,6 +162,7 @@ export function mountOwnerChip() {
168
162
  // Transition events stop at a shadow root, so the chip's own changes are heard here.
169
163
  chip.addEventListener("transitionend", lift);
170
164
  lift();
165
+ const unfollow = followLook(chip, chip, undefined, OWNER_LEAN);
171
166
  // Whoever removes the chip takes its listeners back with it: a chip mounted and unmounted
172
167
  // once per mode session would otherwise leave six behind each time.
173
168
  chip.liftOff = liftOff;
@@ -119,7 +119,7 @@ export async function mountPrompt({ memory, appName, link }) {
119
119
  const t = e.target.closest("[data-hcp]");
120
120
  if (t) {
121
121
  const act = t.getAttribute("data-hcp");
122
- if (act === "open") { set("card", "open"); linkEl.focus(); }
122
+ if (act === "open") { set("card", root.getAttribute("data-hcp-reason") === "error" ? undefined : "open"); linkEl.focus(); }
123
123
  else if (act === "minimize") { set("mini"); tab.focus(); }
124
124
  else if (act === "dismiss") {
125
125
  if (root.getAttribute("data-hcp-reason") === "error") { set("mini"); tab.focus(); }
@@ -149,7 +149,7 @@ export async function mountPrompt({ memory, appName, link }) {
149
149
  function clearError() {
150
150
  if (root.getAttribute("data-hcp-reason") === "error") root.setAttribute("data-hcp-reason", "use");
151
151
  }
152
- const api = { set, root, showError, clearError };
152
+ const api = { set, root, showError, clearError, download: () => linkEl.click() };
153
153
 
154
154
  document.addEventListener("input", onUse, true);
155
155
  document.addEventListener("change", onUse, true);