@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panphora/clayjs",
3
- "version": "1.12.0",
3
+ "version": "1.12.1",
4
4
  "type": "module",
5
5
  "description": "clayjs: malleable documents. Save lifecycle for self-saving HTML.",
6
6
  "license": "MIT-0",
@@ -0,0 +1,31 @@
1
+ export const BOTTOM_UI_CHANGED = "clay:bottom-ui-changed";
2
+
3
+ export function announceBottomUi() {
4
+ document.dispatchEvent(new CustomEvent(BOTTOM_UI_CHANGED));
5
+ }
6
+
7
+ export function bottomBarHeight(x, skip) {
8
+ const h = window.innerHeight;
9
+ let raise = 0;
10
+ const hits = document.elementsFromPoint ? document.elementsFromPoint(x, h - 3) : [];
11
+ for (const hit of hits) {
12
+ for (let el = hit; el && el !== document.body && el !== document.documentElement; el = el.parentElement) {
13
+ if (skip(el)) break;
14
+ const position = getComputedStyle(el).position;
15
+ if (position === "fixed" || position === "sticky") {
16
+ const height = Math.ceil(h - el.getBoundingClientRect().top);
17
+ if (height < h / 3) raise = Math.max(raise, height);
18
+ break;
19
+ }
20
+ }
21
+ }
22
+ return raise;
23
+ }
24
+
25
+ export function promptPanel(box) {
26
+ const panel = document.querySelector("#hcp:not([hidden])");
27
+ if (!panel) return 0;
28
+ const r = panel.getBoundingClientRect();
29
+ if (!r.width || !r.height || r.right <= box.left || r.left >= box.right) return 0;
30
+ return Math.ceil(window.innerHeight - r.top);
31
+ }
@@ -1,156 +1,19 @@
1
- export const INTRO_CSS = `.clay-intro, .clay-about {
2
- --hcp-ground: #FDF8F0;
3
- --hcp-surface: #FFFCF6;
4
- --hcp-sunk: #F4ECDF;
5
- --hcp-ink: #2B241B;
6
- --hcp-ink-2: #4A4033;
7
- --hcp-muted: #6B5942;
8
- --hcp-line: #E8DCC6;
9
- --hcp-line-2: #D8C8AF;
10
- --hcp-brass: #8F6310;
11
- --hcp-brass-soft: #F0E4CC;
12
- --hcp-face: #F1E7D4;
13
- --hcp-edge-hi: #FFF9ED;
14
- --hcp-edge-lo: #D6C3A5;
15
- --hcp-shadow: 0 1px 2px rgba(23, 28, 36, .06), 0 14px 34px -14px rgba(43, 36, 27, .32);
16
- --hcp-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
17
- --hcp-serif: Newsreader, Georgia, "Times New Roman", serif;
18
- --hcp-mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
19
- color-scheme: light;
20
- }
21
- @media (prefers-color-scheme: dark) {
22
- .clay-intro, .clay-about {
23
- --hcp-ground: #0B0C13; --hcp-surface: #11131E; --hcp-sunk: #1A1D2C;
24
- --hcp-ink: #ECEAF2; --hcp-ink-2: #C9CDE0; --hcp-muted: #8A90AB;
25
- --hcp-line: #262B42; --hcp-line-2: #353B52;
26
- --hcp-brass: #D9A445; --hcp-brass-soft: #3A2F17;
27
- --hcp-face: #22273E; --hcp-edge-hi: #3C4260; --hcp-edge-lo: #1B2033;
28
- --hcp-shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 16px 38px -14px rgba(0, 0, 0, .7);
29
- color-scheme: dark;
30
- }
31
- }
32
-
33
- html:has(> body > .clay-intro), body:has(> .clay-intro) { height: auto !important; min-height: 100%; overflow-y: auto !important; }
34
-
35
- .clay-intro, .clay-intro *, .clay-intro *::before, .clay-intro *::after,
36
- .clay-about, .clay-about *::before, .clay-about *::after {
37
- box-sizing: border-box; margin: 0; padding: 0; border: 0;
38
- font: inherit; letter-spacing: normal; text-transform: none; text-shadow: none;
39
- line-height: inherit; color: inherit; background: none; float: none; max-width: none;
40
- }
1
+ export const INTRO_CSS = `html:has(> body > .clay-intro) { height: auto !important; overflow-y: auto !important; }
2
+ body:has(> .clay-intro) { height: auto !important; overflow: visible !important; }
41
3
  .clay-intro {
42
- position: relative; z-index: 1; display: block;
43
- min-height: 100svh;
44
- background: var(--hcp-ground);
45
- font-family: var(--hcp-sans); font-size: 16px; font-weight: 400; line-height: 1.5;
46
- color: var(--hcp-ink); text-align: left;
47
- -webkit-font-smoothing: antialiased;
48
- }
49
- .clay-intro__inner {
50
- display: flex; flex-direction: column; gap: 18px;
51
- width: 100%; max-width: 720px; min-height: 100svh; margin: 0 auto;
52
- padding: max(24px, 6vh) 16px 32px;
53
- }
54
- .clay-intro .clay-intro__kicker {
55
- font-family: var(--hcp-mono); font-size: 11px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase;
56
- color: var(--hcp-brass);
57
- }
58
- .clay-intro__name {
59
- font-family: var(--hcp-serif); font-size: clamp(30px, 7vw, 44px); font-weight: 500; line-height: 1.12;
60
- color: var(--hcp-ink); font-variation-settings: "opsz" 48;
61
- }
62
- .clay-intro__inner > p { font-size: 18px; line-height: 1.55; color: var(--hcp-ink-2); }
63
- .clay-intro__inner > a { color: var(--hcp-brass); text-decoration: underline; text-underline-offset: 2px; }
64
- .clay-intro__inner > video, .clay-intro__inner > img {
65
- display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;
66
- background: var(--hcp-sunk); border: 1px solid var(--hcp-line);
67
- }
68
- .clay-intro__inner > ul, .clay-intro__inner > ol { padding-left: 22px; font-size: 18px; line-height: 1.55; color: var(--hcp-ink-2); }
69
- .clay-intro__inner > ul { list-style: disc; }
70
- .clay-intro__inner > ol { list-style: decimal; }
71
-
72
- .clay-intro__actions { display: flex; flex-direction: column; gap: 14px; align-items: stretch; }
73
- .clay-intro__download {
74
- align-self: flex-start;
75
- display: inline-flex; align-items: center; justify-content: center; gap: 8px;
76
- min-height: 44px; padding: 0 18px;
77
- font-family: var(--hcp-sans); font-size: 15px; font-weight: 500; line-height: 1.5;
78
- white-space: nowrap; text-decoration: none; cursor: pointer; user-select: none;
79
- color: var(--hcp-ground); background: var(--hcp-ink);
80
- border: 2px solid;
81
- border-color: color-mix(in srgb, var(--hcp-ink), white 24%) color-mix(in srgb, var(--hcp-ink), black 32%) color-mix(in srgb, var(--hcp-ink), black 32%) color-mix(in srgb, var(--hcp-ink), white 24%);
82
- transition: background .2s ease;
83
- }
84
- .clay-intro__download:hover { background: color-mix(in srgb, var(--hcp-ink), var(--hcp-ground) 12%); }
85
-
86
- .clay-intro__signup {
87
- display: flex; flex-direction: column; gap: 9px;
88
- padding: 14px 16px;
89
- background: var(--hcp-surface); border: 1px solid var(--hcp-line); border-left: 3px solid var(--hcp-brass);
90
- }
91
- .clay-intro__signup label { font-size: 14px; line-height: 1.45; color: var(--hcp-ink-2); }
92
- .clay-intro__row { display: flex; gap: 8px; }
93
- .clay-intro__row input {
94
- flex: 1 1 auto; min-width: 0; height: 40px; padding: 0 10px;
95
- font-family: var(--hcp-sans); font-size: 15px; color: var(--hcp-ink);
96
- background: var(--hcp-ground); border: 1px solid var(--hcp-line-2);
97
- }
98
- .clay-intro__row input::placeholder { color: var(--hcp-muted); }
99
- .clay-intro__row button {
100
- flex: none; height: 40px; padding: 0 14px;
101
- font-family: var(--hcp-sans); font-size: 14px; font-weight: 500;
102
- color: var(--hcp-ink); background: var(--hcp-face); cursor: pointer;
103
- border: 2px solid; border-color: var(--hcp-edge-hi) var(--hcp-edge-lo) var(--hcp-edge-lo) var(--hcp-edge-hi);
104
- }
105
- .clay-intro__row button:hover { background: color-mix(in srgb, var(--hcp-face), var(--hcp-edge-hi) 45%); }
106
- .clay-intro__hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
107
- .clay-intro__msg { font-size: 14px; line-height: 1.5; color: var(--hcp-ink-2); }
108
- .clay-intro__msg[hidden] { display: none; }
109
- .clay-intro__row[hidden] { display: none; }
110
-
111
- .clay-intro__start {
112
- margin-top: auto;
113
- display: inline-flex; align-items: center; justify-content: center; gap: 10px;
114
- width: 100%; min-height: 56px; padding: 0 20px;
115
- font-family: var(--hcp-sans); font-size: 16px; font-weight: 500;
116
- color: var(--hcp-ground); background: var(--hcp-ink); cursor: pointer;
117
- border: 2px solid;
118
- border-color: color-mix(in srgb, var(--hcp-ink), white 24%) color-mix(in srgb, var(--hcp-ink), black 32%) color-mix(in srgb, var(--hcp-ink), black 32%) color-mix(in srgb, var(--hcp-ink), white 24%);
119
- transition: background .2s ease;
120
- }
121
- .clay-intro__start::after { content: "\\2193"; font-size: 15px; line-height: 1; }
122
- .clay-intro__start:hover { background: color-mix(in srgb, var(--hcp-ink), var(--hcp-ground) 12%); }
123
- .clay-intro__download:focus-visible, .clay-intro__start:focus-visible,
124
- .clay-intro__row button:focus-visible, .clay-intro__row input:focus-visible,
125
- .clay-about:focus-visible { outline: 2px solid var(--hcp-brass); outline-offset: 2px; }
126
-
127
- .clay-about {
128
- position: fixed; top: 12px; right: 12px; z-index: 2147482000;
129
- display: inline-flex; align-items: center; justify-content: center; gap: 7px;
130
- height: 34px; padding: 0 12px;
131
- font-family: var(--hcp-sans); font-size: 13px; font-weight: 500; line-height: 1.5;
132
- white-space: nowrap; cursor: pointer; user-select: none;
133
- color: var(--hcp-ink); background: var(--hcp-face);
134
- border: 2px solid; border-color: var(--hcp-edge-hi) var(--hcp-edge-lo) var(--hcp-edge-lo) var(--hcp-edge-hi);
135
- box-shadow: var(--hcp-shadow);
136
- transition: background .2s ease;
137
- }
138
- .clay-about:hover { background: color-mix(in srgb, var(--hcp-face), var(--hcp-edge-hi) 45%); }
139
-
140
- @media (min-width: 560px) {
141
- .clay-intro__actions { flex-direction: row; flex-wrap: wrap; align-items: center; }
142
- .clay-intro__signup { flex: 1 1 320px; max-width: 520px; }
143
- }
144
- @media (pointer: coarse) {
145
- .clay-intro__download, .clay-intro__start, .clay-intro__row input, .clay-intro__row button { min-height: 44px; }
146
- .clay-about { min-height: 44px; }
147
- }
148
- @media (prefers-reduced-motion: reduce) {
149
- .clay-intro, .clay-intro *, .clay-about { transition: none; animation: none; }
150
- }
151
- @media (forced-colors: active) {
152
- .clay-intro__download, .clay-intro__start, .clay-intro__row button, .clay-about { border: 1px solid ButtonText; }
153
- .clay-intro__signup { border: 1px solid CanvasText; }
154
- }
155
- @media print { .clay-intro, .clay-about { display: none; } }
4
+ position: relative;
5
+ display: block;
6
+ }
7
+ .clay-intro__close {
8
+ position: absolute; top: 10px; right: 10px; z-index: 1;
9
+ display: inline-flex; align-items: center; justify-content: center;
10
+ width: 28px; height: 28px; min-height: 0; margin: 0; padding: 0;
11
+ border: 0; border-radius: 4px; background: none; box-shadow: none;
12
+ color: inherit; opacity: .55; cursor: pointer; line-height: 1;
13
+ }
14
+ .clay-intro .clay-intro__close:hover { opacity: 1; background: none; color: inherit; }
15
+ .clay-intro__close:focus-visible { opacity: 1; outline: 2px solid currentColor; outline-offset: 2px; }
16
+ .clay-intro__close svg { display: block; width: 12px; height: 12px; fill: currentColor; stroke: none; }
17
+ @media (pointer: coarse) { .clay-intro__close { width: 44px; height: 44px; top: 4px; right: 4px; } }
18
+ @media print { .clay-intro { display: none; } }
156
19
  `;
@@ -1,19 +1,20 @@
1
1
  import { INTRO_CSS } from "./intro-style.js";
2
+ import { announceBottomUi } from "./bottom-ui.js";
2
3
 
3
4
  export const COPY = {
4
- kicker: "Free app",
5
- download: "Download free",
6
- signupLabel: "I make one of these about every two weeks. Get the next one by email.",
7
- emailPlaceholder: "you@example.com",
8
- signup: "Send me the next one",
9
- signupThanks: "Thanks. Check your email for a link to confirm.",
10
- signupFailed: "That didn't go through. Check the address and try again.",
11
- start: (appName) => `Start using ${appName}`,
12
- about: "About this app",
5
+ close: "Hide this introduction",
13
6
  label: (appName) => `About ${appName}`,
14
7
  };
15
8
 
16
9
  const FOCUSABLE = "a[href], button, input, select, textarea, [tabindex]:not([tabindex='-1'])";
10
+ const TEXT_ENTRY = "textarea, select";
11
+ const NON_TEXT_INPUT = ["checkbox", "radio", "button", "submit", "reset", "file"];
12
+
13
+ function isTextEntry(el) {
14
+ if (el.isContentEditable || el.closest(TEXT_ENTRY)) return true;
15
+ if (el.tagName !== "INPUT") return false;
16
+ return NON_TEXT_INPUT.indexOf(el.type) < 0;
17
+ }
17
18
 
18
19
  function forced() {
19
20
  try {
@@ -34,40 +35,23 @@ function stylesOnce() {
34
35
 
35
36
  function focusApp() {
36
37
  const next = [...document.querySelectorAll(FOCUSABLE)].find(
37
- (el) => !el.disabled && !el.closest("[clay~='editor-ui']") && (typeof el.checkVisibility !== "function" || el.checkVisibility())
38
+ (el) => !el.disabled && !isTextEntry(el) && !el.closest("[clay~='editor-ui']") && (typeof el.checkVisibility !== "function" || el.checkVisibility())
38
39
  );
39
- if (next) next.focus();
40
- else document.body.focus();
41
- }
42
-
43
- export function mountIntro({ memory, template, appName, link, tag, source, hadDraft, prompt, snapshot }) {
44
- if (!template) return null;
45
- const opts = { memory, template, appName, link, tag, source, hadDraft, prompt, snapshot };
46
- if (forced()) return mount(opts);
47
- if (memory.get().intro.done || hadDraft) return mountAbout(opts);
48
- if (window.location.hash) return null;
49
- if (window.matchMedia && window.matchMedia("print").matches) return null;
50
- return mount(opts);
40
+ if (next) next.focus({ preventScroll: true });
41
+ else document.body.focus({ preventScroll: true });
51
42
  }
52
43
 
53
- function mountAbout(opts) {
54
- stylesOnce();
55
- const button = document.createElement("button");
56
- button.type = "button";
57
- button.className = "clay-about";
58
- button.setAttribute("clay", "editor-ui");
59
- button.setAttribute("sap-ignore", "");
60
- button.textContent = COPY.about;
61
- button.addEventListener("click", () => {
62
- button.remove();
63
- mount({ ...opts, fromAbout: true });
64
- });
65
- document.body.appendChild(button);
66
- return button;
44
+ export function mountIntro({ memory, template, appName, hadDraft }) {
45
+ if (!template || !template.content.textContent.trim() && !template.content.querySelector("img, svg, video, canvas")) return null;
46
+ if (!forced()) {
47
+ if (memory.get().intro.done || hadDraft) return null;
48
+ if (window.location.hash) return null;
49
+ if (window.matchMedia && window.matchMedia("print").matches) return null;
50
+ }
51
+ return mount({ memory, template, appName });
67
52
  }
68
53
 
69
- function mount(opts) {
70
- const { memory, template, appName, link, tag, source, prompt, snapshot, fromAbout } = opts;
54
+ function mount({ memory, template, appName }) {
71
55
  stylesOnce();
72
56
 
73
57
  const section = document.createElement("section");
@@ -75,157 +59,60 @@ function mount(opts) {
75
59
  section.setAttribute("clay", "editor-ui");
76
60
  section.setAttribute("sap-ignore", "");
77
61
  section.setAttribute("aria-label", COPY.label(appName));
62
+ section.appendChild(template.content.cloneNode(true));
78
63
 
79
- const inner = document.createElement("div");
80
- inner.className = "clay-intro__inner";
81
-
82
- const kicker = document.createElement("p");
83
- kicker.className = "clay-intro__kicker";
84
- kicker.textContent = COPY.kicker;
85
-
86
- const name = document.createElement("h1");
87
- name.className = "clay-intro__name";
88
- name.textContent = appName;
89
-
90
- const actions = document.createElement("div");
91
- actions.className = "clay-intro__actions";
92
-
93
- const download = document.createElement("a");
94
- download.className = "clay-intro__download";
95
- download.href = link;
96
- download.textContent = COPY.download;
97
-
98
- const form = document.createElement("form");
99
- form.className = "clay-intro__signup";
100
- form.noValidate = true;
101
-
102
- const label = document.createElement("label");
103
- label.setAttribute("for", "clay-intro-email");
104
- label.textContent = COPY.signupLabel;
105
-
106
- const row = document.createElement("div");
107
- row.className = "clay-intro__row";
108
-
109
- const email = document.createElement("input");
110
- email.id = "clay-intro-email";
111
- email.type = "email";
112
- email.autocomplete = "email";
113
- email.required = true;
114
- email.placeholder = COPY.emailPlaceholder;
115
-
116
- const send = document.createElement("button");
117
- send.type = "submit";
118
- send.textContent = COPY.signup;
64
+ const close = document.createElement("button");
65
+ close.type = "button";
66
+ close.className = "clay-intro__close";
67
+ close.setAttribute("aria-label", COPY.close);
68
+ 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>';
69
+ section.appendChild(close);
119
70
 
120
- row.append(email, send);
121
-
122
- const honey = document.createElement("input");
123
- honey.className = "clay-intro__hp";
124
- honey.name = "website";
125
- honey.tabIndex = -1;
126
- honey.autocomplete = "off";
127
- honey.setAttribute("aria-hidden", "true");
128
-
129
- const message = document.createElement("p");
130
- message.className = "clay-intro__msg";
131
- message.setAttribute("role", "status");
132
- message.hidden = true;
133
-
134
- form.append(label, row, honey, message);
135
- actions.append(download, form);
136
-
137
- const start = document.createElement("button");
138
- start.type = "button";
139
- start.className = "clay-intro__start";
140
- start.textContent = COPY.start(appName);
141
-
142
- inner.append(kicker, name, template.content.cloneNode(true), actions, start);
143
- section.appendChild(inner);
144
-
145
- let observer = null;
146
71
  let gone = false;
72
+ let frame = 0;
147
73
 
148
- function stop() {
149
- document.removeEventListener("input", onUse, true);
150
- document.removeEventListener("change", onUse, true);
151
- document.removeEventListener("clay:dirty", onUse, true);
152
- if (observer) { observer.disconnect(); observer = null; }
153
- if (prompt) prompt.hideForIntro(false);
154
- }
155
-
156
- function dismiss(reason, target) {
74
+ function dismiss(reason) {
157
75
  if (gone) return;
158
76
  gone = true;
159
- const anchor = target && target.getBoundingClientRect ? target : document.activeElement;
160
- const before = anchor && anchor.getBoundingClientRect ? anchor.getBoundingClientRect().top : 0;
77
+ removeEventListener("scroll", onScroll);
78
+ if (frame) cancelAnimationFrame(frame);
79
+ const height = section.getBoundingClientRect().height;
80
+ const before = window.scrollY;
161
81
  memory.get().intro.done = reason;
162
82
  memory.save();
163
- stop();
164
83
  section.remove();
165
- if (reason === "button") {
166
- window.scrollTo({ top: 0 });
167
- focusApp();
168
- } else if (anchor && anchor.getBoundingClientRect) {
169
- window.scrollBy(0, anchor.getBoundingClientRect().top - before);
84
+ if (reason === "scrolled") {
85
+ const locked = /hidden|clip/.test(getComputedStyle(document.body).overflowY + getComputedStyle(document.documentElement).overflowY);
86
+ window.scrollTo(0, locked ? 0 : Math.max(0, before - height));
170
87
  }
171
- if (fromAbout) mountAbout(opts);
88
+ announceBottomUi();
89
+ if (reason !== "scrolled") focusApp();
172
90
  }
173
91
 
174
- function unchanged() {
175
- return !snapshot || snapshot() === baseline;
92
+ function onScroll() {
93
+ if (frame) return;
94
+ frame = requestAnimationFrame(() => {
95
+ frame = 0;
96
+ const bottom = section.getBoundingClientRect().bottom;
97
+ const atEnd = scrollY > 0 && scrollY + innerHeight >= document.documentElement.scrollHeight - 1;
98
+ if (bottom < 1 || (atEnd && bottom < innerHeight)) dismiss("scrolled");
99
+ });
176
100
  }
177
101
 
178
- function onUse(event) {
179
- if (section.contains(event.target)) return;
180
- if (event.type === "clay:dirty") {
181
- if (!unchanged()) dismiss("use", event.target);
102
+ section.addEventListener("click", (event) => {
103
+ if (event.target.closest(".clay-intro__close, [visitor-intro-dismiss]")) {
104
+ dismiss("closed");
182
105
  return;
183
106
  }
184
- if (!unchanged() || event.target?.closest?.("[persist]")) {
185
- dismiss("use", event.target);
186
- return;
107
+ if (event.target.closest("[visitor-intro-start]")) {
108
+ event.preventDefault();
109
+ window.scrollTo({ top: window.scrollY + section.getBoundingClientRect().bottom, behavior: "smooth" });
187
110
  }
188
- const target = event.target;
189
- setTimeout(() => {
190
- if (!gone && !unchanged()) dismiss("use", target);
191
- }, 0);
192
- }
193
-
194
- form.addEventListener("submit", (event) => {
195
- event.preventDefault();
196
- fetch("/_/newsletter/subscribe", {
197
- method: "POST",
198
- headers: { "Content-Type": "application/json" },
199
- credentials: "same-origin",
200
- body: JSON.stringify({ email: email.value.trim(), tags: [tag], source, website: honey.value }),
201
- }).then((response) => {
202
- if (!response.ok) throw new Error("signup failed");
203
- row.hidden = true;
204
- message.textContent = COPY.signupThanks;
205
- message.hidden = false;
206
- }).catch(() => {
207
- message.textContent = COPY.signupFailed;
208
- message.hidden = false;
209
- });
210
111
  });
211
112
 
212
- start.addEventListener("click", () => dismiss("button"));
213
-
214
- const baseline = snapshot ? snapshot() : null;
215
113
  document.body.prepend(section);
216
- document.addEventListener("input", onUse, true);
217
- document.addEventListener("change", onUse, true);
218
- document.addEventListener("clay:dirty", onUse, true);
219
-
220
- if (prompt) {
221
- prompt.hideForIntro(true);
222
- if (typeof IntersectionObserver === "function") {
223
- observer = new IntersectionObserver((entries) => {
224
- for (const entry of entries) prompt.hideForIntro(entry.intersectionRatio > 0.3);
225
- });
226
- observer.observe(section);
227
- }
228
- }
114
+ announceBottomUi();
115
+ addEventListener("scroll", onScroll, { passive: true });
229
116
 
230
117
  return section;
231
118
  }
@@ -1,4 +1,5 @@
1
1
  import { isDraftMode } from "../../core/is-edit-mode.js";
2
+ import { BOTTOM_UI_CHANGED, announceBottomUi, bottomBarHeight, promptPanel } from "./bottom-ui.js";
2
3
 
3
4
  export const COPY = {
4
5
  draft: "You're seeing a private draft, like a visitor.",
@@ -50,17 +51,23 @@ const CHIP_CSS = `.clay-owner {
50
51
  -webkit-font-smoothing: antialiased;
51
52
  }
52
53
  .clay-owner__action {
53
- flex: none; font-weight: 600; color: var(--hcp-brass); cursor: pointer;
54
+ flex: none; min-height: 0; font-weight: 600; color: var(--hcp-brass); cursor: pointer;
54
55
  text-decoration: underline; text-underline-offset: 2px; text-decoration-color: var(--hcp-line-2);
55
56
  white-space: nowrap;
56
57
  }
57
- .clay-owner__action:hover { color: var(--hcp-ink); }
58
+ .clay-owner .clay-owner__action:hover { color: var(--hcp-ink); background: none; }
58
59
  .clay-owner__x {
59
60
  flex: none; display: inline-flex; align-items: center; justify-content: center;
60
- width: 24px; height: 24px; font-size: 15px; color: var(--hcp-muted); cursor: pointer;
61
+ width: 24px; height: 24px; min-height: 0; font-size: 15px; color: var(--hcp-muted); cursor: pointer;
61
62
  }
62
- .clay-owner__x:hover { color: var(--hcp-ink); }
63
+ .clay-owner .clay-owner__x:hover { color: var(--hcp-ink); background: none; }
63
64
  .clay-owner__action:focus-visible, .clay-owner__x:focus-visible { outline: 2px solid var(--hcp-brass); outline-offset: 2px; }
65
+
66
+ .clay-owner svg {
67
+ display: block; flex: none; width: 1em; height: 1em; line-height: 1;
68
+ fill: currentColor; stroke: none; stroke-width: 0; stroke-linecap: butt; stroke-linejoin: miter;
69
+ }
70
+ .clay-owner button, .clay-owner a { border-radius: 0; line-height: 1.45; }
64
71
  @media (pointer: coarse) {
65
72
  .clay-owner__action, .clay-owner__x { min-height: 44px; }
66
73
  .clay-owner__x { width: 44px; }
@@ -111,40 +118,60 @@ export function mountOwnerChip() {
111
118
  close.className = "clay-owner__x";
112
119
  close.setAttribute("aria-label", COPY.close);
113
120
  close.textContent = "\u00d7";
114
- close.addEventListener("click", () => { chip.remove(); liftOff(); });
121
+ close.addEventListener("click", () => { chip.remove(); liftOff(); release(); });
115
122
 
116
123
  chip.append(lead, action, close);
117
124
  document.body.appendChild(chip);
118
125
 
126
+ const vars = document.createElement("style");
127
+ vars.id = "clay-owner-vars";
128
+ vars.setAttribute("clay", "editor-ui");
129
+ document.head.appendChild(vars);
130
+
131
+ let published = "";
132
+ function publish(lift) {
133
+ const next = `:root { --clay-owner-room: ${Math.ceil(chip.getBoundingClientRect().height) + 16}px; }`;
134
+ if (vars.textContent !== next) vars.textContent = next;
135
+ if (published === next + lift) return;
136
+ published = next + lift;
137
+ announceBottomUi();
138
+ }
139
+
140
+ function release() {
141
+ vars.remove();
142
+ announceBottomUi();
143
+ }
144
+
119
145
  let frame = 0;
120
146
  function lift() {
121
147
  if (frame) return;
122
148
  frame = requestAnimationFrame(() => {
123
149
  frame = 0;
124
- const y = window.innerHeight - 3;
125
- let raise = 0;
126
- const hits = document.elementsFromPoint ? document.elementsFromPoint(24, y) : [];
127
- for (const hit of hits) {
128
- for (let el = hit; el && el !== document.body && el !== document.documentElement; el = el.parentElement) {
129
- if (chip.contains(el)) break;
130
- const position = getComputedStyle(el).position;
131
- if (position === "fixed" || position === "sticky") {
132
- raise = Math.max(raise, Math.ceil(window.innerHeight - el.getBoundingClientRect().top));
133
- break;
134
- }
135
- }
136
- }
137
- chip.style.setProperty("--hcp-lift", (raise > 0 && raise < window.innerHeight / 3 ? raise : 0) + "px");
150
+ let lift = bottomBarHeight(24, (el) => chip.contains(el) || el.closest("#hcp"));
151
+ const panel = promptPanel(chip.getBoundingClientRect());
152
+ const covered = panel >= window.innerHeight / 3;
153
+ if (panel && !covered) lift = Math.max(lift, panel - 4);
154
+ chip.style.visibility = covered ? "hidden" : "";
155
+ chip.style.setProperty("--hcp-lift", lift + "px");
156
+ publish(covered ? -1 : lift);
138
157
  });
139
158
  }
140
159
 
141
160
  function liftOff() {
142
161
  removeEventListener("scroll", lift);
143
162
  removeEventListener("resize", lift);
163
+ document.removeEventListener(BOTTOM_UI_CHANGED, lift);
164
+ document.removeEventListener("transitionend", lift, true);
165
+ document.removeEventListener("pointerup", lift, true);
166
+ window.removeEventListener("hashchange", lift);
144
167
  }
145
168
 
146
169
  addEventListener("scroll", lift, { passive: true });
147
170
  addEventListener("resize", lift);
171
+ document.addEventListener(BOTTOM_UI_CHANGED, lift);
172
+ document.addEventListener("transitionend", lift, true);
173
+ document.addEventListener("pointerup", lift, true);
174
+ window.addEventListener("hashchange", lift);
148
175
  lift();
149
176
 
150
177
  return chip;