@panphora/clayjs 1.12.0 → 1.12.1

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.
@@ -54,14 +54,22 @@ export const PROMPT_CSS = `.hcp {
54
54
  transition: bottom .25s ease, opacity .2s ease;
55
55
  pointer-events: none;
56
56
  }
57
- .hcp[hidden], .hcp[data-hcp-state="dismissed"], .hcp[data-hcp-state="off"], .hcp[data-hcp-intro] { display: none; }
57
+ .hcp[hidden], .hcp[data-hcp-state="dismissed"], .hcp[data-hcp-state="off"] { display: none; }
58
58
  .hcp[data-hcp-typing] { opacity: 0; visibility: hidden; }
59
59
  .hcp > * { pointer-events: auto; }
60
- .hcp svg { display: block; flex: none; }
60
+ .hcp svg {
61
+ display: block; flex: none; width: 16px; height: 16px; line-height: 1;
62
+ fill: none; stroke: none; stroke-width: 0; stroke-linecap: butt; stroke-linejoin: miter;
63
+ }
64
+ .hcp .hcp-x svg, .hcp .hcp-actions .hcp-btn svg, .hcp-tab svg, .hcp-file svg { fill: currentColor; }
65
+ .hcp .hcp-file svg { width: 20px; height: 20px; }
66
+ .hcp .hcp-tab svg { width: 16px; height: 18px; }
67
+ .hcp .hcp-dock .hcp-x svg { width: 12px; height: 12px; }
68
+ .hcp button, .hcp a { border-radius: 0; line-height: 1.5; }
61
69
 
62
70
  .hcp-btn {
63
71
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
64
- height: 34px; padding: 0 14px;
72
+ height: 34px; min-height: 0; padding: 0 14px;
65
73
  font-family: var(--hcp-sans); font-size: 14px; font-weight: 500; line-height: 1.5;
66
74
  white-space: nowrap; text-decoration: none; cursor: pointer; user-select: none;
67
75
  color: var(--hcp-ink); background: var(--hcp-face);
@@ -69,7 +77,7 @@ export const PROMPT_CSS = `.hcp {
69
77
  border-color: var(--hcp-edge-hi) var(--hcp-edge-lo) var(--hcp-edge-lo) var(--hcp-edge-hi);
70
78
  outline: none;
71
79
  }
72
- .hcp-btn:hover { background: color-mix(in srgb, var(--hcp-face), var(--hcp-edge-hi) 45%); color: var(--hcp-ink); }
80
+ .hcp .hcp-btn:hover { background: color-mix(in srgb, var(--hcp-face), var(--hcp-edge-hi) 45%); color: var(--hcp-ink); }
73
81
  .hcp-btn:active { border-color: var(--hcp-edge-lo) var(--hcp-edge-hi) var(--hcp-edge-hi) var(--hcp-edge-lo); }
74
82
  .hcp-btn:active > span { translate: 1px 1px; }
75
83
  .hcp-btn > span { display: inline-flex; align-items: center; gap: 7px; }
@@ -80,18 +88,18 @@ export const PROMPT_CSS = `.hcp {
80
88
  --hcp-edge-lo: color-mix(in srgb, var(--hcp-ink), black 32%);
81
89
  color: var(--hcp-ground);
82
90
  }
83
- .hcp-btn--primary:hover { background: color-mix(in srgb, var(--hcp-ink), var(--hcp-ground) 12%); color: var(--hcp-ground); }
91
+ .hcp .hcp-btn--primary:hover { background: color-mix(in srgb, var(--hcp-ink), var(--hcp-ground) 12%); color: var(--hcp-ground); }
84
92
 
85
93
  .hcp-dock { display: flex; align-items: stretch; box-shadow: 0 6px 16px -8px rgba(43, 36, 27, .45); }
86
94
  .hcp-tab { font-size: 13.5px; padding: 0 12px 0 10px; }
87
95
  .hcp-tab svg { color: var(--hcp-brass); }
88
96
  .hcp-dock .hcp-x {
89
- width: 26px; height: 34px; display: inline-flex; align-items: center; justify-content: center;
97
+ width: 26px; height: 34px; min-height: 0; border-radius: 0; display: inline-flex; align-items: center; justify-content: center;
90
98
  background: var(--hcp-face); color: var(--hcp-muted); cursor: pointer;
91
99
  border: 2px solid; border-left: 0;
92
100
  border-color: var(--hcp-edge-hi) var(--hcp-edge-lo) var(--hcp-edge-lo) var(--hcp-edge-hi);
93
101
  }
94
- .hcp-dock .hcp-x:hover { color: var(--hcp-ink); }
102
+ .hcp .hcp-dock .hcp-x:hover { color: var(--hcp-ink); background: var(--hcp-face); }
95
103
  .hcp-tab__label { display: inline; }
96
104
  .hcp[data-hcp-state="mini"] .hcp-tab__label,
97
105
  .hcp[data-hcp-state="mini"] .hcp-dock .hcp-x { display: none; }
@@ -109,11 +117,13 @@ export const PROMPT_CSS = `.hcp {
109
117
  @keyframes hcp-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
110
118
 
111
119
  .hcp-card .hcp-x {
112
- position: absolute; top: 10px; right: 10px; width: 28px; height: 28px;
120
+ position: absolute; top: 12px; right: 12px; width: 24px; height: 24px; min-height: 0;
113
121
  display: inline-flex; align-items: center; justify-content: center;
122
+ border: 1px solid var(--hcp-line-2); border-radius: 3px; background: var(--hcp-surface);
114
123
  color: var(--hcp-muted); cursor: pointer;
115
124
  }
116
- .hcp-card .hcp-x:hover { color: var(--hcp-ink); background: var(--hcp-sunk); }
125
+ .hcp .hcp-card .hcp-x:hover { color: var(--hcp-ink); border-color: var(--hcp-muted); background: var(--hcp-surface); }
126
+ .hcp .hcp-card .hcp-x svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2; }
117
127
  .hcp-kicker {
118
128
  display: flex; align-items: center; gap: 8px;
119
129
  font-family: var(--hcp-mono); font-size: 11px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase;
@@ -141,14 +151,29 @@ export const PROMPT_CSS = `.hcp {
141
151
  .hcp-file small { display: block; font-size: 12px; color: var(--hcp-muted); line-height: 1.4; }
142
152
  .hcp-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
143
153
  .hcp-actions .hcp-btn--primary { flex: 1 1 auto; }
154
+ .hcp-signup { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--hcp-line); }
155
+ .hcp-signup label { display: block; font-size: 13px; line-height: 1.45; color: var(--hcp-ink-2); margin-bottom: 8px; }
156
+ .hcp-signup-row { display: flex; gap: 8px; }
157
+ .hcp-signup-row[hidden] { display: none; }
158
+ .hcp-signup input[type="email"] {
159
+ flex: 1 1 auto; min-width: 0; height: 34px; padding: 0 10px; margin: 0;
160
+ font: 14px var(--hcp-sans); color: var(--hcp-ink); background: var(--hcp-surface);
161
+ border: 1px solid var(--hcp-line-2); border-radius: 0; box-shadow: none;
162
+ }
163
+ .hcp-signup input[type="email"]:focus-visible { outline: 2px solid var(--hcp-brass); outline-offset: 1px; }
164
+ .hcp-hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
165
+ .hcp-msg { margin-top: 8px; font-size: 13px; color: var(--hcp-ink-2); }
166
+ .hcp[data-hcp-reason="error"] .hcp-signup { display: none; }
167
+ @media (max-width: 360px) { .hcp-signup-row { flex-direction: column; } }
144
168
  .hcp-foot { margin-top: 12px; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
145
169
  .hcp-foot small { font-size: 12px; color: var(--hcp-muted); }
146
170
  .hcp-quiet {
171
+ min-height: 0; border-radius: 0;
147
172
  font-size: 12px; color: var(--hcp-muted); cursor: pointer;
148
173
  text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--hcp-line-2);
149
174
  white-space: nowrap;
150
175
  }
151
- .hcp-quiet:hover { color: var(--hcp-ink); }
176
+ .hcp .hcp-quiet:hover { color: var(--hcp-ink); background: none; }
152
177
 
153
178
  @media (max-width: 480px) {
154
179
  .hcp { right: 12px; bottom: calc(12px + var(--hcp-lift)); }
@@ -157,7 +182,7 @@ export const PROMPT_CSS = `.hcp {
157
182
  }
158
183
  @media (pointer: coarse) {
159
184
  .hcp-btn, .hcp-dock .hcp-x, .hcp-card .hcp-x { min-height: 44px; }
160
- .hcp-card .hcp-x { width: 44px; }
185
+ .hcp-card .hcp-x { width: 44px; height: 44px; top: 6px; right: 6px; }
161
186
  .hcp-quiet { display: inline-flex; align-items: center; min-height: 44px; }
162
187
  }
163
188
  @media (prefers-reduced-motion: reduce) {
@@ -1,31 +1,43 @@
1
1
  import { PROMPT_CSS } from "./prompt-style.js";
2
+ import { announceBottomUi, bottomBarHeight } from "./bottom-ui.js";
3
+
4
+ export const SIGNUP_COPY = {
5
+ thanks: "Thanks. Check your email for a link to confirm.",
6
+ failed: "That didn't go through. Check the address and try again.",
7
+ };
2
8
 
3
9
  const FONT_HREF = "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500&family=IBM+Plex+Sans:wght@400;500;600&family=Newsreader:opsz,wght@6..72,500&display=swap";
4
10
 
5
11
  const ASIDE = `<aside class="hcp" id="hcp" hidden aria-label="Download this app" data-hcp-state="tab" data-hcp-reason="open" sap-ignore clay="editor-ui">
6
12
  <section class="hcp-card" id="hcp-card" role="dialog" aria-modal="false" aria-labelledby="hcp-title">
7
- <button type="button" class="hcp-x" data-hcp="minimize" aria-label="Minimize"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M3 12h10v2H3z"/></svg></button>
13
+ <button type="button" class="hcp-x" data-hcp="minimize" aria-label="Minimize"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" aria-hidden="true"><path d="M5 12h14"/></svg></button>
8
14
  <div class="hcp-kicker">Free download</div>
9
- <h2 class="hcp-title" id="hcp-title"><span data-hcp-for="use">Your numbers are saved in this browser only.</span><span data-hcp-for="open">This app is free to keep.</span><span data-hcp-for="return">Your numbers are saved in this browser only.</span><span data-hcp-for="error">Your work isn't being saved in this browser.</span></h2>
10
- <p class="hcp-body"><span data-hcp-for="open">Download it as one file and use it on your computer, no account needed.</span><span data-hcp-for="use">Download the file to keep them on your computer for good.</span><span data-hcp-for="return">Download the file to keep them on your computer for good.</span><span data-hcp-for="error">Storage is full or turned off here. Download the file to keep what you've done.</span></p>
15
+ <h2 class="hcp-title" id="hcp-title"><span data-hcp-for="use">Your work is saved in this browser only.</span><span data-hcp-for="open">This app is free to keep.</span><span data-hcp-for="return">Your work is saved in this browser only.</span><span data-hcp-for="error">Your work isn't being saved in this browser.</span></h2>
16
+ <p class="hcp-body"><span data-hcp-for="open">Download it as one file and use it on your computer, no account needed.</span><span data-hcp-for="use">Download the file to keep it on your computer for good.</span><span data-hcp-for="return">Download the file to keep it on your computer for good.</span><span data-hcp-for="error">Storage is full or turned off here. Download the file to keep what you've done.</span></p>
11
17
  <div class="hcp-file">
12
- <svg width="26" height="30" viewBox="0 0 26 30" fill="none" aria-hidden="true"><path d="M2 1h15l7 7v21H2z" fill="var(--hcp-surface)" stroke="currentColor" stroke-width="2"/><path d="M17 1v7h7" stroke="currentColor" stroke-width="2"/><path d="M7 15h12M7 19h12M7 23h8" stroke="var(--hcp-muted)" stroke-width="2"/></svg>
13
- <div><b class="hcp-name" data-hcp-name>This app</b><small>One file with the whole app and your numbers in it</small></div>
18
+ <svg width="20" height="20" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"/></svg>
19
+ <div><b class="hcp-name" data-hcp-name>This app</b><small>One file with the whole app and your work in it</small></div>
14
20
  </div>
15
- <p class="hcp-body">Open it with HTML Clay, a free app for Mac, Windows and Linux. It opens the file in a window and saves every change into it, so your numbers stay on your computer.</p>
21
+ <p class="hcp-body">Open it with HTML Clay, a free app for Mac, Windows and Linux. It opens the file in a window and saves every change into it, so your work stays on your computer.</p>
16
22
  <div class="hcp-actions">
17
- <a class="hcp-btn hcp-btn--primary" data-hcp-link href="/_/download?as=htmlclay"><span><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M7 1h2v8.6l2.8-2.8 1.4 1.4L8 13.4 2.8 8.2l1.4-1.4L7 9.6zM2 13h12v2H2z"/></svg>Download free</span></a>
23
+ <a class="hcp-btn hcp-btn--primary" data-hcp-link href="/_/download?as=htmlclay"><span><svg width="16" height="16" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"/></svg>Download free</span></a>
18
24
  <button type="button" class="hcp-btn" data-hcp="minimize"><span>Not now</span></button>
19
25
  </div>
26
+ <form class="hcp-signup" data-hcp-signup novalidate>
27
+ <label for="hcp-email">I make one of these about every two weeks. Get the next one by email.</label>
28
+ <div class="hcp-signup-row"><input id="hcp-email" type="email" autocomplete="email" required placeholder="you@example.com"><button type="submit" class="hcp-btn"><span>Send me the next one</span></button></div>
29
+ <input class="hcp-hp" name="website" tabindex="-1" autocomplete="off" aria-hidden="true">
30
+ <p class="hcp-msg" role="status" hidden></p>
31
+ </form>
20
32
  <div class="hcp-foot"><small>Free, and the file is yours to keep.</small><button type="button" class="hcp-quiet" data-hcp="dismiss">Don&rsquo;t show again</button></div>
21
33
  </section>
22
34
  <div class="hcp-dock">
23
- <button type="button" class="hcp-btn hcp-tab" data-hcp="open" aria-controls="hcp-card" aria-expanded="false" aria-label="This app is free to keep"><span><svg width="16" height="18" viewBox="0 0 16 18" fill="none" aria-hidden="true"><path d="M1.5 1h8.5l4.5 4.5V17h-13z" stroke="currentColor" stroke-width="2"/><path d="M10 1v5h4.5" stroke="currentColor" stroke-width="2"/><path d="M8 8v6M5.5 11.5 8 14l2.5-2.5" stroke="currentColor" stroke-width="1.8"/></svg><span class="hcp-tab__label">This app is free to keep</span></span></button>
35
+ <button type="button" class="hcp-btn hcp-tab" data-hcp="open" aria-controls="hcp-card" aria-expanded="false" aria-label="This app is free to keep"><span><svg width="16" height="16" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V32a8,8,0,0,0-16,0v92.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"/></svg><span class="hcp-tab__label">This app is free to keep</span></span></button>
24
36
  <button type="button" class="hcp-x" data-hcp="dismiss" aria-label="Hide this for good"><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></button>
25
37
  </div>
26
38
  </aside>`;
27
39
 
28
- export function mountPrompt({ memory, appName, link, onError }) {
40
+ export function mountPrompt({ memory, appName, link, tag, source }) {
29
41
  if (!document.getElementById("hcp-style")) {
30
42
  document.head.insertAdjacentHTML("beforeend", `<link rel="stylesheet" clay="editor-ui" href="${FONT_HREF}">\n<style clay="editor-ui" id="hcp-style">${PROMPT_CSS}</style>`);
31
43
  }
@@ -38,10 +50,36 @@ export function mountPrompt({ memory, appName, link, onError }) {
38
50
  const USES_TO_SHOW = 3;
39
51
  const IDLE_MS = 2500;
40
52
  const TEXTY = /^(INPUT|TEXTAREA|SELECT)$/;
53
+ const OUR_UI = "#hcp, .clay-intro, .clay-owner";
41
54
 
42
55
  const linkEl = root.querySelector("[data-hcp-link]");
43
56
  linkEl.href = link;
44
57
 
58
+ const signup = root.querySelector("[data-hcp-signup]");
59
+ if (signup) {
60
+ const email = signup.querySelector("#hcp-email");
61
+ const honey = signup.querySelector(".hcp-hp");
62
+ const row = signup.querySelector(".hcp-signup-row");
63
+ const message = signup.querySelector(".hcp-msg");
64
+ signup.addEventListener("submit", (event) => {
65
+ event.preventDefault();
66
+ fetch("/_/newsletter/subscribe", {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ credentials: "same-origin",
70
+ body: JSON.stringify({ email: email.value.trim(), tags: [tag], source, website: honey.value }),
71
+ }).then((response) => {
72
+ if (!response.ok) throw new Error("signup failed");
73
+ row.hidden = true;
74
+ message.textContent = SIGNUP_COPY.thanks;
75
+ message.hidden = false;
76
+ }).catch(() => {
77
+ message.textContent = SIGNUP_COPY.failed;
78
+ message.hidden = false;
79
+ });
80
+ });
81
+ }
82
+
45
83
  if (appName) root.querySelector("[data-hcp-name]").textContent = appName;
46
84
 
47
85
  const tab = root.querySelector("[data-hcp=open]");
@@ -55,7 +93,7 @@ export function mountPrompt({ memory, appName, link, onError }) {
55
93
  }
56
94
 
57
95
  function isHostField(el) {
58
- return el && !root.contains(el) && (TEXTY.test(el.tagName) || el.isContentEditable);
96
+ return el && !root.contains(el) && !el.closest(OUR_UI) && (TEXTY.test(el.tagName) || el.isContentEditable);
59
97
  }
60
98
 
61
99
  let timer = null;
@@ -96,17 +134,10 @@ export function mountPrompt({ memory, appName, link, onError }) {
96
134
  if (liftFrame) return;
97
135
  liftFrame = requestAnimationFrame(() => {
98
136
  liftFrame = 0;
99
- const y = innerHeight - 3, x = innerWidth - 24;
100
- let raise = 0;
101
- const hits = document.elementsFromPoint ? document.elementsFromPoint(x, y) : [];
102
- for (let i = 0; i < hits.length; i++) {
103
- for (let el = hits[i]; el && el !== document.body && el !== document.documentElement; el = el.parentElement) {
104
- if (root.contains(el) || el.closest("[data-hcp-preview]")) break;
105
- const pos = getComputedStyle(el).position;
106
- if (pos === "fixed" || pos === "sticky") { raise = Math.max(raise, Math.ceil(innerHeight - el.getBoundingClientRect().top)); break; }
107
- }
108
- }
109
- root.style.setProperty("--hcp-lift", (raise > 0 && raise < innerHeight / 3 ? raise : 0) + "px");
137
+ const raise = bottomBarHeight(innerWidth - 24, (el) => root.contains(el) || el.closest("[data-hcp-preview], .clay-owner"));
138
+ const next = raise + "px";
139
+ if (root.style.getPropertyValue("--hcp-lift") !== next) root.style.setProperty("--hcp-lift", next);
140
+ announceBottomUi();
110
141
  });
111
142
  }
112
143
 
@@ -153,12 +184,7 @@ export function mountPrompt({ memory, appName, link, onError }) {
153
184
  function clearError() {
154
185
  if (root.getAttribute("data-hcp-reason") === "error") root.setAttribute("data-hcp-reason", "use");
155
186
  }
156
- function hideForIntro(on) {
157
- if (on) root.setAttribute("data-hcp-intro", "");
158
- else root.removeAttribute("data-hcp-intro");
159
- }
160
-
161
- const api = { set, root, showError, clearError, hideForIntro };
187
+ const api = { set, root, showError, clearError };
162
188
 
163
189
  if (mem.dismissed) { root.setAttribute("data-hcp-state", "dismissed"); return api; }
164
190
 
@@ -30,8 +30,8 @@ if (isDraftMode && slot && rulesTag) {
30
30
  const appName = document.querySelector('meta[name="application-name"]')?.content?.trim() || document.title.split(/:|\s\|\s/)[0].trim() || "This app";
31
31
  const template = document.querySelector("template[visitor-intro]");
32
32
  const start = () => {
33
- const prompt = mountPrompt({ memory, appName, link: downloadLink });
34
- mountIntro({ memory, template, appName, link: downloadLink, tag: signupTag(slot, template), source, hadDraft: result.hadDraft, prompt, snapshot: store.snapshot });
33
+ const prompt = mountPrompt({ memory, appName, link: downloadLink, tag: signupTag(slot, template), source });
34
+ mountIntro({ memory, template, appName, hadDraft: result.hadDraft });
35
35
  if (isOwner) mountOwnerChip();
36
36
  document.addEventListener("clay:drafts-store-failed", () => prompt.showError());
37
37
  document.addEventListener("clay:drafts-saved", () => prompt.clearError());