overplay 4.0.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.
- package/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/index.d.ts +229 -0
- package/dist/overplay.esm.js +3907 -0
- package/dist/overplay.esm.js.map +1 -0
- package/dist/overplay.iife.js +2 -0
- package/dist/overplay.iife.js.map +1 -0
- package/dist/overplay.umd.js +2 -0
- package/dist/overplay.umd.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,3907 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key2, value) => key2 in obj ? __defProp(obj, key2, { enumerable: true, configurable: true, writable: true, value }) : obj[key2] = value;
|
|
3
|
+
var __publicField = (obj, key2, value) => __defNormalProp(obj, typeof key2 !== "symbol" ? key2 + "" : key2, value);
|
|
4
|
+
function record(value) {
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid comment data.");
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function string(value, field) {
|
|
9
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`Invalid ${field}.`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function date(value) {
|
|
13
|
+
const result = new Date(value);
|
|
14
|
+
if (!Number.isFinite(result.getTime())) throw new Error("Invalid comment date.");
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function reply(value) {
|
|
18
|
+
const r = record(value);
|
|
19
|
+
return {
|
|
20
|
+
id: string(r.id, "reply ID"),
|
|
21
|
+
text: string(r.text, "reply text"),
|
|
22
|
+
author: string(r.author, "author"),
|
|
23
|
+
authorId: typeof r.authorId === "string" ? r.authorId : void 0,
|
|
24
|
+
via: typeof r.via === "string" ? r.via.slice(0, 60) : void 0,
|
|
25
|
+
avatarUrl: typeof r.avatarUrl === "string" ? r.avatarUrl : void 0,
|
|
26
|
+
timestamp: date(r.timestamp),
|
|
27
|
+
commentId: string(r.commentId, "comment ID")
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function revealSteps(value) {
|
|
31
|
+
if (!Array.isArray(value)) return {};
|
|
32
|
+
const short = (v, max) => typeof v === "string" && v.trim() ? v.trim().slice(0, max) : void 0;
|
|
33
|
+
const steps = value.slice(0, 6).flatMap((step) => {
|
|
34
|
+
const s = step && typeof step === "object" ? step : {};
|
|
35
|
+
const t = s.target && typeof s.target === "object" ? s.target : {};
|
|
36
|
+
const selector = short(t.selector, 500);
|
|
37
|
+
if (!["press", "hover", "focus"].includes(s.action) || !selector) return [];
|
|
38
|
+
const role = short(t.role, 40), name = short(t.name, 120), tag = short(t.tag, 40);
|
|
39
|
+
return [{ action: s.action, target: { selector, ...role ? { role } : {}, ...name ? { name } : {}, ...tag ? { tag } : {} } }];
|
|
40
|
+
});
|
|
41
|
+
return steps.length ? { reveal: steps } : {};
|
|
42
|
+
}
|
|
43
|
+
function normalizeComments(value) {
|
|
44
|
+
if (!Array.isArray(value)) throw new Error("Expected a list of comments.");
|
|
45
|
+
return value.map((value2) => {
|
|
46
|
+
const c = record(value2);
|
|
47
|
+
if (typeof c.x !== "number" || typeof c.y !== "number" || !Number.isFinite(c.x) || !Number.isFinite(c.y)) {
|
|
48
|
+
throw new Error("Invalid comment position.");
|
|
49
|
+
}
|
|
50
|
+
const anchor = c.anchor ? record(c.anchor) : null;
|
|
51
|
+
const info = c.element && typeof c.element === "object" ? c.element : null;
|
|
52
|
+
const text = (v) => typeof v === "string" && v.trim() ? v.trim().slice(0, 120) : void 0;
|
|
53
|
+
return {
|
|
54
|
+
id: string(c.id, "comment ID"),
|
|
55
|
+
text: string(c.text, "comment text"),
|
|
56
|
+
author: string(c.author, "author"),
|
|
57
|
+
authorId: typeof c.authorId === "string" ? c.authorId : void 0,
|
|
58
|
+
via: typeof c.via === "string" ? c.via.slice(0, 60) : void 0,
|
|
59
|
+
revision: typeof c.revision === "number" ? c.revision : void 0,
|
|
60
|
+
updatedAt: typeof c.updatedAt === "string" ? c.updatedAt : void 0,
|
|
61
|
+
avatarUrl: typeof c.avatarUrl === "string" ? c.avatarUrl : void 0,
|
|
62
|
+
timestamp: date(c.timestamp),
|
|
63
|
+
x: c.x,
|
|
64
|
+
y: c.y,
|
|
65
|
+
resolved: c.resolved === true,
|
|
66
|
+
pageUrl: string(c.pageUrl, "page URL"),
|
|
67
|
+
pageTitle: typeof c.pageTitle === "string" && c.pageTitle.trim() ? c.pageTitle.trim().slice(0, 300) : void 0,
|
|
68
|
+
replies: Array.isArray(c.replies) ? c.replies.map(reply) : [],
|
|
69
|
+
selector: typeof c.selector === "string" ? c.selector : void 0,
|
|
70
|
+
element: info && text(info.label) ? { label: text(info.label), ...text(info.place) ? { place: text(info.place) } : {}, ...revealSteps(info.reveal) } : void 0,
|
|
71
|
+
anchor: anchor && typeof anchor.x === "number" && typeof anchor.y === "number" && Number.isFinite(anchor.x) && Number.isFinite(anchor.y) ? { x: Math.max(0, Math.min(1, anchor.x)), y: Math.max(0, Math.min(1, anchor.y)) } : void 0
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function copyComments(comments) {
|
|
76
|
+
return normalizeComments(comments);
|
|
77
|
+
}
|
|
78
|
+
function safePageUrl(path, base) {
|
|
79
|
+
const url = new URL(path, base);
|
|
80
|
+
if (!["http:", "https:"].includes(url.protocol) || url.origin !== new URL(base).origin) {
|
|
81
|
+
throw new Error("Comments can only link to pages on this site.");
|
|
82
|
+
}
|
|
83
|
+
return url;
|
|
84
|
+
}
|
|
85
|
+
function withoutCommentLink(search) {
|
|
86
|
+
const parts = search.replace(/^\?/, "").split("&").filter((part) => {
|
|
87
|
+
try {
|
|
88
|
+
return decodeURIComponent(part.split("=")[0].replace(/\+/g, " ")) !== "cw_comment";
|
|
89
|
+
} catch {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const value = parts.join("&");
|
|
94
|
+
return value ? "?" + value : "";
|
|
95
|
+
}
|
|
96
|
+
function defaultPageId(location2) {
|
|
97
|
+
return `${location2.pathname}${withoutCommentLink(location2.search)}${/^#!?\//.test(location2.hash) ? location2.hash : ""}`;
|
|
98
|
+
}
|
|
99
|
+
function samePage(pageUrl, pageId) {
|
|
100
|
+
return pageUrl === pageId || pageUrl.replace(/#(?!!?\/).*$/, "") === pageId;
|
|
101
|
+
}
|
|
102
|
+
const MOVED_SERVICES = {
|
|
103
|
+
"comments.artsvit.com": "https://overplay.app"
|
|
104
|
+
};
|
|
105
|
+
function serviceOrigin(origin) {
|
|
106
|
+
return MOVED_SERVICES[new URL(origin).hostname] ?? origin;
|
|
107
|
+
}
|
|
108
|
+
class HostedAdapter {
|
|
109
|
+
constructor(serverUrl, projectId, page, sessionChanged) {
|
|
110
|
+
__publicField(this, "base");
|
|
111
|
+
__publicField(this, "key");
|
|
112
|
+
__publicField(this, "accessToken", "");
|
|
113
|
+
__publicField(this, "expiresAt", 0);
|
|
114
|
+
__publicField(this, "initialized", false);
|
|
115
|
+
__publicField(this, "pending");
|
|
116
|
+
__publicField(this, "cancelSignIn");
|
|
117
|
+
__publicField(this, "user");
|
|
118
|
+
this.projectId = projectId;
|
|
119
|
+
this.page = page;
|
|
120
|
+
this.sessionChanged = sessionChanged;
|
|
121
|
+
const url = new URL(serverUrl);
|
|
122
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname)))
|
|
123
|
+
throw new Error("serverUrl must use HTTPS.");
|
|
124
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/")
|
|
125
|
+
throw new Error("serverUrl must be a service origin without a path.");
|
|
126
|
+
this.base = serviceOrigin(url.origin);
|
|
127
|
+
this.key = `overplay:${this.base}:${projectId}:session`;
|
|
128
|
+
try {
|
|
129
|
+
const saved = JSON.parse(sessionStorage.getItem(this.key) || "null");
|
|
130
|
+
if ((saved == null ? void 0 : saved.expiresAt) > Date.now()) {
|
|
131
|
+
this.accessToken = saved.accessToken;
|
|
132
|
+
this.expiresAt = saved.expiresAt;
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
get signedIn() {
|
|
138
|
+
return !!this.user && this.expiresAt > Date.now();
|
|
139
|
+
}
|
|
140
|
+
clear() {
|
|
141
|
+
this.accessToken = "";
|
|
142
|
+
this.expiresAt = 0;
|
|
143
|
+
this.user = void 0;
|
|
144
|
+
try {
|
|
145
|
+
sessionStorage.removeItem(this.key);
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
this.sessionChanged();
|
|
149
|
+
}
|
|
150
|
+
async request(path, method = "GET", body) {
|
|
151
|
+
const url = new URL(`/api${path}`, this.base);
|
|
152
|
+
url.searchParams.set("projectId", this.projectId);
|
|
153
|
+
const response = await fetch(url, {
|
|
154
|
+
method,
|
|
155
|
+
credentials: "omit",
|
|
156
|
+
headers: {
|
|
157
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {},
|
|
158
|
+
...this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {}
|
|
159
|
+
},
|
|
160
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
161
|
+
});
|
|
162
|
+
const result = await response.json().catch(() => null);
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
if (response.status === 401 || response.status === 403 && method === "GET")
|
|
165
|
+
this.clear();
|
|
166
|
+
throw new Error(
|
|
167
|
+
(result == null ? void 0 : result.error) || `Comment service returned ${response.status}.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (result === null)
|
|
171
|
+
throw new Error("Comment service returned an invalid response.");
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
async initialize() {
|
|
175
|
+
if (this.initialized) return;
|
|
176
|
+
this.initialized = true;
|
|
177
|
+
if (!this.accessToken) return;
|
|
178
|
+
try {
|
|
179
|
+
this.user = (await this.request("/me")).user;
|
|
180
|
+
this.sessionChanged(this.user);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (this.accessToken) this.initialized = false;
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
signIn() {
|
|
187
|
+
if (this.pending) return this.pending;
|
|
188
|
+
const popup = window.open(
|
|
189
|
+
"about:blank",
|
|
190
|
+
`overplay-${this.projectId}`,
|
|
191
|
+
"popup,width=520,height=760"
|
|
192
|
+
);
|
|
193
|
+
if (!popup)
|
|
194
|
+
return Promise.reject(
|
|
195
|
+
new Error("Allow popups to sign in to Overplay.")
|
|
196
|
+
);
|
|
197
|
+
this.pending = new Promise((resolve, reject) => {
|
|
198
|
+
let done = false;
|
|
199
|
+
const finish = (error) => {
|
|
200
|
+
if (done) return;
|
|
201
|
+
done = true;
|
|
202
|
+
window.removeEventListener("message", receive);
|
|
203
|
+
clearInterval(closed);
|
|
204
|
+
clearTimeout(timeout);
|
|
205
|
+
popup.close();
|
|
206
|
+
this.pending = void 0;
|
|
207
|
+
this.cancelSignIn = void 0;
|
|
208
|
+
if (error) reject(error);
|
|
209
|
+
else resolve();
|
|
210
|
+
};
|
|
211
|
+
const verifier = Array.from(
|
|
212
|
+
crypto.getRandomValues(new Uint8Array(32)),
|
|
213
|
+
(b) => b.toString(16).padStart(2, "0")
|
|
214
|
+
).join("");
|
|
215
|
+
let exchanging = false;
|
|
216
|
+
const receive = (event) => {
|
|
217
|
+
var _a;
|
|
218
|
+
if (exchanging || event.origin !== this.base || event.source !== popup || ((_a = event.data) == null ? void 0 : _a.type) !== "overplay:authorization" || event.data.projectId !== this.projectId || typeof event.data.code !== "string")
|
|
219
|
+
return;
|
|
220
|
+
exchanging = true;
|
|
221
|
+
void (async () => {
|
|
222
|
+
const result = await this.request("/connect/token", "POST", {
|
|
223
|
+
code: event.data.code,
|
|
224
|
+
verifier
|
|
225
|
+
});
|
|
226
|
+
if (done) return;
|
|
227
|
+
this.accessToken = result.accessToken;
|
|
228
|
+
this.expiresAt = result.expiresAt;
|
|
229
|
+
this.user = (await this.request("/me")).user;
|
|
230
|
+
if (done) {
|
|
231
|
+
this.clear();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.initialized = true;
|
|
235
|
+
try {
|
|
236
|
+
sessionStorage.setItem(
|
|
237
|
+
this.key,
|
|
238
|
+
JSON.stringify({
|
|
239
|
+
accessToken: this.accessToken,
|
|
240
|
+
expiresAt: this.expiresAt
|
|
241
|
+
})
|
|
242
|
+
);
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
this.sessionChanged(this.user);
|
|
246
|
+
finish();
|
|
247
|
+
})().catch((e) => {
|
|
248
|
+
this.clear();
|
|
249
|
+
finish(e instanceof Error ? e : new Error("Sign-in failed."));
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
const closed = setInterval(() => {
|
|
253
|
+
if (popup.closed && !exchanging)
|
|
254
|
+
finish(new Error("Sign-in window was closed."));
|
|
255
|
+
}, 750);
|
|
256
|
+
const timeout = setTimeout(
|
|
257
|
+
() => finish(new Error("Sign-in timed out. Please try again.")),
|
|
258
|
+
5 * 6e4
|
|
259
|
+
);
|
|
260
|
+
this.cancelSignIn = () => finish(new Error("Widget was closed."));
|
|
261
|
+
window.addEventListener("message", receive);
|
|
262
|
+
void crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)).then((hash) => {
|
|
263
|
+
if (done) return;
|
|
264
|
+
const encoded = btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
265
|
+
const url = new URL("/connect", this.base);
|
|
266
|
+
url.search = new URLSearchParams({
|
|
267
|
+
projectId: this.projectId,
|
|
268
|
+
origin: location.origin,
|
|
269
|
+
challenge: encoded
|
|
270
|
+
}).toString();
|
|
271
|
+
popup.location.href = url.href;
|
|
272
|
+
}).catch(() => finish(new Error("Secure sign-in is unavailable.")));
|
|
273
|
+
});
|
|
274
|
+
return this.pending;
|
|
275
|
+
}
|
|
276
|
+
async signOut() {
|
|
277
|
+
try {
|
|
278
|
+
if (this.accessToken) await this.request("/connect/logout", "POST", {});
|
|
279
|
+
} finally {
|
|
280
|
+
this.clear();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
destroy() {
|
|
284
|
+
var _a;
|
|
285
|
+
(_a = this.cancelSignIn) == null ? void 0 : _a.call(this);
|
|
286
|
+
}
|
|
287
|
+
async loadComments() {
|
|
288
|
+
await this.initialize();
|
|
289
|
+
if (this.expiresAt && this.expiresAt <= Date.now()) this.clear();
|
|
290
|
+
if (!this.signedIn) return [];
|
|
291
|
+
const sessionToken = this.accessToken;
|
|
292
|
+
const comments = [];
|
|
293
|
+
let after = "";
|
|
294
|
+
do {
|
|
295
|
+
const query = new URLSearchParams({ after });
|
|
296
|
+
const page = await this.request(`/comments?${query}`);
|
|
297
|
+
comments.push(...page.comments);
|
|
298
|
+
after = page.next || "";
|
|
299
|
+
} while (after);
|
|
300
|
+
return sessionToken === this.accessToken && this.signedIn ? normalizeComments(comments) : [];
|
|
301
|
+
}
|
|
302
|
+
subscribeToChanges(callback) {
|
|
303
|
+
let running = false;
|
|
304
|
+
let stopped = false;
|
|
305
|
+
const poll = async () => {
|
|
306
|
+
if (stopped || running || document.visibilityState === "hidden" || !this.user)
|
|
307
|
+
return;
|
|
308
|
+
running = true;
|
|
309
|
+
const pageId = this.page();
|
|
310
|
+
const sessionToken = this.accessToken;
|
|
311
|
+
try {
|
|
312
|
+
const comments = await this.loadComments();
|
|
313
|
+
if (!stopped && this.signedIn && sessionToken === this.accessToken && pageId === this.page())
|
|
314
|
+
callback(comments);
|
|
315
|
+
} catch {
|
|
316
|
+
} finally {
|
|
317
|
+
running = false;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const interval = setInterval(() => {
|
|
321
|
+
void poll();
|
|
322
|
+
}, 3e4);
|
|
323
|
+
const visible = () => {
|
|
324
|
+
if (document.visibilityState === "visible") void poll();
|
|
325
|
+
};
|
|
326
|
+
document.addEventListener("visibilitychange", visible);
|
|
327
|
+
return () => {
|
|
328
|
+
stopped = true;
|
|
329
|
+
clearInterval(interval);
|
|
330
|
+
document.removeEventListener("visibilitychange", visible);
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async saveComments() {
|
|
334
|
+
throw new Error("Hosted storage requires individual comment mutations.");
|
|
335
|
+
}
|
|
336
|
+
async upsertComment(comment) {
|
|
337
|
+
await this.request(
|
|
338
|
+
`/comments/${encodeURIComponent(comment.id)}`,
|
|
339
|
+
"PUT",
|
|
340
|
+
comment
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
async deleteComment(id) {
|
|
344
|
+
await this.request(`/comments/${encodeURIComponent(id)}`, "DELETE");
|
|
345
|
+
}
|
|
346
|
+
async addReply(id, reply2) {
|
|
347
|
+
await this.request(
|
|
348
|
+
`/comments/${encodeURIComponent(id)}/replies`,
|
|
349
|
+
"POST",
|
|
350
|
+
reply2
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
async setResolved(id, resolved) {
|
|
354
|
+
await this.request(`/comments/${encodeURIComponent(id)}`, "PATCH", {
|
|
355
|
+
resolved
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
class LocalStorageAdapter {
|
|
360
|
+
constructor(storageKey = "overplay-comments") {
|
|
361
|
+
this.storageKey = storageKey;
|
|
362
|
+
}
|
|
363
|
+
async loadComments() {
|
|
364
|
+
const value = localStorage.getItem(this.storageKey);
|
|
365
|
+
return value === null ? [] : normalizeComments(JSON.parse(value));
|
|
366
|
+
}
|
|
367
|
+
async saveComments(comments) {
|
|
368
|
+
localStorage.setItem(this.storageKey, JSON.stringify(comments));
|
|
369
|
+
}
|
|
370
|
+
async upsertComment(comment) {
|
|
371
|
+
await this.change((comments) => [...comments.filter((c) => c.id !== comment.id), comment]);
|
|
372
|
+
}
|
|
373
|
+
async deleteComment(id) {
|
|
374
|
+
await this.change((comments) => comments.filter((c) => c.id !== id));
|
|
375
|
+
}
|
|
376
|
+
async addReply(id, reply2) {
|
|
377
|
+
await this.change((comments) => {
|
|
378
|
+
if (!comments.some((c) => c.id === id)) throw new Error("This comment was removed. Refresh to see the latest notes.");
|
|
379
|
+
return comments.map((c) => c.id === id && !c.replies.some((r) => r.id === reply2.id) ? { ...c, replies: [...c.replies, reply2] } : c);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async setResolved(id, resolved) {
|
|
383
|
+
await this.change((comments) => {
|
|
384
|
+
if (!comments.some((c) => c.id === id)) throw new Error("This comment was removed. Refresh to see the latest notes.");
|
|
385
|
+
return comments.map((c) => c.id === id ? { ...c, resolved } : c);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
async change(update) {
|
|
389
|
+
const write = async () => this.saveComments(update(await this.loadComments()));
|
|
390
|
+
if (typeof navigator !== "undefined" && navigator.locks) {
|
|
391
|
+
await navigator.locks.request(`overplay:${this.storageKey}`, write);
|
|
392
|
+
} else await write();
|
|
393
|
+
}
|
|
394
|
+
subscribeToChanges(callback) {
|
|
395
|
+
const listener = (event) => {
|
|
396
|
+
if (event.storageArea !== localStorage || event.key !== this.storageKey && event.key !== null) return;
|
|
397
|
+
void this.loadComments().then(callback).catch(() => {
|
|
398
|
+
});
|
|
399
|
+
};
|
|
400
|
+
window.addEventListener("storage", listener);
|
|
401
|
+
return () => window.removeEventListener("storage", listener);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function parseShortcut(value) {
|
|
405
|
+
const parts = value.toLowerCase().split("+").map((part) => part.trim());
|
|
406
|
+
const key2 = parts.pop();
|
|
407
|
+
if (!key2) return null;
|
|
408
|
+
const shortcut = { key: key2, alt: false, shift: false, ctrl: false, meta: false };
|
|
409
|
+
for (const part of parts) {
|
|
410
|
+
if (part === "alt" || part === "option") shortcut.alt = true;
|
|
411
|
+
else if (part === "shift") shortcut.shift = true;
|
|
412
|
+
else if (part === "ctrl" || part === "control") shortcut.ctrl = true;
|
|
413
|
+
else if (part === "meta" || part === "cmd") shortcut.meta = true;
|
|
414
|
+
else return null;
|
|
415
|
+
}
|
|
416
|
+
return shortcut;
|
|
417
|
+
}
|
|
418
|
+
function matchesShortcut(event, shortcut) {
|
|
419
|
+
if (event.altKey !== shortcut.alt || event.ctrlKey !== shortcut.ctrl || event.metaKey !== shortcut.meta) return false;
|
|
420
|
+
if (shortcut.shift && !event.shiftKey) return false;
|
|
421
|
+
const code = /^[a-z]$/.test(shortcut.key) ? `Key${shortcut.key.toUpperCase()}` : /^\d$/.test(shortcut.key) ? `Digit${shortcut.key}` : "";
|
|
422
|
+
return event.key.toLowerCase() === shortcut.key || !!code && event.code === code;
|
|
423
|
+
}
|
|
424
|
+
function newId() {
|
|
425
|
+
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
426
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
427
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
428
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
429
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
430
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
431
|
+
}
|
|
432
|
+
function validateUser(user) {
|
|
433
|
+
if (!user || typeof user.id !== "string" || !user.id.trim() || typeof user.name !== "string" || !user.name.trim()) {
|
|
434
|
+
throw new Error("A user needs a stable ID and a name.");
|
|
435
|
+
}
|
|
436
|
+
const role = user.role === "owner" || user.role === "admin" || user.role === "member" ? { role: user.role } : {};
|
|
437
|
+
return { id: user.id.trim(), name: user.name.trim().slice(0, 80), avatarUrl: user.avatarUrl, ...role };
|
|
438
|
+
}
|
|
439
|
+
function guestIdentity(key2) {
|
|
440
|
+
let saved;
|
|
441
|
+
try {
|
|
442
|
+
const value = localStorage.getItem(key2);
|
|
443
|
+
if (value) saved = validateUser(JSON.parse(value));
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
const user = saved ?? { id: newId(), name: "Guest" };
|
|
447
|
+
return { user, persistent: saveGuest(key2, user) };
|
|
448
|
+
}
|
|
449
|
+
function saveGuest(key2, user) {
|
|
450
|
+
try {
|
|
451
|
+
localStorage.setItem(key2, JSON.stringify(user));
|
|
452
|
+
return true;
|
|
453
|
+
} catch {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function newActivity(comments, seen, selfId) {
|
|
458
|
+
const found = [];
|
|
459
|
+
const visit = (key2, kind, commentId, item) => {
|
|
460
|
+
if (seen.has(key2)) return;
|
|
461
|
+
seen.add(key2);
|
|
462
|
+
if (item.authorId !== selfId) found.push({ kind, commentId, author: item.author, avatarUrl: item.avatarUrl, text: item.text });
|
|
463
|
+
};
|
|
464
|
+
for (const comment of comments) {
|
|
465
|
+
visit(comment.id, "comment", comment.id, comment);
|
|
466
|
+
for (const reply2 of comment.replies) visit(`${comment.id}/${reply2.id}`, "reply", comment.id, reply2);
|
|
467
|
+
}
|
|
468
|
+
return found;
|
|
469
|
+
}
|
|
470
|
+
function sameComments(a, b) {
|
|
471
|
+
return a.length === b.length && JSON.stringify(a) === JSON.stringify(b);
|
|
472
|
+
}
|
|
473
|
+
const version = "4.0.0";
|
|
474
|
+
const VERSION = version;
|
|
475
|
+
const PACKAGE = "overplay";
|
|
476
|
+
const UPDATE_COMMAND = `npm install ${PACKAGE}@latest`;
|
|
477
|
+
function isNewer(latest, current) {
|
|
478
|
+
const parse = (v) => {
|
|
479
|
+
var _a;
|
|
480
|
+
return (_a = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim())) == null ? void 0 : _a.slice(1).map(Number);
|
|
481
|
+
};
|
|
482
|
+
const a = parse(latest), b = parse(current);
|
|
483
|
+
if (!a || !b) return false;
|
|
484
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
function developmentHost(hostname) {
|
|
488
|
+
return ["localhost", "127.0.0.1", "[::1]", "::1"].includes(hostname) || /\.(localhost|local|test)$/.test(hostname);
|
|
489
|
+
}
|
|
490
|
+
const CACHE_MS = 12 * 60 * 60 * 1e3;
|
|
491
|
+
async function latestVersion(fetcher = fetch, now = Date.now()) {
|
|
492
|
+
const key2 = "overplay:latest-version";
|
|
493
|
+
try {
|
|
494
|
+
const cached = JSON.parse(localStorage.getItem(key2) || "null");
|
|
495
|
+
if (cached && typeof cached.version === "string" && now - cached.checkedAt < CACHE_MS) return cached.version;
|
|
496
|
+
} catch {
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
const response = await fetcher(`https://registry.npmjs.org/${PACKAGE}/latest`, { credentials: "omit", referrerPolicy: "no-referrer" });
|
|
500
|
+
if (!response.ok) return null;
|
|
501
|
+
const result = await response.json();
|
|
502
|
+
if (typeof result.version !== "string") return null;
|
|
503
|
+
try {
|
|
504
|
+
localStorage.setItem(key2, JSON.stringify({ version: result.version, checkedAt: now }));
|
|
505
|
+
} catch {
|
|
506
|
+
}
|
|
507
|
+
return result.version;
|
|
508
|
+
} catch {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const PIN_COLORS = {
|
|
513
|
+
indigo: { label: "Indigo", color: "#575ce5" },
|
|
514
|
+
violet: { label: "Violet", color: "#8048d8" },
|
|
515
|
+
blue: { label: "Blue", color: "#0068f9" },
|
|
516
|
+
teal: { label: "Teal", color: "#0f8a8c" },
|
|
517
|
+
amber: { label: "Amber", color: "#b97a06" },
|
|
518
|
+
orange: { label: "Orange", color: "#d9641e" },
|
|
519
|
+
red: { label: "Red", color: "#d23c3c" },
|
|
520
|
+
pink: { label: "Pink", color: "#d0427f" },
|
|
521
|
+
graphite: { label: "Graphite", color: "#242736" }
|
|
522
|
+
};
|
|
523
|
+
const CHOICES = {
|
|
524
|
+
theme: ["light", "dark", "system"],
|
|
525
|
+
pinColor: ["site", "indigo", "violet", "blue", "teal", "amber", "orange", "red", "pink", "graphite"],
|
|
526
|
+
buttonColor: ["default", "site", "indigo", "violet", "blue", "teal", "amber", "orange", "red", "pink"],
|
|
527
|
+
resolvedPins: ["green", "gray"],
|
|
528
|
+
pinSize: ["regular", "small"],
|
|
529
|
+
pinLabel: ["number", "initials"],
|
|
530
|
+
pushContent: [true, false],
|
|
531
|
+
notifications: [true, false],
|
|
532
|
+
shortcut: [true, false]
|
|
533
|
+
};
|
|
534
|
+
function readSettings(key2, defaults) {
|
|
535
|
+
const settings = { ...defaults };
|
|
536
|
+
try {
|
|
537
|
+
const saved = JSON.parse(localStorage.getItem(key2) || "{}");
|
|
538
|
+
for (const name of Object.keys(CHOICES)) {
|
|
539
|
+
if (CHOICES[name].includes(saved[name])) Object.assign(settings, { [name]: saved[name] });
|
|
540
|
+
}
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
return settings;
|
|
544
|
+
}
|
|
545
|
+
function writeSettings(key2, settings, defaults) {
|
|
546
|
+
const changed = Object.fromEntries(Object.entries(settings).filter(([name, value]) => defaults[name] !== value));
|
|
547
|
+
try {
|
|
548
|
+
if (Object.keys(changed).length) localStorage.setItem(key2, JSON.stringify(changed));
|
|
549
|
+
else localStorage.removeItem(key2);
|
|
550
|
+
return true;
|
|
551
|
+
} catch {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function initials(name) {
|
|
556
|
+
const words2 = name.trim().split(/\s+/).filter(Boolean);
|
|
557
|
+
const letters = words2.length > 1 ? `${[...words2[0]][0]}${[...words2[words2.length - 1]][0]}` : [...words2[0] ?? "?"].slice(0, 2).join("");
|
|
558
|
+
return letters.toUpperCase();
|
|
559
|
+
}
|
|
560
|
+
function element(tag, className = "", text) {
|
|
561
|
+
const el = document.createElement(tag);
|
|
562
|
+
if (className) el.className = className;
|
|
563
|
+
if (text !== void 0) el.textContent = text;
|
|
564
|
+
return el;
|
|
565
|
+
}
|
|
566
|
+
const paths = {
|
|
567
|
+
comment: "M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.4 8.4 0 0 1 3.8-.9h.5a8.5 8.5 0 0 1 8 8v.5Z",
|
|
568
|
+
plus: "M12 5v14M5 12h14",
|
|
569
|
+
close: "m6 6 12 12M6 18 18 6",
|
|
570
|
+
check: "m5 12 4 4L19 6",
|
|
571
|
+
back: "m12 5-7 7 7 7M5 12h14",
|
|
572
|
+
arrow: "M12 19V5m-6 6 6-6 6 6",
|
|
573
|
+
refresh: "M20.5 12a8.5 8.5 0 1 1-2.6-6.1L20.5 8.5M20.5 3.5v5h-5",
|
|
574
|
+
open: "M7 17 17 7M9 7h8v8",
|
|
575
|
+
trash: "M3 6h18M9 6V4h6v2M5 6l1 14h12l1-14M10 10v6m4-6v6",
|
|
576
|
+
pin: "M12 21s7-6 7-13A7 7 0 0 0 5 8c0 7 7 13 7 13ZM12 6v4M10 8h4",
|
|
577
|
+
search: "m21 21-5-5M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Z",
|
|
578
|
+
eye: "M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12ZM12 14.8a2.8 2.8 0 1 0 0-5.6 2.8 2.8 0 0 0 0 5.6Z",
|
|
579
|
+
eyeOff: "M3 3l18 18M10.6 5.6A9 9 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a16 16 0 0 1-2.9 3.6M6.5 6.9A16 16 0 0 0 2.5 12S6 18.5 12 18.5a9 9 0 0 0 4.6-1.3M10 10a2.8 2.8 0 0 0 4 4",
|
|
580
|
+
unlink: "M4.5 4.5h15v15h-15zM9.5 9.5l5 5M14.5 9.5l-5 5",
|
|
581
|
+
dock: "M4 5h16v14H4zM15 5v14",
|
|
582
|
+
settings: "M4 7h9m4 0h3M4 17h3m4 0h9M15 5v4M9 15v4",
|
|
583
|
+
edit: "M4 20h4L19 9l-4-4L4 16v4ZM13 7l4 4",
|
|
584
|
+
signin: "M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3",
|
|
585
|
+
signout: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9",
|
|
586
|
+
more: "M5 12h.01M12 12h.01M19 12h.01",
|
|
587
|
+
popout: "M14 4h6v6M20 4l-8 8M18 14v5H5V6h5"
|
|
588
|
+
};
|
|
589
|
+
function icon(name) {
|
|
590
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
591
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
592
|
+
svg.setAttribute("fill", "none");
|
|
593
|
+
svg.setAttribute("stroke", "currentColor");
|
|
594
|
+
svg.setAttribute("stroke-width", "1.65");
|
|
595
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
596
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
597
|
+
svg.setAttribute("aria-hidden", "true");
|
|
598
|
+
const path = document.createElementNS(svg.namespaceURI, "path");
|
|
599
|
+
path.setAttribute("d", paths[name]);
|
|
600
|
+
svg.append(path);
|
|
601
|
+
return svg;
|
|
602
|
+
}
|
|
603
|
+
function button(label, className, onClick, name) {
|
|
604
|
+
const el = element("button", className);
|
|
605
|
+
el.type = "button";
|
|
606
|
+
el.setAttribute("aria-label", label);
|
|
607
|
+
el.title = label;
|
|
608
|
+
if (name) el.append(icon(name));
|
|
609
|
+
if (!className.includes("icon-button")) el.append(element("span", "", label));
|
|
610
|
+
el.addEventListener("click", onClick);
|
|
611
|
+
return el;
|
|
612
|
+
}
|
|
613
|
+
function avatar(name, url) {
|
|
614
|
+
const el = element("span", "avatar", initials(name));
|
|
615
|
+
el.setAttribute("aria-hidden", "true");
|
|
616
|
+
if (url) {
|
|
617
|
+
try {
|
|
618
|
+
const safe = new URL(url, location.href);
|
|
619
|
+
if (safe.protocol === "https:" || safe.protocol === "http:" && safe.origin === location.origin) {
|
|
620
|
+
const img = element("img");
|
|
621
|
+
img.src = safe.href;
|
|
622
|
+
img.alt = "";
|
|
623
|
+
img.referrerPolicy = "no-referrer";
|
|
624
|
+
if (img.complete && img.naturalWidth) el.replaceChildren(img);
|
|
625
|
+
else img.addEventListener("load", () => el.replaceChildren(img));
|
|
626
|
+
}
|
|
627
|
+
} catch {
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return el;
|
|
631
|
+
}
|
|
632
|
+
function relativeTime(date2) {
|
|
633
|
+
const minutes = Math.max(0, Math.floor((Date.now() - date2.getTime()) / 6e4));
|
|
634
|
+
if (minutes < 1) return "Just now";
|
|
635
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
636
|
+
if (minutes < 1440) return `${Math.floor(minutes / 60)}h ago`;
|
|
637
|
+
return date2.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
638
|
+
}
|
|
639
|
+
const tokens = {
|
|
640
|
+
// Brand: Overplay blue. One accent for buttons, links, open pins and focus.
|
|
641
|
+
accent: "#0068f9",
|
|
642
|
+
"accent-strong": "#0055d4",
|
|
643
|
+
"accent-soft": "#eef4ff",
|
|
644
|
+
"accent-line": "#d3e3fd",
|
|
645
|
+
"accent-focus": "#99c2ff",
|
|
646
|
+
// Text
|
|
647
|
+
ink: "#121722",
|
|
648
|
+
"ink-soft": "#3a3f4a",
|
|
649
|
+
muted: "#636874",
|
|
650
|
+
subtle: "#8a8e97",
|
|
651
|
+
// Surfaces and lines
|
|
652
|
+
surface: "#ffffff",
|
|
653
|
+
canvas: "#f7f7f8",
|
|
654
|
+
"surface-soft": "#f0f1f3",
|
|
655
|
+
line: "#e8e9ec",
|
|
656
|
+
"line-strong": "#dedfe3",
|
|
657
|
+
// People
|
|
658
|
+
"avatar-bg": "#e8f1ff",
|
|
659
|
+
"avatar-ink": "#2560c4",
|
|
660
|
+
// Raised dark chips on the page: the launcher button, toasts and the placing notice.
|
|
661
|
+
inverse: "#121722",
|
|
662
|
+
"on-inverse": "#ffffff",
|
|
663
|
+
"inverse-muted": "#b8bcc6",
|
|
664
|
+
// States. "Resolved" is the one positive state: resolved pins, saved, verified, approved.
|
|
665
|
+
resolved: "#0f9a6c",
|
|
666
|
+
"resolved-text": "#0b7a55",
|
|
667
|
+
"resolved-soft": "#e7f6ef",
|
|
668
|
+
danger: "#c43d3d",
|
|
669
|
+
"danger-soft": "#fff4f3",
|
|
670
|
+
"danger-line": "#f3cfcf",
|
|
671
|
+
warning: "#745320",
|
|
672
|
+
"warning-soft": "#fff8e8",
|
|
673
|
+
"warning-line": "#ebd59f",
|
|
674
|
+
// Shape. Buttons and chips are pills; cards and windows have soft corners; big page panels have 26px.
|
|
675
|
+
"radius-sm": "6px",
|
|
676
|
+
radius: "10px",
|
|
677
|
+
"radius-md": "12px",
|
|
678
|
+
"radius-lg": "16px",
|
|
679
|
+
"radius-xl": "26px",
|
|
680
|
+
"radius-pill": "999px",
|
|
681
|
+
// Hairline rings instead of heavy shadows.
|
|
682
|
+
"shadow-sm": "0 0 0 1px #1217221a,0 1px 2px #12172229",
|
|
683
|
+
shadow: "0 1px 3px #1217221a,0 16px 32px -12px #12172233",
|
|
684
|
+
font: '"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif'
|
|
685
|
+
};
|
|
686
|
+
const darkTokens = {
|
|
687
|
+
"accent-strong": "color-mix(in srgb,var(--cw-accent) 66%,#ffffff)",
|
|
688
|
+
"accent-soft": "color-mix(in srgb,var(--cw-accent) 18%,#16181d)",
|
|
689
|
+
"accent-line": "color-mix(in srgb,var(--cw-accent) 42%,#16181d)",
|
|
690
|
+
"accent-focus": "color-mix(in srgb,var(--cw-accent) 60%,#16181d)",
|
|
691
|
+
ink: "#eef0f3",
|
|
692
|
+
"ink-soft": "#c6c9d0",
|
|
693
|
+
muted: "#9a9ea8",
|
|
694
|
+
subtle: "#767a84",
|
|
695
|
+
surface: "#16181d",
|
|
696
|
+
canvas: "#0e0f12",
|
|
697
|
+
"surface-soft": "#1f2228",
|
|
698
|
+
line: "#262930",
|
|
699
|
+
"line-strong": "#33373f",
|
|
700
|
+
"avatar-bg": "#1b2a45",
|
|
701
|
+
"avatar-ink": "#9cc3ff",
|
|
702
|
+
inverse: "#23262d",
|
|
703
|
+
"inverse-muted": "#a6aab3",
|
|
704
|
+
resolved: "#16a877",
|
|
705
|
+
"resolved-text": "#48d3a0",
|
|
706
|
+
"resolved-soft": "#13302a",
|
|
707
|
+
danger: "#ec6a6a",
|
|
708
|
+
"danger-soft": "#341c21",
|
|
709
|
+
"danger-line": "#5b2b31",
|
|
710
|
+
warning: "#efc56e",
|
|
711
|
+
"warning-soft": "#2f2718",
|
|
712
|
+
"warning-line": "#57472a",
|
|
713
|
+
"shadow-sm": "0 0 0 1px #ffffff14,0 1px 2px #00000066",
|
|
714
|
+
shadow: "0 1px 3px #00000066,0 16px 40px -12px #000000b3"
|
|
715
|
+
};
|
|
716
|
+
function tokenDeclarations(set = tokens) {
|
|
717
|
+
return Object.entries(set).map(([name, value]) => `--cw-${name}:${value};`).join("");
|
|
718
|
+
}
|
|
719
|
+
const styles = `
|
|
720
|
+
:host{all:initial;${tokenDeclarations()}color:var(--cw-ink);font:16px/1.5 var(--cw-font);letter-spacing:normal;text-align:left;color-scheme:light}
|
|
721
|
+
:host([data-theme=dark]){${tokenDeclarations(darkTokens)}color-scheme:dark}
|
|
722
|
+
@media(prefers-color-scheme:dark){:host([data-theme=system]){${tokenDeclarations(darkTokens)}color-scheme:dark}}
|
|
723
|
+
:host>div{font:16px/1.5 var(--cw-font);color:var(--cw-ink);letter-spacing:normal;text-align:left}*,*::before,*::after{box-sizing:border-box}button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.45}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:3px solid var(--cw-accent);outline-offset:3px}button{color:inherit}svg{width:18px;height:18px;flex-shrink:0}button{touch-action:manipulation}img{max-width:100%}[hidden]{display:none!important}
|
|
724
|
+
.launcher{position:fixed;bottom:24px;right:24px;display:flex;align-items:center;justify-content:center;width:52px;height:52px;border:1px solid #ffffff24;background:var(--cw-inverse);color:var(--cw-on-inverse);padding:0;border-radius:50%;box-shadow:0 6px 24px #12172233;font-weight:600;font-size:16px;pointer-events:auto;touch-action:none;user-select:none;-webkit-user-select:none;cursor:grab;transition:transform .16s,box-shadow .16s}.launcher:hover{transform:translateY(-2px);box-shadow:0 8px 28px #20243733}.launcher.dragging{cursor:grabbing;transition:none;transform:scale(1.06);box-shadow:0 12px 32px #2024374d}.launcher[aria-expanded=true]{background:var(--cw-accent)}.launcher>svg{width:23px;height:23px}.badge{position:absolute;top:-5px;right:-5px;font-weight:650;font-size:12px;line-height:18px;min-width:22px;height:22px;border:2px solid var(--cw-surface);border-radius:999px;background:var(--cw-accent);color:#fff;text-align:center;padding:0 4px}.launcher[aria-expanded=true] .badge{background:var(--cw-inverse)}
|
|
725
|
+
.panel{position:fixed;top:0;right:0;bottom:0;width:400px;max-width:100vw;display:flex;flex-direction:column;background:var(--cw-surface);border-left:1px solid var(--cw-line);box-shadow:var(--cw-shadow);pointer-events:auto;overflow:hidden}.panel.entering{animation:slide-in .2s ease-out}.panel.left{right:auto;left:0;border-left:0;border-right:1px solid var(--cw-line);--cw-slide:-24px}.detail{display:flex;flex-direction:column;flex:1;min-height:0}.detail.entering{animation:enter .16s ease-out}.detail:focus{outline:0}.head{display:flex;align-items:center;gap:10px;padding:22px 20px 16px}.brand-icon{width:34px;height:34px;display:grid;place-items:center;background:var(--cw-accent-soft);color:var(--cw-accent);border-radius:50%}.heading{flex:1;min-width:0}.heading h2{font-size:19px;line-height:1.3;letter-spacing:-.4px;margin:0;font-weight:650}.eyebrow{font-size:12px;font-weight:700;letter-spacing:1.4px;color:var(--cw-muted);text-transform:uppercase;margin:0 0 4px}.subtext{color:var(--cw-muted);font-size:14px;margin:4px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.icon-button{width:32px;height:32px;display:inline-grid;place-items:center;background:transparent;border:0;border-radius:50%;flex-shrink:0}.icon-button:hover{background:var(--cw-surface-soft)}.icon-button.danger{color:var(--cw-danger)}.icon-button svg{width:16px;height:16px}.icon-button.outlined{width:42px;height:42px;border:1px solid var(--cw-line-strong);border-radius:50%;color:var(--cw-ink-soft)}.icon-button.outlined svg{width:18px;height:18px}.icon-button.busy:disabled{opacity:1;color:var(--cw-accent)}.icon-button.busy svg{animation:spin .9s linear infinite}.toolbar{padding:0 20px 16px;display:flex;gap:10px;align-items:center}.primary{border:0;border-radius:var(--cw-radius-pill);background:var(--cw-accent);color:#fff;display:inline-flex;gap:7px;align-items:center;justify-content:center;padding:10px 16px;font-size:14px;font-weight:550;white-space:nowrap;box-shadow:inset 0 -1px 0 #12172238;transition:filter .16s}.primary:hover{filter:brightness(.94)}.toolbar .primary{flex:1}.quiet{border:1px solid var(--cw-line-strong);background:var(--cw-surface);color:var(--cw-ink);border-radius:var(--cw-radius-pill);padding:9px 16px;font-size:14px}.quiet:hover{background:var(--cw-canvas)}.quiet.update{display:inline-flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;color:var(--cw-accent);border-color:var(--cw-accent-line);background:var(--cw-accent-soft);font-weight:600}.quiet.update:hover{border-color:var(--cw-accent)}.quiet.update svg{width:14px;height:14px}.scope{display:grid;grid-template-columns:1fr 1fr;gap:4px;margin:0 20px 8px;padding:4px;background:var(--cw-surface-soft);border-radius:var(--cw-radius-pill)}.scope-option{display:flex;gap:7px;align-items:center;justify-content:center;border:0;background:transparent;border-radius:var(--cw-radius-pill);padding:8px 6px;font-size:14px;font-weight:550;color:var(--cw-muted);white-space:nowrap}.scope-option:hover{color:var(--cw-ink)}.scope-option[aria-pressed=true]{background:var(--cw-surface);color:var(--cw-ink);box-shadow:0 1px 3px #2024371f}.scope-option[aria-pressed=true] .count{background:var(--cw-accent-soft);color:var(--cw-accent)}.tabs{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;margin:0 20px 8px;padding:4px;border:1px solid var(--cw-line);border-radius:var(--cw-radius-pill)}.tab{display:flex;gap:7px;align-items:center;justify-content:center;border:0;border-radius:var(--cw-radius-pill);background:transparent;padding:7px 6px;color:var(--cw-muted);font-size:14px;font-weight:550;white-space:nowrap}.tab[aria-pressed=true]{color:var(--cw-on-inverse);background:var(--cw-inverse)}.tab[aria-pressed=true] .count{background:#ffffff2e;color:var(--cw-on-inverse)}.count{background:var(--cw-surface-soft);padding:0 6px;border-radius:5px;font-size:12px;line-height:18px}.search{position:relative;margin:4px 20px 8px}.search>svg{position:absolute;left:11px;top:11px;width:15px;height:15px;color:var(--cw-subtle);pointer-events:none}.search input{border:1px solid var(--cw-line);border-radius:var(--cw-radius);background:var(--cw-canvas);width:100%;padding:9px 38px 9px 34px;color:var(--cw-ink);font-size:14px}.search input::placeholder{color:var(--cw-subtle)}.search input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}.search-clear{position:absolute;right:5px;top:50%;transform:translateY(-50%);width:28px;height:28px;color:var(--cw-muted)}.search-clear svg{width:14px;height:14px}.people{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 20px 6px}.filter-chip{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--cw-line);background:var(--cw-surface);color:var(--cw-ink-soft);border-radius:999px;padding:4px 6px 4px 11px;font-size:13px;font-weight:600;white-space:nowrap}.filter-chip:hover{background:var(--cw-canvas)}.filter-chip[aria-pressed=true]{background:var(--cw-accent-soft);border-color:var(--cw-accent-line);color:var(--cw-accent)}.filter-chip[aria-pressed=true] .count{background:var(--cw-surface);color:var(--cw-accent)}.select{position:relative;display:inline-flex;min-width:0;max-width:220px;color:var(--cw-muted)}.select::after{content:'';position:absolute;right:12px;top:50%;width:6px;height:6px;border:solid currentColor;border-width:0 1.5px 1.5px 0;transform:translateY(-70%) rotate(45deg);pointer-events:none}.select select{appearance:none;-webkit-appearance:none;border:1px solid var(--cw-line);background:var(--cw-surface);color:var(--cw-ink-soft);border-radius:999px;padding:5px 30px 5px 11px;font-size:13px;font-weight:600;line-height:18px;max-width:100%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}.select select:hover{background:var(--cw-canvas)}.select.chosen{color:var(--cw-accent)}.select.chosen select{background:var(--cw-accent-soft);border-color:var(--cw-accent-line);color:var(--cw-accent)}.select option{color:var(--cw-ink);background:var(--cw-surface)}.link-button{margin-left:auto;border:0;background:transparent;color:var(--cw-accent);font-size:13px;font-weight:600;padding:4px 2px;white-space:nowrap}.link-button:hover{text-decoration:underline}mark.hit{background:color-mix(in srgb,var(--cw-accent) 22%,transparent);color:inherit;border-radius:3px;padding:0 1px;-webkit-box-decoration-break:clone;box-decoration-break:clone}.note-match{display:flex;align-items:center;gap:5px;margin:4px 0 0 31px;font-size:12.5px;line-height:1.5;color:var(--cw-muted);min-width:0}.note-match svg{width:12px;height:12px}.note-match-label{font-weight:600;color:var(--cw-ink-soft);white-space:nowrap;flex-shrink:0;max-width:45%;overflow:hidden;text-overflow:ellipsis}.note-match-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.list{flex:1;overflow:auto;overscroll-behavior:contain;padding:8px 12px 16px;min-height:0}.list-title{display:flex;align-items:center;gap:8px;padding:12px 8px 6px;color:var(--cw-muted);font-size:12px;letter-spacing:1.15px;text-transform:uppercase;font-weight:600}.page-path{text-transform:none;letter-spacing:0;font-size:13px;color:var(--cw-ink-soft);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.page-name{text-transform:none;letter-spacing:0;font-size:13px;font-weight:600;color:var(--cw-ink-soft);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.here{flex-shrink:0;padding:0 6px;border-radius:5px;background:var(--cw-accent-soft);color:var(--cw-accent);font-size:11px;letter-spacing:.4px;line-height:18px}.go{margin-left:auto;display:inline-flex;align-items:center;gap:4px;color:var(--cw-accent);font-weight:600}.note.current{background:var(--cw-accent-soft);border-color:var(--cw-accent-line)}.note{display:block;text-align:left;width:100%;border:1px solid transparent;background:var(--cw-surface);color:var(--cw-ink);border-radius:10px;padding:9px 10px;margin-bottom:2px}.note .note-header .via{color:var(--cw-muted);font-size:10px;max-width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.note-header{margin-bottom:4px;gap:7px}.note .avatar{width:24px;height:24px;font-size:11px}.note .name{font-size:13px}.note .time{margin-left:0}.note-meta{margin-left:auto;display:inline-flex;align-items:center;gap:8px;font-size:12px;color:var(--cw-muted);white-space:nowrap;flex-shrink:0}.note-meta svg{width:12px;height:12px}.note:hover{background:var(--cw-canvas);border-color:var(--cw-line)}.note-header{display:flex;align-items:center;gap:8px;margin-bottom:9px}.avatar{display:inline-grid;place-items:center;width:29px;height:29px;border-radius:50%;background:var(--cw-avatar-bg);color:var(--cw-avatar-ink);font-size:12px;font-weight:700;flex-shrink:0;overflow:hidden}.avatar img{height:100%;width:100%;object-fit:cover}.name{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.time{font-size:12px;color:var(--cw-muted);margin-left:auto;white-space:nowrap}.note-text{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin:0 0 0 31px;line-height:1.5;font-size:14px;color:var(--cw-ink-soft);overflow-wrap:anywhere}.note-footer{display:flex;gap:6px;align-items:center;margin:6px 0 0 31px;color:var(--cw-muted);font-size:12px}.note-footer svg{width:12px;height:12px}.note-number{color:var(--cw-accent);font-weight:700}.status{display:inline-flex;align-items:center;gap:4px}.status.resolved{color:var(--cw-resolved-text)}.status-dot{width:5px;height:5px;background:var(--cw-accent);border-radius:50%;display:inline-block}.empty{height:100%;min-height:230px;display:flex;align-items:center;justify-content:center;text-align:center;padding:32px 16px}.empty-icon{position:relative;display:grid;place-items:center;margin:0 auto 18px;width:64px;height:64px;border:1px solid var(--cw-line);border-radius:21px;transform:rotate(-7deg);background:var(--cw-canvas);color:var(--cw-accent)}.empty-icon svg{width:27px;height:27px}.empty h3{font-size:17px;letter-spacing:-.25px;margin:0 0 8px}.empty p{font-size:14px;color:var(--cw-muted);line-height:1.7;margin:0 auto 18px;max-width:230px}.footer{display:flex;align-items:center;gap:9px;border-top:1px solid var(--cw-line);padding:14px 20px;background:var(--cw-canvas)}.footer .avatar{width:25px;height:25px;font-size:11px}.identity{border:0;background:transparent;text-align:left;padding:0;min-width:0;flex:1}.identity strong{display:block;font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.identity small{font-size:12px;color:var(--cw-muted)}.storage-label{font-size:11px;color:var(--cw-muted);text-align:right;line-height:1.5;max-width:105px}.storage-dot{display:inline-block;width:5px;height:5px;background:var(--cw-resolved);border-radius:50%;margin-right:4px}.error{padding:12px 16px;margin:12px 20px;border:1px solid var(--cw-danger-line);background:var(--cw-danger-soft);color:var(--cw-danger);border-radius:10px;font-size:14px;overflow-wrap:anywhere}.error button{display:block;margin-top:8px}.notice{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);z-index:1;background:var(--cw-inverse);color:var(--cw-on-inverse);border:1px solid #ffffff20;border-radius:12px;padding:12px 15px;display:flex;align-items:center;gap:12px;font-size:14px;box-shadow:0 6px 30px #0002;pointer-events:auto;max-width:calc(100vw - 32px)}.notice kbd{font-size:12px;color:var(--cw-inverse-muted);border:1px solid #ffffff30;border-radius:4px;padding:2px 5px}.notice button{color:var(--cw-on-inverse)}.notice.beside-panel{left:calc(50% - 200px)}.notice.beside-panel.left{left:calc(50% + 200px)}.pins{position:fixed;inset:0;pointer-events:none}.pin{position:absolute;width:32px;height:36px;transform:translate(-50%,-100%);border:2px solid #fff;border-radius:15px 15px 15px 4px;background:var(--cw-accent);color:#fff;box-shadow:var(--cw-shadow-sm);display:grid;place-items:center;font-size:13px;font-weight:650;pointer-events:auto;transition:scale .15s}.pin:hover,.pin[aria-pressed=true]{scale:1.12}.pin.resolved{background:var(--cw-resolved)}.pin.highlight{animation:pulse 1s ease-out 2}.pin.hovered{animation:pulse 1s ease-out infinite;scale:1.12;opacity:1!important}.focus-ring.hover-ring{animation:none;border-style:dashed}.focus-ring{position:absolute;border:2px solid var(--cw-accent);border-radius:var(--cw-radius);background:color-mix(in srgb,var(--cw-accent) 6%,transparent);pointer-events:none;animation:enter .16s ease-out}.pick-ring{border-style:dashed;animation:none;transition:left .08s,top .08s,width .08s,height .08s}.pin:focus-visible{outline-offset:3px}.pin.preview{pointer-events:none;opacity:.7}.pin.movable{cursor:grab;touch-action:none}.pins.focused .pin:not([aria-pressed=true]):not(.dragging){opacity:.3;transition:opacity .15s,scale .15s}.pins.focused .pin:hover,.pins.focused .pin:focus-visible{opacity:1}.pin.dragging{cursor:grabbing;scale:1.15;pointer-events:none;box-shadow:var(--cw-shadow);transition:none;z-index:1}.detail-hint{margin:16px 20px 12px;color:var(--cw-muted);font-size:14px;line-height:1.6}.compose{padding:0 20px 20px}.compose textarea,.reply-bar textarea{scrollbar-width:none}
|
|
726
|
+
.compose textarea::-webkit-scrollbar,.reply-bar textarea::-webkit-scrollbar{display:none}
|
|
727
|
+
.compose textarea{width:100%;display:block;resize:vertical;min-height:120px;max-height:40vh;border:1px solid var(--cw-line);border-radius:var(--cw-radius);padding:12px;color:var(--cw-ink);background:var(--cw-canvas);font-size:16px;line-height:1.6}.compose textarea::placeholder{color:var(--cw-subtle)}.compose-bar{display:flex;align-items:center;gap:10px;justify-content:space-between;margin-top:12px}.compose-hint{font-size:11px;color:var(--cw-muted)}.thread-body{overflow:auto;overscroll-behavior:contain;flex:1;min-height:0;padding:18px 20px 4px}.message{padding:16px 0;border-bottom:1px solid var(--cw-line)}.message:first-child{padding-top:0}.message:last-child{border:0}.message-text{font-size:16px;line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere;margin:10px 0 0 37px}.thread-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:12px 20px;border-top:1px solid var(--cw-line)}.resolve{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;font-weight:600}.resolve svg{width:16px;height:16px;color:var(--cw-resolved)}.resolve:hover{border-color:var(--cw-resolved);background:var(--cw-resolved-soft);color:var(--cw-resolved-text)}.resolve.done svg{color:var(--cw-muted)}.resolve.done:hover{border-color:var(--cw-line-strong);background:var(--cw-canvas);color:var(--cw-ink)}.confirm-delete{display:flex;align-items:center;gap:8px}.danger-button{display:inline-flex;align-items:center;gap:6px;border:0;border-radius:var(--cw-radius-pill);background:var(--cw-danger);color:#fff;padding:10px 12px;font-size:14px;font-weight:600;white-space:nowrap}.danger-button svg{width:15px;height:15px}.detail-head{padding:12px 20px 16px;border-bottom:1px solid var(--cw-line)}.detail-bar{display:flex;align-items:center;justify-content:space-between;margin:0 -8px 12px -10px}.back-link{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;border-radius:var(--cw-radius-sm);padding:6px 10px;font-size:14px;font-weight:600;color:var(--cw-muted)}.back-link:hover{background:var(--cw-surface-soft);color:var(--cw-ink)}.back-link svg{width:16px;height:16px}.detail-title{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.detail-title h2{font-size:20px;line-height:1.3;letter-spacing:-.4px;margin:0;font-weight:650}.detail-head .subtext{white-space:normal}.chip{display:inline-flex;align-items:center;gap:6px;padding:1px 9px;border-radius:999px;background:var(--cw-accent-soft);color:var(--cw-accent);font-size:12px;font-weight:600;line-height:20px}.chip svg{width:12px;height:12px}.chip.resolved{background:var(--cw-resolved-soft);color:var(--cw-resolved-text)}.thread .compose{padding-top:12px;border-top:1px solid var(--cw-line)}.thread textarea{min-height:88px}.anchor-status{margin:0 20px 12px;padding:12px;background:var(--cw-canvas);border:1px solid var(--cw-line);border-radius:10px;color:var(--cw-muted);font-size:13px;line-height:1.55}.anchor-head{display:flex;align-items:flex-start;gap:10px;margin-bottom:8px}.anchor-icon{display:grid;place-items:center;flex-shrink:0;width:28px;height:28px;border-radius:8px;background:var(--cw-warning-soft);color:var(--cw-warning)}.anchor-status.removed .anchor-icon{background:var(--cw-danger-soft);color:var(--cw-danger)}.anchor-icon svg{width:16px;height:16px}.anchor-titles{display:flex;flex-direction:column;min-width:0}.anchor-titles strong{color:var(--cw-ink);font-size:13px;font-weight:650;line-height:1.35}.anchor-target{color:var(--cw-ink-soft);overflow-wrap:anywhere}.identity-form{padding:0 20px 20px}.identity-form label{font-size:14px;font-weight:600;display:block;margin-bottom:7px}.identity-form input{width:100%;border:1px solid var(--cw-line);background:var(--cw-canvas);color:var(--cw-ink);padding:10px;border-radius:var(--cw-radius);margin-bottom:14px}.identity-form p{font-size:13px;color:var(--cw-muted);line-height:1.7;margin:0 0 14px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
728
|
+
@keyframes enter{from{opacity:0;translate:0 5px}to{opacity:1;translate:0 0}}@keyframes slide-in{from{opacity:0;translate:var(--cw-slide,24px) 0}to{opacity:1;translate:0 0}}@keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb,var(--cw-accent) 55%,transparent)}100%{box-shadow:0 0 0 16px transparent}}@keyframes spin{to{rotate:360deg}}@keyframes toast-out{to{opacity:0;translate:0 6px}}
|
|
729
|
+
.toasts{position:fixed;right:24px;bottom:88px;width:320px;max-width:calc(100vw - 32px);display:flex;flex-direction:column;gap:8px;pointer-events:none}.toasts.left{right:auto;left:24px}.toasts.top{bottom:auto;top:88px;flex-direction:column-reverse}.toasts.beside-panel{right:424px;bottom:24px;max-width:calc(100vw - 448px)}.toasts.beside-panel.left{right:auto;left:424px}.toasts.beside-panel.top{top:24px;bottom:auto}.toast{display:flex;gap:10px;align-items:flex-start;background:var(--cw-inverse);color:var(--cw-on-inverse);border:1px solid #ffffff20;border-radius:var(--cw-radius-md,12px);padding:12px 14px;box-shadow:var(--cw-shadow,var(--cw-shadow));animation:enter .18s ease-out}.toast.leaving{animation:toast-out .2s ease-in forwards}.toast-icon{background:var(--cw-accent-soft,var(--cw-accent-soft));color:var(--cw-accent)}.toast-icon svg{width:15px;height:15px}.toast-body{min-width:0;flex:1}.toast-title{margin:0;font-size:14px;line-height:1.4;color:var(--cw-inverse-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.toast-title strong{color:var(--cw-on-inverse);font-weight:600}.toast-number{margin-left:6px;padding:0 6px;border-radius:5px;background:var(--cw-accent-soft,var(--cw-accent-soft));color:var(--cw-accent);font-size:12px;font-weight:650;line-height:18px;display:inline-block}.toast-text{margin:3px 0 0;font-size:14px;line-height:1.5;color:#fff;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}
|
|
730
|
+
.popover{position:fixed;left:0;top:0;z-index:1;width:320px;max-height:min(560px,calc(100vh - 24px));display:flex;flex-direction:column;background:var(--cw-surface);border:1px solid var(--cw-line);border-radius:var(--cw-radius-lg);box-shadow:var(--cw-shadow);pointer-events:auto;color:var(--cw-ink)}.popover>.detail{min-height:0}.popover .detail.entering{animation:pop .18s cubic-bezier(.2,.9,.3,1.15)}@keyframes pop{from{opacity:0;transform:translateY(6px) scale(.97)}}.popover.sheet{left:8px;right:8px;top:auto;bottom:8px;width:auto;max-height:75vh}
|
|
731
|
+
.card-head .detail-bar{margin:0;padding:12px 12px 0 10px}.pop-head{position:relative;display:flex;align-items:center;gap:10px;padding:14px 10px 12px 14px}.panel .pop-head{padding:12px 14px 12px 20px}.pop-icon{display:grid;place-items:center;flex-shrink:0;width:30px;height:30px;border-radius:50%;background:var(--cw-accent-soft);color:var(--cw-accent)}.pop-icon svg{width:15px;height:15px}.pop-title{flex:1;min-width:0}.pop-title h2{margin:0;font-size:14px;font-weight:650;letter-spacing:-.2px;line-height:1.35}.pop-sub{margin:0;font-size:12px;line-height:1.5;color:var(--cw-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pop-head .icon-button{width:28px;height:28px;color:var(--cw-muted)}.pop-head .icon-button:hover{background:var(--cw-surface-soft);color:var(--cw-ink)}.pop-head .icon-button svg{width:16px;height:16px}.pop-head .more svg{stroke-width:3.2}
|
|
732
|
+
.pop-menu{position:absolute;top:46px;right:10px;z-index:2;min-width:190px;padding:4px;background:var(--cw-surface);border:1px solid var(--cw-line);border-radius:var(--cw-radius-md);box-shadow:var(--cw-shadow)}.menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 10px;border:0;border-radius:8px;background:transparent;font-size:13px;text-align:left;color:var(--cw-ink)}.menu-item:hover{background:var(--cw-surface-soft)}.menu-item svg{width:15px;height:15px;color:var(--cw-muted)}.menu-item.danger,.menu-item.danger svg{color:var(--cw-danger)}
|
|
733
|
+
.thread .thread-body{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;padding:0 14px}.panel .thread .thread-body{padding:0 20px}.thread .message{padding:12px 0;border-top:1px solid var(--cw-line);border-bottom:0}.thread .message:first-child{padding-top:12px}.thread .note-header{gap:8px;margin:0}.thread .avatar{width:26px;height:26px;font-size:10px}.thread .name{font-size:14px}.thread .time{font-size:12px}.thread .message-text{margin:4px 0 0 34px;font-size:14px;line-height:1.55;color:var(--cw-ink-soft)}.panel .thread .message-text{font-size:15px}
|
|
734
|
+
.thread-actions{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px 14px;border-top:1px solid var(--cw-line)}.panel .thread-actions{padding:12px 20px}.reply-bar{flex:1;min-width:0;display:flex;align-items:flex-end;gap:8px}.reply-bar textarea{flex:1;min-width:0;min-height:32px;max-height:120px;resize:none;padding:6px 10px;border:1px solid var(--cw-line-strong);border-radius:var(--cw-radius);background:var(--cw-surface);color:var(--cw-ink);font-size:13px;line-height:1.45;overflow:auto}.reply-bar textarea:focus-visible{outline:0;border-color:var(--cw-accent);box-shadow:0 0 0 3px var(--cw-accent-line)}.reply-bar textarea::placeholder{color:var(--cw-subtle)}.thread-actions .primary{padding:7px 12px;font-size:13px;border-radius:var(--cw-radius-pill);white-space:nowrap}.thread-actions .primary svg{width:14px;height:14px}.thread-actions .primary.resolve,.thread-actions .primary.resolve:hover{border:0;background:var(--cw-accent);color:#fff}.thread-actions .primary.resolve:hover{filter:brightness(.94)}.thread-actions .primary.resolve svg{color:currentColor}.thread-actions .status{display:inline-flex;align-items:center;gap:5px;font-size:13px;font-weight:600;color:var(--cw-resolved-text)}.thread-actions .status svg{width:14px;height:14px}.thread-actions .reopen{padding:6px 12px;font-size:13px;font-weight:500}
|
|
735
|
+
.popover .anchor-status,.popover .error{margin:0 14px 10px}.new-comment .compose{margin:0 14px;padding:14px 0;border-top:1px solid var(--cw-line)}.panel .new-comment .compose{margin:0 20px;padding:16px 0 20px}.popover .compose textarea:focus-visible{outline:0;border-color:var(--cw-accent);box-shadow:0 0 0 3px var(--cw-accent-line)}.popover .new-comment .compose textarea{min-height:76px;font-size:14px}.popover .compose-hint{font-size:11px}
|
|
736
|
+
.pin.below{transform:translate(-50%,0);border-radius:4px 15px 15px 15px}
|
|
737
|
+
.pick-ring{border-style:solid;background:transparent}.head-actions{display:flex;align-items:center;gap:2px;margin-right:-6px}
|
|
738
|
+
.head-actions>.icon-button{color:var(--cw-muted)}
|
|
739
|
+
.head-actions>.icon-button:hover{color:var(--cw-ink)}
|
|
740
|
+
.menu-wrap{position:relative;display:flex}
|
|
741
|
+
.user-button{display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:50%;background:transparent}
|
|
742
|
+
.user-button .avatar{width:28px;height:28px;font-size:11px;box-shadow:0 0 0 2px var(--cw-surface),0 0 0 3px var(--cw-line)}
|
|
743
|
+
.user-button:hover .avatar,.user-button[aria-expanded=true] .avatar{box-shadow:0 0 0 2px var(--cw-surface),0 0 0 3px var(--cw-accent)}
|
|
744
|
+
.quiet.sign-in{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;font-size:13px;font-weight:600;margin-right:2px}
|
|
745
|
+
.quiet.sign-in svg{width:14px;height:14px}
|
|
746
|
+
.user-menu{top:calc(100% + 8px);right:-40px;min-width:256px;padding:6px}
|
|
747
|
+
.menu-user{display:flex;align-items:center;gap:10px;padding:8px 8px 10px;margin-bottom:4px;border-bottom:1px solid var(--cw-line)}
|
|
748
|
+
.menu-user .avatar{width:34px;height:34px;font-size:13px}
|
|
749
|
+
.menu-user-text{min-width:0}
|
|
750
|
+
.menu-user-text strong{display:block;font-size:14px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
751
|
+
.menu-user-text small{display:block;font-size:12px;color:var(--cw-muted)}
|
|
752
|
+
.menu-note{display:flex;align-items:center;gap:2px;margin:4px 0 0;padding:8px 10px 4px;border-top:1px solid var(--cw-line);font-size:12px;color:var(--cw-muted)}
|
|
753
|
+
.footer{justify-content:space-between}
|
|
754
|
+
.footer-note{font-size:13px;color:var(--cw-muted)}
|
|
755
|
+
.settings-body{flex:1;min-height:0;overflow:auto;overscroll-behavior:contain;padding:4px 20px 12px}
|
|
756
|
+
.setting-group{padding:14px 0 6px}
|
|
757
|
+
.setting-group+.setting-group{border-top:1px solid var(--cw-line)}
|
|
758
|
+
.setting-group h3{margin:0 0 6px;font-size:12px;font-weight:650;letter-spacing:1.1px;text-transform:uppercase;color:var(--cw-muted)}
|
|
759
|
+
.setting{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:9px 0}
|
|
760
|
+
.setting-text{min-width:0}
|
|
761
|
+
.setting-text strong{display:block;font-size:14px;font-weight:600}
|
|
762
|
+
.setting-text small{display:block;font-size:12px;line-height:1.5;color:var(--cw-muted)}
|
|
763
|
+
.segmented{display:inline-flex;flex-shrink:0;gap:2px;padding:3px;background:var(--cw-surface-soft);border-radius:var(--cw-radius-pill)}
|
|
764
|
+
.segment{border:0;background:transparent;border-radius:var(--cw-radius-pill);padding:5px 10px;font-size:13px;font-weight:600;color:var(--cw-muted);white-space:nowrap}
|
|
765
|
+
.segment:hover{color:var(--cw-ink)}
|
|
766
|
+
.segment[aria-pressed=true]{background:var(--cw-surface);color:var(--cw-ink);box-shadow:0 1px 3px #2024371f}
|
|
767
|
+
.switch{position:relative;flex-shrink:0;width:38px;height:22px;padding:0;border:0;border-radius:999px;background:var(--cw-line-strong);transition:background .16s}
|
|
768
|
+
.switch::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #20243733;transition:translate .16s}
|
|
769
|
+
.switch[aria-checked=true]{background:var(--cw-accent)}
|
|
770
|
+
.switch[aria-checked=true]::after{translate:16px 0}
|
|
771
|
+
.switch:disabled{opacity:.45;cursor:not-allowed}
|
|
772
|
+
.swatches{display:flex;justify-content:space-between;gap:6px;width:100%}
|
|
773
|
+
.setting.wide{flex-direction:column;align-items:stretch;gap:10px}
|
|
774
|
+
.swatch{width:22px;height:22px;padding:0;border:0;border-radius:50%;background:var(--swatch);box-shadow:inset 0 0 0 1px var(--cw-line-strong)}
|
|
775
|
+
.swatch[aria-pressed=true]{box-shadow:0 0 0 2px var(--cw-surface),0 0 0 4px var(--swatch)}
|
|
776
|
+
.swatch:hover{scale:1.1}
|
|
777
|
+
.pin-sample{display:flex;align-items:flex-end;gap:14px;height:62px;margin:2px 0 6px;padding:0 16px 12px;border:1px dashed var(--cw-line-strong);border-radius:var(--cw-radius-md);background:var(--cw-canvas)}
|
|
778
|
+
.pin-sample .pin{position:static;transform:none;pointer-events:none}
|
|
779
|
+
.pin-sample.small .pin{width:26px;height:30px;border-radius:13px 13px 13px 4px;font-size:11px}
|
|
780
|
+
.pin-sample.resolved-gray .pin.resolved{background:var(--cw-subtle)}
|
|
781
|
+
.launcher.colored,.launcher.colored[aria-expanded=true]{background:var(--cw-launcher)}.launcher.colored .badge,.launcher-mini.colored .badge{background:var(--cw-inverse)}
|
|
782
|
+
.launcher-sample{display:flex;align-items:center;gap:14px;height:72px;margin:2px 0 6px;padding:0 16px;border:1px dashed var(--cw-line-strong);border-radius:var(--cw-radius-md);background:var(--cw-canvas)}
|
|
783
|
+
.launcher-mini{position:relative;display:grid;place-items:center;width:42px;height:42px;border:1px solid #ffffff24;border-radius:50%;background:var(--cw-inverse);color:var(--cw-on-inverse);box-shadow:0 6px 24px #12172233}.launcher-mini.colored{background:var(--cw-launcher)}
|
|
784
|
+
.launcher-mini>svg{width:19px;height:19px}
|
|
785
|
+
.pin-sample-text{margin-left:auto;font-size:12px;color:var(--cw-muted)}
|
|
786
|
+
.settings-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 20px;border-top:1px solid var(--cw-line);background:var(--cw-canvas);font-size:12px;color:var(--cw-muted)}
|
|
787
|
+
.settings-foot .quiet{padding:7px 11px;font-size:13px}
|
|
788
|
+
.pins.small .pin{width:26px;height:30px;border-radius:13px 13px 13px 4px;font-size:11px}
|
|
789
|
+
.pins.resolved-gray .pin.resolved{background:var(--cw-subtle)}
|
|
790
|
+
.pins.small .pin.below{border-radius:4px 13px 13px 13px}
|
|
791
|
+
.note{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:12px;align-items:start}
|
|
792
|
+
.note-main{min-width:0}
|
|
793
|
+
.note .time{margin-left:0}
|
|
794
|
+
.note-meta{flex-direction:column;align-items:flex-end;gap:2px;margin:2px 0 0;min-width:64px;line-height:18px}
|
|
795
|
+
.note-number,.count{font-variant-numeric:tabular-nums}
|
|
796
|
+
.count{min-width:20px;text-align:center}
|
|
797
|
+
.hidden-flag{display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:1px 7px 1px 6px;border-radius:6px;background:var(--cw-warning-soft);color:var(--cw-warning);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.hidden-flag.removed{background:var(--cw-danger-soft);color:var(--cw-danger)}.note-footer .hidden-flag svg{width:12px;height:12px;flex-shrink:0}
|
|
798
|
+
.pin.detached{border-style:dashed;opacity:.85;background:color-mix(in srgb,var(--cw-accent) 70%,var(--cw-subtle))}
|
|
799
|
+
.anchor-status p{margin:0}
|
|
800
|
+
.page-path.sub{font-size:11px;color:var(--cw-subtle);flex-shrink:1}
|
|
801
|
+
.anchor-status .quiet{display:inline-flex;align-items:center;gap:6px;margin-top:10px;padding:6px 11px 6px 9px;font-size:13px;font-weight:600}.anchor-status .quiet svg{width:15px;height:15px;flex-shrink:0}
|
|
802
|
+
@media(max-width:600px){.panel,.panel.left{inset:0;width:auto;border:0}.panel.placing{visibility:hidden}.panel.behind{visibility:hidden}.launcher{right:16px;bottom:16px}.notice,.notice.beside-panel,.notice.beside-panel.left{left:50%;bottom:24px;width:calc(100vw - 32px);font-size:13px}.compose-hint{display:none}.compose-bar{justify-content:flex-end}.toasts,.toasts.left,.toasts.beside-panel,.toasts.beside-panel.left{left:16px;right:16px;width:auto;max-width:none;bottom:82px}.toasts.top,.toasts.beside-panel.top{top:82px;bottom:auto}.toasts.beside-panel{display:none}.head{padding-top:18px}}
|
|
803
|
+
.popover.moving{visibility:hidden;pointer-events:none}
|
|
804
|
+
.update-bar{display:grid;grid-template-columns:30px 1fr auto;align-items:start;gap:4px 12px;padding:14px 12px 16px 20px;background:var(--cw-accent-soft);border-top-color:var(--cw-accent-line)}.update-icon{display:grid;place-items:center;width:30px;height:30px;border-radius:9px;background:var(--cw-surface);color:var(--cw-accent);box-shadow:var(--cw-shadow-sm)}.update-icon svg{width:16px;height:16px}.update-text{display:flex;flex-direction:column;gap:1px;min-width:0;color:var(--cw-muted);font-size:12.5px;line-height:1.45}.update-text strong{color:var(--cw-ink);font-size:13px;font-weight:650}.update-command{grid-column:1/-1;display:flex;align-items:center;gap:8px;margin:8px 8px 0 0;padding:4px 4px 4px 10px;border:1px solid var(--cw-line);border-radius:9px;background:var(--cw-surface)}.update-command code{flex:1;min-width:0;color:var(--cw-ink);font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.update-bar .quiet.update{padding:5px 12px;border-radius:var(--cw-radius-pill)}
|
|
805
|
+
@media(max-width:600px){.search input{font-size:16px}.select{max-width:100%}}
|
|
806
|
+
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
|
|
807
|
+
`;
|
|
808
|
+
const WINDOW = 700;
|
|
809
|
+
const MAX_AGE = 12e4;
|
|
810
|
+
const MAX_STEPS = 6;
|
|
811
|
+
function isVisible(el) {
|
|
812
|
+
if (!el || !el.isConnected || !el.getClientRects().length) return false;
|
|
813
|
+
const rect = el.getBoundingClientRect();
|
|
814
|
+
if (rect.width < 1 || rect.height < 1 || getComputedStyle(el).visibility !== "visible" || el.closest("[inert]")) return false;
|
|
815
|
+
const check = el.checkVisibility;
|
|
816
|
+
if (check && !check.call(el, { opacityProperty: true, visibilityProperty: true })) return false;
|
|
817
|
+
let fixed = false;
|
|
818
|
+
for (let parent = el; parent && parent !== document.documentElement; parent = parent.parentElement) {
|
|
819
|
+
const style = getComputedStyle(parent);
|
|
820
|
+
if (!check && parseFloat(style.opacity) < 0.05) return false;
|
|
821
|
+
if (style.position === "fixed") fixed = true;
|
|
822
|
+
if (parent === el || parent === document.body) continue;
|
|
823
|
+
const clips = /(hidden|clip)/.test(style.overflowX + style.overflowY);
|
|
824
|
+
const collapsed = /(auto|scroll)/.test(style.overflowX + style.overflowY) && (parent.clientWidth < 1 || parent.clientHeight < 1);
|
|
825
|
+
if (clips || collapsed) {
|
|
826
|
+
const box = parent.getBoundingClientRect();
|
|
827
|
+
if (Math.min(rect.right, box.right) - Math.max(rect.left, box.left) < 1 || Math.min(rect.bottom, box.bottom) - Math.max(rect.top, box.top) < 1) return false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return !(fixed && (rect.right <= 0 || rect.bottom <= 0 || rect.left >= innerWidth || rect.top >= innerHeight));
|
|
831
|
+
}
|
|
832
|
+
const clean$1 = (text, max = 120) => (text ?? "").replace(/\s+/g, " ").trim().slice(0, max);
|
|
833
|
+
const GENERATED = /:|(^|[-_])(r\d+|radix|headlessui|mui|v-\d|react-aria|\d{3,})/i;
|
|
834
|
+
const NESTED = "ul,ol,[role=menu],[role=listbox],[popover],dialog,nav";
|
|
835
|
+
function ownText(el) {
|
|
836
|
+
var _a;
|
|
837
|
+
let text = "";
|
|
838
|
+
for (const node of Array.from(el.childNodes)) {
|
|
839
|
+
if (node.nodeType === Node.TEXT_NODE) text += node.textContent;
|
|
840
|
+
else if (node instanceof Element && !node.matches(NESTED) && !node.querySelector(`${NESTED},a,button`)) text += ` ${node.textContent}`;
|
|
841
|
+
}
|
|
842
|
+
return clean$1(text) || clean$1((_a = el.firstElementChild) == null ? void 0 : _a.textContent);
|
|
843
|
+
}
|
|
844
|
+
function nameOf$1(el) {
|
|
845
|
+
var _a, _b;
|
|
846
|
+
const ids = el.getAttribute("aria-labelledby");
|
|
847
|
+
return clean$1(el.getAttribute("aria-label")) || clean$1(ids == null ? void 0 : ids.split(/\s+/).map((id) => {
|
|
848
|
+
var _a2;
|
|
849
|
+
return ((_a2 = document.getElementById(id)) == null ? void 0 : _a2.textContent) ?? "";
|
|
850
|
+
}).join(" ")) || clean$1(el.getAttribute("title")) || ownText(el) || clean$1((_a = el.querySelector("img[alt]")) == null ? void 0 : _a.getAttribute("alt")) || clean$1((_b = el.querySelector("svg title")) == null ? void 0 : _b.textContent);
|
|
851
|
+
}
|
|
852
|
+
const sameName = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
853
|
+
const ROLES = { BUTTON: "button", SUMMARY: "button", A: "link", LABEL: "label", INPUT: "input" };
|
|
854
|
+
function selectorOf(el) {
|
|
855
|
+
const parts = [];
|
|
856
|
+
for (let node = el; node && node !== document.body && node !== document.documentElement; node = node.parentElement) {
|
|
857
|
+
for (const attr of ["data-overplay-anchor", "data-testid", "data-test", "data-cy"]) {
|
|
858
|
+
const value = node.getAttribute(attr);
|
|
859
|
+
const selector = value && `[${attr}="${CSS.escape(value)}"]`;
|
|
860
|
+
if (selector && document.querySelectorAll(selector).length === 1) {
|
|
861
|
+
parts.unshift(selector);
|
|
862
|
+
return parts.join(" > ");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (node.id && !GENERATED.test(node.id) && document.querySelectorAll(`#${CSS.escape(node.id)}`).length === 1) {
|
|
866
|
+
parts.unshift(`#${CSS.escape(node.id)}`);
|
|
867
|
+
return parts.join(" > ");
|
|
868
|
+
}
|
|
869
|
+
const tag = node.tagName;
|
|
870
|
+
const siblings = node.parentElement ? Array.from(node.parentElement.children).filter((child) => child.tagName === tag) : [node];
|
|
871
|
+
parts.unshift(`${tag.toLowerCase()}:nth-of-type(${siblings.indexOf(node) + 1})`);
|
|
872
|
+
}
|
|
873
|
+
return parts.join(" > ");
|
|
874
|
+
}
|
|
875
|
+
function describe(el) {
|
|
876
|
+
const name = nameOf$1(el);
|
|
877
|
+
return { selector: selectorOf(el), role: el.getAttribute("role") || ROLES[el.tagName] || el.tagName.toLowerCase(), tag: el.tagName.toLowerCase(), ...name ? { name } : {} };
|
|
878
|
+
}
|
|
879
|
+
function findTarget(target) {
|
|
880
|
+
const pool = [];
|
|
881
|
+
const bySelector = () => {
|
|
882
|
+
try {
|
|
883
|
+
return document.querySelector(target.selector);
|
|
884
|
+
} catch {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
const first = bySelector();
|
|
889
|
+
if (first && (!target.name || sameName(nameOf$1(first), target.name))) pool.push(first);
|
|
890
|
+
if (target.name) {
|
|
891
|
+
const selector = [target.tag, target.role && /^[a-z]+$/.test(target.role) ? `[role="${target.role}"]` : ""].filter(Boolean).join(",");
|
|
892
|
+
try {
|
|
893
|
+
pool.push(...Array.from(document.querySelectorAll(selector || "*")).filter((el) => sameName(nameOf$1(el), target.name)));
|
|
894
|
+
} catch {
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (!pool.length && first) pool.push(first);
|
|
898
|
+
return pool.find(isVisible) ?? pool[0] ?? null;
|
|
899
|
+
}
|
|
900
|
+
const CONTROL$1 = 'button,summary,label,[role=button],[role=tab],[role=menuitem],[aria-haspopup],[aria-expanded],[aria-controls],[popovertarget],[data-bs-toggle],a[href^="#"],input[type=checkbox],input[type=radio]';
|
|
901
|
+
const unsafe = (el) => el.matches('a[href]:not([href^="#"]):not([href=""]),[type=submit],form button:not([type])') && !el.matches("[aria-haspopup],[aria-expanded]");
|
|
902
|
+
const ACTION = /\b(delete|remove|erase|destroy|pay|buy|purchase|checkout|order|send|submit|publish|confirm|sign ?out|log ?out|unsubscribe|cancel subscription|deactivate)\b/i;
|
|
903
|
+
const risky = (el) => unsafe(el) || !el.matches("[aria-haspopup],[aria-expanded],[aria-controls],summary,[popovertarget],[data-bs-toggle]") && ACTION.test(nameOf$1(el));
|
|
904
|
+
const KEYS = ["Enter", " ", "ArrowDown", "ArrowRight"];
|
|
905
|
+
const VISUAL = /^(display|visibility|opacity|transform|translate|max-height|height|clip|clip-path|left|top|right|bottom|inset|pointer-events)$/;
|
|
906
|
+
function pressable(el) {
|
|
907
|
+
const control = el.closest(CONTROL$1);
|
|
908
|
+
if (control) return control;
|
|
909
|
+
let best = el;
|
|
910
|
+
for (let parent = el; parent && parent !== document.body; parent = parent.parentElement) {
|
|
911
|
+
if (getComputedStyle(parent).cursor === "pointer") best = parent;
|
|
912
|
+
else if (best !== el) break;
|
|
913
|
+
}
|
|
914
|
+
return best;
|
|
915
|
+
}
|
|
916
|
+
class RevealRecorder {
|
|
917
|
+
/**
|
|
918
|
+
* @param ignore The widget's own elements.
|
|
919
|
+
* @param paused True while the widget takes page presses itself (placing a note, dragging a pin). Those presses open
|
|
920
|
+
* nothing on the page, and page changes right after them are not theirs.
|
|
921
|
+
*/
|
|
922
|
+
constructor(ignore, paused = () => false) {
|
|
923
|
+
__publicField(this, "actions", []);
|
|
924
|
+
__publicField(this, "open", /* @__PURE__ */ new Set());
|
|
925
|
+
__publicField(this, "lastHover", null);
|
|
926
|
+
__publicField(this, "observer", null);
|
|
927
|
+
__publicField(this, "onPress", (event) => {
|
|
928
|
+
if (!event.isTrusted || event instanceof KeyboardEvent && !KEYS.includes(event.key)) return;
|
|
929
|
+
const el = this.target(event);
|
|
930
|
+
if (!el) return;
|
|
931
|
+
if (this.paused()) {
|
|
932
|
+
this.open.clear();
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
const last = this.lastAction("press");
|
|
936
|
+
if (last && (last.el === el || last.el.control === el) && performance.now() - last.time < 500) {
|
|
937
|
+
this.keep(last);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
this.track("press", el);
|
|
941
|
+
});
|
|
942
|
+
__publicField(this, "onHover", (event) => {
|
|
943
|
+
if (!event.isTrusted) return;
|
|
944
|
+
const el = this.target(event);
|
|
945
|
+
if (!el || el === this.lastHover) return;
|
|
946
|
+
this.lastHover = el;
|
|
947
|
+
this.actions = this.actions.filter((action) => action.kind === "press" || this.open.has(action) || action.changed.size);
|
|
948
|
+
this.track("hover", el);
|
|
949
|
+
});
|
|
950
|
+
__publicField(this, "onState", (event) => {
|
|
951
|
+
if (event.target instanceof Element) this.note(event.target, false);
|
|
952
|
+
});
|
|
953
|
+
__publicField(this, "onMutations", (records) => {
|
|
954
|
+
if (!this.open.size) return;
|
|
955
|
+
for (const record2 of records) {
|
|
956
|
+
if (record2.type === "attributes" && record2.attributeName === "style" && (record2.target === document.body || record2.target === document.documentElement)) continue;
|
|
957
|
+
const nodes = record2.type === "childList" ? Array.from(record2.addedNodes) : [record2.target];
|
|
958
|
+
for (const node of nodes) if (node instanceof Element) this.note(node, record2.type === "childList");
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
this.ignore = ignore;
|
|
962
|
+
this.paused = paused;
|
|
963
|
+
}
|
|
964
|
+
start(signal) {
|
|
965
|
+
const options = { capture: true, signal };
|
|
966
|
+
for (const type of ["pointerdown", "mousedown", "click", "keydown"]) document.addEventListener(type, this.onPress, options);
|
|
967
|
+
document.addEventListener("mouseover", this.onHover, options);
|
|
968
|
+
document.addEventListener("toggle", this.onState, options);
|
|
969
|
+
document.addEventListener("change", this.onState, options);
|
|
970
|
+
window.addEventListener("hashchange", () => {
|
|
971
|
+
const id = decodeURIComponent(location.hash.slice(1));
|
|
972
|
+
const el = id && document.getElementById(id);
|
|
973
|
+
if (el) this.note(el, false);
|
|
974
|
+
}, { signal });
|
|
975
|
+
this.observer = new MutationObserver(this.onMutations);
|
|
976
|
+
this.observer.observe(document, {
|
|
977
|
+
subtree: true,
|
|
978
|
+
childList: true,
|
|
979
|
+
attributes: true,
|
|
980
|
+
attributeFilter: ["class", "style", "hidden", "open", "aria-expanded", "aria-hidden", "data-state", "data-open", "data-headlessui-state"]
|
|
981
|
+
});
|
|
982
|
+
signal.addEventListener("abort", () => {
|
|
983
|
+
var _a;
|
|
984
|
+
(_a = this.observer) == null ? void 0 : _a.disconnect();
|
|
985
|
+
this.actions = [];
|
|
986
|
+
this.open.clear();
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
track(kind, el) {
|
|
990
|
+
const action = { kind, el, time: performance.now(), changed: /* @__PURE__ */ new Set(), added: /* @__PURE__ */ new Set() };
|
|
991
|
+
this.actions.push(action);
|
|
992
|
+
this.keep(action);
|
|
993
|
+
if (this.actions.length > 40) this.actions.shift();
|
|
994
|
+
}
|
|
995
|
+
keep(action) {
|
|
996
|
+
this.open.add(action);
|
|
997
|
+
setTimeout(() => this.open.delete(action), WINDOW);
|
|
998
|
+
}
|
|
999
|
+
target(event) {
|
|
1000
|
+
const el = event.composedPath().find((node) => node instanceof Element);
|
|
1001
|
+
return el && !this.ignore(el) ? el : null;
|
|
1002
|
+
}
|
|
1003
|
+
/** A change belongs to the newest action still in its window, or a submenu hover is credited to the first click. */
|
|
1004
|
+
note(el, added) {
|
|
1005
|
+
if (this.ignore(el)) return;
|
|
1006
|
+
let action;
|
|
1007
|
+
for (const candidate of this.actions) if (this.open.has(candidate)) action = candidate;
|
|
1008
|
+
if (!action) return;
|
|
1009
|
+
action.changed.add(el);
|
|
1010
|
+
if (added) action.added.add(el);
|
|
1011
|
+
}
|
|
1012
|
+
lastAction(kind) {
|
|
1013
|
+
for (let i = this.actions.length - 1; i >= 0; i--) if (this.actions[i].kind === kind) return this.actions[i];
|
|
1014
|
+
return void 0;
|
|
1015
|
+
}
|
|
1016
|
+
/** The steps that show `target` again. Call it when the person picks the element. Empty when it is on the page itself. */
|
|
1017
|
+
capture(target) {
|
|
1018
|
+
const now = performance.now();
|
|
1019
|
+
const steps = [];
|
|
1020
|
+
const elements = [];
|
|
1021
|
+
const used = /* @__PURE__ */ new Set();
|
|
1022
|
+
let want = target;
|
|
1023
|
+
for (let i = this.actions.length - 1; i >= 0 && steps.length < MAX_STEPS; i--) {
|
|
1024
|
+
const action = this.actions[i];
|
|
1025
|
+
if (now - action.time > MAX_AGE || used.has(action) || !action.el.isConnected || want.contains(action.el)) continue;
|
|
1026
|
+
const el = action.kind === "press" ? pressable(action.el) : action.el;
|
|
1027
|
+
if (el.contains(want) && !el.matches("label") || risky(el) || !this.opened(action, el, want)) continue;
|
|
1028
|
+
if (action.kind === "hover" && !Array.from(action.added).some((node) => node.isConnected && node.contains(want)) && this.actions.some((press2) => press2.kind === "press" && press2.time <= action.time && action.time - press2.time < 800 && this.changedFor(press2, want))) continue;
|
|
1029
|
+
if (action.kind === "hover" && elements[0] === el) continue;
|
|
1030
|
+
steps.unshift({ action: action.kind, target: describe(el) });
|
|
1031
|
+
elements.unshift(el);
|
|
1032
|
+
used.add(action);
|
|
1033
|
+
want = el;
|
|
1034
|
+
if (this.onPage(el)) break;
|
|
1035
|
+
}
|
|
1036
|
+
const pseudo2 = pseudoSteps(target).filter((step) => !elements.includes(step.el)).map((step) => ({ action: step.action, target: describe(step.el) }));
|
|
1037
|
+
return [...pseudo2, ...steps].slice(0, MAX_STEPS);
|
|
1038
|
+
}
|
|
1039
|
+
changedFor(action, want) {
|
|
1040
|
+
for (const node of action.changed) {
|
|
1041
|
+
if (!node.isConnected) continue;
|
|
1042
|
+
if (node === want || node.contains(want)) return true;
|
|
1043
|
+
if (node instanceof HTMLInputElement && /^(checkbox|radio)$/.test(node.type)) {
|
|
1044
|
+
for (let sibling = node.nextElementSibling; sibling; sibling = sibling.nextElementSibling) if (sibling.contains(want)) return true;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
opened(action, el, want) {
|
|
1050
|
+
var _a, _b, _c;
|
|
1051
|
+
if (this.changedFor(action, want)) return true;
|
|
1052
|
+
if (action.kind !== "press") return false;
|
|
1053
|
+
const control = el instanceof HTMLLabelElement ? el.control : null;
|
|
1054
|
+
const popover = el.getAttribute("popovertarget");
|
|
1055
|
+
const hash = el.matches('a[href^="#"]') ? el.getAttribute("href") : "";
|
|
1056
|
+
return !!(popover && ((_a = document.getElementById(popover)) == null ? void 0 : _a.contains(want))) || !!(hash.length > 1 && location.hash === hash && ((_b = document.getElementById(hash.slice(1))) == null ? void 0 : _b.contains(want))) || !!(control instanceof HTMLInputElement && control.checked) || el.matches("summary") && !!((_c = el.parentElement) == null ? void 0 : _c.open) && el.parentElement.contains(want);
|
|
1057
|
+
}
|
|
1058
|
+
/** The trigger sits on the page itself, not inside a layer that another action opened. */
|
|
1059
|
+
onPage(el) {
|
|
1060
|
+
var _a;
|
|
1061
|
+
if ((_a = el.parentElement) == null ? void 0 : _a.closest("[role=menu],[role=dialog],[role=listbox],[popover],dialog")) return false;
|
|
1062
|
+
return !this.actions.some((action) => action.el !== el && !action.el.contains(el) && Array.from(action.changed).some((node) => node.isConnected && node !== document.body && node !== document.documentElement && node.contains(el)));
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
function pseudoSteps(target) {
|
|
1066
|
+
const rules = [];
|
|
1067
|
+
const walk = (list) => {
|
|
1068
|
+
for (const rule of Array.from(list)) {
|
|
1069
|
+
if (rule instanceof CSSStyleRule) {
|
|
1070
|
+
if (/:(hover|focus-within)\b/.test(rule.selectorText) && Array.from(rule.style).some((p) => VISUAL.test(p))) rules.push(rule);
|
|
1071
|
+
} else if ("cssRules" in rule) walk(rule.cssRules);
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
1075
|
+
try {
|
|
1076
|
+
walk(sheet.cssRules);
|
|
1077
|
+
} catch {
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (!rules.length) return [];
|
|
1081
|
+
const steps = [];
|
|
1082
|
+
for (const kind of ["hover", "focus-within"]) {
|
|
1083
|
+
for (const el of Array.from(document.querySelectorAll(`:${kind}`))) {
|
|
1084
|
+
if (el === document.documentElement || el === document.body || target.contains(el)) continue;
|
|
1085
|
+
const shows = rules.some((rule) => rule.selectorText.split(",").some((selector) => {
|
|
1086
|
+
const at = selector.indexOf(`:${kind}`);
|
|
1087
|
+
if (at < 0 || !/^\s*[\s>~+]/.test(selector.slice(at + kind.length + 1)) || !selector.slice(at + kind.length + 1).trim()) return false;
|
|
1088
|
+
try {
|
|
1089
|
+
return el.matches(selector.slice(0, at).trim() || "*") && Array.from(document.querySelectorAll(selector.split(`:${kind}`).join(""))).some((shown) => shown !== el && (el.contains(shown) || shown.parentElement === el.parentElement) && shown.contains(target));
|
|
1090
|
+
} catch {
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
}));
|
|
1094
|
+
if (shows) steps.push({ action: kind === "hover" ? "hover" : "focus", el });
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return steps.sort((a, b) => a.el.contains(b.el) ? -1 : b.el.contains(a.el) ? 1 : 0);
|
|
1098
|
+
}
|
|
1099
|
+
const frame = () => new Promise((done) => requestAnimationFrame(() => done()));
|
|
1100
|
+
async function waitFor(check, ms) {
|
|
1101
|
+
const end = performance.now() + ms;
|
|
1102
|
+
while (performance.now() < end) {
|
|
1103
|
+
if (check()) return true;
|
|
1104
|
+
await frame();
|
|
1105
|
+
}
|
|
1106
|
+
return check();
|
|
1107
|
+
}
|
|
1108
|
+
function pointer(el, type) {
|
|
1109
|
+
const rect = el.getBoundingClientRect();
|
|
1110
|
+
const init2 = {
|
|
1111
|
+
bubbles: true,
|
|
1112
|
+
cancelable: true,
|
|
1113
|
+
composed: true,
|
|
1114
|
+
view: window,
|
|
1115
|
+
button: 0,
|
|
1116
|
+
buttons: type.endsWith("down") ? 1 : 0,
|
|
1117
|
+
clientX: rect.left + rect.width / 2,
|
|
1118
|
+
clientY: rect.top + rect.height / 2
|
|
1119
|
+
};
|
|
1120
|
+
el.dispatchEvent(type.startsWith("pointer") ? new PointerEvent(type, { ...init2, pointerId: 1, pointerType: "mouse", isPrimary: true }) : new MouseEvent(type, init2));
|
|
1121
|
+
}
|
|
1122
|
+
function press(el) {
|
|
1123
|
+
var _a, _b;
|
|
1124
|
+
for (const type of ["pointerover", "pointerenter", "mouseover", "mouseenter", "pointermove", "mousemove", "pointerdown", "mousedown"]) pointer(el, type);
|
|
1125
|
+
(_a = el.focus) == null ? void 0 : _a.call(el, { preventScroll: true });
|
|
1126
|
+
for (const type of ["pointerup", "mouseup"]) pointer(el, type);
|
|
1127
|
+
(_b = el.click) == null ? void 0 : _b.call(el);
|
|
1128
|
+
}
|
|
1129
|
+
function key(el, name) {
|
|
1130
|
+
var _a;
|
|
1131
|
+
(_a = el.focus) == null ? void 0 : _a.call(el, { preventScroll: true });
|
|
1132
|
+
for (const type of ["keydown", "keyup"]) el.dispatchEvent(new KeyboardEvent(type, { key: name, code: name === " " ? "Space" : name, bubbles: true, cancelable: true, composed: true }));
|
|
1133
|
+
}
|
|
1134
|
+
const looksOpen = (el) => {
|
|
1135
|
+
var _a;
|
|
1136
|
+
return el.matches("[aria-expanded=true],[data-state=open],[role=tab][aria-selected=true]") || el.matches("summary") && !!((_a = el.parentElement) == null ? void 0 : _a.open) || el instanceof HTMLInputElement && el.checked;
|
|
1137
|
+
};
|
|
1138
|
+
class PseudoStates {
|
|
1139
|
+
constructor() {
|
|
1140
|
+
__publicField(this, "sheet", null);
|
|
1141
|
+
__publicField(this, "marked", []);
|
|
1142
|
+
}
|
|
1143
|
+
set(el, kind) {
|
|
1144
|
+
const attr = `data-overplay-${kind}`;
|
|
1145
|
+
const pattern = kind === "hover" ? /:hover\b/g : /:focus-within\b/g;
|
|
1146
|
+
if (!this.sheet) {
|
|
1147
|
+
try {
|
|
1148
|
+
this.sheet = new CSSStyleSheet();
|
|
1149
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, this.sheet];
|
|
1150
|
+
} catch {
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const copy = (list, wrap) => {
|
|
1155
|
+
for (const rule of Array.from(list)) {
|
|
1156
|
+
if (rule instanceof CSSStyleRule && rule.selectorText.match(pattern)) {
|
|
1157
|
+
try {
|
|
1158
|
+
this.sheet.insertRule(wrap(`${rule.selectorText.replace(pattern, `[${attr}]`)}{${rule.style.cssText}}`), this.sheet.cssRules.length);
|
|
1159
|
+
} catch {
|
|
1160
|
+
}
|
|
1161
|
+
} else if (rule instanceof CSSMediaRule) copy(rule.cssRules, (text) => wrap(`@media ${rule.media.mediaText}{${text}}`));
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
if (!this.marked.some(([, a]) => a === attr)) for (const sheet of Array.from(document.styleSheets)) {
|
|
1165
|
+
try {
|
|
1166
|
+
copy(sheet.cssRules, (text) => text);
|
|
1167
|
+
} catch {
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
el.setAttribute(attr, "");
|
|
1171
|
+
this.marked.push([el, attr]);
|
|
1172
|
+
}
|
|
1173
|
+
clear() {
|
|
1174
|
+
for (const [el, attr] of this.marked) el.removeAttribute(attr);
|
|
1175
|
+
this.marked = [];
|
|
1176
|
+
if (this.sheet) {
|
|
1177
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.filter((sheet) => sheet !== this.sheet);
|
|
1178
|
+
this.sheet = null;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
const pseudo = new PseudoStates();
|
|
1183
|
+
function clearReveal() {
|
|
1184
|
+
pseudo.clear();
|
|
1185
|
+
}
|
|
1186
|
+
async function replay(steps, target) {
|
|
1187
|
+
var _a;
|
|
1188
|
+
const goal = () => isVisible(target());
|
|
1189
|
+
if (goal()) return { shown: true };
|
|
1190
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1191
|
+
const step = steps[i];
|
|
1192
|
+
const el = findTarget(step.target);
|
|
1193
|
+
if (step.action === "press" && el && risky(el)) return { shown: false };
|
|
1194
|
+
if (!el || step.action !== "focus" && !isVisible(el)) return { shown: false, failed: step };
|
|
1195
|
+
const next = steps[i + 1] ? () => isVisible(findTarget(steps[i + 1].target)) : goal;
|
|
1196
|
+
if (step.action === "hover") {
|
|
1197
|
+
for (const type of ["pointerover", "pointerenter", "mouseover", "mouseenter", "pointermove", "mousemove"]) pointer(el, type);
|
|
1198
|
+
if (!await waitFor(next, 500)) {
|
|
1199
|
+
pseudo.set(el, "hover");
|
|
1200
|
+
if (!await waitFor(next, 300)) {
|
|
1201
|
+
press(el);
|
|
1202
|
+
if (!await waitFor(next, 500)) key(el, "ArrowRight");
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
} else if (step.action === "focus") {
|
|
1206
|
+
(_a = el.focus) == null ? void 0 : _a.call(el, { preventScroll: true });
|
|
1207
|
+
if (!await waitFor(next, 200)) pseudo.set(el, "focus");
|
|
1208
|
+
} else if (!looksOpen(el)) {
|
|
1209
|
+
const changed = () => next() || looksOpen(el);
|
|
1210
|
+
press(el);
|
|
1211
|
+
if (!await waitFor(changed, 900)) {
|
|
1212
|
+
let worked = false;
|
|
1213
|
+
for (const name of KEYS) {
|
|
1214
|
+
key(el, name);
|
|
1215
|
+
if (await waitFor(changed, 400)) {
|
|
1216
|
+
worked = true;
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (!worked) return { shown: false, failed: step };
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
await waitFor(next, 1200);
|
|
1224
|
+
}
|
|
1225
|
+
return { shown: await waitFor(goal, 1500) };
|
|
1226
|
+
}
|
|
1227
|
+
const CONTROL = "a[href],button,label,summary,select,textarea,input,[role=button],[role=link],[role=tab],[role=menuitem],[role=checkbox],[role=switch],[role=option]";
|
|
1228
|
+
const REPLACED = /* @__PURE__ */ new Set(["IMG", "svg", "VIDEO", "CANVAS", "IFRAME", "PICTURE", "INPUT", "SELECT", "TEXTAREA", "BUTTON"]);
|
|
1229
|
+
const area = (el) => {
|
|
1230
|
+
const r = el.getBoundingClientRect();
|
|
1231
|
+
return r.width * r.height;
|
|
1232
|
+
};
|
|
1233
|
+
function componentTarget(target, container) {
|
|
1234
|
+
var _a;
|
|
1235
|
+
const permitted = (el2) => el2 !== document.body && el2 !== document.documentElement && (!container || container.contains(el2)) && !el2.closest("[data-overplay]");
|
|
1236
|
+
if (target.closest("[data-overplay]")) return null;
|
|
1237
|
+
const keyed = target.closest("[data-overplay-anchor]");
|
|
1238
|
+
const key2 = keyed == null ? void 0 : keyed.getAttribute("data-overplay-anchor");
|
|
1239
|
+
const anchor = keyed && key2 && permitted(keyed) && resolveComponent(`[data-overplay-anchor="${CSS.escape(key2)}"]`, container) === keyed ? keyed : null;
|
|
1240
|
+
let el = target;
|
|
1241
|
+
if (el instanceof SVGElement) for (let svg = el.closest("svg"); svg; svg = ((_a = svg.parentElement) == null ? void 0 : _a.closest("svg")) ?? null) el = svg;
|
|
1242
|
+
const control = el.closest(CONTROL);
|
|
1243
|
+
if (control && permitted(control)) el = control;
|
|
1244
|
+
else {
|
|
1245
|
+
while (!REPLACED.has(el.tagName) && el.parentElement && permitted(el.parentElement) && /^(inline|contents)$/.test(getComputedStyle(el).display)) el = el.parentElement;
|
|
1246
|
+
}
|
|
1247
|
+
if (anchor && (el === anchor || !anchor.contains(el) || !permitted(el) || area(anchor) <= innerWidth * innerHeight / 4)) return anchor;
|
|
1248
|
+
if (permitted(el)) return el;
|
|
1249
|
+
if (container) return container;
|
|
1250
|
+
return document.body.getClientRects().length && document.body.getBoundingClientRect().height ? document.body : document.documentElement;
|
|
1251
|
+
}
|
|
1252
|
+
const GENERATED_ID = /^(radix-|headlessui-|react-aria\d*-|mui-\d|ember\d|_r_)|[:«»]|^[a-f0-9]{8}-[a-f0-9]{4}-/i;
|
|
1253
|
+
const generatedId = (id) => GENERATED_ID.test(id);
|
|
1254
|
+
function componentSelector(target, container) {
|
|
1255
|
+
const key2 = target.getAttribute("data-overplay-anchor");
|
|
1256
|
+
if (key2 && resolveComponent(`[data-overplay-anchor="${CSS.escape(key2)}"]`, container) === target) {
|
|
1257
|
+
return `[data-overplay-anchor="${CSS.escape(key2)}"]`;
|
|
1258
|
+
}
|
|
1259
|
+
if (target === document.body) return "body";
|
|
1260
|
+
if (target === document.documentElement) return "html";
|
|
1261
|
+
const parts = [];
|
|
1262
|
+
for (let el = target; el && el !== document.body; el = el.parentElement) {
|
|
1263
|
+
if (el.id && !generatedId(el.id) && document.querySelectorAll(`#${CSS.escape(el.id)}`).length === 1) {
|
|
1264
|
+
parts.unshift(`#${CSS.escape(el.id)}`);
|
|
1265
|
+
break;
|
|
1266
|
+
}
|
|
1267
|
+
const key22 = el !== target ? el.getAttribute("data-overplay-anchor") : null;
|
|
1268
|
+
if (key22 && document.querySelectorAll(`[data-overplay-anchor="${CSS.escape(key22)}"]`).length === 1) {
|
|
1269
|
+
parts.unshift(`[data-overplay-anchor="${CSS.escape(key22)}"]`);
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
const siblings = el.parentElement ? Array.from(el.parentElement.children).filter((sibling) => sibling.tagName === el.tagName) : [];
|
|
1273
|
+
parts.unshift(`${el.tagName.toLowerCase()}:nth-of-type(${siblings.indexOf(el) + 1})`);
|
|
1274
|
+
}
|
|
1275
|
+
return parts.join(" > ");
|
|
1276
|
+
}
|
|
1277
|
+
function resolveComponent(selector, container) {
|
|
1278
|
+
try {
|
|
1279
|
+
const matches = Array.from(document.querySelectorAll(selector)).filter((el) => !container || container.contains(el));
|
|
1280
|
+
if (matches.length === 1) return matches[0];
|
|
1281
|
+
const visible = matches.filter((el) => {
|
|
1282
|
+
const rect = el.getBoundingClientRect();
|
|
1283
|
+
return el.getClientRects().length && rect.width && rect.height && getComputedStyle(el).visibility === "visible";
|
|
1284
|
+
});
|
|
1285
|
+
return visible.length === 1 ? visible[0] : null;
|
|
1286
|
+
} catch {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function componentPosition(comment, container) {
|
|
1291
|
+
if (!comment.selector) return { x: comment.x - scrollX, y: comment.y - scrollY };
|
|
1292
|
+
const target = resolveComponent(comment.selector, container);
|
|
1293
|
+
if (!target || !target.getClientRects().length) return null;
|
|
1294
|
+
const rect = target.getBoundingClientRect();
|
|
1295
|
+
if (!rect.width || !rect.height || getComputedStyle(target).visibility !== "visible") return null;
|
|
1296
|
+
const anchor = comment.anchor ?? { x: 0.5, y: 0.5 };
|
|
1297
|
+
const point = { x: rect.left + rect.width * anchor.x, y: rect.top + rect.height * anchor.y };
|
|
1298
|
+
for (let parent = target.parentElement; parent && parent !== document.body && parent !== document.documentElement; parent = parent.parentElement) {
|
|
1299
|
+
const style = getComputedStyle(parent);
|
|
1300
|
+
const bounds = parent.getBoundingClientRect();
|
|
1301
|
+
const left = bounds.left + parent.clientLeft, top = bounds.top + parent.clientTop;
|
|
1302
|
+
if (/(auto|scroll|hidden|clip)/.test(style.overflowX) && (point.x < left || point.x > left + parent.clientWidth)) return null;
|
|
1303
|
+
if (/(auto|scroll|hidden|clip)/.test(style.overflowY) && (point.y < top || point.y > top + parent.clientHeight)) return null;
|
|
1304
|
+
}
|
|
1305
|
+
return point;
|
|
1306
|
+
}
|
|
1307
|
+
function revealElement(target) {
|
|
1308
|
+
const chain = [];
|
|
1309
|
+
for (let el = target instanceof HTMLElement ? target : target.parentElement; el && el !== document.body; el = el.parentElement) chain.unshift(el);
|
|
1310
|
+
let changed = false;
|
|
1311
|
+
for (const el of chain) {
|
|
1312
|
+
if (el instanceof HTMLDetailsElement && !el.open) {
|
|
1313
|
+
el.open = true;
|
|
1314
|
+
changed = true;
|
|
1315
|
+
}
|
|
1316
|
+
if (el.hasAttribute("popover") && !el.matches(":popover-open")) {
|
|
1317
|
+
try {
|
|
1318
|
+
el.showPopover();
|
|
1319
|
+
changed = true;
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
if (!el.id || !(el.hidden || getComputedStyle(el).display === "none")) continue;
|
|
1324
|
+
const id = CSS.escape(el.id);
|
|
1325
|
+
const toggle = document.querySelector(`[aria-controls~="${id}"][aria-expanded="false"]`) ?? (el.getAttribute("role") === "tabpanel" ? document.querySelector(`[role=tab][aria-controls~="${id}"][aria-selected="false"]`) : null);
|
|
1326
|
+
if (toggle) {
|
|
1327
|
+
toggle.click();
|
|
1328
|
+
changed = true;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return changed;
|
|
1332
|
+
}
|
|
1333
|
+
function elementState(comment, container) {
|
|
1334
|
+
if (!comment.selector) return "shown";
|
|
1335
|
+
let found = false;
|
|
1336
|
+
try {
|
|
1337
|
+
found = Array.from(document.querySelectorAll(comment.selector)).some((el) => !container || container.contains(el));
|
|
1338
|
+
} catch {
|
|
1339
|
+
}
|
|
1340
|
+
if (!found) return "removed";
|
|
1341
|
+
return isVisible(resolveComponent(comment.selector, container)) ? "shown" : "hidden";
|
|
1342
|
+
}
|
|
1343
|
+
function isDetached(comment, container) {
|
|
1344
|
+
return elementState(comment, container) !== "shown";
|
|
1345
|
+
}
|
|
1346
|
+
const KINDS = [
|
|
1347
|
+
["input[type=checkbox],[role=checkbox]", "Checkbox"],
|
|
1348
|
+
["input[type=radio],[role=radio]", "Option"],
|
|
1349
|
+
["[role=switch]", "Switch"],
|
|
1350
|
+
["button,input[type=button],input[type=submit],[role=button]", "Button"],
|
|
1351
|
+
["a[href],[role=link]", "Link"],
|
|
1352
|
+
["[role=tab]", "Tab"],
|
|
1353
|
+
["[role=menuitem]", "Menu item"],
|
|
1354
|
+
["[role=option],option", "Option"],
|
|
1355
|
+
["select", "Menu"],
|
|
1356
|
+
["input,textarea", "Field"],
|
|
1357
|
+
["img,picture,[role=img]", "Image"],
|
|
1358
|
+
["svg", "Icon"],
|
|
1359
|
+
["video", "Video"],
|
|
1360
|
+
["canvas", "Canvas"],
|
|
1361
|
+
["iframe", "Frame"],
|
|
1362
|
+
["h1,h2,h3,h4,h5,h6,[role=heading]", "Heading"],
|
|
1363
|
+
["label", "Label"],
|
|
1364
|
+
["li", "List item"],
|
|
1365
|
+
["tr", "Row"],
|
|
1366
|
+
["td,th", "Cell"],
|
|
1367
|
+
["table", "Table"],
|
|
1368
|
+
["nav", "Navigation"],
|
|
1369
|
+
["form", "Form"],
|
|
1370
|
+
["p", "Text"],
|
|
1371
|
+
["summary", "Button"]
|
|
1372
|
+
];
|
|
1373
|
+
const clean = (text, max = 48) => {
|
|
1374
|
+
const value = (text ?? "").replace(/\s+/g, " ").trim();
|
|
1375
|
+
return value.length > max ? `${value.slice(0, max - 1).trimEnd()}…` : value;
|
|
1376
|
+
};
|
|
1377
|
+
const byIds = (ids) => clean(ids == null ? void 0 : ids.split(/\s+/).map((id) => {
|
|
1378
|
+
var _a;
|
|
1379
|
+
return ((_a = document.getElementById(id)) == null ? void 0 : _a.textContent) ?? "";
|
|
1380
|
+
}).join(" "));
|
|
1381
|
+
function nameOf(el) {
|
|
1382
|
+
var _a, _b;
|
|
1383
|
+
const field = el;
|
|
1384
|
+
const label = el.id ? document.querySelector(`label[for="${CSS.escape(el.id)}"]`) : null;
|
|
1385
|
+
return clean(el.getAttribute("aria-label")) || byIds(el.getAttribute("aria-labelledby")) || clean(el.getAttribute("alt")) || clean(label == null ? void 0 : label.textContent) || clean((_a = el.closest("label")) == null ? void 0 : _a.textContent) || clean(field.placeholder) || clean(el.getAttribute("title")) || clean((_b = el.querySelector("title")) == null ? void 0 : _b.textContent) || clean(el.innerText ?? el.textContent);
|
|
1386
|
+
}
|
|
1387
|
+
const words = (key2) => clean(key2.replace(/[-_:./]+/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase()));
|
|
1388
|
+
function describeElement(el) {
|
|
1389
|
+
var _a, _b, _c;
|
|
1390
|
+
const key2 = el.getAttribute("data-overplay-anchor");
|
|
1391
|
+
const kind = key2 ? words(key2) : ((_a = KINDS.find(([selector]) => el.matches(selector))) == null ? void 0 : _a[1]) ?? (el === document.body || el === document.documentElement ? "Page" : "Element");
|
|
1392
|
+
const name = key2 || el === document.body || el === document.documentElement ? "" : nameOf(el);
|
|
1393
|
+
const info = { label: name && name !== kind ? `${kind} “${name}”` : kind };
|
|
1394
|
+
for (let parent = el.parentElement; parent && parent !== document.body; parent = parent.parentElement) {
|
|
1395
|
+
const id = parent.id ? CSS.escape(parent.id) : "";
|
|
1396
|
+
const toggle = id ? document.querySelector(`[aria-controls~="${id}"]`) : null;
|
|
1397
|
+
const place = toggle ? nameOf(toggle) : parent instanceof HTMLDetailsElement ? clean((_b = parent.querySelector("summary")) == null ? void 0 : _b.textContent) : parent instanceof HTMLFieldSetElement ? clean((_c = parent.querySelector("legend")) == null ? void 0 : _c.textContent) : parent.hasAttribute("data-overplay-anchor") ? words(parent.getAttribute("data-overplay-anchor")) : parent.matches("[role=dialog],[role=menu],[role=listbox],[role=tabpanel],dialog,nav,section,aside,form,[popover]") ? clean(parent.getAttribute("aria-label")) || byIds(parent.getAttribute("aria-labelledby")) : "";
|
|
1398
|
+
if (place && place !== name) {
|
|
1399
|
+
info.place = place;
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return info;
|
|
1404
|
+
}
|
|
1405
|
+
const DEFAULT_FILTERS = Object.freeze({ status: "open", query: "", mine: false, author: "" });
|
|
1406
|
+
const MAX_QUERY = 200, MAX_WORDS = 8;
|
|
1407
|
+
function foldChar(char) {
|
|
1408
|
+
return char.normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase();
|
|
1409
|
+
}
|
|
1410
|
+
function fold(text) {
|
|
1411
|
+
let folded = "";
|
|
1412
|
+
const map = [];
|
|
1413
|
+
for (let i = 0; i < text.length; ) {
|
|
1414
|
+
const code = text.codePointAt(i), char = String.fromCodePoint(code), part = foldChar(char);
|
|
1415
|
+
for (let j = 0; j < part.length; j++) map.push(i);
|
|
1416
|
+
folded += part;
|
|
1417
|
+
i += char.length;
|
|
1418
|
+
}
|
|
1419
|
+
map.push(text.length);
|
|
1420
|
+
return { folded, map };
|
|
1421
|
+
}
|
|
1422
|
+
function parseQuery(query) {
|
|
1423
|
+
const words2 = fold(query.slice(0, MAX_QUERY)).folded.split(/\s+/).filter(Boolean);
|
|
1424
|
+
return [...new Set(words2)].slice(0, MAX_WORDS);
|
|
1425
|
+
}
|
|
1426
|
+
const includesAll = (text, words2) => {
|
|
1427
|
+
const folded = fold(text).folded;
|
|
1428
|
+
return words2.filter((word) => folded.includes(word));
|
|
1429
|
+
};
|
|
1430
|
+
function highlightRanges(text, words2) {
|
|
1431
|
+
if (!words2.length || !text) return [];
|
|
1432
|
+
const { folded, map } = fold(text), ranges = [];
|
|
1433
|
+
for (const word of words2) {
|
|
1434
|
+
for (let at = folded.indexOf(word); at !== -1; at = folded.indexOf(word, at + word.length)) {
|
|
1435
|
+
const start = map[at], last = map[at + word.length - 1];
|
|
1436
|
+
const end = map.slice(at + word.length).find((index) => index > last) ?? text.length;
|
|
1437
|
+
ranges.push([start, end]);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
1441
|
+
const merged = [];
|
|
1442
|
+
for (const range of ranges) {
|
|
1443
|
+
const previous = merged[merged.length - 1];
|
|
1444
|
+
if (previous && range[0] <= previous[1]) previous[1] = Math.max(previous[1], range[1]);
|
|
1445
|
+
else merged.push([...range]);
|
|
1446
|
+
}
|
|
1447
|
+
return merged;
|
|
1448
|
+
}
|
|
1449
|
+
function excerpt(text, words2, size = 90) {
|
|
1450
|
+
var _a;
|
|
1451
|
+
const clean2 = text.replace(/\s+/g, " ").trim();
|
|
1452
|
+
if (clean2.length <= size) return clean2;
|
|
1453
|
+
const first = ((_a = highlightRanges(clean2, words2)[0]) == null ? void 0 : _a[0]) ?? 0;
|
|
1454
|
+
const start = Math.max(0, Math.min(first - Math.floor(size / 3), clean2.length - size));
|
|
1455
|
+
return `${start > 0 ? "…" : ""}${clean2.slice(start, start + size).trim()}${start + size < clean2.length ? "…" : ""}`;
|
|
1456
|
+
}
|
|
1457
|
+
function elementName(info) {
|
|
1458
|
+
return info ? info.place ? `${info.label} in ${info.place}` : info.label : "";
|
|
1459
|
+
}
|
|
1460
|
+
function matchNote(comment, words2, context = {}) {
|
|
1461
|
+
if (!words2.length) return {};
|
|
1462
|
+
const element2 = elementName(context.elementOf ? context.elementOf(comment) : comment.element);
|
|
1463
|
+
const own = [comment.text, comment.author, context.includePage ? `${comment.pageTitle ?? ""} ${comment.pageUrl}` : ""].join("\n");
|
|
1464
|
+
const found = new Set(includesAll(own, words2));
|
|
1465
|
+
const match = {};
|
|
1466
|
+
if (found.size < words2.length) {
|
|
1467
|
+
for (const reply2 of comment.replies) {
|
|
1468
|
+
const hits = includesAll(`${reply2.text}
|
|
1469
|
+
${reply2.author}`, words2);
|
|
1470
|
+
if (hits.some((word) => !found.has(word))) {
|
|
1471
|
+
match.reply ?? (match.reply = reply2);
|
|
1472
|
+
hits.forEach((word) => found.add(word));
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (found.size < words2.length && element2) {
|
|
1477
|
+
const hits = includesAll(element2, words2);
|
|
1478
|
+
if (hits.some((word) => !found.has(word))) {
|
|
1479
|
+
match.element = element2;
|
|
1480
|
+
hits.forEach((word) => found.add(word));
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return found.size === words2.length ? match : null;
|
|
1484
|
+
}
|
|
1485
|
+
function authorKey(note) {
|
|
1486
|
+
return note.authorId ? `id:${note.authorId}` : `name:${note.author.trim().toLowerCase()}`;
|
|
1487
|
+
}
|
|
1488
|
+
function isMine(note, userId) {
|
|
1489
|
+
return !!note.authorId && note.authorId === userId;
|
|
1490
|
+
}
|
|
1491
|
+
function statusMatches(comment, status) {
|
|
1492
|
+
return status === "all" || (status === "resolved" ? comment.resolved : !comment.resolved);
|
|
1493
|
+
}
|
|
1494
|
+
function filterNotes(comments, filters, context) {
|
|
1495
|
+
const words2 = parseQuery(filters.query);
|
|
1496
|
+
return comments.filter((c) => statusMatches(c, filters.status) && (!filters.mine || isMine(c, context.userId)) && (!filters.author || authorKey(c) === filters.author) && !!matchNote(c, words2, context));
|
|
1497
|
+
}
|
|
1498
|
+
function filterCounts(comments, filters, context) {
|
|
1499
|
+
const words2 = parseQuery(filters.query);
|
|
1500
|
+
const searched = comments.filter((c) => !!matchNote(c, words2, context));
|
|
1501
|
+
const byPerson = (c) => (!filters.mine || isMine(c, context.userId)) && (!filters.author || authorKey(c) === filters.author);
|
|
1502
|
+
const people = searched.filter(byPerson);
|
|
1503
|
+
const inStatus = searched.filter((c) => statusMatches(c, filters.status));
|
|
1504
|
+
const authors = /* @__PURE__ */ new Map();
|
|
1505
|
+
for (const c of comments) {
|
|
1506
|
+
const key2 = authorKey(c), entry = authors.get(key2) ?? { key: key2, name: c.author, count: 0 };
|
|
1507
|
+
entry.name = c.author;
|
|
1508
|
+
authors.set(key2, entry);
|
|
1509
|
+
}
|
|
1510
|
+
for (const c of inStatus) {
|
|
1511
|
+
const entry = authors.get(authorKey(c));
|
|
1512
|
+
if (entry) entry.count++;
|
|
1513
|
+
}
|
|
1514
|
+
return {
|
|
1515
|
+
open: people.filter((c) => !c.resolved).length,
|
|
1516
|
+
resolved: people.filter((c) => c.resolved).length,
|
|
1517
|
+
all: people.length,
|
|
1518
|
+
mine: inStatus.filter((c) => isMine(c, context.userId)).length,
|
|
1519
|
+
authors: [...authors.values()].sort((a, b) => a.name.localeCompare(b.name) || a.key.localeCompare(b.key))
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function filtersActive(filters) {
|
|
1523
|
+
return !!filters.query.trim() || filters.mine || !!filters.author;
|
|
1524
|
+
}
|
|
1525
|
+
function clearFilters(comments) {
|
|
1526
|
+
return { ...DEFAULT_FILTERS, status: comments.some((c) => !c.resolved) || !comments.length ? "open" : "all" };
|
|
1527
|
+
}
|
|
1528
|
+
function readFilters(value) {
|
|
1529
|
+
const v = value && typeof value === "object" ? value : {};
|
|
1530
|
+
return {
|
|
1531
|
+
status: v.status === "resolved" || v.status === "all" ? v.status : "open",
|
|
1532
|
+
query: typeof v.query === "string" ? v.query.slice(0, MAX_QUERY) : "",
|
|
1533
|
+
mine: v.mine === true,
|
|
1534
|
+
author: typeof v.author === "string" ? v.author.slice(0, 300) : ""
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
const THEMES = ["light", "dark", "system"];
|
|
1538
|
+
const LAUNCHER = 52, EDGE = 12, SIDEBAR = 400, PIN_HEIGHT = 36, PIN_HALF = 16;
|
|
1539
|
+
const UPDATE_DISMISSED = "overplay:update-dismissed";
|
|
1540
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(Math.max(min, max), value));
|
|
1541
|
+
function eventInit(event) {
|
|
1542
|
+
const e = event;
|
|
1543
|
+
return {
|
|
1544
|
+
view: window,
|
|
1545
|
+
clientX: e.clientX,
|
|
1546
|
+
clientY: e.clientY,
|
|
1547
|
+
screenX: e.screenX,
|
|
1548
|
+
screenY: e.screenY,
|
|
1549
|
+
button: e.button,
|
|
1550
|
+
buttons: e.buttons,
|
|
1551
|
+
ctrlKey: e.ctrlKey,
|
|
1552
|
+
shiftKey: e.shiftKey,
|
|
1553
|
+
altKey: e.altKey,
|
|
1554
|
+
metaKey: e.metaKey,
|
|
1555
|
+
pointerId: e.pointerId,
|
|
1556
|
+
pointerType: e.pointerType,
|
|
1557
|
+
isPrimary: e.isPrimary,
|
|
1558
|
+
width: e.width,
|
|
1559
|
+
height: e.height,
|
|
1560
|
+
pressure: e.pressure
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
function matchAll(selector) {
|
|
1564
|
+
try {
|
|
1565
|
+
return Array.from(document.querySelectorAll(selector));
|
|
1566
|
+
} catch {
|
|
1567
|
+
return [];
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
function readSpot(key2) {
|
|
1571
|
+
try {
|
|
1572
|
+
const value = JSON.parse(localStorage.getItem(key2) || "null");
|
|
1573
|
+
return value && Number.isFinite(value.x) && Number.isFinite(value.y) ? { x: clamp(value.x, 0, 1), y: clamp(value.y, 0, 1) } : null;
|
|
1574
|
+
} catch {
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function pageLabel(pageUrl) {
|
|
1579
|
+
try {
|
|
1580
|
+
const url = new URL(pageUrl, location.href);
|
|
1581
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
1582
|
+
} catch {
|
|
1583
|
+
return pageUrl;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
function pageTitle() {
|
|
1587
|
+
const clean2 = (text) => (text ?? "").replace(/\s+/g, " ").trim();
|
|
1588
|
+
const title = clean2(document.title);
|
|
1589
|
+
const heading = Array.from(document.querySelectorAll("main h1, [role=main] h1, h1")).find((h) => !h.closest("[data-overplay]") && h.getClientRects().length);
|
|
1590
|
+
const name = clean2(heading == null ? void 0 : heading.textContent).slice(0, 120);
|
|
1591
|
+
const generic = !title || title.toLowerCase() === location.hostname.toLowerCase() || !/[-–—|·:]/.test(title) && !!name && !title.includes(name);
|
|
1592
|
+
return (generic && name ? name : title || name).slice(0, 300) || void 0;
|
|
1593
|
+
}
|
|
1594
|
+
function marked(target, text, words2) {
|
|
1595
|
+
let at = 0;
|
|
1596
|
+
for (const [start, end] of highlightRanges(text, words2.filter((word) => word.length > 1))) {
|
|
1597
|
+
if (start > at) target.append(document.createTextNode(text.slice(at, start)));
|
|
1598
|
+
target.append(element("mark", "hit", text.slice(start, end)));
|
|
1599
|
+
at = end;
|
|
1600
|
+
}
|
|
1601
|
+
target.append(document.createTextNode(text.slice(at)));
|
|
1602
|
+
return target;
|
|
1603
|
+
}
|
|
1604
|
+
class Overlay {
|
|
1605
|
+
constructor(config = {}) {
|
|
1606
|
+
__publicField(this, "ready");
|
|
1607
|
+
__publicField(this, "config");
|
|
1608
|
+
__publicField(this, "storage");
|
|
1609
|
+
__publicField(this, "host", element("div"));
|
|
1610
|
+
__publicField(this, "shadow");
|
|
1611
|
+
__publicField(this, "surface", element("div"));
|
|
1612
|
+
__publicField(this, "pins", element("div", "pins"));
|
|
1613
|
+
/** While placing: the component under the pointer, outlined before the click. */
|
|
1614
|
+
__publicField(this, "pickTarget", null);
|
|
1615
|
+
__publicField(this, "live", element("div", "sr-only"));
|
|
1616
|
+
__publicField(this, "toasts", element("div", "toasts"));
|
|
1617
|
+
__publicField(this, "toastTimers", /* @__PURE__ */ new Set());
|
|
1618
|
+
__publicField(this, "seen", /* @__PURE__ */ new Set());
|
|
1619
|
+
__publicField(this, "seenKey", "");
|
|
1620
|
+
__publicField(this, "detailKey", "");
|
|
1621
|
+
__publicField(this, "events", new AbortController());
|
|
1622
|
+
__publicField(this, "resizeObserver");
|
|
1623
|
+
__publicField(this, "mutationObserver");
|
|
1624
|
+
__publicField(this, "anchorObserver");
|
|
1625
|
+
__publicField(this, "observedAnchors", /* @__PURE__ */ new Set());
|
|
1626
|
+
__publicField(this, "unsubscribe");
|
|
1627
|
+
__publicField(this, "routeTimer");
|
|
1628
|
+
__publicField(this, "frame", 0);
|
|
1629
|
+
__publicField(this, "motionFrame", 0);
|
|
1630
|
+
__publicField(this, "destroyed", false);
|
|
1631
|
+
__publicField(this, "visible", false);
|
|
1632
|
+
__publicField(this, "placing", false);
|
|
1633
|
+
/** A pin being dragged, in viewport coordinates. It stays set until the move is saved, so the pin does not jump back. */
|
|
1634
|
+
__publicField(this, "pinDrag", null);
|
|
1635
|
+
__publicField(this, "suppressPinClick", false);
|
|
1636
|
+
__publicField(this, "loading", true);
|
|
1637
|
+
/** The current page and account have loaded once. Later loads keep the list on screen. */
|
|
1638
|
+
__publicField(this, "loaded", false);
|
|
1639
|
+
__publicField(this, "loadFailed", false);
|
|
1640
|
+
__publicField(this, "saving", false);
|
|
1641
|
+
__publicField(this, "error", "");
|
|
1642
|
+
__publicField(this, "comments", []);
|
|
1643
|
+
__publicField(this, "activeId", null);
|
|
1644
|
+
__publicField(this, "draft", null);
|
|
1645
|
+
__publicField(this, "text", "");
|
|
1646
|
+
__publicField(this, "replyText", "");
|
|
1647
|
+
__publicField(this, "replyId", newId());
|
|
1648
|
+
/** Search, status, Mine and author for the list and the pins. Kept for this browser tab (session storage). */
|
|
1649
|
+
__publicField(this, "filters", { ...DEFAULT_FILTERS });
|
|
1650
|
+
__publicField(this, "scope", "page");
|
|
1651
|
+
__publicField(this, "filterKey");
|
|
1652
|
+
__publicField(this, "searchTimer");
|
|
1653
|
+
__publicField(this, "launcherKey");
|
|
1654
|
+
__publicField(this, "openKey");
|
|
1655
|
+
__publicField(this, "launcherSpot");
|
|
1656
|
+
__publicField(this, "dragging", false);
|
|
1657
|
+
__publicField(this, "suppressClick", false);
|
|
1658
|
+
__publicField(this, "highlightId", null);
|
|
1659
|
+
__publicField(this, "hoverId", null);
|
|
1660
|
+
__publicField(this, "highlightTimer");
|
|
1661
|
+
__publicField(this, "sidebarOpen", false);
|
|
1662
|
+
/** Show the open comment in the sidebar instead of next to its pin. The person's choice is saved. */
|
|
1663
|
+
__publicField(this, "docked", false);
|
|
1664
|
+
__publicField(this, "dockKey");
|
|
1665
|
+
__publicField(this, "project");
|
|
1666
|
+
/** The person's display choices for this project, read again when the account changes. */
|
|
1667
|
+
__publicField(this, "settingsCache", null);
|
|
1668
|
+
/** A newer npm version, found when the sidebar first opens. See checkForUpdate(). */
|
|
1669
|
+
__publicField(this, "update", null);
|
|
1670
|
+
__publicField(this, "updateChecked", false);
|
|
1671
|
+
/** The page margin we replaced to make room for the sidebar, restored when it closes. */
|
|
1672
|
+
__publicField(this, "pushed", null);
|
|
1673
|
+
/** Records how menus were opened, so a note in a menu can open it again. See src/core/reveal.ts. */
|
|
1674
|
+
__publicField(this, "recorder", new RevealRecorder((el) => el === this.host || el.getRootNode() === this.shadow, () => this.placing || !!this.pinDrag));
|
|
1675
|
+
/** The note whose menu Show Element opened, and the result. The menu stays open while that note is open. */
|
|
1676
|
+
__publicField(this, "revealed", null);
|
|
1677
|
+
/** Show Element is pressing the page's menu buttons. A label's click on its checkbox counts as trusted, so we also check this. */
|
|
1678
|
+
__publicField(this, "replaying", false);
|
|
1679
|
+
/** A view that fills the sidebar instead of the list: the guest name form or the settings. */
|
|
1680
|
+
__publicField(this, "editing", null);
|
|
1681
|
+
__publicField(this, "user");
|
|
1682
|
+
__publicField(this, "managedUser");
|
|
1683
|
+
__publicField(this, "identityKey");
|
|
1684
|
+
__publicField(this, "persistentGuest");
|
|
1685
|
+
__publicField(this, "pageId");
|
|
1686
|
+
__publicField(this, "loadVersion", 0);
|
|
1687
|
+
__publicField(this, "queuedSnapshot", null);
|
|
1688
|
+
__publicField(this, "returnFocus", null);
|
|
1689
|
+
__publicField(this, "shortcut");
|
|
1690
|
+
__publicField(this, "composing", false);
|
|
1691
|
+
__publicField(this, "renderDeferred", false);
|
|
1692
|
+
__publicField(this, "applyQueuedSnapshot", () => {
|
|
1693
|
+
if (this.destroyed || !this.queuedSnapshot || this.saving || this.loading || this.typing()) return;
|
|
1694
|
+
const next = this.queuedSnapshot;
|
|
1695
|
+
this.queuedSnapshot = null;
|
|
1696
|
+
this.apply(next);
|
|
1697
|
+
});
|
|
1698
|
+
__publicField(this, "receiveSnapshot", (comments) => {
|
|
1699
|
+
if (this.destroyed) return;
|
|
1700
|
+
try {
|
|
1701
|
+
const normalized = normalizeComments(comments);
|
|
1702
|
+
if (this.saving || this.loading || this.typing()) this.queuedSnapshot = normalized;
|
|
1703
|
+
else this.apply(normalized);
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
this.report(error);
|
|
1706
|
+
this.render();
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
__publicField(this, "shieldPress", (event) => {
|
|
1710
|
+
if (!event.isTrusted || !event.composedPath().includes(this.host)) return;
|
|
1711
|
+
event.stopImmediatePropagation();
|
|
1712
|
+
const inner = event.composedPath()[0];
|
|
1713
|
+
if (event.type === "touchstart" || !(inner instanceof Element) || inner.getRootNode() !== this.shadow) return;
|
|
1714
|
+
if (!event.composedPath().some((el) => el instanceof Element && el.matches("[role=menu],[aria-haspopup=menu]"))) this.closeMenus();
|
|
1715
|
+
const Copy = event.constructor;
|
|
1716
|
+
inner.dispatchEvent(new Copy(event.type, Object.assign({}, eventInit(event), { bubbles: true, cancelable: true, composed: false })));
|
|
1717
|
+
});
|
|
1718
|
+
__publicField(this, "onRouteChange", () => {
|
|
1719
|
+
if (this.destroyed) return;
|
|
1720
|
+
const next = this.currentPage();
|
|
1721
|
+
if (next !== this.pageId) {
|
|
1722
|
+
this.pageId = next;
|
|
1723
|
+
this.activeId = null;
|
|
1724
|
+
this.draft = null;
|
|
1725
|
+
this.placing = false;
|
|
1726
|
+
this.text = "";
|
|
1727
|
+
this.replyText = "";
|
|
1728
|
+
this.noticeActivity();
|
|
1729
|
+
this.render();
|
|
1730
|
+
if (this.storage instanceof HostedAdapter) void this.refresh();
|
|
1731
|
+
else this.openPending();
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
__publicField(this, "onKeyDown", (event) => {
|
|
1735
|
+
if (event.defaultPrevented || event.isComposing) return;
|
|
1736
|
+
const opener = event.key === "Escape" ? this.closeMenus() : null;
|
|
1737
|
+
if (opener) {
|
|
1738
|
+
event.preventDefault();
|
|
1739
|
+
opener.focus();
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
if (event.key === "Escape" && this.visible) {
|
|
1743
|
+
event.preventDefault();
|
|
1744
|
+
if (this.draft || this.placing || this.activeId || this.editing) this.closePopup();
|
|
1745
|
+
else this.hide();
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const target = event.composedPath()[0];
|
|
1749
|
+
if (this.placing && event.key === "Enter" && !event.composedPath().includes(this.host)) {
|
|
1750
|
+
const el = target instanceof Element && target !== document.body ? target : this.config.container ?? document.documentElement;
|
|
1751
|
+
if (this.config.container && !this.config.container.contains(el) && el !== this.config.container) return;
|
|
1752
|
+
const rect = el.getBoundingClientRect();
|
|
1753
|
+
const x = Math.max(16, Math.min(innerWidth - 16, rect.left + rect.width / 2));
|
|
1754
|
+
const y = Math.max(48, Math.min(innerHeight - 48, rect.top + Math.min(rect.height / 2, innerHeight / 2)));
|
|
1755
|
+
event.preventDefault();
|
|
1756
|
+
this.choosePoint({ x: x + scrollX, y: y + scrollY }, el);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (target instanceof Element && (target.closest("input,textarea,select,[contenteditable]:not([contenteditable=false])") || target.closest("[role=textbox]"))) return;
|
|
1760
|
+
if (!event.repeat && this.settings.shortcut && this.shortcut && matchesShortcut(event, this.shortcut)) {
|
|
1761
|
+
event.preventDefault();
|
|
1762
|
+
if (this.visible) this.hide();
|
|
1763
|
+
else this.show();
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
__publicField(this, "onPageClick", (event) => {
|
|
1767
|
+
if (event.composedPath().includes(this.host)) return;
|
|
1768
|
+
if (!this.placing) {
|
|
1769
|
+
if (!event.isTrusted || this.replaying) return;
|
|
1770
|
+
const open = this.activeId ? !this.replyText.trim() : this.draft ? !this.text.trim() : false;
|
|
1771
|
+
if (this.visible && open && !this.docked && !this.saving) this.closePopup(false);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
1775
|
+
const target = event.target;
|
|
1776
|
+
if (!(target instanceof Element) || this.config.container && !this.config.container.contains(target)) return;
|
|
1777
|
+
event.preventDefault();
|
|
1778
|
+
event.stopPropagation();
|
|
1779
|
+
this.choosePoint({ x: event.clientX + scrollX, y: event.clientY + scrollY }, target);
|
|
1780
|
+
});
|
|
1781
|
+
__publicField(this, "onPointerMove", (event) => {
|
|
1782
|
+
const preview = this.pins.querySelector(".preview");
|
|
1783
|
+
if (!preview) return;
|
|
1784
|
+
const target = event.target;
|
|
1785
|
+
const inside = !this.config.container || target instanceof Node && this.config.container.contains(target);
|
|
1786
|
+
preview.hidden = event.composedPath().includes(this.host) || !inside;
|
|
1787
|
+
preview.style.left = `${event.clientX}px`;
|
|
1788
|
+
preview.style.top = `${event.clientY}px`;
|
|
1789
|
+
const component = !preview.hidden && target instanceof Element ? componentTarget(target, this.config.container) : null;
|
|
1790
|
+
this.pickTarget = component === document.body || component === document.documentElement ? null : component;
|
|
1791
|
+
this.placePickRing();
|
|
1792
|
+
});
|
|
1793
|
+
__publicField(this, "trackLayoutMotion", () => {
|
|
1794
|
+
if (this.motionFrame || !this.visible || this.destroyed) return;
|
|
1795
|
+
const tick = () => {
|
|
1796
|
+
this.motionFrame = 0;
|
|
1797
|
+
if (!this.visible || this.destroyed) return;
|
|
1798
|
+
this.positionPins();
|
|
1799
|
+
if (document.getAnimations().some((animation) => animation.playState === "running")) {
|
|
1800
|
+
this.motionFrame = requestAnimationFrame(tick);
|
|
1801
|
+
}
|
|
1802
|
+
};
|
|
1803
|
+
this.motionFrame = requestAnimationFrame(tick);
|
|
1804
|
+
});
|
|
1805
|
+
__publicField(this, "schedulePosition", () => {
|
|
1806
|
+
if (this.frame || this.destroyed || !this.visible) return;
|
|
1807
|
+
this.frame = requestAnimationFrame(() => {
|
|
1808
|
+
this.frame = 0;
|
|
1809
|
+
this.positionPins();
|
|
1810
|
+
});
|
|
1811
|
+
});
|
|
1812
|
+
/**
|
|
1813
|
+
* The open sidebar pushes the page aside instead of covering it, so every part of the page can still get a comment.
|
|
1814
|
+
* It adds a margin to <html> on the sidebar's side and restores the page's own value when it closes.
|
|
1815
|
+
* Phones keep the full-screen sidebar. Set pushContent: false to keep the sidebar above the page.
|
|
1816
|
+
*/
|
|
1817
|
+
__publicField(this, "pushPage", () => {
|
|
1818
|
+
var _a;
|
|
1819
|
+
const root = document.documentElement;
|
|
1820
|
+
const want = this.sidebarOpen && this.settings.pushContent && innerWidth > 600;
|
|
1821
|
+
const property = ((_a = this.config.position) == null ? void 0 : _a.endsWith("left")) ? "margin-left" : "margin-right";
|
|
1822
|
+
if (want && !this.pushed) {
|
|
1823
|
+
this.pushed = { property, value: root.style.getPropertyValue(property), priority: root.style.getPropertyPriority(property) };
|
|
1824
|
+
root.style.setProperty(property, `${SIDEBAR}px`, "important");
|
|
1825
|
+
} else if (!want && this.pushed) {
|
|
1826
|
+
const { property: property2, value, priority } = this.pushed;
|
|
1827
|
+
this.pushed = null;
|
|
1828
|
+
if (value) root.style.setProperty(property2, value, priority);
|
|
1829
|
+
else root.style.removeProperty(property2);
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
/** Keeps a dragged launcher inside the viewport when the window size changes. */
|
|
1833
|
+
__publicField(this, "placeLauncher", () => {
|
|
1834
|
+
const launcher = this.shadow.querySelector(".launcher");
|
|
1835
|
+
if (!launcher || !this.launcherSpot) return;
|
|
1836
|
+
const left = clamp(this.launcherSpot.x * innerWidth - LAUNCHER / 2, EDGE, innerWidth - LAUNCHER - EDGE);
|
|
1837
|
+
const top = clamp(this.launcherSpot.y * innerHeight - LAUNCHER / 2, EDGE, innerHeight - LAUNCHER - EDGE);
|
|
1838
|
+
Object.assign(launcher.style, { left: `${left}px`, top: `${top}px`, right: "auto", bottom: "auto" });
|
|
1839
|
+
});
|
|
1840
|
+
__publicField(this, "submitComment", () => {
|
|
1841
|
+
if (!this.draft || !this.text.trim()) return;
|
|
1842
|
+
const draft = this.draft;
|
|
1843
|
+
const comment = {
|
|
1844
|
+
id: draft.id,
|
|
1845
|
+
x: draft.position.x,
|
|
1846
|
+
y: draft.position.y,
|
|
1847
|
+
text: this.text.trim(),
|
|
1848
|
+
author: this.user.name,
|
|
1849
|
+
authorId: this.user.id,
|
|
1850
|
+
avatarUrl: this.user.avatarUrl,
|
|
1851
|
+
timestamp: draft.timestamp,
|
|
1852
|
+
replies: [],
|
|
1853
|
+
resolved: false,
|
|
1854
|
+
pageUrl: draft.pageUrl,
|
|
1855
|
+
pageTitle: pageTitle(),
|
|
1856
|
+
selector: draft.selector,
|
|
1857
|
+
anchor: draft.anchor,
|
|
1858
|
+
element: draft.element
|
|
1859
|
+
};
|
|
1860
|
+
const next = [...this.getAllComments(), comment];
|
|
1861
|
+
void this.mutate(next, () => this.storage.upsertComment ? this.storage.upsertComment(comment) : this.storage.saveComments(next), () => {
|
|
1862
|
+
this.draft = null;
|
|
1863
|
+
this.text = "";
|
|
1864
|
+
this.activeId = comment.id;
|
|
1865
|
+
});
|
|
1866
|
+
});
|
|
1867
|
+
__publicField(this, "submitReply", () => {
|
|
1868
|
+
const comment = this.comments.find((c) => c.id === this.activeId);
|
|
1869
|
+
if (!comment || !this.replyText.trim()) return;
|
|
1870
|
+
const reply2 = {
|
|
1871
|
+
id: this.replyId,
|
|
1872
|
+
text: this.replyText.trim(),
|
|
1873
|
+
author: this.user.name,
|
|
1874
|
+
authorId: this.user.id,
|
|
1875
|
+
avatarUrl: this.user.avatarUrl,
|
|
1876
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1877
|
+
commentId: comment.id
|
|
1878
|
+
};
|
|
1879
|
+
const updated = { ...comment, replies: [...comment.replies, reply2] };
|
|
1880
|
+
const next = this.getAllComments().map((c) => c.id === comment.id ? updated : c);
|
|
1881
|
+
void this.mutate(next, () => this.storage.addReply ? this.storage.addReply(comment.id, reply2) : this.storage.upsertComment ? this.storage.upsertComment(updated) : this.storage.saveComments(next), () => {
|
|
1882
|
+
this.replyText = "";
|
|
1883
|
+
this.replyId = newId();
|
|
1884
|
+
});
|
|
1885
|
+
});
|
|
1886
|
+
var _a, _b, _c;
|
|
1887
|
+
this.config = { primaryColor: "#0068f9", position: "bottom-right", keyboardShortcut: "alt+shift+c", enableKeyboardShortcuts: true, ...config };
|
|
1888
|
+
if (!/^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(this.config.primaryColor ?? "")) {
|
|
1889
|
+
throw new Error("primaryColor must be a 3- or 6-digit hex color.");
|
|
1890
|
+
}
|
|
1891
|
+
if (this.config.theme !== void 0 && !THEMES.includes(this.config.theme)) throw new Error('theme must be "light", "dark" or "system".');
|
|
1892
|
+
this.shortcut = parseShortcut(this.config.keyboardShortcut ?? "");
|
|
1893
|
+
if (this.config.enableKeyboardShortcuts && !this.shortcut) throw new Error('keyboardShortcut must be a key such as "alt+shift+c".');
|
|
1894
|
+
const project = config.projectId || location.hostname || "default";
|
|
1895
|
+
this.identityKey = `overplay:${project}:user`;
|
|
1896
|
+
this.launcherKey = `overplay:${project}:launcher`;
|
|
1897
|
+
this.openKey = `overplay:${project}:open`;
|
|
1898
|
+
this.dockKey = `overplay:${project}:docked`;
|
|
1899
|
+
this.filterKey = `overplay:${project}:filters`;
|
|
1900
|
+
this.restoreFilters();
|
|
1901
|
+
this.project = project;
|
|
1902
|
+
try {
|
|
1903
|
+
this.docked = localStorage.getItem(this.dockKey) === "1";
|
|
1904
|
+
} catch {
|
|
1905
|
+
}
|
|
1906
|
+
this.launcherSpot = readSpot(this.launcherKey);
|
|
1907
|
+
const guest = guestIdentity(this.identityKey);
|
|
1908
|
+
this.user = config.user ? validateUser(config.user) : guest.user;
|
|
1909
|
+
this.persistentGuest = guest.persistent;
|
|
1910
|
+
this.managedUser = !!config.user;
|
|
1911
|
+
if (config.serverUrl && (!config.projectId || config.storage || config.user)) throw new Error("Hosted mode requires projectId and manages storage and user itself.");
|
|
1912
|
+
this.storage = config.serverUrl ? new HostedAdapter(config.serverUrl, project, () => this.currentPage(), (user) => this.hostedSession(user)) : config.storage ?? new LocalStorageAdapter(`overplay:${project}:comments`);
|
|
1913
|
+
if (config.serverUrl) {
|
|
1914
|
+
this.user = { id: "signed-out", name: "Sign in" };
|
|
1915
|
+
this.managedUser = true;
|
|
1916
|
+
}
|
|
1917
|
+
this.pageId = this.currentPage();
|
|
1918
|
+
this.host.dataset.overplay = "root";
|
|
1919
|
+
this.host.style.cssText = "all:initial;position:fixed;inset:0;pointer-events:none;z-index:2147483646;";
|
|
1920
|
+
this.shadow = this.host.attachShadow({ mode: "open" });
|
|
1921
|
+
const style = element("style");
|
|
1922
|
+
style.textContent = styles;
|
|
1923
|
+
if (config.nonce) style.nonce = config.nonce;
|
|
1924
|
+
this.host.style.setProperty("--cw-accent", this.config.primaryColor);
|
|
1925
|
+
this.live.setAttribute("role", "status");
|
|
1926
|
+
this.live.setAttribute("aria-live", "polite");
|
|
1927
|
+
this.toasts.setAttribute("role", "status");
|
|
1928
|
+
this.toasts.setAttribute("aria-live", "polite");
|
|
1929
|
+
this.shadow.append(style, this.pins, this.surface, this.toasts, this.live);
|
|
1930
|
+
document.body.append(this.host);
|
|
1931
|
+
const options = { signal: this.events.signal };
|
|
1932
|
+
if (this.config.autoReveal !== false) this.recorder.start(this.events.signal);
|
|
1933
|
+
for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click", "touchstart", "touchend", "focusin", "focusout"]) {
|
|
1934
|
+
this.host.addEventListener(type, (event) => event.stopPropagation(), options);
|
|
1935
|
+
}
|
|
1936
|
+
for (const type of ["pointerdown", "mousedown", "touchstart"]) window.addEventListener(type, this.shieldPress, { ...options, capture: true });
|
|
1937
|
+
window.addEventListener("focusin", (event) => {
|
|
1938
|
+
if (event.composedPath().includes(this.host)) event.stopImmediatePropagation();
|
|
1939
|
+
}, { ...options, capture: true });
|
|
1940
|
+
window.addEventListener("focusout", (event) => {
|
|
1941
|
+
if (event.relatedTarget === this.host && !event.composedPath().includes(this.host)) event.stopImmediatePropagation();
|
|
1942
|
+
}, { ...options, capture: true });
|
|
1943
|
+
const unhide = new MutationObserver(() => {
|
|
1944
|
+
this.host.removeAttribute("aria-hidden");
|
|
1945
|
+
this.host.removeAttribute("inert");
|
|
1946
|
+
});
|
|
1947
|
+
unhide.observe(this.host, { attributes: true, attributeFilter: ["aria-hidden", "inert"] });
|
|
1948
|
+
this.events.signal.addEventListener("abort", () => unhide.disconnect());
|
|
1949
|
+
this.shadow.addEventListener("compositionstart", () => {
|
|
1950
|
+
this.composing = true;
|
|
1951
|
+
}, options);
|
|
1952
|
+
this.shadow.addEventListener("compositionend", () => {
|
|
1953
|
+
this.composing = false;
|
|
1954
|
+
if (this.renderDeferred) {
|
|
1955
|
+
this.renderDeferred = false;
|
|
1956
|
+
this.render();
|
|
1957
|
+
}
|
|
1958
|
+
}, options);
|
|
1959
|
+
this.shadow.addEventListener("focusout", () => setTimeout(this.applyQueuedSnapshot), options);
|
|
1960
|
+
document.addEventListener("keydown", this.onKeyDown, options);
|
|
1961
|
+
document.addEventListener("click", this.onPageClick, { ...options, capture: true });
|
|
1962
|
+
document.addEventListener("pointerdown", (event) => {
|
|
1963
|
+
if (!event.composedPath().some((el) => el instanceof Element && el.matches("[role=menu],[aria-haspopup=menu]"))) this.closeMenus();
|
|
1964
|
+
}, { ...options, capture: true });
|
|
1965
|
+
document.addEventListener("pointermove", this.onPointerMove, { ...options, passive: true });
|
|
1966
|
+
window.addEventListener("scroll", this.schedulePosition, { ...options, capture: true, passive: true });
|
|
1967
|
+
window.addEventListener("resize", this.schedulePosition, options);
|
|
1968
|
+
window.addEventListener("resize", this.placeLauncher, options);
|
|
1969
|
+
window.addEventListener("resize", this.pushPage, options);
|
|
1970
|
+
(_a = window.visualViewport) == null ? void 0 : _a.addEventListener("resize", this.schedulePosition, options);
|
|
1971
|
+
(_b = window.visualViewport) == null ? void 0 : _b.addEventListener("scroll", this.schedulePosition, options);
|
|
1972
|
+
window.addEventListener("popstate", this.onRouteChange, options);
|
|
1973
|
+
window.addEventListener("hashchange", this.onRouteChange, options);
|
|
1974
|
+
this.resizeObserver = new ResizeObserver(this.schedulePosition);
|
|
1975
|
+
this.resizeObserver.observe(config.container ?? document.documentElement);
|
|
1976
|
+
this.anchorObserver = new ResizeObserver(this.schedulePosition);
|
|
1977
|
+
document.addEventListener("load", this.schedulePosition, { ...options, capture: true });
|
|
1978
|
+
(_c = document.fonts) == null ? void 0 : _c.addEventListener("loadingdone", this.schedulePosition, options);
|
|
1979
|
+
document.addEventListener("transitionrun", this.trackLayoutMotion, { ...options, capture: true });
|
|
1980
|
+
document.addEventListener("animationstart", this.trackLayoutMotion, { ...options, capture: true });
|
|
1981
|
+
this.mutationObserver = new MutationObserver(this.schedulePosition);
|
|
1982
|
+
this.mutationObserver.observe(config.container ?? document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "id", "data-overplay-anchor", "hidden", "open"] });
|
|
1983
|
+
this.routeTimer = setInterval(this.onRouteChange, 500);
|
|
1984
|
+
this.render();
|
|
1985
|
+
this.ready = this.refresh().then(() => {
|
|
1986
|
+
if (!this.destroyed && this.storage.subscribeToChanges) {
|
|
1987
|
+
try {
|
|
1988
|
+
this.unsubscribe = this.storage.subscribeToChanges(this.receiveSnapshot);
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
this.report(error);
|
|
1991
|
+
this.render();
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
/** Site defaults come from the configuration. Each person can change them for this project. */
|
|
1997
|
+
settingDefaults() {
|
|
1998
|
+
return {
|
|
1999
|
+
theme: this.config.theme ?? "light",
|
|
2000
|
+
pinColor: "site",
|
|
2001
|
+
buttonColor: "default",
|
|
2002
|
+
resolvedPins: "green",
|
|
2003
|
+
pinSize: "regular",
|
|
2004
|
+
pinLabel: "number",
|
|
2005
|
+
pushContent: this.config.pushContent !== false,
|
|
2006
|
+
notifications: this.config.showNotifications !== false,
|
|
2007
|
+
shortcut: !!this.config.enableKeyboardShortcuts && !!this.shortcut
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
/** Saved per project and per account, so people who share a browser keep their own choices. */
|
|
2011
|
+
settingsKey() {
|
|
2012
|
+
return `overplay:${this.project}:settings:${this.user.id}`;
|
|
2013
|
+
}
|
|
2014
|
+
get settings() {
|
|
2015
|
+
var _a;
|
|
2016
|
+
const key2 = this.settingsKey();
|
|
2017
|
+
if (((_a = this.settingsCache) == null ? void 0 : _a.key) !== key2) this.settingsCache = { key: key2, value: readSettings(key2, this.settingDefaults()) };
|
|
2018
|
+
return this.settingsCache.value;
|
|
2019
|
+
}
|
|
2020
|
+
setSetting(name, value) {
|
|
2021
|
+
const key2 = this.settingsKey(), settings = { ...this.settings, [name]: value };
|
|
2022
|
+
this.settingsCache = { key: key2, value: settings };
|
|
2023
|
+
if (!writeSettings(key2, settings, this.settingDefaults())) this.announce("Browser storage is unavailable. This choice lasts for this page view.");
|
|
2024
|
+
this.render();
|
|
2025
|
+
}
|
|
2026
|
+
setTheme(theme) {
|
|
2027
|
+
if (!THEMES.includes(theme)) throw new Error('theme must be "light", "dark" or "system".');
|
|
2028
|
+
if (this.destroyed || this.config.theme === theme) return;
|
|
2029
|
+
this.config.theme = theme;
|
|
2030
|
+
this.settingsCache = null;
|
|
2031
|
+
this.render();
|
|
2032
|
+
}
|
|
2033
|
+
resetSettings() {
|
|
2034
|
+
const key2 = this.settingsKey(), defaults = this.settingDefaults();
|
|
2035
|
+
writeSettings(key2, defaults, defaults);
|
|
2036
|
+
this.settingsCache = { key: key2, value: defaults };
|
|
2037
|
+
this.docked = false;
|
|
2038
|
+
try {
|
|
2039
|
+
localStorage.removeItem(this.dockKey);
|
|
2040
|
+
} catch {
|
|
2041
|
+
}
|
|
2042
|
+
this.render();
|
|
2043
|
+
this.announce("Settings reset to the site defaults.");
|
|
2044
|
+
}
|
|
2045
|
+
currentPage() {
|
|
2046
|
+
var _a, _b;
|
|
2047
|
+
return ((_b = (_a = this.config).getPageId) == null ? void 0 : _b.call(_a)) ?? defaultPageId(location);
|
|
2048
|
+
}
|
|
2049
|
+
onCurrentPage(pageUrl) {
|
|
2050
|
+
const page = this.currentPage();
|
|
2051
|
+
return this.config.getPageId ? pageUrl === page : samePage(pageUrl, page);
|
|
2052
|
+
}
|
|
2053
|
+
/** A field in the widget has focus. Background updates wait so typing, undo and IME input are not interrupted. */
|
|
2054
|
+
typing() {
|
|
2055
|
+
const active = this.shadow.activeElement;
|
|
2056
|
+
return this.composing || active instanceof HTMLTextAreaElement || active instanceof HTMLInputElement;
|
|
2057
|
+
}
|
|
2058
|
+
announce(message) {
|
|
2059
|
+
this.live.textContent = message;
|
|
2060
|
+
}
|
|
2061
|
+
report(error) {
|
|
2062
|
+
var _a, _b;
|
|
2063
|
+
const failure = error instanceof Error ? error : new Error("Could not save your changes.");
|
|
2064
|
+
this.error = failure.message;
|
|
2065
|
+
this.announce(failure.message);
|
|
2066
|
+
try {
|
|
2067
|
+
(_b = (_a = this.config).onError) == null ? void 0 : _b.call(_a, failure);
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
changed() {
|
|
2072
|
+
var _a, _b;
|
|
2073
|
+
try {
|
|
2074
|
+
(_b = (_a = this.config).onChange) == null ? void 0 : _b.call(_a, this.getAllComments());
|
|
2075
|
+
} catch {
|
|
2076
|
+
}
|
|
2077
|
+
this.host.dispatchEvent(new CustomEvent("comments-changed", { detail: this.getAllComments(), bubbles: true, composed: true }));
|
|
2078
|
+
}
|
|
2079
|
+
/** Shows a background snapshot. An unchanged snapshot does not touch the DOM, so polling never makes the overlay flash. */
|
|
2080
|
+
apply(comments) {
|
|
2081
|
+
if (sameComments(comments, this.comments)) return;
|
|
2082
|
+
this.comments = comments;
|
|
2083
|
+
this.changed();
|
|
2084
|
+
this.noticeActivity();
|
|
2085
|
+
this.render();
|
|
2086
|
+
}
|
|
2087
|
+
/** Shows a short notification for each comment or reply that someone else added since the last load. */
|
|
2088
|
+
noticeActivity() {
|
|
2089
|
+
const key2 = `${this.user.id}
|
|
2090
|
+
${this.currentPage()}`;
|
|
2091
|
+
if (key2 !== this.seenKey) {
|
|
2092
|
+
this.seenKey = key2;
|
|
2093
|
+
this.seen.clear();
|
|
2094
|
+
newActivity(this.comments, this.seen, this.user.id);
|
|
2095
|
+
return;
|
|
2096
|
+
}
|
|
2097
|
+
if (!this.settings.notifications) {
|
|
2098
|
+
newActivity(this.comments, this.seen, this.user.id);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
const found = newActivity(this.pageComments(), this.seen, this.user.id);
|
|
2102
|
+
newActivity(this.comments, this.seen, this.user.id);
|
|
2103
|
+
if (found.length > 3) this.toast({ kind: "comment", commentId: "", author: `${found.length} new comments`, text: "Open the comments panel to read them." }, true);
|
|
2104
|
+
else for (const item of found) this.toast(item);
|
|
2105
|
+
}
|
|
2106
|
+
toast(item, summary = false) {
|
|
2107
|
+
var _a;
|
|
2108
|
+
const toast = element("div", "toast");
|
|
2109
|
+
const body = element("div", "toast-body");
|
|
2110
|
+
const title = element("p", "toast-title");
|
|
2111
|
+
title.append(element("strong", "", item.author));
|
|
2112
|
+
if (!summary) {
|
|
2113
|
+
title.append(document.createTextNode(item.kind === "reply" ? " replied" : " commented"));
|
|
2114
|
+
const number = this.pageComments().findIndex((c) => c.id === item.commentId) + 1;
|
|
2115
|
+
if (number) title.append(element("span", "toast-number", `#${number}`));
|
|
2116
|
+
}
|
|
2117
|
+
body.append(title, element("p", "toast-text", item.text));
|
|
2118
|
+
if (summary) {
|
|
2119
|
+
const mark = element("span", "avatar toast-icon");
|
|
2120
|
+
mark.append(icon("comment"));
|
|
2121
|
+
toast.append(mark);
|
|
2122
|
+
} else toast.append(avatar(item.author, item.avatarUrl));
|
|
2123
|
+
toast.append(body);
|
|
2124
|
+
while (this.toasts.childElementCount >= 3) (_a = this.toasts.firstElementChild) == null ? void 0 : _a.remove();
|
|
2125
|
+
this.toasts.append(toast);
|
|
2126
|
+
const later = (ms, run) => {
|
|
2127
|
+
const timer = setTimeout(() => {
|
|
2128
|
+
this.toastTimers.delete(timer);
|
|
2129
|
+
run();
|
|
2130
|
+
}, ms);
|
|
2131
|
+
this.toastTimers.add(timer);
|
|
2132
|
+
};
|
|
2133
|
+
later(7e3, () => {
|
|
2134
|
+
toast.classList.add("leaving");
|
|
2135
|
+
later(this.reducedMotion() ? 0 : 200, () => toast.remove());
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
async refresh() {
|
|
2139
|
+
if (this.destroyed || this.saving) return;
|
|
2140
|
+
const version2 = ++this.loadVersion;
|
|
2141
|
+
this.loading = true;
|
|
2142
|
+
this.render();
|
|
2143
|
+
try {
|
|
2144
|
+
const comments = normalizeComments(await this.storage.loadComments());
|
|
2145
|
+
if (this.destroyed || version2 !== this.loadVersion) return;
|
|
2146
|
+
const next = this.queuedSnapshot ?? comments;
|
|
2147
|
+
const changed = !this.loaded || !sameComments(next, this.comments);
|
|
2148
|
+
this.comments = next;
|
|
2149
|
+
this.queuedSnapshot = null;
|
|
2150
|
+
this.loadFailed = false;
|
|
2151
|
+
this.error = "";
|
|
2152
|
+
this.loaded = true;
|
|
2153
|
+
if (changed) {
|
|
2154
|
+
this.changed();
|
|
2155
|
+
this.noticeActivity();
|
|
2156
|
+
}
|
|
2157
|
+
} catch (error) {
|
|
2158
|
+
if (this.destroyed || version2 !== this.loadVersion) return;
|
|
2159
|
+
this.loadFailed = true;
|
|
2160
|
+
this.report(error);
|
|
2161
|
+
} finally {
|
|
2162
|
+
if (!this.destroyed && version2 === this.loadVersion) {
|
|
2163
|
+
this.loading = false;
|
|
2164
|
+
this.render();
|
|
2165
|
+
if (this.loaded) this.openPending();
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
getAllComments() {
|
|
2170
|
+
return copyComments(this.comments);
|
|
2171
|
+
}
|
|
2172
|
+
getUser() {
|
|
2173
|
+
return { ...this.user };
|
|
2174
|
+
}
|
|
2175
|
+
hostedSession(user) {
|
|
2176
|
+
if (this.destroyed) return;
|
|
2177
|
+
this.comments = [];
|
|
2178
|
+
this.queuedSnapshot = null;
|
|
2179
|
+
this.loaded = false;
|
|
2180
|
+
this.user = user ?? { id: "signed-out", name: "Sign in" };
|
|
2181
|
+
this.managedUser = true;
|
|
2182
|
+
this.draft = null;
|
|
2183
|
+
this.text = "";
|
|
2184
|
+
this.replyText = "";
|
|
2185
|
+
this.placing = false;
|
|
2186
|
+
this.activeId = null;
|
|
2187
|
+
this.changed();
|
|
2188
|
+
this.render();
|
|
2189
|
+
}
|
|
2190
|
+
async signIn() {
|
|
2191
|
+
if (!(this.storage instanceof HostedAdapter)) return;
|
|
2192
|
+
try {
|
|
2193
|
+
await this.storage.signIn();
|
|
2194
|
+
await this.refresh();
|
|
2195
|
+
} catch (error) {
|
|
2196
|
+
if (!this.destroyed) {
|
|
2197
|
+
this.report(error);
|
|
2198
|
+
this.render();
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
async signOut() {
|
|
2203
|
+
if (!(this.storage instanceof HostedAdapter) || this.saving) return;
|
|
2204
|
+
try {
|
|
2205
|
+
await this.storage.signOut();
|
|
2206
|
+
await this.refresh();
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
this.report(error);
|
|
2209
|
+
this.render();
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
setUser(user) {
|
|
2213
|
+
if (this.storage instanceof HostedAdapter) throw new Error("Hosted mode manages the signed-in account. Use signIn or signOut.");
|
|
2214
|
+
if (this.destroyed) return;
|
|
2215
|
+
if (this.saving) throw new Error("Wait until the current comment is saved before changing users.");
|
|
2216
|
+
this.user = user ? validateUser(user) : guestIdentity(this.identityKey).user;
|
|
2217
|
+
this.managedUser = !!user;
|
|
2218
|
+
this.editing = null;
|
|
2219
|
+
this.draft = null;
|
|
2220
|
+
this.text = "";
|
|
2221
|
+
this.replyText = "";
|
|
2222
|
+
this.placing = false;
|
|
2223
|
+
this.render();
|
|
2224
|
+
}
|
|
2225
|
+
show() {
|
|
2226
|
+
var _a;
|
|
2227
|
+
if (this.destroyed) return;
|
|
2228
|
+
if (!this.visible) this.returnFocus = document.activeElement;
|
|
2229
|
+
this.visible = true;
|
|
2230
|
+
this.render();
|
|
2231
|
+
this.trackLayoutMotion();
|
|
2232
|
+
(_a = this.shadow.querySelector(".panel button, .launcher")) == null ? void 0 : _a.focus();
|
|
2233
|
+
this.checkForUpdate();
|
|
2234
|
+
}
|
|
2235
|
+
hide() {
|
|
2236
|
+
var _a;
|
|
2237
|
+
if (this.destroyed) return;
|
|
2238
|
+
this.visible = false;
|
|
2239
|
+
this.placing = false;
|
|
2240
|
+
this.draft = null;
|
|
2241
|
+
this.activeId = null;
|
|
2242
|
+
this.editing = null;
|
|
2243
|
+
this.text = "";
|
|
2244
|
+
this.replyText = "";
|
|
2245
|
+
this.render();
|
|
2246
|
+
if (this.returnFocus instanceof HTMLElement && this.returnFocus.isConnected) this.returnFocus.focus();
|
|
2247
|
+
else (_a = this.shadow.querySelector(".launcher")) == null ? void 0 : _a.focus();
|
|
2248
|
+
}
|
|
2249
|
+
startComment(position) {
|
|
2250
|
+
var _a;
|
|
2251
|
+
if (this.storage instanceof HostedAdapter && !this.storage.signedIn) {
|
|
2252
|
+
void this.signIn();
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
if (this.destroyed || this.initialLoad() || this.loadFailed || this.saving) return;
|
|
2256
|
+
this.visible = true;
|
|
2257
|
+
this.activeId = null;
|
|
2258
|
+
this.editing = null;
|
|
2259
|
+
this.text = "";
|
|
2260
|
+
if (position) {
|
|
2261
|
+
if (!Number.isFinite(position.x) || !Number.isFinite(position.y)) throw new Error("Comment coordinates must be finite.");
|
|
2262
|
+
const x = position.x - window.scrollX, y = position.y - window.scrollY;
|
|
2263
|
+
const target = document.elementFromPoint(x, y);
|
|
2264
|
+
if (this.config.container && (!target || !this.config.container.contains(target))) {
|
|
2265
|
+
throw new Error("Choose a position inside the configured container.");
|
|
2266
|
+
}
|
|
2267
|
+
this.draft = this.makeDraft(position, target);
|
|
2268
|
+
this.placing = false;
|
|
2269
|
+
} else {
|
|
2270
|
+
this.placing = true;
|
|
2271
|
+
this.draft = null;
|
|
2272
|
+
}
|
|
2273
|
+
this.render();
|
|
2274
|
+
this.announce(position ? "Write your comment." : "Click a place on the page, or Tab to an element and press Enter. Press Escape to cancel.");
|
|
2275
|
+
(_a = this.shadow.querySelector("textarea")) == null ? void 0 : _a.focus();
|
|
2276
|
+
}
|
|
2277
|
+
navigateToComment(id) {
|
|
2278
|
+
var _a;
|
|
2279
|
+
if (this.destroyed) return;
|
|
2280
|
+
const comment = this.comments.find((c) => c.id === id);
|
|
2281
|
+
if (!comment) return;
|
|
2282
|
+
try {
|
|
2283
|
+
const url = safePageUrl(comment.pageUrl, location.href);
|
|
2284
|
+
if (!this.onCurrentPage(comment.pageUrl)) {
|
|
2285
|
+
try {
|
|
2286
|
+
sessionStorage.setItem(this.openKey, id);
|
|
2287
|
+
} catch {
|
|
2288
|
+
}
|
|
2289
|
+
location.assign(url.href);
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
this.visible = true;
|
|
2293
|
+
this.placing = false;
|
|
2294
|
+
this.draft = null;
|
|
2295
|
+
this.editing = null;
|
|
2296
|
+
this.activeId = id;
|
|
2297
|
+
this.replyText = "";
|
|
2298
|
+
this.replyId = newId();
|
|
2299
|
+
this.flash(id);
|
|
2300
|
+
this.render();
|
|
2301
|
+
const state = comment.selector ? this.elementState(comment) : "shown";
|
|
2302
|
+
if (state === "hidden" || state === "removed" && this.canReveal(comment)) void this.reveal(comment);
|
|
2303
|
+
else this.scrollToComment(comment);
|
|
2304
|
+
(_a = this.shadow.querySelector(".detail")) == null ? void 0 : _a.focus({ preventScroll: true });
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
this.report(error);
|
|
2307
|
+
this.visible = true;
|
|
2308
|
+
this.render();
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
scrollToComment(comment) {
|
|
2312
|
+
const behavior = this.reducedMotion() ? "instant" : "smooth";
|
|
2313
|
+
const target = this.anchorElement(comment);
|
|
2314
|
+
if (target && !this.detached(comment)) target.scrollIntoView({ block: "center", behavior });
|
|
2315
|
+
else window.scrollTo({ top: Math.max(0, comment.y - window.innerHeight / 2), behavior });
|
|
2316
|
+
}
|
|
2317
|
+
detached(comment) {
|
|
2318
|
+
return isDetached(comment, this.config.container);
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Many menus (Radix, Headless UI, MUI, Vue) take a closed menu's items out of the page. A note that saved how its menu
|
|
2322
|
+
* was opened counts as hidden while the button that opens the menu is on the page; it counts as removed only when that
|
|
2323
|
+
* button is gone too, or opening the menu did not bring the element back.
|
|
2324
|
+
*/
|
|
2325
|
+
elementState(comment) {
|
|
2326
|
+
var _a, _b, _c, _d;
|
|
2327
|
+
const state = elementState(comment, this.config.container);
|
|
2328
|
+
if (state !== "removed" || ((_a = this.revealed) == null ? void 0 : _a.id) === comment.id && !this.revealed.result.shown) return state;
|
|
2329
|
+
const opener = this.config.autoReveal !== false ? (_d = (_c = (_b = comment.element) == null ? void 0 : _b.reveal) == null ? void 0 : _c[0]) == null ? void 0 : _d.target : void 0;
|
|
2330
|
+
return opener && findTarget(opener) ? "hidden" : state;
|
|
2331
|
+
}
|
|
2332
|
+
/** The saved name of the note's element. Older notes have none; their element is named from the page while it exists. */
|
|
2333
|
+
elementInfo(comment) {
|
|
2334
|
+
if (comment.element) return comment.element;
|
|
2335
|
+
const target = this.anchorElement(comment) ?? (comment.selector ? (() => {
|
|
2336
|
+
try {
|
|
2337
|
+
return document.querySelector(comment.selector);
|
|
2338
|
+
} catch {
|
|
2339
|
+
return null;
|
|
2340
|
+
}
|
|
2341
|
+
})() : null);
|
|
2342
|
+
return target ? describeElement(target) : void 0;
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* Tells people who did not write the note what happened to its element and where to look: "Element hidden" with the
|
|
2346
|
+
* menu or section it is in, or "Element removed" when nothing on the page matches it now.
|
|
2347
|
+
*/
|
|
2348
|
+
anchorStatus(comment, state) {
|
|
2349
|
+
var _a, _b;
|
|
2350
|
+
const box = element("div", `anchor-status ${state}`);
|
|
2351
|
+
box.dataset.anchorStatus = state;
|
|
2352
|
+
box.hidden = state === "shown";
|
|
2353
|
+
if (state === "shown") return box;
|
|
2354
|
+
const info = this.elementInfo(comment);
|
|
2355
|
+
const hidden = state === "hidden";
|
|
2356
|
+
const head = element("div", "anchor-head");
|
|
2357
|
+
const mark = element("span", "anchor-icon");
|
|
2358
|
+
mark.append(icon(hidden ? "eyeOff" : "unlink"));
|
|
2359
|
+
const titles = element("div", "anchor-titles");
|
|
2360
|
+
titles.append(element("strong", "", hidden ? "Element hidden" : "Element removed"));
|
|
2361
|
+
if (info) titles.append(element("span", "anchor-target", info.place ? `${info.label} in ${info.place}` : info.label));
|
|
2362
|
+
head.append(mark, titles);
|
|
2363
|
+
box.append(head);
|
|
2364
|
+
const failed = ((_a = this.revealed) == null ? void 0 : _a.id) === comment.id ? (_b = this.revealed.result.failed) == null ? void 0 : _b.target.name : void 0;
|
|
2365
|
+
const opener = failed ?? (info == null ? void 0 : info.place);
|
|
2366
|
+
const where = hidden || failed ? opener ? `Open “${opener}” to see it.` : "It is on this page, but not shown now. It can be in a closed menu, tab or filter." : "Nothing on this page matches it now. It was deleted, or it shows only after an action.";
|
|
2367
|
+
box.append(element("p", "", `${where} The dashed pin shows where the note was left.${this.canMove(comment) ? " Drag the pin to attach it again." : ""}`));
|
|
2368
|
+
if (hidden || this.canReveal(comment)) box.append(button("Show Element", "quiet anchor-action", () => {
|
|
2369
|
+
void this.reveal(comment);
|
|
2370
|
+
}, "eye"));
|
|
2371
|
+
return box;
|
|
2372
|
+
}
|
|
2373
|
+
/** A note on a removed element can still be shown when the site has a hook or the note saved how its menu was opened. */
|
|
2374
|
+
canReveal(comment) {
|
|
2375
|
+
var _a, _b;
|
|
2376
|
+
return !!this.config.revealComponent || this.config.autoReveal !== false && !!((_b = (_a = comment.element) == null ? void 0 : _a.reveal) == null ? void 0 : _b.length);
|
|
2377
|
+
}
|
|
2378
|
+
/**
|
|
2379
|
+
* Shows a component that is hidden: first the site's revealComponent hook (a map can zoom to a marker), then the
|
|
2380
|
+
* steps saved with the note (the menu, drawer or tab that was open), then closed details, popovers, menus,
|
|
2381
|
+
* accordions and tabs found from the page's ARIA. Frameworks render after a click, so each step waits for the next.
|
|
2382
|
+
*/
|
|
2383
|
+
async reveal(comment) {
|
|
2384
|
+
var _a, _b, _c;
|
|
2385
|
+
let handled = false;
|
|
2386
|
+
try {
|
|
2387
|
+
handled = !!await ((_b = (_a = this.config).revealComponent) == null ? void 0 : _b.call(_a, copyComments([comment])[0], this.anchorElement(comment)));
|
|
2388
|
+
} catch (error) {
|
|
2389
|
+
this.report(error);
|
|
2390
|
+
}
|
|
2391
|
+
const steps = (_c = comment.element) == null ? void 0 : _c.reveal;
|
|
2392
|
+
if (!handled && this.config.autoReveal !== false && (steps == null ? void 0 : steps.length)) {
|
|
2393
|
+
this.endReveal();
|
|
2394
|
+
this.replaying = true;
|
|
2395
|
+
let result;
|
|
2396
|
+
try {
|
|
2397
|
+
result = await replay(steps, () => this.anchorElement(comment));
|
|
2398
|
+
} finally {
|
|
2399
|
+
this.replaying = false;
|
|
2400
|
+
}
|
|
2401
|
+
if (this.destroyed || this.activeId !== comment.id) {
|
|
2402
|
+
clearReveal();
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
this.revealed = { id: comment.id, result };
|
|
2406
|
+
handled = result.shown;
|
|
2407
|
+
}
|
|
2408
|
+
if (!handled && this.config.autoReveal !== false) {
|
|
2409
|
+
const target = this.anchorElement(comment);
|
|
2410
|
+
if (target && revealElement(target)) {
|
|
2411
|
+
handled = true;
|
|
2412
|
+
await new Promise((done) => setTimeout(done, 250));
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
if (this.destroyed || this.activeId !== comment.id) return;
|
|
2416
|
+
this.scrollToComment(comment);
|
|
2417
|
+
this.render();
|
|
2418
|
+
const state = this.elementState(comment);
|
|
2419
|
+
this.announce(state === "shown" ? "Showing the comment’s element." : state === "hidden" ? "The comment’s element is still hidden. A dashed pin shows where it was." : "The comment’s element was removed. A dashed pin shows where it was.");
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* A modal <dialog> makes the rest of the page inert, and a press outside an open popover closes it. While the note
|
|
2423
|
+
* being written or read is on an element in one of these top-layer parts, the widget moves inside it.
|
|
2424
|
+
*/
|
|
2425
|
+
syncLayer() {
|
|
2426
|
+
const active = this.activeId ? this.comments.find((c) => c.id === this.activeId) : null;
|
|
2427
|
+
const target = this.draft ? this.anchorElement(this.draft) : active ? this.anchorElement(active) : null;
|
|
2428
|
+
const layers = target ? [...matchAll("dialog:modal"), ...matchAll(":popover-open")].filter((el) => el.contains(target)) : [];
|
|
2429
|
+
const parent = layers.find((el) => layers.every((other) => other.contains(el))) ?? document.body;
|
|
2430
|
+
if (this.host.parentElement !== parent) parent.append(this.host);
|
|
2431
|
+
}
|
|
2432
|
+
/** The note that Show Element opened a menu for has closed: end the CSS hover states it set. */
|
|
2433
|
+
endReveal() {
|
|
2434
|
+
if (!this.revealed) return;
|
|
2435
|
+
this.revealed = null;
|
|
2436
|
+
clearReveal();
|
|
2437
|
+
}
|
|
2438
|
+
/** Opens the comment that a click in "All comments" on another page asked for. */
|
|
2439
|
+
openPending() {
|
|
2440
|
+
const linkId = new URL(location.href).searchParams.get("cw_comment");
|
|
2441
|
+
const linked = linkId ? this.comments.find((c) => c.id === linkId && this.onCurrentPage(c.pageUrl)) : null;
|
|
2442
|
+
if (linked) {
|
|
2443
|
+
const url = new URL(location.href);
|
|
2444
|
+
url.search = withoutCommentLink(url.search);
|
|
2445
|
+
history.replaceState(history.state, "", url.href);
|
|
2446
|
+
this.navigateToComment(linked.id);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
let id = null;
|
|
2450
|
+
try {
|
|
2451
|
+
id = sessionStorage.getItem(this.openKey);
|
|
2452
|
+
sessionStorage.removeItem(this.openKey);
|
|
2453
|
+
} catch {
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
const comment = id ? this.comments.find((c) => c.id === id) : null;
|
|
2457
|
+
if (comment && this.onCurrentPage(comment.pageUrl)) this.navigateToComment(comment.id);
|
|
2458
|
+
}
|
|
2459
|
+
/** Pulses the pin and outlines its element while the pointer or focus is on its card in the list. */
|
|
2460
|
+
hoverNote(id) {
|
|
2461
|
+
if (this.hoverId === id) return;
|
|
2462
|
+
this.hoverId = id;
|
|
2463
|
+
for (const pin of this.pins.querySelectorAll(".pin[data-id]")) pin.classList.toggle("hovered", pin.dataset.id === id);
|
|
2464
|
+
this.positionPins();
|
|
2465
|
+
}
|
|
2466
|
+
/** Makes the pin pulse for a moment, so the eye finds it after a jump. */
|
|
2467
|
+
flash(id) {
|
|
2468
|
+
this.highlightId = id;
|
|
2469
|
+
clearTimeout(this.highlightTimer);
|
|
2470
|
+
this.highlightTimer = setTimeout(() => {
|
|
2471
|
+
var _a;
|
|
2472
|
+
this.highlightId = null;
|
|
2473
|
+
(_a = this.pins.querySelector(".highlight")) == null ? void 0 : _a.classList.remove("highlight");
|
|
2474
|
+
}, 2400);
|
|
2475
|
+
}
|
|
2476
|
+
destroy() {
|
|
2477
|
+
var _a;
|
|
2478
|
+
if (this.destroyed) return;
|
|
2479
|
+
this.destroyed = true;
|
|
2480
|
+
++this.loadVersion;
|
|
2481
|
+
clearTimeout(this.highlightTimer);
|
|
2482
|
+
clearTimeout(this.searchTimer);
|
|
2483
|
+
if (this.storage instanceof HostedAdapter) this.storage.destroy();
|
|
2484
|
+
this.events.abort();
|
|
2485
|
+
this.resizeObserver.disconnect();
|
|
2486
|
+
this.mutationObserver.disconnect();
|
|
2487
|
+
this.anchorObserver.disconnect();
|
|
2488
|
+
this.observedAnchors.clear();
|
|
2489
|
+
clearInterval(this.routeTimer);
|
|
2490
|
+
cancelAnimationFrame(this.frame);
|
|
2491
|
+
cancelAnimationFrame(this.motionFrame);
|
|
2492
|
+
for (const timer of this.toastTimers) clearTimeout(timer);
|
|
2493
|
+
this.toastTimers.clear();
|
|
2494
|
+
this.sidebarOpen = false;
|
|
2495
|
+
this.pushPage();
|
|
2496
|
+
clearReveal();
|
|
2497
|
+
try {
|
|
2498
|
+
(_a = this.unsubscribe) == null ? void 0 : _a.call(this);
|
|
2499
|
+
} finally {
|
|
2500
|
+
this.host.remove();
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
/** The first load for this page and account. Only then does the list show a loading state. */
|
|
2504
|
+
initialLoad() {
|
|
2505
|
+
return this.loading && !this.loaded;
|
|
2506
|
+
}
|
|
2507
|
+
reducedMotion() {
|
|
2508
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2509
|
+
}
|
|
2510
|
+
choosePoint(position, target) {
|
|
2511
|
+
var _a;
|
|
2512
|
+
let draft;
|
|
2513
|
+
try {
|
|
2514
|
+
draft = this.makeDraft(position, target);
|
|
2515
|
+
} catch (error) {
|
|
2516
|
+
this.report(error);
|
|
2517
|
+
this.render();
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
this.placing = false;
|
|
2521
|
+
this.draft = draft;
|
|
2522
|
+
this.render();
|
|
2523
|
+
(_a = this.shadow.querySelector("textarea")) == null ? void 0 : _a.focus();
|
|
2524
|
+
}
|
|
2525
|
+
/** Drag a pin to a new place. A press without movement still opens the comment. */
|
|
2526
|
+
startPinDrag(event, comment) {
|
|
2527
|
+
this.suppressPinClick = false;
|
|
2528
|
+
if (event.button !== 0 || !event.isPrimary || this.saving || this.pinDrag) return;
|
|
2529
|
+
const pin = event.currentTarget;
|
|
2530
|
+
const dx = event.clientX - parseFloat(pin.style.left), dy = event.clientY - parseFloat(pin.style.top);
|
|
2531
|
+
const startX = event.clientX, startY = event.clientY;
|
|
2532
|
+
const stop = new AbortController();
|
|
2533
|
+
const options = { signal: stop.signal };
|
|
2534
|
+
this.events.signal.addEventListener("abort", () => stop.abort(), options);
|
|
2535
|
+
let moved = false;
|
|
2536
|
+
window.addEventListener("pointermove", (move) => {
|
|
2537
|
+
if (move.pointerId !== event.pointerId || !moved && Math.hypot(move.clientX - startX, move.clientY - startY) < 5) return;
|
|
2538
|
+
moved = true;
|
|
2539
|
+
const [minX, maxX] = this.pinBounds();
|
|
2540
|
+
this.pinDrag = { id: comment.id, x: clamp(move.clientX - dx, minX, maxX), y: clamp(move.clientY - dy, PIN_HEIGHT + 10, innerHeight - 6) };
|
|
2541
|
+
const under = document.elementFromPoint(this.pinDrag.x, this.pinDrag.y);
|
|
2542
|
+
const component = under && under !== this.host ? componentTarget(under, this.config.container) : null;
|
|
2543
|
+
this.pickTarget = component === document.body || component === document.documentElement ? null : component;
|
|
2544
|
+
this.positionPins();
|
|
2545
|
+
}, options);
|
|
2546
|
+
const end = (up) => {
|
|
2547
|
+
if (up.pointerId !== event.pointerId) return;
|
|
2548
|
+
stop.abort();
|
|
2549
|
+
this.pickTarget = null;
|
|
2550
|
+
if (!moved) return;
|
|
2551
|
+
this.suppressPinClick = true;
|
|
2552
|
+
const drop = this.pinDrag;
|
|
2553
|
+
if (up.type === "pointercancel" || !drop) {
|
|
2554
|
+
this.pinDrag = null;
|
|
2555
|
+
this.positionPins();
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
void this.movePin(comment.id, drop).finally(() => {
|
|
2559
|
+
this.pinDrag = null;
|
|
2560
|
+
this.positionPins();
|
|
2561
|
+
});
|
|
2562
|
+
};
|
|
2563
|
+
window.addEventListener("pointerup", end, { ...options, capture: true });
|
|
2564
|
+
window.addEventListener("pointercancel", end, { ...options, capture: true });
|
|
2565
|
+
}
|
|
2566
|
+
/** The horizontal range where a pin stays fully visible: inside the screen and outside the open sidebar. */
|
|
2567
|
+
pinBounds() {
|
|
2568
|
+
var _a;
|
|
2569
|
+
const side = this.sidebarOpen && innerWidth > 600 ? SIDEBAR : 0, onLeft = !!((_a = this.config.position) == null ? void 0 : _a.endsWith("left"));
|
|
2570
|
+
return [PIN_HALF + 2 + (onLeft ? side : 0), innerWidth - PIN_HALF - 2 - (onLeft ? 0 : side)];
|
|
2571
|
+
}
|
|
2572
|
+
async movePin(id, drop) {
|
|
2573
|
+
var _a, _b;
|
|
2574
|
+
const comment = this.comments.find((c) => c.id === id);
|
|
2575
|
+
const target = document.elementFromPoint(drop.x, drop.y);
|
|
2576
|
+
if (!comment || !target || target === this.host || this.config.container && !this.config.container.contains(target)) {
|
|
2577
|
+
this.announce("Drop the pin on the page to move it.");
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
const position = { x: drop.x + scrollX, y: drop.y + scrollY };
|
|
2581
|
+
let draft;
|
|
2582
|
+
try {
|
|
2583
|
+
draft = this.makeDraft(position, target);
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
this.report(error);
|
|
2586
|
+
this.render();
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
if (draft.element && !draft.element.reveal && ((_a = this.revealed) == null ? void 0 : _a.id) === id && ((_b = comment.element) == null ? void 0 : _b.reveal)) draft.element.reveal = comment.element.reveal;
|
|
2590
|
+
const updated = { ...comment, x: position.x, y: position.y, selector: draft.selector, anchor: draft.anchor, element: draft.element, pageTitle: pageTitle() ?? comment.pageTitle };
|
|
2591
|
+
const next = this.getAllComments().map((c) => c.id === id ? updated : c);
|
|
2592
|
+
this.activeId = id;
|
|
2593
|
+
await this.mutate(next, () => this.storage.upsertComment ? this.storage.upsertComment(updated) : this.storage.saveComments(next), () => {
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
placePickRing() {
|
|
2597
|
+
var _a;
|
|
2598
|
+
const ring = this.pins.querySelector(".pick-ring");
|
|
2599
|
+
if (!ring) return;
|
|
2600
|
+
const rect = (this.placing || this.pinDrag) && ((_a = this.pickTarget) == null ? void 0 : _a.isConnected) ? this.pickTarget.getBoundingClientRect() : null;
|
|
2601
|
+
ring.hidden = !rect || !rect.width || !rect.height;
|
|
2602
|
+
if (rect) Object.assign(ring.style, { left: `${rect.left - 3}px`, top: `${rect.top - 3}px`, width: `${rect.width + 6}px`, height: `${rect.height + 6}px` });
|
|
2603
|
+
}
|
|
2604
|
+
/** Places the open comment or the new comment form next to its pin, inside the free part of the screen. */
|
|
2605
|
+
placePopover() {
|
|
2606
|
+
var _a, _b;
|
|
2607
|
+
const pop = this.surface.querySelector(".popover");
|
|
2608
|
+
if (!pop) return;
|
|
2609
|
+
const sheet = innerWidth <= 600;
|
|
2610
|
+
pop.classList.toggle("sheet", sheet);
|
|
2611
|
+
if (sheet) {
|
|
2612
|
+
pop.style.left = "";
|
|
2613
|
+
pop.style.top = "";
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
const comment = this.draft ? null : this.comments.find((c) => c.id === this.activeId);
|
|
2617
|
+
const dragging = !!comment && ((_a = this.pinDrag) == null ? void 0 : _a.id) === comment.id;
|
|
2618
|
+
pop.classList.toggle("moving", dragging);
|
|
2619
|
+
if (dragging) return;
|
|
2620
|
+
const pos = dragging ? this.pinDrag : this.draft ? this.position({ ...this.draft, ...this.draft.position }) : comment ? this.position(comment) ?? (this.detached(comment) ? { x: comment.x - scrollX, y: comment.y - scrollY } : null) : null;
|
|
2621
|
+
const target = dragging ? null : this.draft ? this.anchorElement(this.draft) : comment ? this.anchorElement(comment) : null;
|
|
2622
|
+
const width = pop.offsetWidth, height = pop.offsetHeight;
|
|
2623
|
+
const side = this.sidebarOpen ? SIDEBAR : 0, onLeft = !!((_b = this.config.position) == null ? void 0 : _b.endsWith("left"));
|
|
2624
|
+
const minX = EDGE + (onLeft ? side : 0), maxX = innerWidth - EDGE - (onLeft ? 0 : side) - width;
|
|
2625
|
+
let x = maxX, y = 72;
|
|
2626
|
+
if (pos && pos.x >= 0 && pos.y >= 0 && pos.x <= innerWidth && pos.y <= innerHeight) {
|
|
2627
|
+
const rect = target == null ? void 0 : target.getBoundingClientRect();
|
|
2628
|
+
if (rect && rect.right + 12 <= maxX) x = rect.right + 12;
|
|
2629
|
+
else if (rect && rect.left - 12 - width >= minX) x = rect.left - 12 - width;
|
|
2630
|
+
else x = pos.x + 24 <= maxX ? pos.x + 24 : pos.x - 24 - width;
|
|
2631
|
+
y = pos.y - 40;
|
|
2632
|
+
}
|
|
2633
|
+
pop.style.left = `${clamp(x, minX, maxX)}px`;
|
|
2634
|
+
pop.style.top = `${clamp(y, EDGE, innerHeight - EDGE - height)}px`;
|
|
2635
|
+
}
|
|
2636
|
+
makeDraft(position, target) {
|
|
2637
|
+
const draft = { id: newId(), timestamp: /* @__PURE__ */ new Date(), position, pageUrl: this.currentPage() };
|
|
2638
|
+
target = target ? componentTarget(target, this.config.container) : null;
|
|
2639
|
+
if (target && !this.host.contains(target)) {
|
|
2640
|
+
const rect = target.getBoundingClientRect();
|
|
2641
|
+
if (rect.width && rect.height) {
|
|
2642
|
+
draft.selector = componentSelector(target, this.config.container);
|
|
2643
|
+
draft.element = describeElement(target);
|
|
2644
|
+
const reveal = this.config.autoReveal !== false ? this.recorder.capture(target) : [];
|
|
2645
|
+
if (reveal.length) draft.element.reveal = reveal;
|
|
2646
|
+
draft.anchor = {
|
|
2647
|
+
x: Math.max(0, Math.min(1, (position.x - scrollX - rect.left) / rect.width)),
|
|
2648
|
+
y: Math.max(0, Math.min(1, (position.y - scrollY - rect.top) / rect.height))
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
if (!draft.selector || !draft.anchor) throw new Error("Choose a visible page element to attach this note.");
|
|
2653
|
+
return draft;
|
|
2654
|
+
}
|
|
2655
|
+
anchorElement(comment) {
|
|
2656
|
+
return comment.selector ? resolveComponent(comment.selector, this.config.container) : null;
|
|
2657
|
+
}
|
|
2658
|
+
position(comment) {
|
|
2659
|
+
return componentPosition(comment, this.config.container);
|
|
2660
|
+
}
|
|
2661
|
+
observeAnchors() {
|
|
2662
|
+
var _a;
|
|
2663
|
+
const targets = /* @__PURE__ */ new Set();
|
|
2664
|
+
for (const note of this.pageComments()) {
|
|
2665
|
+
const target = this.anchorElement(note);
|
|
2666
|
+
if (target) targets.add(target);
|
|
2667
|
+
}
|
|
2668
|
+
if ((_a = this.draft) == null ? void 0 : _a.selector) {
|
|
2669
|
+
const target = this.anchorElement(this.draft);
|
|
2670
|
+
if (target) targets.add(target);
|
|
2671
|
+
}
|
|
2672
|
+
for (const target of this.observedAnchors) if (!targets.has(target)) this.anchorObserver.unobserve(target);
|
|
2673
|
+
for (const target of targets) if (!this.observedAnchors.has(target)) this.anchorObserver.observe(target);
|
|
2674
|
+
this.observedAnchors = targets;
|
|
2675
|
+
}
|
|
2676
|
+
positionPins() {
|
|
2677
|
+
var _a, _b, _c, _d;
|
|
2678
|
+
this.observeAnchors();
|
|
2679
|
+
const layer = this.host.parentElement;
|
|
2680
|
+
if (layer && layer !== document.body && ![...matchAll("dialog[open]"), ...matchAll(":popover-open")].includes(layer)) document.body.append(this.host);
|
|
2681
|
+
for (const pin of this.pins.querySelectorAll("[data-id]")) {
|
|
2682
|
+
const comment = this.comments.find((c) => c.id === pin.dataset.id);
|
|
2683
|
+
if (!comment) continue;
|
|
2684
|
+
const dragging = ((_a = this.pinDrag) == null ? void 0 : _a.id) === comment.id;
|
|
2685
|
+
pin.classList.toggle("dragging", dragging);
|
|
2686
|
+
const lost = !dragging && comment.id === this.activeId && this.detached(comment);
|
|
2687
|
+
pin.classList.toggle("detached", lost);
|
|
2688
|
+
const pos = dragging ? this.pinDrag : lost ? { x: comment.x - scrollX, y: comment.y - scrollY } : this.position(comment);
|
|
2689
|
+
if (!pos) {
|
|
2690
|
+
pin.hidden = true;
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
const [minX, maxX] = this.pinBounds();
|
|
2694
|
+
pin.style.left = `${clamp(pos.x, minX, maxX)}px`;
|
|
2695
|
+
pin.style.top = `${pos.y}px`;
|
|
2696
|
+
pin.classList.toggle("below", pos.y < PIN_HEIGHT + 4);
|
|
2697
|
+
pin.hidden = pos.x < 0 || pos.y < 0 || pos.x > innerWidth || pos.y > innerHeight;
|
|
2698
|
+
if (this.config.container) {
|
|
2699
|
+
const rect = this.config.container.getBoundingClientRect();
|
|
2700
|
+
pin.hidden || (pin.hidden = pos.x < rect.left || pos.x > rect.right || pos.y < rect.top || pos.y > rect.bottom);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
const active = this.comments.find((c) => c.id === this.activeId);
|
|
2704
|
+
const ring = this.pins.querySelector(".focus-ring");
|
|
2705
|
+
if (ring) {
|
|
2706
|
+
const rect = active ? (_b = this.anchorElement(active)) == null ? void 0 : _b.getBoundingClientRect() : void 0;
|
|
2707
|
+
ring.hidden = !rect || !rect.width || !rect.height || ((_c = this.pinDrag) == null ? void 0 : _c.id) === (active == null ? void 0 : active.id);
|
|
2708
|
+
if (rect) Object.assign(ring.style, { left: `${rect.left - 4}px`, top: `${rect.top - 4}px`, width: `${rect.width + 8}px`, height: `${rect.height + 8}px` });
|
|
2709
|
+
}
|
|
2710
|
+
const hoverRing = this.pins.querySelector(".hover-ring");
|
|
2711
|
+
if (hoverRing) {
|
|
2712
|
+
const hovered = this.hoverId ? this.comments.find((c) => c.id === this.hoverId) : void 0;
|
|
2713
|
+
const rect = hovered && hovered.id !== this.activeId ? (_d = this.anchorElement(hovered)) == null ? void 0 : _d.getBoundingClientRect() : void 0;
|
|
2714
|
+
hoverRing.hidden = !rect || !rect.width || !rect.height;
|
|
2715
|
+
if (rect) Object.assign(hoverRing.style, { left: `${rect.left - 4}px`, top: `${rect.top - 4}px`, width: `${rect.width + 8}px`, height: `${rect.height + 8}px` });
|
|
2716
|
+
}
|
|
2717
|
+
this.placePickRing();
|
|
2718
|
+
this.placePopover();
|
|
2719
|
+
const status = this.surface.querySelector("[data-anchor-status]");
|
|
2720
|
+
if (status && active) {
|
|
2721
|
+
const state = this.elementState(active);
|
|
2722
|
+
if (status.dataset.anchorStatus !== state) status.replaceWith(this.anchorStatus(active, state));
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* Looks once for a newer npm release. A browser cannot update the site's code, so the notice copies the update command.
|
|
2727
|
+
* Only development hosts check by default: that is where the person reading the notice can run npm.
|
|
2728
|
+
*/
|
|
2729
|
+
checkForUpdate() {
|
|
2730
|
+
if (this.updateChecked || !(this.config.updateNotice ?? developmentHost(location.hostname))) return;
|
|
2731
|
+
this.updateChecked = true;
|
|
2732
|
+
void latestVersion().then((latest) => {
|
|
2733
|
+
if (this.destroyed || !latest || !isNewer(latest, VERSION)) return;
|
|
2734
|
+
try {
|
|
2735
|
+
if (localStorage.getItem(UPDATE_DISMISSED) === latest) return;
|
|
2736
|
+
} catch {
|
|
2737
|
+
}
|
|
2738
|
+
this.update = latest;
|
|
2739
|
+
this.render();
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
async copyUpdate() {
|
|
2743
|
+
let copied = false;
|
|
2744
|
+
try {
|
|
2745
|
+
await navigator.clipboard.writeText(UPDATE_COMMAND);
|
|
2746
|
+
copied = true;
|
|
2747
|
+
} catch {
|
|
2748
|
+
}
|
|
2749
|
+
this.toast({
|
|
2750
|
+
kind: "comment",
|
|
2751
|
+
commentId: "",
|
|
2752
|
+
author: copied ? `Update command copied` : "Run this to update",
|
|
2753
|
+
text: copied ? `Run it in your project, then reload: ${UPDATE_COMMAND}` : UPDATE_COMMAND
|
|
2754
|
+
}, true);
|
|
2755
|
+
this.announce(copied ? "Update command copied." : `Run ${UPDATE_COMMAND} to update.`);
|
|
2756
|
+
}
|
|
2757
|
+
/** "Later" hides the notice until the next version comes out. */
|
|
2758
|
+
dismissUpdate() {
|
|
2759
|
+
try {
|
|
2760
|
+
localStorage.setItem(UPDATE_DISMISSED, this.update ?? "");
|
|
2761
|
+
} catch {
|
|
2762
|
+
}
|
|
2763
|
+
this.update = null;
|
|
2764
|
+
this.render();
|
|
2765
|
+
}
|
|
2766
|
+
/** Drags the launcher anywhere on the screen. A press without movement stays a click. */
|
|
2767
|
+
startDrag(event) {
|
|
2768
|
+
this.suppressClick = false;
|
|
2769
|
+
if (event.button !== 0 || !event.isPrimary) return;
|
|
2770
|
+
const launcher = event.currentTarget;
|
|
2771
|
+
const rect = launcher.getBoundingClientRect();
|
|
2772
|
+
const offsetX = event.clientX - rect.left, offsetY = event.clientY - rect.top;
|
|
2773
|
+
const startX = event.clientX, startY = event.clientY;
|
|
2774
|
+
const stop = new AbortController();
|
|
2775
|
+
const options = { signal: stop.signal };
|
|
2776
|
+
this.events.signal.addEventListener("abort", () => stop.abort(), { signal: stop.signal });
|
|
2777
|
+
window.addEventListener("pointermove", (move) => {
|
|
2778
|
+
var _a;
|
|
2779
|
+
if (move.pointerId !== event.pointerId) return;
|
|
2780
|
+
if (!this.dragging && Math.hypot(move.clientX - startX, move.clientY - startY) < 5) return;
|
|
2781
|
+
this.dragging = true;
|
|
2782
|
+
const left = clamp(move.clientX - offsetX, EDGE, innerWidth - LAUNCHER - EDGE);
|
|
2783
|
+
const top = clamp(move.clientY - offsetY, EDGE, innerHeight - LAUNCHER - EDGE);
|
|
2784
|
+
this.launcherSpot = { x: (left + LAUNCHER / 2) / innerWidth, y: (top + LAUNCHER / 2) / innerHeight };
|
|
2785
|
+
(_a = this.shadow.querySelector(".launcher")) == null ? void 0 : _a.classList.add("dragging");
|
|
2786
|
+
this.placeLauncher();
|
|
2787
|
+
}, options);
|
|
2788
|
+
const end = (up) => {
|
|
2789
|
+
var _a;
|
|
2790
|
+
if (up.pointerId !== event.pointerId) return;
|
|
2791
|
+
stop.abort();
|
|
2792
|
+
if (!this.dragging) return;
|
|
2793
|
+
this.dragging = false;
|
|
2794
|
+
this.suppressClick = true;
|
|
2795
|
+
(_a = this.shadow.querySelector(".launcher")) == null ? void 0 : _a.classList.remove("dragging");
|
|
2796
|
+
try {
|
|
2797
|
+
localStorage.setItem(this.launcherKey, JSON.stringify(this.launcherSpot));
|
|
2798
|
+
} catch {
|
|
2799
|
+
}
|
|
2800
|
+
};
|
|
2801
|
+
window.addEventListener("pointerup", end, { ...options, capture: true });
|
|
2802
|
+
window.addEventListener("pointercancel", end, { ...options, capture: true });
|
|
2803
|
+
}
|
|
2804
|
+
pageComments() {
|
|
2805
|
+
return this.comments.filter((c) => this.onCurrentPage(c.pageUrl)).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime() || a.id.localeCompare(b.id));
|
|
2806
|
+
}
|
|
2807
|
+
closePopup(focus = true) {
|
|
2808
|
+
var _a;
|
|
2809
|
+
this.draft = null;
|
|
2810
|
+
this.activeId = null;
|
|
2811
|
+
this.placing = false;
|
|
2812
|
+
this.editing = null;
|
|
2813
|
+
this.text = "";
|
|
2814
|
+
this.replyText = "";
|
|
2815
|
+
this.render();
|
|
2816
|
+
if (focus) (_a = this.shadow.querySelector(".panel .primary, .launcher")) == null ? void 0 : _a.focus();
|
|
2817
|
+
}
|
|
2818
|
+
setDocked(docked) {
|
|
2819
|
+
var _a;
|
|
2820
|
+
this.docked = docked;
|
|
2821
|
+
try {
|
|
2822
|
+
localStorage.setItem(this.dockKey, docked ? "1" : "0");
|
|
2823
|
+
} catch {
|
|
2824
|
+
}
|
|
2825
|
+
this.render();
|
|
2826
|
+
(_a = this.shadow.querySelector(".detail")) == null ? void 0 : _a.focus({ preventScroll: true });
|
|
2827
|
+
}
|
|
2828
|
+
/** Comments for the chosen view: this page, or every page of the project. */
|
|
2829
|
+
scopeComments() {
|
|
2830
|
+
if (this.scope === "page") return this.pageComments();
|
|
2831
|
+
return [...this.comments].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime() || a.id.localeCompare(b.id));
|
|
2832
|
+
}
|
|
2833
|
+
/** Note numbers count per page, in the order the notes were made. */
|
|
2834
|
+
noteNumbers() {
|
|
2835
|
+
const counters = /* @__PURE__ */ new Map(), numbers = /* @__PURE__ */ new Map();
|
|
2836
|
+
for (const comment of [...this.comments].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime() || a.id.localeCompare(b.id))) {
|
|
2837
|
+
const page = this.onCurrentPage(comment.pageUrl) ? "" : comment.pageUrl;
|
|
2838
|
+
const next = (counters.get(page) ?? 0) + 1;
|
|
2839
|
+
counters.set(page, next);
|
|
2840
|
+
numbers.set(comment.id, next);
|
|
2841
|
+
}
|
|
2842
|
+
return numbers;
|
|
2843
|
+
}
|
|
2844
|
+
async mutate(next, write, done) {
|
|
2845
|
+
if (this.saving || this.initialLoad() || this.loadFailed || this.destroyed) return;
|
|
2846
|
+
if (this.loading) {
|
|
2847
|
+
++this.loadVersion;
|
|
2848
|
+
this.loading = false;
|
|
2849
|
+
}
|
|
2850
|
+
this.saving = true;
|
|
2851
|
+
this.error = "";
|
|
2852
|
+
this.render();
|
|
2853
|
+
try {
|
|
2854
|
+
await write();
|
|
2855
|
+
if (this.destroyed) return;
|
|
2856
|
+
this.comments = next;
|
|
2857
|
+
try {
|
|
2858
|
+
this.comments = normalizeComments(await this.storage.loadComments());
|
|
2859
|
+
} catch {
|
|
2860
|
+
this.error = "Saved. Could not refresh other comments. Use Refresh to try again.";
|
|
2861
|
+
}
|
|
2862
|
+
if (this.destroyed) return;
|
|
2863
|
+
this.queuedSnapshot = null;
|
|
2864
|
+
done();
|
|
2865
|
+
this.changed();
|
|
2866
|
+
this.noticeActivity();
|
|
2867
|
+
this.announce(this.error || "Changes saved.");
|
|
2868
|
+
} catch (error) {
|
|
2869
|
+
if (!this.destroyed) this.report(error);
|
|
2870
|
+
} finally {
|
|
2871
|
+
if (!this.destroyed) {
|
|
2872
|
+
this.saving = false;
|
|
2873
|
+
this.render();
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
toggleResolved(comment) {
|
|
2878
|
+
const updated = { ...comment, resolved: !comment.resolved };
|
|
2879
|
+
const next = this.getAllComments().map((c) => c.id === comment.id ? updated : c);
|
|
2880
|
+
void this.mutate(next, () => this.storage.setResolved ? this.storage.setResolved(comment.id, updated.resolved) : this.storage.upsertComment ? this.storage.upsertComment(updated) : this.storage.saveComments(next), () => {
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
deleteNote(comment) {
|
|
2884
|
+
const next = this.getAllComments().filter((c) => c.id !== comment.id);
|
|
2885
|
+
void this.mutate(next, () => this.storage.deleteComment ? this.storage.deleteComment(comment.id) : this.storage.saveComments(next), () => {
|
|
2886
|
+
this.activeId = null;
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
render() {
|
|
2890
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
2891
|
+
if (this.destroyed) return;
|
|
2892
|
+
if (this.composing) {
|
|
2893
|
+
this.renderDeferred = true;
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
if (this.revealed && this.activeId !== this.revealed.id) this.endReveal();
|
|
2897
|
+
this.syncLayer();
|
|
2898
|
+
if (!this.sidebarOpen || this.activeId || this.draft) this.hoverId = null;
|
|
2899
|
+
const active = this.shadow.activeElement;
|
|
2900
|
+
const focusKey = active instanceof HTMLElement ? active.dataset.focus : void 0;
|
|
2901
|
+
const selection = active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement ? [active.selectionStart, active.selectionEnd] : null;
|
|
2902
|
+
const scroll = ((_a = this.shadow.querySelector(".list")) == null ? void 0 : _a.scrollTop) ?? 0;
|
|
2903
|
+
const threadScroll = ((_b = this.shadow.querySelector(".thread-body")) == null ? void 0 : _b.scrollTop) ?? 0;
|
|
2904
|
+
const hadPanel = !!this.surface.querySelector(".panel");
|
|
2905
|
+
this.host.dataset.theme = this.settings.theme;
|
|
2906
|
+
this.surface.replaceChildren();
|
|
2907
|
+
this.pins.replaceChildren();
|
|
2908
|
+
const detail = this.visible ? this.detail() : null;
|
|
2909
|
+
const docked = !!detail && (this.docked || !!this.editing);
|
|
2910
|
+
const sidebar = this.visible && (docked || !this.config.hideSidebar);
|
|
2911
|
+
if (!this.config.hideButton && !sidebar) this.surface.append(this.launcher());
|
|
2912
|
+
if (this.visible) {
|
|
2913
|
+
if (sidebar) this.surface.append(this.sidebar(docked ? detail : null));
|
|
2914
|
+
if (detail && !docked) {
|
|
2915
|
+
const popover = element("div", "popover");
|
|
2916
|
+
popover.append(detail);
|
|
2917
|
+
this.surface.append(popover);
|
|
2918
|
+
(_c = this.surface.querySelector(".panel")) == null ? void 0 : _c.classList.add("behind");
|
|
2919
|
+
}
|
|
2920
|
+
this.renderPins();
|
|
2921
|
+
if (this.placing) {
|
|
2922
|
+
const notice = element("div", `notice${sidebar ? " beside-panel" : ""}${((_d = this.config.position) == null ? void 0 : _d.endsWith("left")) ? " left" : ""}`);
|
|
2923
|
+
notice.append(icon("pin"), element("span", "", `Click anywhere to leave a note, or Tab to an element and press Enter.`));
|
|
2924
|
+
const escape = element("kbd", "", "Esc");
|
|
2925
|
+
notice.append(escape, button("Cancel placement", "icon-button", () => this.closePopup(), "close"));
|
|
2926
|
+
this.surface.append(notice);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
(_e = this.surface.querySelector(".panel")) == null ? void 0 : _e.classList.toggle("entering", !hadPanel);
|
|
2930
|
+
const detailKey = !detail ? "" : this.editing ? this.editing : this.draft ? `draft:${this.draft.id}` : `thread:${this.activeId}`;
|
|
2931
|
+
detail == null ? void 0 : detail.classList.toggle("entering", (hadPanel || !docked) && detailKey !== this.detailKey);
|
|
2932
|
+
this.detailKey = detailKey;
|
|
2933
|
+
this.toasts.classList.toggle("beside-panel", sidebar);
|
|
2934
|
+
this.sidebarOpen = sidebar;
|
|
2935
|
+
this.pushPage();
|
|
2936
|
+
this.toasts.classList.toggle("left", !!((_f = this.config.position) == null ? void 0 : _f.endsWith("left")));
|
|
2937
|
+
this.toasts.classList.toggle("top", !!((_g = this.config.position) == null ? void 0 : _g.startsWith("top")));
|
|
2938
|
+
const list = this.shadow.querySelector(".list");
|
|
2939
|
+
if (list) list.scrollTop = scroll;
|
|
2940
|
+
const body = this.shadow.querySelector(".thread-body");
|
|
2941
|
+
if (body) body.scrollTop = threadScroll;
|
|
2942
|
+
if (focusKey) {
|
|
2943
|
+
const focus = Array.from(this.shadow.querySelectorAll("[data-focus]")).find((el) => el.dataset.focus === focusKey);
|
|
2944
|
+
focus == null ? void 0 : focus.focus({ preventScroll: true });
|
|
2945
|
+
if (selection && (focus instanceof HTMLInputElement || focus instanceof HTMLTextAreaElement)) {
|
|
2946
|
+
focus.setSelectionRange(selection[0], selection[1]);
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
this.positionPins();
|
|
2950
|
+
}
|
|
2951
|
+
launcher() {
|
|
2952
|
+
const launcher = button("Comments", `launcher${this.dragging ? " dragging" : ""}`, () => {
|
|
2953
|
+
if (this.suppressClick) {
|
|
2954
|
+
this.suppressClick = false;
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
if (this.visible) this.hide();
|
|
2958
|
+
else this.show();
|
|
2959
|
+
}, "comment");
|
|
2960
|
+
launcher.replaceChildren(icon("comment"));
|
|
2961
|
+
this.paintLauncher(launcher);
|
|
2962
|
+
launcher.dataset.focus = "launcher";
|
|
2963
|
+
launcher.setAttribute("aria-expanded", String(this.visible));
|
|
2964
|
+
launcher.title = "Comments. Drag to move.";
|
|
2965
|
+
launcher.addEventListener("pointerdown", (event) => this.startDrag(event));
|
|
2966
|
+
const count = this.pageComments().filter((c) => !c.resolved).length;
|
|
2967
|
+
launcher.setAttribute("aria-label", count ? `Comments, ${count} open` : "Comments");
|
|
2968
|
+
if (count) launcher.append(element("span", "badge", String(count)));
|
|
2969
|
+
const position = this.config.position ?? "bottom-right";
|
|
2970
|
+
if (position.endsWith("left")) {
|
|
2971
|
+
launcher.style.left = "24px";
|
|
2972
|
+
launcher.style.right = "auto";
|
|
2973
|
+
}
|
|
2974
|
+
if (position.startsWith("top")) {
|
|
2975
|
+
launcher.style.top = "24px";
|
|
2976
|
+
launcher.style.bottom = "auto";
|
|
2977
|
+
}
|
|
2978
|
+
if (this.launcherSpot) queueMicrotask(this.placeLauncher);
|
|
2979
|
+
return launcher;
|
|
2980
|
+
}
|
|
2981
|
+
heading(title, subtitle) {
|
|
2982
|
+
const head = element("div", "head");
|
|
2983
|
+
const brand = element("span", "brand-icon");
|
|
2984
|
+
brand.append(icon("comment"));
|
|
2985
|
+
const heading = element("div", "heading");
|
|
2986
|
+
heading.append(element("h2", "", title));
|
|
2987
|
+
if (subtitle) heading.append(element("p", "subtext", subtitle));
|
|
2988
|
+
const actions = element("div", "head-actions");
|
|
2989
|
+
const settings = button("Settings", "icon-button", () => this.openSettings(), "settings");
|
|
2990
|
+
settings.dataset.focus = "settings";
|
|
2991
|
+
actions.append(settings, this.userMenu(), button("Close comments", "icon-button", () => this.hide(), "close"));
|
|
2992
|
+
head.append(brand, heading, actions);
|
|
2993
|
+
return head;
|
|
2994
|
+
}
|
|
2995
|
+
menuItem(label, iconName, run, cls = "") {
|
|
2996
|
+
const entry = button(label, `menu-item ${cls}`, run, iconName);
|
|
2997
|
+
entry.setAttribute("role", "menuitem");
|
|
2998
|
+
return entry;
|
|
2999
|
+
}
|
|
3000
|
+
/** A button that opens a menu below it. A click outside or Escape closes the menu. */
|
|
3001
|
+
menuToggle(toggle, menu) {
|
|
3002
|
+
menu.hidden = true;
|
|
3003
|
+
menu.setAttribute("role", "menu");
|
|
3004
|
+
toggle.setAttribute("aria-haspopup", "menu");
|
|
3005
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
3006
|
+
toggle.addEventListener("click", () => {
|
|
3007
|
+
var _a;
|
|
3008
|
+
const open = menu.hidden;
|
|
3009
|
+
this.closeMenus();
|
|
3010
|
+
menu.hidden = !open;
|
|
3011
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
3012
|
+
if (open) (_a = menu.querySelector("button:not(:disabled)")) == null ? void 0 : _a.focus();
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
/** Closes every open menu. Returns the button of the menu that was open, so Escape can put focus back on it. */
|
|
3016
|
+
closeMenus() {
|
|
3017
|
+
let opener = null;
|
|
3018
|
+
for (const menu of this.shadow.querySelectorAll("[role=menu]")) menu.hidden = true;
|
|
3019
|
+
for (const toggle of this.shadow.querySelectorAll("[aria-haspopup=menu]")) {
|
|
3020
|
+
if (toggle.getAttribute("aria-expanded") === "true") opener = toggle;
|
|
3021
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
3022
|
+
}
|
|
3023
|
+
return opener;
|
|
3024
|
+
}
|
|
3025
|
+
/** The account button: the person's picture, and a menu with their name, settings and sign-out. */
|
|
3026
|
+
userMenu() {
|
|
3027
|
+
const hosted = this.storage instanceof HostedAdapter ? this.storage : null;
|
|
3028
|
+
if (hosted && !hosted.signedIn) {
|
|
3029
|
+
const signIn = button("Sign In", "quiet sign-in", () => {
|
|
3030
|
+
void this.signIn();
|
|
3031
|
+
}, "signin");
|
|
3032
|
+
signIn.title = "Sign in to join this project";
|
|
3033
|
+
return signIn;
|
|
3034
|
+
}
|
|
3035
|
+
const wrap = element("div", "menu-wrap");
|
|
3036
|
+
const toggle = element("button", "user-button");
|
|
3037
|
+
toggle.type = "button";
|
|
3038
|
+
toggle.dataset.focus = "user";
|
|
3039
|
+
toggle.setAttribute("aria-label", `Account: ${this.user.name}`);
|
|
3040
|
+
toggle.title = this.user.name;
|
|
3041
|
+
toggle.append(avatar(this.user.name, this.user.avatarUrl));
|
|
3042
|
+
const menu = element("div", "pop-menu user-menu");
|
|
3043
|
+
const card = element("div", "menu-user");
|
|
3044
|
+
const about = element("div", "menu-user-text");
|
|
3045
|
+
about.append(element("strong", "", this.user.name), element("small", "", hosted ? "Signed in to this project" : this.managedUser ? "Your project account" : this.persistentGuest ? "Guest in this browser" : "Guest for this page view"));
|
|
3046
|
+
card.append(avatar(this.user.name, this.user.avatarUrl), about);
|
|
3047
|
+
menu.append(card);
|
|
3048
|
+
if (!hosted && !this.managedUser) menu.append(this.menuItem("Edit Your Name", "edit", () => {
|
|
3049
|
+
var _a;
|
|
3050
|
+
this.editing = "identity";
|
|
3051
|
+
this.draft = null;
|
|
3052
|
+
this.activeId = null;
|
|
3053
|
+
this.placing = false;
|
|
3054
|
+
this.render();
|
|
3055
|
+
(_a = this.shadow.querySelector("#guest-name")) == null ? void 0 : _a.focus();
|
|
3056
|
+
}));
|
|
3057
|
+
menu.append(this.menuItem("Settings", "settings", () => this.openSettings()));
|
|
3058
|
+
if (hosted) {
|
|
3059
|
+
const out = this.menuItem("Sign Out", "signout", () => {
|
|
3060
|
+
void this.signOut();
|
|
3061
|
+
});
|
|
3062
|
+
out.disabled = this.saving;
|
|
3063
|
+
menu.append(out);
|
|
3064
|
+
}
|
|
3065
|
+
const note = element("p", "menu-note");
|
|
3066
|
+
note.append(
|
|
3067
|
+
element("span", "storage-dot"),
|
|
3068
|
+
element("span", "", hosted ? "Comments sync with your team" : this.storage instanceof LocalStorageAdapter ? "Comments are saved in this browser" : "Comments use connected storage")
|
|
3069
|
+
);
|
|
3070
|
+
menu.append(note);
|
|
3071
|
+
this.menuToggle(toggle, menu);
|
|
3072
|
+
wrap.append(toggle, menu);
|
|
3073
|
+
return wrap;
|
|
3074
|
+
}
|
|
3075
|
+
openSettings() {
|
|
3076
|
+
var _a;
|
|
3077
|
+
this.editing = "settings";
|
|
3078
|
+
this.draft = null;
|
|
3079
|
+
this.activeId = null;
|
|
3080
|
+
this.placing = false;
|
|
3081
|
+
this.render();
|
|
3082
|
+
(_a = this.shadow.querySelector(".detail")) == null ? void 0 : _a.focus({ preventScroll: true });
|
|
3083
|
+
}
|
|
3084
|
+
/** Header of a detail view: a back link and close on one bar, then the title. It reads as a step inside the list. */
|
|
3085
|
+
detailHeading(title, subtitle, extra) {
|
|
3086
|
+
const wrap = element("div", "detail-head");
|
|
3087
|
+
const bar = element("div", "detail-bar");
|
|
3088
|
+
const back = button("Back to comments", "back-link", () => this.closePopup(), "back");
|
|
3089
|
+
back.querySelector("span").textContent = "Comments";
|
|
3090
|
+
bar.append(back, button("Close comments", "icon-button", () => this.hide(), "close"));
|
|
3091
|
+
const titleRow = element("div", "detail-title");
|
|
3092
|
+
titleRow.append(element("h2", "", title));
|
|
3093
|
+
if (extra) titleRow.append(extra);
|
|
3094
|
+
wrap.append(bar, titleRow);
|
|
3095
|
+
if (subtitle) wrap.append(element("p", "subtext", subtitle));
|
|
3096
|
+
return wrap;
|
|
3097
|
+
}
|
|
3098
|
+
/** Header of a comment card: icon, title, status line, a menu for less frequent actions, and close. */
|
|
3099
|
+
cardHeading(title, subtitle, name, hint = "", remove) {
|
|
3100
|
+
const wrap = element("div", "card-head");
|
|
3101
|
+
if (this.docked) {
|
|
3102
|
+
const bar = element("div", "detail-bar");
|
|
3103
|
+
const back = button("Back to comments", "back-link", () => this.closePopup(), "back");
|
|
3104
|
+
back.querySelector("span").textContent = "Comments";
|
|
3105
|
+
bar.append(back, button("Close comments", "icon-button", () => this.hide(), "close"));
|
|
3106
|
+
wrap.append(bar);
|
|
3107
|
+
}
|
|
3108
|
+
const head = element("div", "pop-head");
|
|
3109
|
+
if (hint) head.title = hint;
|
|
3110
|
+
const tile = element("span", "pop-icon");
|
|
3111
|
+
tile.append(icon(name));
|
|
3112
|
+
const text = element("div", "pop-title");
|
|
3113
|
+
text.append(element("h2", "", title), element("p", "pop-sub", subtitle));
|
|
3114
|
+
const menu = element("div", "pop-menu");
|
|
3115
|
+
const item = this.menuItem.bind(this);
|
|
3116
|
+
if (remove) {
|
|
3117
|
+
const del = item("Delete Comment", "trash", () => {
|
|
3118
|
+
if (del.dataset.confirm) {
|
|
3119
|
+
remove();
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
del.dataset.confirm = "1";
|
|
3123
|
+
del.querySelector("span").textContent = "Click Again to Delete";
|
|
3124
|
+
}, "danger");
|
|
3125
|
+
del.disabled = this.saving;
|
|
3126
|
+
menu.append(del);
|
|
3127
|
+
}
|
|
3128
|
+
head.append(tile, text);
|
|
3129
|
+
if (menu.childElementCount) {
|
|
3130
|
+
const more = button("More actions", "icon-button more", () => {
|
|
3131
|
+
}, "more");
|
|
3132
|
+
this.menuToggle(more, menu);
|
|
3133
|
+
head.append(more);
|
|
3134
|
+
}
|
|
3135
|
+
if (!this.docked) head.append(button("Close", "icon-button", () => this.closePopup(), "close"));
|
|
3136
|
+
if (menu.childElementCount) head.append(menu);
|
|
3137
|
+
wrap.append(head);
|
|
3138
|
+
return wrap;
|
|
3139
|
+
}
|
|
3140
|
+
errorBox() {
|
|
3141
|
+
const error = element("div", "error", this.error);
|
|
3142
|
+
if (this.loadFailed || this.error.startsWith("Saved.")) error.append(button("Try Again", "quiet", () => {
|
|
3143
|
+
void this.refresh();
|
|
3144
|
+
}));
|
|
3145
|
+
return error;
|
|
3146
|
+
}
|
|
3147
|
+
/** The open comment, the new comment form, or the guest name form. Each fills the sidebar. */
|
|
3148
|
+
detail() {
|
|
3149
|
+
if (this.editing === "settings") return this.settingsView();
|
|
3150
|
+
if (this.editing === "identity") return this.identityForm();
|
|
3151
|
+
if (this.draft) return this.newCommentForm();
|
|
3152
|
+
const comment = this.comments.find((c) => c.id === this.activeId && this.onCurrentPage(c.pageUrl));
|
|
3153
|
+
return comment ? this.thread(comment) : null;
|
|
3154
|
+
}
|
|
3155
|
+
sidebar(detail) {
|
|
3156
|
+
var _a;
|
|
3157
|
+
const panel = element("section", `panel${((_a = this.config.position) == null ? void 0 : _a.endsWith("left")) ? " left" : ""}${this.placing ? " placing" : ""}`);
|
|
3158
|
+
panel.setAttribute("aria-label", "Comments");
|
|
3159
|
+
if (detail) {
|
|
3160
|
+
panel.append(detail);
|
|
3161
|
+
return panel;
|
|
3162
|
+
}
|
|
3163
|
+
panel.append(this.heading("Comments", ""));
|
|
3164
|
+
const toolbar = element("div", "toolbar");
|
|
3165
|
+
const add = button("Add Comment", "primary", () => this.startComment(), "plus");
|
|
3166
|
+
add.disabled = this.initialLoad() || this.loadFailed || this.saving;
|
|
3167
|
+
const refresh = button("Refresh comments", `icon-button outlined${this.loading ? " busy" : ""}`, () => {
|
|
3168
|
+
void this.refresh();
|
|
3169
|
+
}, "refresh");
|
|
3170
|
+
refresh.disabled = this.loading || this.saving;
|
|
3171
|
+
toolbar.append(add, refresh);
|
|
3172
|
+
panel.append(toolbar);
|
|
3173
|
+
panel.append(this.scopeBar());
|
|
3174
|
+
const comments = this.scopeComments();
|
|
3175
|
+
if (comments.length) panel.append(this.searchField(), this.peopleBar());
|
|
3176
|
+
panel.append(this.statusTabs());
|
|
3177
|
+
if (this.error) panel.append(this.errorBox());
|
|
3178
|
+
panel.append(this.commentList(comments));
|
|
3179
|
+
const footer = this.footer();
|
|
3180
|
+
if (footer) panel.append(footer);
|
|
3181
|
+
panel.addEventListener("keydown", (event) => {
|
|
3182
|
+
const target = event.composedPath()[0];
|
|
3183
|
+
if (event.key !== "/" || event.ctrlKey || event.metaKey || event.altKey || event.isComposing) return;
|
|
3184
|
+
if (target instanceof Element && target.closest("input,textarea,select")) return;
|
|
3185
|
+
const field = this.shadow.querySelector(".search input");
|
|
3186
|
+
if (field) {
|
|
3187
|
+
event.preventDefault();
|
|
3188
|
+
field.focus();
|
|
3189
|
+
field.select();
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
return panel;
|
|
3193
|
+
}
|
|
3194
|
+
/** Search, the Open/Resolved tabs and the author choices for the notes list. */
|
|
3195
|
+
filterContext(scope = this.scope) {
|
|
3196
|
+
return { userId: this.user.id, includePage: scope === "all", elementOf: (c) => this.onCurrentPage(c.pageUrl) ? this.elementInfo(c) : c.element };
|
|
3197
|
+
}
|
|
3198
|
+
restoreFilters() {
|
|
3199
|
+
try {
|
|
3200
|
+
const saved = JSON.parse(sessionStorage.getItem(this.filterKey) || "null");
|
|
3201
|
+
if (!saved) return;
|
|
3202
|
+
this.filters = readFilters(saved);
|
|
3203
|
+
this.scope = saved.scope === "all" ? "all" : "page";
|
|
3204
|
+
} catch {
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
saveFilters() {
|
|
3208
|
+
try {
|
|
3209
|
+
sessionStorage.setItem(this.filterKey, JSON.stringify({ ...this.filters, scope: this.scope }));
|
|
3210
|
+
} catch {
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
/** Changes the filters, keeps them for this tab and says how many notes are shown. */
|
|
3214
|
+
setFilters(patch, scope) {
|
|
3215
|
+
this.filters = { ...this.filters, ...patch };
|
|
3216
|
+
if (scope) this.scope = scope;
|
|
3217
|
+
this.saveFilters();
|
|
3218
|
+
this.render();
|
|
3219
|
+
this.announceResults();
|
|
3220
|
+
}
|
|
3221
|
+
announceResults() {
|
|
3222
|
+
const all = this.scopeComments();
|
|
3223
|
+
if (!this.sidebarOpen || !all.length) return;
|
|
3224
|
+
const shown = filterNotes(all, this.filters, this.filterContext()).length;
|
|
3225
|
+
this.announce(shown ? `${shown} ${shown === 1 ? "note" : "notes"} shown.` : "No notes match.");
|
|
3226
|
+
}
|
|
3227
|
+
/** Rebuilds the counts, the list and the pins, not the search field, so typing keeps its undo history and IME state. */
|
|
3228
|
+
refreshResults() {
|
|
3229
|
+
const comments = this.scopeComments();
|
|
3230
|
+
const swap = (selector, next) => {
|
|
3231
|
+
var _a;
|
|
3232
|
+
return (_a = this.shadow.querySelector(selector)) == null ? void 0 : _a.replaceWith(next);
|
|
3233
|
+
};
|
|
3234
|
+
swap(".scope", this.scopeBar());
|
|
3235
|
+
swap(".people", this.peopleBar());
|
|
3236
|
+
swap(".tabs", this.statusTabs());
|
|
3237
|
+
swap(".list", this.commentList(comments));
|
|
3238
|
+
this.pins.replaceChildren();
|
|
3239
|
+
this.renderPins();
|
|
3240
|
+
this.positionPins();
|
|
3241
|
+
}
|
|
3242
|
+
/** Only the search, Mine and author apply to these counts: each count is what that view shows. */
|
|
3243
|
+
scopeBar() {
|
|
3244
|
+
const scopes = element("div", "scope");
|
|
3245
|
+
scopes.setAttribute("role", "group");
|
|
3246
|
+
scopes.setAttribute("aria-label", "Show comments from");
|
|
3247
|
+
const people = { ...this.filters, status: "all" };
|
|
3248
|
+
for (const [scope, label] of [["page", "This Page"], ["all", "All Pages"]]) {
|
|
3249
|
+
const notes = scope === "page" ? this.pageComments() : this.comments;
|
|
3250
|
+
const count = filterNotes(notes, people, this.filterContext(scope)).length;
|
|
3251
|
+
const option = button(label, "scope-option", () => this.setFilters({}, scope));
|
|
3252
|
+
option.setAttribute("aria-label", `${label}, ${count} ${count === 1 ? "note" : "notes"}`);
|
|
3253
|
+
option.setAttribute("aria-pressed", String(this.scope === scope));
|
|
3254
|
+
option.dataset.focus = `scope-${scope}`;
|
|
3255
|
+
option.append(element("span", "count", String(count)));
|
|
3256
|
+
scopes.append(option);
|
|
3257
|
+
}
|
|
3258
|
+
return scopes;
|
|
3259
|
+
}
|
|
3260
|
+
searchField() {
|
|
3261
|
+
const search = element("div", "search");
|
|
3262
|
+
search.setAttribute("role", "search");
|
|
3263
|
+
const input = element("input");
|
|
3264
|
+
input.type = "search";
|
|
3265
|
+
input.placeholder = "Search notes, replies, people…";
|
|
3266
|
+
input.setAttribute("aria-label", "Search comments");
|
|
3267
|
+
input.value = this.filters.query;
|
|
3268
|
+
input.dataset.focus = "search";
|
|
3269
|
+
input.maxLength = 200;
|
|
3270
|
+
input.setAttribute("aria-keyshortcuts", "/");
|
|
3271
|
+
input.autocomplete = "off";
|
|
3272
|
+
input.spellcheck = false;
|
|
3273
|
+
const clear = button("Clear search", "icon-button search-clear", () => {
|
|
3274
|
+
input.value = "";
|
|
3275
|
+
apply(true);
|
|
3276
|
+
input.focus();
|
|
3277
|
+
}, "close");
|
|
3278
|
+
clear.hidden = !input.value;
|
|
3279
|
+
const apply = (now) => {
|
|
3280
|
+
clearTimeout(this.searchTimer);
|
|
3281
|
+
clear.hidden = !input.value;
|
|
3282
|
+
const run = () => {
|
|
3283
|
+
if (this.destroyed || this.filters.query === input.value) return;
|
|
3284
|
+
this.filters = { ...this.filters, query: input.value };
|
|
3285
|
+
this.saveFilters();
|
|
3286
|
+
this.refreshResults();
|
|
3287
|
+
this.announceResults();
|
|
3288
|
+
};
|
|
3289
|
+
if (now) run();
|
|
3290
|
+
else this.searchTimer = setTimeout(run, 180);
|
|
3291
|
+
};
|
|
3292
|
+
input.addEventListener("input", () => apply(false));
|
|
3293
|
+
input.addEventListener("keydown", (event) => {
|
|
3294
|
+
if (event.key === "Escape" && input.value && !event.isComposing) {
|
|
3295
|
+
event.preventDefault();
|
|
3296
|
+
input.value = "";
|
|
3297
|
+
apply(true);
|
|
3298
|
+
} else if (event.key === "Enter" && !event.isComposing) {
|
|
3299
|
+
event.preventDefault();
|
|
3300
|
+
apply(true);
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
search.append(icon("search"), input, clear);
|
|
3304
|
+
return search;
|
|
3305
|
+
}
|
|
3306
|
+
/** Mine, the author list and Clear Filters. */
|
|
3307
|
+
peopleBar() {
|
|
3308
|
+
const bar = element("div", "people");
|
|
3309
|
+
bar.setAttribute("role", "group");
|
|
3310
|
+
bar.setAttribute("aria-label", "Filter by person");
|
|
3311
|
+
const counts = filterCounts(this.scopeComments(), this.filters, this.filterContext());
|
|
3312
|
+
const hosted = this.storage instanceof HostedAdapter ? this.storage : null;
|
|
3313
|
+
if (!hosted || hosted.signedIn) {
|
|
3314
|
+
const mine = button("Mine", "filter-chip", () => this.setFilters({ mine: !this.filters.mine, author: "" }));
|
|
3315
|
+
mine.title = "Only notes you started";
|
|
3316
|
+
mine.dataset.focus = "filter-mine";
|
|
3317
|
+
mine.setAttribute("aria-label", `Mine, ${counts.mine} ${counts.mine === 1 ? "note" : "notes"}`);
|
|
3318
|
+
mine.setAttribute("aria-pressed", String(this.filters.mine));
|
|
3319
|
+
mine.append(element("span", "count", String(counts.mine)));
|
|
3320
|
+
bar.append(mine);
|
|
3321
|
+
}
|
|
3322
|
+
const authors = counts.authors;
|
|
3323
|
+
if (this.filters.author && !authors.some((a) => a.key === this.filters.author)) authors.push({ key: this.filters.author, name: "Earlier author", count: 0 });
|
|
3324
|
+
if (authors.length > 1 || this.filters.author) {
|
|
3325
|
+
const wrap = element("span", `select${this.filters.author ? " chosen" : ""}`);
|
|
3326
|
+
const select = element("select");
|
|
3327
|
+
select.setAttribute("aria-label", "Author");
|
|
3328
|
+
select.dataset.focus = "filter-author";
|
|
3329
|
+
select.append(new Option("All Authors", ""));
|
|
3330
|
+
for (const author of authors) select.append(new Option(`${author.name} (${author.count})`, author.key));
|
|
3331
|
+
select.value = this.filters.author;
|
|
3332
|
+
select.addEventListener("change", () => this.setFilters({ author: select.value, mine: false }));
|
|
3333
|
+
wrap.append(select);
|
|
3334
|
+
bar.append(wrap);
|
|
3335
|
+
}
|
|
3336
|
+
if (filtersActive(this.filters)) {
|
|
3337
|
+
const clear = button("Clear Filters", "link-button", () => this.clearAllFilters());
|
|
3338
|
+
clear.dataset.focus = "filter-clear";
|
|
3339
|
+
bar.append(clear);
|
|
3340
|
+
}
|
|
3341
|
+
bar.hidden = !bar.childElementCount;
|
|
3342
|
+
return bar;
|
|
3343
|
+
}
|
|
3344
|
+
statusTabs() {
|
|
3345
|
+
const tabs = element("div", "tabs");
|
|
3346
|
+
tabs.setAttribute("role", "group");
|
|
3347
|
+
tabs.setAttribute("aria-label", "Filter by status");
|
|
3348
|
+
const counts = filterCounts(this.scopeComments(), this.filters, this.filterContext());
|
|
3349
|
+
for (const [status, label] of [["open", "Open"], ["resolved", "Resolved"], ["all", "All"]]) {
|
|
3350
|
+
const tab = button(label, "tab", () => this.setFilters({ status }));
|
|
3351
|
+
tab.setAttribute("aria-label", `${label}, ${counts[status]} ${counts[status] === 1 ? "note" : "notes"}`);
|
|
3352
|
+
tab.setAttribute("aria-pressed", String(this.filters.status === status));
|
|
3353
|
+
tab.dataset.focus = `tab-${status}`;
|
|
3354
|
+
tab.append(element("span", "count", String(counts[status])));
|
|
3355
|
+
tabs.append(tab);
|
|
3356
|
+
}
|
|
3357
|
+
return tabs;
|
|
3358
|
+
}
|
|
3359
|
+
clearAllFilters() {
|
|
3360
|
+
var _a;
|
|
3361
|
+
this.setFilters(clearFilters(this.scopeComments()));
|
|
3362
|
+
(_a = this.shadow.querySelector(".search input, .panel .primary")) == null ? void 0 : _a.focus();
|
|
3363
|
+
}
|
|
3364
|
+
commentList(comments) {
|
|
3365
|
+
var _a;
|
|
3366
|
+
const list = element("div", "list");
|
|
3367
|
+
list.setAttribute("aria-busy", String(this.loading));
|
|
3368
|
+
const filtered = filterNotes(comments, this.filters, this.filterContext());
|
|
3369
|
+
const pageScope = this.scope === "page";
|
|
3370
|
+
if (this.initialLoad()) list.append(this.empty("Loading your notes", "Your comments will appear here in a moment."));
|
|
3371
|
+
else if (this.loadFailed) list.append(this.empty("Could not load comments", "Your stored notes have not been changed. Try loading them again."));
|
|
3372
|
+
else if (!comments.length) {
|
|
3373
|
+
list.append(this.empty(
|
|
3374
|
+
"Good ideas start here",
|
|
3375
|
+
pageScope ? "Pin a thought to any part of this page. Keep the conversation in context." : "Comments from every page of this site appear here.",
|
|
3376
|
+
pageScope ? button("Leave the First Note", "quiet", () => this.startComment()) : void 0
|
|
3377
|
+
));
|
|
3378
|
+
} else if (!filtered.length) {
|
|
3379
|
+
const query = this.filters.query.trim();
|
|
3380
|
+
const why = query ? `Nothing ${pageScope ? "on this page" : "on any page"} matches “${query.length > 60 ? `${query.slice(0, 60)}…` : query}”.` : filtersActive(this.filters) ? "No notes fit these filters." : this.filters.status === "open" ? "Every note here is resolved." : "No note here is resolved yet.";
|
|
3381
|
+
list.append(this.empty("No notes match", why, button("Clear Filters", "quiet", () => this.clearAllFilters())));
|
|
3382
|
+
} else {
|
|
3383
|
+
const numbers = this.noteNumbers();
|
|
3384
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3385
|
+
for (const comment of [...filtered].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())) {
|
|
3386
|
+
const key2 = this.onCurrentPage(comment.pageUrl) ? "" : comment.pageUrl;
|
|
3387
|
+
groups.set(key2, [...groups.get(key2) ?? [], comment]);
|
|
3388
|
+
}
|
|
3389
|
+
const ordered = [...groups].sort(([a], [b]) => Number(a !== "") - Number(b !== ""));
|
|
3390
|
+
for (const [page, notes] of ordered) {
|
|
3391
|
+
const title = element("div", "list-title");
|
|
3392
|
+
if (pageScope) title.textContent = "On this page";
|
|
3393
|
+
else {
|
|
3394
|
+
const path = pageLabel(page || this.currentPage());
|
|
3395
|
+
const name = (page ? (_a = notes.find((c) => c.pageTitle)) == null ? void 0 : _a.pageTitle : pageTitle()) ?? path;
|
|
3396
|
+
const label = element("span", name === path ? "page-path" : "page-name", name);
|
|
3397
|
+
label.title = path;
|
|
3398
|
+
title.append(label);
|
|
3399
|
+
if (name !== path) {
|
|
3400
|
+
const where = element("span", "page-path sub", path);
|
|
3401
|
+
where.title = path;
|
|
3402
|
+
title.append(where);
|
|
3403
|
+
}
|
|
3404
|
+
if (!page) title.append(element("span", "here", "This page"));
|
|
3405
|
+
}
|
|
3406
|
+
list.append(title);
|
|
3407
|
+
for (const comment of notes) list.append(this.noteRow(comment, numbers.get(comment.id) ?? 0, !!page));
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
return list;
|
|
3411
|
+
}
|
|
3412
|
+
noteRow(comment, number, elsewhere) {
|
|
3413
|
+
const page = comment.pageTitle || pageLabel(comment.pageUrl);
|
|
3414
|
+
const label = elsewhere ? `Open comment by ${comment.author} on ${page}` : `Open comment by ${comment.author}`;
|
|
3415
|
+
const note = button(label, `note${this.activeId === comment.id ? " current" : ""}`, () => this.navigateToComment(comment.id));
|
|
3416
|
+
note.replaceChildren();
|
|
3417
|
+
const words2 = parseQuery(this.filters.query);
|
|
3418
|
+
const main = element("div", "note-main");
|
|
3419
|
+
main.append(this.messageHeader(comment.author, comment.timestamp, comment.avatarUrl, comment.via, words2), marked(element("p", "note-text"), comment.text, words2));
|
|
3420
|
+
const match = words2.length ? matchNote(comment, words2, this.filterContext()) : null;
|
|
3421
|
+
const where = (match == null ? void 0 : match.reply) ? [`${match.reply.author}: `, match.reply.text] : (match == null ? void 0 : match.element) ? ["Element: ", match.element] : null;
|
|
3422
|
+
if (where) {
|
|
3423
|
+
const line = element("p", "note-match");
|
|
3424
|
+
line.append(icon((match == null ? void 0 : match.reply) ? "comment" : "pin"), element("span", "note-match-label", where[0]));
|
|
3425
|
+
line.append(marked(element("span", "note-match-text"), excerpt(where[1], words2), words2));
|
|
3426
|
+
main.append(line);
|
|
3427
|
+
}
|
|
3428
|
+
const state = elsewhere ? "shown" : this.elementState(comment);
|
|
3429
|
+
if (comment.replies.length || elsewhere || state !== "shown") {
|
|
3430
|
+
const footer = element("div", "note-footer");
|
|
3431
|
+
if (comment.replies.length) footer.append(icon("comment"), element("span", "", `${comment.replies.length} ${comment.replies.length === 1 ? "reply" : "replies"}`));
|
|
3432
|
+
if (state !== "shown") {
|
|
3433
|
+
const info = this.elementInfo(comment);
|
|
3434
|
+
const flag = element("span", `hidden-flag ${state}`, state === "removed" ? "Element removed" : (info == null ? void 0 : info.place) ? `Hidden in ${info.place}` : "Element hidden");
|
|
3435
|
+
flag.prepend(icon(state === "removed" ? "unlink" : "eyeOff"));
|
|
3436
|
+
flag.title = info ? `${info.label}${info.place ? ` in ${info.place}` : ""}. Open the comment to see where it was.` : "Open the comment to see where it was.";
|
|
3437
|
+
footer.append(flag);
|
|
3438
|
+
}
|
|
3439
|
+
if (elsewhere) {
|
|
3440
|
+
const go = element("span", "go", "Go to Page");
|
|
3441
|
+
go.append(icon("open"));
|
|
3442
|
+
footer.append(go);
|
|
3443
|
+
}
|
|
3444
|
+
main.append(footer);
|
|
3445
|
+
}
|
|
3446
|
+
const meta = element("span", "note-meta");
|
|
3447
|
+
const status = element("span", `status${comment.resolved ? " resolved" : ""}`, comment.resolved ? "Resolved" : "Open");
|
|
3448
|
+
if (comment.resolved) status.prepend(icon("check"));
|
|
3449
|
+
else status.prepend(element("span", "status-dot"));
|
|
3450
|
+
meta.append(element("span", "note-number", `#${number}`), status);
|
|
3451
|
+
note.append(main, meta);
|
|
3452
|
+
if (!elsewhere) {
|
|
3453
|
+
note.addEventListener("pointerenter", () => this.hoverNote(comment.id));
|
|
3454
|
+
note.addEventListener("pointerleave", () => this.hoverNote(null));
|
|
3455
|
+
note.addEventListener("focus", () => this.hoverNote(comment.id));
|
|
3456
|
+
note.addEventListener("blur", () => this.hoverNote(null));
|
|
3457
|
+
}
|
|
3458
|
+
return note;
|
|
3459
|
+
}
|
|
3460
|
+
empty(title, text, action) {
|
|
3461
|
+
const empty = element("div", "empty");
|
|
3462
|
+
const wrap = element("div");
|
|
3463
|
+
const mark = element("div", "empty-icon");
|
|
3464
|
+
mark.append(icon(title === "No notes match" ? "search" : "comment"));
|
|
3465
|
+
wrap.append(mark, element("h3", "", title), element("p", "", text));
|
|
3466
|
+
if (action) wrap.append(action);
|
|
3467
|
+
empty.append(wrap);
|
|
3468
|
+
return empty;
|
|
3469
|
+
}
|
|
3470
|
+
/** A new npm version is the one thing left at the bottom. The account and settings live in the header. */
|
|
3471
|
+
footer() {
|
|
3472
|
+
if (!this.update) return null;
|
|
3473
|
+
const footer = element("div", "footer update-bar");
|
|
3474
|
+
footer.setAttribute("role", "status");
|
|
3475
|
+
const mark = element("span", "update-icon");
|
|
3476
|
+
mark.append(icon("refresh"));
|
|
3477
|
+
const text = element("div", "update-text");
|
|
3478
|
+
text.append(element("strong", "", `Version ${this.update} is available`), element("span", "", `You have ${VERSION}. Update the package, then reload.`));
|
|
3479
|
+
const command = element("div", "update-command");
|
|
3480
|
+
const code = element("code", "", UPDATE_COMMAND);
|
|
3481
|
+
code.title = UPDATE_COMMAND;
|
|
3482
|
+
const copy = button(`Copy the command to update to version ${this.update}`, "quiet update", () => {
|
|
3483
|
+
void this.copyUpdate();
|
|
3484
|
+
});
|
|
3485
|
+
copy.querySelector("span").textContent = "Copy";
|
|
3486
|
+
command.append(code, copy);
|
|
3487
|
+
footer.append(mark, text, button("Hide until the next version", "icon-button", () => this.dismissUpdate(), "close"), command);
|
|
3488
|
+
return footer;
|
|
3489
|
+
}
|
|
3490
|
+
messageHeader(name, timestamp, url, via, words2 = []) {
|
|
3491
|
+
const head = element("div", "note-header");
|
|
3492
|
+
const time = element("time", "time", relativeTime(timestamp));
|
|
3493
|
+
time.dateTime = timestamp.toISOString();
|
|
3494
|
+
time.title = timestamp.toLocaleString();
|
|
3495
|
+
head.append(avatar(name, url), marked(element("span", "name"), name, words2));
|
|
3496
|
+
if (via) {
|
|
3497
|
+
const source = element("span", "via", `via ${via}`);
|
|
3498
|
+
source.title = `Posted through MCP by ${via}`;
|
|
3499
|
+
head.append(source);
|
|
3500
|
+
}
|
|
3501
|
+
head.append(time);
|
|
3502
|
+
return head;
|
|
3503
|
+
}
|
|
3504
|
+
view(title, className = "") {
|
|
3505
|
+
const view = element("div", `detail ${className}`);
|
|
3506
|
+
view.setAttribute("role", "group");
|
|
3507
|
+
view.setAttribute("aria-label", title);
|
|
3508
|
+
view.tabIndex = -1;
|
|
3509
|
+
return view;
|
|
3510
|
+
}
|
|
3511
|
+
composer(isReply) {
|
|
3512
|
+
const form = element("form", "compose");
|
|
3513
|
+
const textarea = element("textarea");
|
|
3514
|
+
textarea.setAttribute("aria-label", isReply ? "Reply" : "Comment");
|
|
3515
|
+
textarea.placeholder = isReply ? "Keep the conversation going…" : "What’s on your mind?";
|
|
3516
|
+
textarea.maxLength = 1e4;
|
|
3517
|
+
textarea.required = true;
|
|
3518
|
+
textarea.value = isReply ? this.replyText : this.text;
|
|
3519
|
+
textarea.dataset.focus = isReply ? "reply" : "comment";
|
|
3520
|
+
textarea.disabled = this.saving;
|
|
3521
|
+
const bar = element("div", "compose-bar");
|
|
3522
|
+
const hint = element("span", "compose-hint", "⌘ / Ctrl + Enter to send");
|
|
3523
|
+
const submit = button(this.saving ? "Saving…" : isReply ? "Send Reply" : "Post Comment", "primary", () => {
|
|
3524
|
+
}, "arrow");
|
|
3525
|
+
submit.type = "submit";
|
|
3526
|
+
submit.disabled = this.saving || !textarea.value.trim();
|
|
3527
|
+
textarea.addEventListener("input", () => {
|
|
3528
|
+
if (isReply) this.replyText = textarea.value;
|
|
3529
|
+
else this.text = textarea.value;
|
|
3530
|
+
submit.disabled = this.saving || !textarea.value.trim();
|
|
3531
|
+
});
|
|
3532
|
+
textarea.addEventListener("keydown", (event) => {
|
|
3533
|
+
if ((event.ctrlKey || event.metaKey) && event.key === "Enter" && !event.isComposing) {
|
|
3534
|
+
event.preventDefault();
|
|
3535
|
+
if (!submit.disabled) form.requestSubmit();
|
|
3536
|
+
}
|
|
3537
|
+
});
|
|
3538
|
+
form.addEventListener("submit", (event) => {
|
|
3539
|
+
event.preventDefault();
|
|
3540
|
+
if (!this.saving) {
|
|
3541
|
+
if (isReply) this.submitReply();
|
|
3542
|
+
else this.submitComment();
|
|
3543
|
+
}
|
|
3544
|
+
});
|
|
3545
|
+
bar.append(hint, submit);
|
|
3546
|
+
form.append(textarea, bar);
|
|
3547
|
+
return form;
|
|
3548
|
+
}
|
|
3549
|
+
newCommentForm() {
|
|
3550
|
+
const view = this.view("New comment", "new-comment");
|
|
3551
|
+
view.append(this.cardHeading("New comment", `Commenting as ${this.user.name}`, "plus"));
|
|
3552
|
+
if (this.error) view.append(this.errorBox());
|
|
3553
|
+
view.append(this.composer(false));
|
|
3554
|
+
return view;
|
|
3555
|
+
}
|
|
3556
|
+
thread(comment) {
|
|
3557
|
+
var _a, _b;
|
|
3558
|
+
const view = this.view("Comment thread", `thread${comment.resolved ? " is-resolved" : ""}`);
|
|
3559
|
+
const canDelete = ((_b = (_a = this.config).canDelete) == null ? void 0 : _b.call(_a, comment, this.getUser())) ?? this.manages(comment);
|
|
3560
|
+
view.append(this.cardHeading(
|
|
3561
|
+
`Note #${this.pageComments().indexOf(comment) + 1}`,
|
|
3562
|
+
comment.resolved ? "Resolved conversation" : "Open conversation",
|
|
3563
|
+
"comment",
|
|
3564
|
+
this.canMove(comment) ? "Drag its pin to move it." : "",
|
|
3565
|
+
canDelete ? () => this.deleteNote(comment) : void 0
|
|
3566
|
+
));
|
|
3567
|
+
if (this.error) view.append(this.errorBox());
|
|
3568
|
+
if (comment.selector) view.append(this.anchorStatus(comment, this.elementState(comment)));
|
|
3569
|
+
const body = element("div", "thread-body");
|
|
3570
|
+
for (const message of [comment, ...comment.replies]) {
|
|
3571
|
+
const row = element("div", "message");
|
|
3572
|
+
row.append(this.messageHeader(message.author, message.timestamp, message.avatarUrl, message.via), element("p", "message-text", message.text));
|
|
3573
|
+
body.append(row);
|
|
3574
|
+
}
|
|
3575
|
+
view.append(body);
|
|
3576
|
+
const actions = element("div", "thread-actions");
|
|
3577
|
+
if (comment.resolved) {
|
|
3578
|
+
const status = element("span", "status resolved", "Resolved");
|
|
3579
|
+
status.prepend(icon("check"));
|
|
3580
|
+
const reopen = button("Reopen Conversation", "quiet reopen", () => this.toggleResolved(comment));
|
|
3581
|
+
reopen.disabled = this.saving;
|
|
3582
|
+
actions.append(status, reopen);
|
|
3583
|
+
} else actions.append(...this.replyBar(comment));
|
|
3584
|
+
view.append(actions);
|
|
3585
|
+
return view;
|
|
3586
|
+
}
|
|
3587
|
+
/** One line to reply. The button resolves the conversation, or sends the reply once there is text. */
|
|
3588
|
+
replyBar(comment) {
|
|
3589
|
+
const form = element("form", "reply-bar");
|
|
3590
|
+
const field = element("textarea");
|
|
3591
|
+
field.rows = 1;
|
|
3592
|
+
field.placeholder = "Reply…";
|
|
3593
|
+
field.setAttribute("aria-label", "Reply");
|
|
3594
|
+
field.maxLength = 1e4;
|
|
3595
|
+
field.value = this.replyText;
|
|
3596
|
+
field.dataset.focus = "reply";
|
|
3597
|
+
field.disabled = this.saving;
|
|
3598
|
+
const send = button(this.saving ? "Saving…" : "Send Reply", "primary send", () => {
|
|
3599
|
+
}, "arrow");
|
|
3600
|
+
send.type = "submit";
|
|
3601
|
+
const resolve = button("Mark as Resolved", "primary resolve", () => this.toggleResolved(comment), "check");
|
|
3602
|
+
resolve.querySelector("span").textContent = "Resolve";
|
|
3603
|
+
resolve.disabled = this.saving;
|
|
3604
|
+
send.disabled = this.saving;
|
|
3605
|
+
const sync = () => {
|
|
3606
|
+
const typed = !!field.value.trim();
|
|
3607
|
+
send.hidden = !typed;
|
|
3608
|
+
resolve.hidden = typed;
|
|
3609
|
+
field.style.height = "auto";
|
|
3610
|
+
field.style.height = `${Math.min(field.scrollHeight, 120)}px`;
|
|
3611
|
+
};
|
|
3612
|
+
field.addEventListener("input", () => {
|
|
3613
|
+
this.replyText = field.value;
|
|
3614
|
+
sync();
|
|
3615
|
+
});
|
|
3616
|
+
field.addEventListener("keydown", (event) => {
|
|
3617
|
+
if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
|
|
3618
|
+
event.preventDefault();
|
|
3619
|
+
if (field.value.trim()) form.requestSubmit();
|
|
3620
|
+
}
|
|
3621
|
+
});
|
|
3622
|
+
form.addEventListener("submit", (event) => {
|
|
3623
|
+
event.preventDefault();
|
|
3624
|
+
if (!this.saving) this.submitReply();
|
|
3625
|
+
});
|
|
3626
|
+
form.append(field, send);
|
|
3627
|
+
queueMicrotask(sync);
|
|
3628
|
+
return [form, resolve];
|
|
3629
|
+
}
|
|
3630
|
+
/** Authors manage their own comments. The project owner and admins manage every comment. */
|
|
3631
|
+
manages(comment) {
|
|
3632
|
+
return comment.authorId === this.user.id || this.user.role === "owner" || this.user.role === "admin";
|
|
3633
|
+
}
|
|
3634
|
+
canMove(comment) {
|
|
3635
|
+
var _a, _b;
|
|
3636
|
+
return ((_b = (_a = this.config).canMove) == null ? void 0 : _b.call(_a, comment, this.getUser())) ?? this.manages(comment);
|
|
3637
|
+
}
|
|
3638
|
+
/** Display choices for this project. Each person has their own, saved in this browser. */
|
|
3639
|
+
settingsView() {
|
|
3640
|
+
var _a;
|
|
3641
|
+
const view = this.view("Settings", "settings");
|
|
3642
|
+
view.append(this.detailHeading("Settings", "Your choices for this project. Other people keep their own."));
|
|
3643
|
+
const current = this.settings, body = element("div", "settings-body");
|
|
3644
|
+
const section = (title) => {
|
|
3645
|
+
const group = element("section", "setting-group");
|
|
3646
|
+
group.append(element("h3", "", title));
|
|
3647
|
+
body.append(group);
|
|
3648
|
+
return group;
|
|
3649
|
+
};
|
|
3650
|
+
const row = (group, label2, hint, control) => {
|
|
3651
|
+
const line = element("div", "setting");
|
|
3652
|
+
const text = element("div", "setting-text");
|
|
3653
|
+
text.append(element("strong", "", label2));
|
|
3654
|
+
if (hint) text.append(element("small", "", hint));
|
|
3655
|
+
line.append(text, control);
|
|
3656
|
+
group.append(line);
|
|
3657
|
+
return line;
|
|
3658
|
+
};
|
|
3659
|
+
const choice = (name, label2, options) => {
|
|
3660
|
+
const group = element("div", "segmented");
|
|
3661
|
+
group.setAttribute("role", "group");
|
|
3662
|
+
group.setAttribute("aria-label", label2);
|
|
3663
|
+
for (const [value, text] of options) {
|
|
3664
|
+
const option = button(text, "segment", () => this.setSetting(name, value));
|
|
3665
|
+
option.setAttribute("aria-pressed", String(current[name] === value));
|
|
3666
|
+
option.dataset.focus = `set-${name}-${value}`;
|
|
3667
|
+
group.append(option);
|
|
3668
|
+
}
|
|
3669
|
+
return group;
|
|
3670
|
+
};
|
|
3671
|
+
const toggle = (name, label2, disabled = false) => {
|
|
3672
|
+
const on = current[name] === true;
|
|
3673
|
+
const control = element("button", "switch");
|
|
3674
|
+
control.type = "button";
|
|
3675
|
+
control.setAttribute("role", "switch");
|
|
3676
|
+
control.setAttribute("aria-checked", String(on));
|
|
3677
|
+
control.setAttribute("aria-label", label2);
|
|
3678
|
+
control.dataset.focus = `set-${name}`;
|
|
3679
|
+
control.disabled = disabled;
|
|
3680
|
+
control.addEventListener("click", () => this.setSetting(name, !on));
|
|
3681
|
+
return control;
|
|
3682
|
+
};
|
|
3683
|
+
const look = section("Appearance");
|
|
3684
|
+
row(look, "Theme", current.theme === "system" ? "Follows your device" : "", choice("theme", "Theme", [["light", "Light"], ["dark", "Dark"], ["system", "System"]]));
|
|
3685
|
+
const pins = section("Pins");
|
|
3686
|
+
const sample = element("div", `pin-sample${current.pinSize === "small" ? " small" : ""}${current.resolvedPins === "gray" ? " resolved-gray" : ""}`);
|
|
3687
|
+
sample.setAttribute("aria-hidden", "true");
|
|
3688
|
+
sample.style.setProperty("--cw-accent", this.pinColor());
|
|
3689
|
+
const label = (n) => current.pinLabel === "initials" ? initials(this.user.name) : String(n);
|
|
3690
|
+
sample.append(element("span", "pin", label(1)), element("span", "pin resolved", label(2)), element("span", "pin-sample-text", "Open and resolved"));
|
|
3691
|
+
pins.append(sample);
|
|
3692
|
+
const swatches = element("div", "swatches");
|
|
3693
|
+
swatches.setAttribute("role", "group");
|
|
3694
|
+
swatches.setAttribute("aria-label", "Pin color");
|
|
3695
|
+
const site = this.config.primaryColor.toLowerCase();
|
|
3696
|
+
const palette = Object.entries(PIN_COLORS).filter(([key2, c]) => c.color !== site || current.pinColor === key2);
|
|
3697
|
+
for (const [value, name, color] of [["site", "Site color", site], ...palette.map(([key2, c]) => [key2, c.label, c.color])]) {
|
|
3698
|
+
const swatch = button(name, "swatch", () => this.setSetting("pinColor", value));
|
|
3699
|
+
swatch.replaceChildren();
|
|
3700
|
+
swatch.style.setProperty("--swatch", color);
|
|
3701
|
+
swatch.setAttribute("aria-pressed", String(current.pinColor === value));
|
|
3702
|
+
swatch.dataset.focus = `set-pinColor-${value}`;
|
|
3703
|
+
swatches.append(swatch);
|
|
3704
|
+
}
|
|
3705
|
+
row(pins, "Color", current.pinColor === "site" ? "The color of this site" : PIN_COLORS[current.pinColor].label, swatches).classList.add("wide");
|
|
3706
|
+
row(pins, "Resolved pins", "", choice("resolvedPins", "Resolved pins", [["green", "Green"], ["gray", "Gray"]]));
|
|
3707
|
+
row(pins, "Size", "", choice("pinSize", "Pin size", [["regular", "Regular"], ["small", "Small"]]));
|
|
3708
|
+
row(pins, "Label", "", choice("pinLabel", "Pin label", [["number", "Number"], ["initials", "Initials"]]));
|
|
3709
|
+
const floating = section("Comments button");
|
|
3710
|
+
const buttonSample = element("div", "launcher-sample");
|
|
3711
|
+
buttonSample.setAttribute("aria-hidden", "true");
|
|
3712
|
+
const mini = element("span", "launcher-mini");
|
|
3713
|
+
mini.append(icon("comment"), element("span", "badge", "3"));
|
|
3714
|
+
this.paintLauncher(mini);
|
|
3715
|
+
buttonSample.append(mini, element("span", "pin-sample-text", "On the page"));
|
|
3716
|
+
floating.append(buttonSample);
|
|
3717
|
+
const buttonSwatches = element("div", "swatches");
|
|
3718
|
+
buttonSwatches.setAttribute("role", "group");
|
|
3719
|
+
buttonSwatches.setAttribute("aria-label", "Button color");
|
|
3720
|
+
const buttonPalette = Object.entries(PIN_COLORS).filter(([key2, c]) => key2 !== "graphite" && (c.color !== site || current.buttonColor === key2));
|
|
3721
|
+
for (const [value, name, color] of [["default", "Default", "var(--cw-inverse)"], ["site", "Site color", site], ...buttonPalette.map(([key2, c]) => [key2, c.label, c.color])]) {
|
|
3722
|
+
const swatch = button(name, "swatch", () => this.setSetting("buttonColor", value));
|
|
3723
|
+
swatch.replaceChildren();
|
|
3724
|
+
swatch.style.setProperty("--swatch", color);
|
|
3725
|
+
swatch.setAttribute("aria-pressed", String(current.buttonColor === value));
|
|
3726
|
+
swatch.dataset.focus = `set-buttonColor-${value}`;
|
|
3727
|
+
buttonSwatches.append(swatch);
|
|
3728
|
+
}
|
|
3729
|
+
const buttonHint = current.buttonColor === "default" ? "Dark. Takes the site color while open." : current.buttonColor === "site" ? "The color of this site" : PIN_COLORS[current.buttonColor].label;
|
|
3730
|
+
row(floating, "Color", buttonHint, buttonSwatches).classList.add("wide");
|
|
3731
|
+
const panel = section("Panel");
|
|
3732
|
+
const open = element("div", "segmented");
|
|
3733
|
+
open.setAttribute("role", "group");
|
|
3734
|
+
open.setAttribute("aria-label", "Open comments");
|
|
3735
|
+
for (const [docked, text] of [[false, "Next to Pin"], [true, "In Sidebar"]]) {
|
|
3736
|
+
const option = button(text, "segment", () => {
|
|
3737
|
+
if (this.docked !== docked) this.setDocked(docked);
|
|
3738
|
+
});
|
|
3739
|
+
option.setAttribute("aria-pressed", String(this.docked === docked));
|
|
3740
|
+
option.dataset.focus = `set-docked-${docked}`;
|
|
3741
|
+
open.append(option);
|
|
3742
|
+
}
|
|
3743
|
+
row(panel, "Open comments", "", open);
|
|
3744
|
+
row(panel, "Push the page aside", "The open panel does not cover the page.", toggle("pushContent", "Push the page aside"));
|
|
3745
|
+
const alerts = section("Notifications and keys");
|
|
3746
|
+
row(alerts, "New activity", "Show a short notice when others comment or reply.", toggle("notifications", "Show new activity"));
|
|
3747
|
+
const keys = ((_a = this.config.keyboardShortcut) == null ? void 0 : _a.split("+").map((key2) => key2.length === 1 ? key2.toUpperCase() : key2[0].toUpperCase() + key2.slice(1)).join(" + ")) ?? "";
|
|
3748
|
+
row(alerts, "Keyboard shortcut", this.shortcut ? `${keys} opens and closes comments.` : "This site has no shortcut.", toggle("shortcut", "Keyboard shortcut", !this.shortcut));
|
|
3749
|
+
const foot = element("div", "settings-foot");
|
|
3750
|
+
foot.append(element("span", "", "Saved in this browser"), button("Reset to Defaults", "quiet", () => this.resetSettings()));
|
|
3751
|
+
view.append(body, foot);
|
|
3752
|
+
return view;
|
|
3753
|
+
}
|
|
3754
|
+
/** Colors the floating button with the person's choice. The default button keeps its own dark and open styles. */
|
|
3755
|
+
paintLauncher(launcher) {
|
|
3756
|
+
const choice = this.settings.buttonColor;
|
|
3757
|
+
if (choice === "default") return;
|
|
3758
|
+
launcher.classList.add("colored");
|
|
3759
|
+
launcher.style.setProperty("--cw-launcher", choice === "site" ? this.config.primaryColor : PIN_COLORS[choice].color);
|
|
3760
|
+
}
|
|
3761
|
+
/** The pin color the person chose, or the site's own color. */
|
|
3762
|
+
pinColor() {
|
|
3763
|
+
const choice = this.settings.pinColor;
|
|
3764
|
+
return choice === "site" ? this.config.primaryColor : PIN_COLORS[choice].color;
|
|
3765
|
+
}
|
|
3766
|
+
identityForm() {
|
|
3767
|
+
const view = this.view("Your guest profile");
|
|
3768
|
+
view.append(this.detailHeading("Your name", "A name to put to your notes."));
|
|
3769
|
+
const form = element("form", "identity-form");
|
|
3770
|
+
const label = element("label", "", "Display name");
|
|
3771
|
+
label.htmlFor = "guest-name";
|
|
3772
|
+
const input = element("input");
|
|
3773
|
+
input.id = "guest-name";
|
|
3774
|
+
input.value = this.user.name === "Guest" ? "" : this.user.name;
|
|
3775
|
+
input.placeholder = "Your name";
|
|
3776
|
+
input.maxLength = 80;
|
|
3777
|
+
input.required = true;
|
|
3778
|
+
input.setAttribute("autocomplete", "nickname");
|
|
3779
|
+
input.dataset.focus = "guest-name";
|
|
3780
|
+
const explanation = element("p", "", this.persistentGuest ? "Your name and guest ID stay in this browser. No account or email is needed. You can change your name without changing your ID." : "Browser storage is unavailable. This guest profile lasts for this session.");
|
|
3781
|
+
const submit = button("Save Name", "primary", () => {
|
|
3782
|
+
});
|
|
3783
|
+
submit.type = "submit";
|
|
3784
|
+
form.addEventListener("submit", (event) => {
|
|
3785
|
+
event.preventDefault();
|
|
3786
|
+
if (!input.value.trim()) return;
|
|
3787
|
+
this.user = { ...this.user, name: input.value.trim() };
|
|
3788
|
+
this.persistentGuest = saveGuest(this.identityKey, this.user);
|
|
3789
|
+
this.editing = null;
|
|
3790
|
+
this.render();
|
|
3791
|
+
this.announce("Guest name saved.");
|
|
3792
|
+
});
|
|
3793
|
+
form.append(label, input, explanation, submit);
|
|
3794
|
+
view.append(form);
|
|
3795
|
+
return view;
|
|
3796
|
+
}
|
|
3797
|
+
renderPins() {
|
|
3798
|
+
this.pins.classList.toggle("focused", !!this.activeId && !this.draft);
|
|
3799
|
+
const settings = this.settings;
|
|
3800
|
+
this.pins.classList.toggle("small", settings.pinSize === "small");
|
|
3801
|
+
this.pins.classList.toggle("resolved-gray", settings.resolvedPins === "gray");
|
|
3802
|
+
this.pins.style.setProperty("--cw-accent", this.pinColor());
|
|
3803
|
+
if (this.activeId) {
|
|
3804
|
+
const ring = element("div", "focus-ring");
|
|
3805
|
+
ring.hidden = true;
|
|
3806
|
+
this.pins.append(ring);
|
|
3807
|
+
}
|
|
3808
|
+
const hoverRing = element("div", "focus-ring hover-ring");
|
|
3809
|
+
hoverRing.hidden = true;
|
|
3810
|
+
this.pins.append(hoverRing);
|
|
3811
|
+
const comments = this.pageComments();
|
|
3812
|
+
const filters = this.config.hideSidebar ? { ...DEFAULT_FILTERS, status: this.filters.status } : this.filters;
|
|
3813
|
+
const shown = new Set(filterNotes(comments, filters, this.filterContext("page")).map((c) => c.id));
|
|
3814
|
+
for (const [index, comment] of comments.entries()) {
|
|
3815
|
+
if (comment.id !== this.activeId && !shown.has(comment.id)) continue;
|
|
3816
|
+
const movable = !this.saving && this.canMove(comment);
|
|
3817
|
+
const pin = button(`Comment ${index + 1} by ${comment.author}: ${comment.text.slice(0, 100)}`, `pin${comment.resolved ? " resolved" : ""}${this.highlightId === comment.id ? " highlight" : ""}${this.hoverId === comment.id ? " hovered" : ""}${movable ? " movable" : ""}`, () => {
|
|
3818
|
+
if (this.suppressPinClick) {
|
|
3819
|
+
this.suppressPinClick = false;
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
this.navigateToComment(comment.id);
|
|
3823
|
+
});
|
|
3824
|
+
if (movable) {
|
|
3825
|
+
pin.title += ". Drag to move.";
|
|
3826
|
+
pin.addEventListener("pointerdown", (event) => this.startPinDrag(event, comment));
|
|
3827
|
+
}
|
|
3828
|
+
pin.replaceChildren(document.createTextNode(settings.pinLabel === "initials" ? initials(comment.author) : String(index + 1)));
|
|
3829
|
+
pin.dataset.id = comment.id;
|
|
3830
|
+
pin.setAttribute("aria-pressed", String(this.activeId === comment.id));
|
|
3831
|
+
this.pins.append(pin);
|
|
3832
|
+
}
|
|
3833
|
+
const pickRing = element("div", "focus-ring pick-ring");
|
|
3834
|
+
pickRing.hidden = true;
|
|
3835
|
+
this.pins.append(pickRing);
|
|
3836
|
+
if (this.placing) {
|
|
3837
|
+
const preview = element("span", "pin preview", "+");
|
|
3838
|
+
preview.hidden = true;
|
|
3839
|
+
this.pins.append(preview);
|
|
3840
|
+
}
|
|
3841
|
+
if (this.draft) {
|
|
3842
|
+
const preview = element("span", "pin", "+");
|
|
3843
|
+
const pos = this.position({ ...this.draft, ...this.draft.position });
|
|
3844
|
+
if (pos) {
|
|
3845
|
+
preview.style.left = `${pos.x}px`;
|
|
3846
|
+
preview.style.top = `${pos.y}px`;
|
|
3847
|
+
} else preview.hidden = true;
|
|
3848
|
+
this.pins.append(preview);
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
class APIAdapter {
|
|
3853
|
+
constructor(config) {
|
|
3854
|
+
this.config = config;
|
|
3855
|
+
}
|
|
3856
|
+
async request(path, method = "GET", body) {
|
|
3857
|
+
const headers = typeof this.config.headers === "function" ? await this.config.headers() : this.config.headers;
|
|
3858
|
+
const url = new URL(`${this.config.baseUrl.replace(/\/$/, "")}${path}`, window.location.href);
|
|
3859
|
+
if (this.config.projectId) url.searchParams.set("projectId", this.config.projectId);
|
|
3860
|
+
const response = await fetch(url, {
|
|
3861
|
+
method,
|
|
3862
|
+
credentials: this.config.credentials ?? "same-origin",
|
|
3863
|
+
headers: { ...body !== void 0 ? { "Content-Type": "application/json" } : {}, ...headers },
|
|
3864
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
3865
|
+
});
|
|
3866
|
+
if (!response.ok) throw new Error(`Could not ${method === "GET" ? "load" : "save"} comments (${response.status}).`);
|
|
3867
|
+
if (method !== "GET" || response.status === 204) return void 0;
|
|
3868
|
+
return response.json();
|
|
3869
|
+
}
|
|
3870
|
+
async loadComments() {
|
|
3871
|
+
var _a;
|
|
3872
|
+
return normalizeComments(await this.request(((_a = this.config.endpoints) == null ? void 0 : _a.load) ?? "/comments"));
|
|
3873
|
+
}
|
|
3874
|
+
async saveComments(comments) {
|
|
3875
|
+
var _a;
|
|
3876
|
+
await this.request(((_a = this.config.endpoints) == null ? void 0 : _a.save) ?? "/comments", "POST", comments);
|
|
3877
|
+
}
|
|
3878
|
+
async upsertComment(comment) {
|
|
3879
|
+
var _a;
|
|
3880
|
+
await this.request(`${((_a = this.config.endpoints) == null ? void 0 : _a.save) ?? "/comments"}/${encodeURIComponent(comment.id)}`, "PUT", comment);
|
|
3881
|
+
}
|
|
3882
|
+
async deleteComment(id) {
|
|
3883
|
+
var _a;
|
|
3884
|
+
await this.request(`${((_a = this.config.endpoints) == null ? void 0 : _a.save) ?? "/comments"}/${encodeURIComponent(id)}`, "DELETE");
|
|
3885
|
+
}
|
|
3886
|
+
async addReply(id, reply2) {
|
|
3887
|
+
var _a;
|
|
3888
|
+
await this.request(`${((_a = this.config.endpoints) == null ? void 0 : _a.save) ?? "/comments"}/${encodeURIComponent(id)}/replies`, "POST", reply2);
|
|
3889
|
+
}
|
|
3890
|
+
async setResolved(id, resolved) {
|
|
3891
|
+
var _a;
|
|
3892
|
+
await this.request(`${((_a = this.config.endpoints) == null ? void 0 : _a.save) ?? "/comments"}/${encodeURIComponent(id)}`, "PATCH", { resolved });
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
function initOverplay(config = {}) {
|
|
3896
|
+
if (typeof document === "undefined" || !document.body) throw new Error("Initialize Overplay in the browser after the page has mounted.");
|
|
3897
|
+
return new Overlay(config);
|
|
3898
|
+
}
|
|
3899
|
+
const init = initOverplay;
|
|
3900
|
+
export {
|
|
3901
|
+
APIAdapter,
|
|
3902
|
+
HostedAdapter,
|
|
3903
|
+
LocalStorageAdapter,
|
|
3904
|
+
init,
|
|
3905
|
+
initOverplay
|
|
3906
|
+
};
|
|
3907
|
+
//# sourceMappingURL=overplay.esm.js.map
|