@nuniapp/widget 0.1.7 → 0.1.9
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/dist/dom-BWcT-q7S.js +255 -0
- package/dist/index.d.ts +94 -8
- package/dist/index.js +791 -162
- package/dist/nuni-screenshot.global.js +41 -7
- package/dist/nuni.global.js +116 -12
- package/dist/{screenshot-DcJ_SzKF.js → tools-gIAPu66h.js} +225 -14
- package/package.json +1 -1
- package/dist/mark-B8wojiFu.js +0 -90
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as REACTIONS, i as LIMITS, n as
|
|
1
|
+
import { _ as isProjectId, a as initials, c as IMAGE_TYPES, d as CAPTURE_MARK, f as MASK_ATTRIBUTE, g as generateSecret, h as REACTIONS, i as icon, m as LIMITS, n as copyText, o as parseColor, p as MASK_SELECTOR, r as h, s as readableOn, t as colorFor, u as imageFromFile } from "./dom-BWcT-q7S.js";
|
|
2
2
|
//#region ../shared/src/path.ts
|
|
3
3
|
/**
|
|
4
4
|
* Normalize a URL to the key comments are matched by. Query and hash are
|
|
@@ -120,7 +120,16 @@ function buildCommentPrompt(comment, options = {}) {
|
|
|
120
120
|
const includeContext = options.includeContext ?? true;
|
|
121
121
|
const { anchor, page } = comment;
|
|
122
122
|
const out = [];
|
|
123
|
-
out.push("Fix this piece of feedback that was left on the live site with Nuni.", "", "## Comment", "", quote(comment.body), ""
|
|
123
|
+
out.push("Fix this piece of feedback that was left on the live site with Nuni.", "", "## Comment", "", ...comment.body ? [quote(comment.body), ""] : [], ...comment.suggestion ? [
|
|
124
|
+
"Suggested text change. Replace:",
|
|
125
|
+
"",
|
|
126
|
+
quote(comment.suggestion.before),
|
|
127
|
+
"",
|
|
128
|
+
"With:",
|
|
129
|
+
"",
|
|
130
|
+
quote(comment.suggestion.after),
|
|
131
|
+
""
|
|
132
|
+
] : [], `From ${comment.authorName} on ${new Date(comment.createdAt).toISOString().slice(0, 10)}` + (comment.status === "resolved" ? comment.resolution === "outdated" ? " (closed as outdated)" : " (already resolved)" : ""));
|
|
124
133
|
if (comment.replies?.length) {
|
|
125
134
|
out.push("", `## Replies (${comment.replies.length})`, "");
|
|
126
135
|
for (const reply of comment.replies) out.push(`${reply.authorName}${reply.isOwner ? " (owner)" : ""}:`, quote(reply.body), "");
|
|
@@ -132,6 +141,7 @@ function buildCommentPrompt(comment, options = {}) {
|
|
|
132
141
|
out.push(`- Viewport: ${comment.viewport.w}×${comment.viewport.h} at ${comment.viewport.dpr}x`);
|
|
133
142
|
if (includeContext && comment.userAgent) out.push(`- Browser: ${comment.userAgent}`);
|
|
134
143
|
out.push("", "## Element", "");
|
|
144
|
+
if (comment.pinLostAt) out.push(`- Not found on the page since ${new Date(comment.pinLostAt).toISOString().slice(0, 10)}: it may have changed or been removed. Check whether the feedback still applies.`);
|
|
135
145
|
out.push(`- Tag: <${anchor.tag}>`);
|
|
136
146
|
if (anchor.text) out.push(`- Text: ${inline(anchor.text)}`);
|
|
137
147
|
if (anchor.quote) out.push(`- Selected text: ${inline(anchor.quote.exact)}`);
|
|
@@ -159,13 +169,14 @@ function buildCommentPrompt(comment, options = {}) {
|
|
|
159
169
|
out.push("", `## Failed requests (${context.network.length})`, "");
|
|
160
170
|
for (const r of context.network) out.push(`- ${r.method} ${r.url} → ${r.status === 0 ? "network error" : r.status}`);
|
|
161
171
|
}
|
|
172
|
+
if (comment.imageUrls?.length) out.push("", `## Images from ${comment.authorName} (${comment.imageUrls.length})`, "", ...comment.imageUrls.map((url) => `- ${url}`));
|
|
162
173
|
if (includeContext && comment.screenshotUrl) out.push("", "## Screenshot", "", comment.screenshotUrl);
|
|
163
|
-
out.push("", "## Steps", "", "1. Find the code that renders this element. Search for the text, the component name, the test ID or the selector.", "2. Make the change the comment asks for, and keep the fix focused on it.", "3. Tell me what you changed, so the comment can be resolved in Nuni.", ` If the Nuni CLI is signed in to this project (\`npx ${PACKAGES.cli}@latest login\`), resolve it with \`npx ${PACKAGES.cli}@latest resolve ${comment._id}\` or the \`resolve_comment\` MCP tool.`);
|
|
174
|
+
out.push("", "## Steps", "", "1. Find the code that renders this element. Search for the text, the component name, the test ID or the selector.", comment.suggestion ? "2. Replace the text exactly as suggested (wherever it comes from: markup, a component, a translation or content file), and keep the fix focused on it." : "2. Make the change the comment asks for, and keep the fix focused on it.", "3. Tell me what you changed, so the comment can be resolved in Nuni.", ` If the Nuni CLI is signed in to this project (\`npx ${PACKAGES.cli}@latest login\`), resolve it with \`npx ${PACKAGES.cli}@latest resolve ${comment._id}\` or the \`resolve_comment\` MCP tool.`);
|
|
164
175
|
return out.join("\n");
|
|
165
176
|
}
|
|
166
177
|
//#endregion
|
|
167
178
|
//#region src/config.ts
|
|
168
|
-
const VERSION = "0.1.
|
|
179
|
+
const VERSION = "0.1.9";
|
|
169
180
|
function resolveConfig(options) {
|
|
170
181
|
return {
|
|
171
182
|
project: options.project,
|
|
@@ -181,6 +192,7 @@ function resolveConfig(options) {
|
|
|
181
192
|
zIndex: typeof options.zIndex === "number" && Number.isFinite(options.zIndex) ? Math.round(options.zIndex) : null,
|
|
182
193
|
locale: options.locale?.trim() || void 0,
|
|
183
194
|
messages: options.messages ?? {},
|
|
195
|
+
images: options.images ?? true,
|
|
184
196
|
capture: {
|
|
185
197
|
console: options.capture?.console ?? true,
|
|
186
198
|
network: options.capture?.network ?? true,
|
|
@@ -191,8 +203,10 @@ function resolveConfig(options) {
|
|
|
191
203
|
}
|
|
192
204
|
const POSITIONS = [
|
|
193
205
|
"bottom-right",
|
|
206
|
+
"bottom-center",
|
|
194
207
|
"bottom-left",
|
|
195
208
|
"top-right",
|
|
209
|
+
"top-center",
|
|
196
210
|
"top-left"
|
|
197
211
|
];
|
|
198
212
|
const THEMES = [
|
|
@@ -5270,6 +5284,13 @@ var NuniApi = class {
|
|
|
5270
5284
|
path
|
|
5271
5285
|
}, (comments) => cb(comments), onError);
|
|
5272
5286
|
}
|
|
5287
|
+
/** The visitor's own comments on every page, live. */
|
|
5288
|
+
onMine(authorSecret, cb) {
|
|
5289
|
+
return this.client.onUpdate(api.comments.listMine, {
|
|
5290
|
+
publicId: this.config.project,
|
|
5291
|
+
authorSecret
|
|
5292
|
+
}, (comments) => cb(comments), () => {});
|
|
5293
|
+
}
|
|
5273
5294
|
onPages(cb) {
|
|
5274
5295
|
return this.client.onUpdate(api.comments.pagesWithComments, { publicId: this.config.project }, cb, () => {});
|
|
5275
5296
|
}
|
|
@@ -5338,9 +5359,16 @@ var NuniApi = class {
|
|
|
5338
5359
|
if (!response.ok || !data.id) throw new NuniApiError(data.message ?? "Couldn't post the reply", data.code ?? "error");
|
|
5339
5360
|
return data.id;
|
|
5340
5361
|
}
|
|
5341
|
-
/** Attach
|
|
5342
|
-
|
|
5343
|
-
|
|
5362
|
+
/** Attach the element's screenshot (for the owner) to a just-posted comment. */
|
|
5363
|
+
uploadScreenshot(id, authorSecret, image) {
|
|
5364
|
+
return this.upload("screenshot", id, authorSecret, image);
|
|
5365
|
+
}
|
|
5366
|
+
/** An image for everyone: picked, pasted or a marked-up screenshot. */
|
|
5367
|
+
uploadImage(id, authorSecret, image) {
|
|
5368
|
+
return this.upload("image", id, authorSecret, image);
|
|
5369
|
+
}
|
|
5370
|
+
async upload(kind, id, authorSecret, image) {
|
|
5371
|
+
const response = await fetch(`${this.config.convexSiteUrl}/widget/${kind}`, {
|
|
5344
5372
|
method: "POST",
|
|
5345
5373
|
headers: {
|
|
5346
5374
|
"Content-Type": image.type,
|
|
@@ -5352,7 +5380,7 @@ var NuniApi = class {
|
|
|
5352
5380
|
});
|
|
5353
5381
|
if (!response.ok) {
|
|
5354
5382
|
const data = await response.json().catch(() => ({}));
|
|
5355
|
-
throw new NuniApiError(data.message ?? "Couldn't attach the
|
|
5383
|
+
throw new NuniApiError(data.message ?? "Couldn't attach the image", data.code ?? "error");
|
|
5356
5384
|
}
|
|
5357
5385
|
}
|
|
5358
5386
|
async run(promise) {
|
|
@@ -5362,11 +5390,12 @@ var NuniApi = class {
|
|
|
5362
5390
|
throw new NuniApiError(errorMessage(error), "error");
|
|
5363
5391
|
}
|
|
5364
5392
|
}
|
|
5365
|
-
editOwn(id, authorSecret, body) {
|
|
5393
|
+
editOwn(id, authorSecret, body, suggestion) {
|
|
5366
5394
|
return this.run(this.client.mutation(api.comments.editOwn, {
|
|
5367
5395
|
id,
|
|
5368
5396
|
authorSecret,
|
|
5369
|
-
body
|
|
5397
|
+
body,
|
|
5398
|
+
suggestion
|
|
5370
5399
|
}));
|
|
5371
5400
|
}
|
|
5372
5401
|
deleteOwn(id, authorSecret) {
|
|
@@ -5375,12 +5404,40 @@ var NuniApi = class {
|
|
|
5375
5404
|
authorSecret
|
|
5376
5405
|
}));
|
|
5377
5406
|
}
|
|
5378
|
-
resolve(id, sessionToken) {
|
|
5407
|
+
resolve(id, sessionToken, resolution) {
|
|
5379
5408
|
return this.run(this.client.mutation(api.comments.resolve, {
|
|
5380
5409
|
id,
|
|
5410
|
+
resolution,
|
|
5411
|
+
sessionToken
|
|
5412
|
+
}));
|
|
5413
|
+
}
|
|
5414
|
+
/** The owner points a comment at another element on its page. */
|
|
5415
|
+
repin(id, anchor, sessionToken) {
|
|
5416
|
+
return this.run(this.client.mutation(api.comments.repin, {
|
|
5417
|
+
id,
|
|
5418
|
+
anchor,
|
|
5381
5419
|
sessionToken
|
|
5382
5420
|
}));
|
|
5383
5421
|
}
|
|
5422
|
+
/**
|
|
5423
|
+
* Tell the backend which pins on this page couldn't be found, or were
|
|
5424
|
+
* found again, so the owner can see them. Best effort.
|
|
5425
|
+
*/
|
|
5426
|
+
async reportPins(path, lost, found) {
|
|
5427
|
+
try {
|
|
5428
|
+
await fetch(`${this.config.convexSiteUrl}/widget/pins`, {
|
|
5429
|
+
method: "POST",
|
|
5430
|
+
headers: { "Content-Type": "application/json" },
|
|
5431
|
+
body: JSON.stringify({
|
|
5432
|
+
publicId: this.config.project,
|
|
5433
|
+
path,
|
|
5434
|
+
lost,
|
|
5435
|
+
found
|
|
5436
|
+
}),
|
|
5437
|
+
keepalive: true
|
|
5438
|
+
});
|
|
5439
|
+
} catch {}
|
|
5440
|
+
}
|
|
5384
5441
|
reopen(id, sessionToken) {
|
|
5385
5442
|
return this.run(this.client.mutation(api.comments.reopen, {
|
|
5386
5443
|
id,
|
|
@@ -5423,106 +5480,6 @@ var NuniApi = class {
|
|
|
5423
5480
|
}
|
|
5424
5481
|
};
|
|
5425
5482
|
//#endregion
|
|
5426
|
-
//#region src/dom.ts
|
|
5427
|
-
/** Tiny element factory. Strings become text nodes (never HTML). */
|
|
5428
|
-
function h(tag, attrs = {}, ...children) {
|
|
5429
|
-
const el = document.createElement(tag);
|
|
5430
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
5431
|
-
if (value == null || value === false) continue;
|
|
5432
|
-
if (key.startsWith("on") && typeof value === "function") el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
5433
|
-
else if (key === "class") el.className = String(value);
|
|
5434
|
-
else if (value === true) el.setAttribute(key, "");
|
|
5435
|
-
else el.setAttribute(key, String(value));
|
|
5436
|
-
}
|
|
5437
|
-
for (const child of children.flat()) {
|
|
5438
|
-
if (child == null || child === false) continue;
|
|
5439
|
-
el.append(typeof child === "string" ? document.createTextNode(child) : child);
|
|
5440
|
-
}
|
|
5441
|
-
return el;
|
|
5442
|
-
}
|
|
5443
|
-
/** Renders one of the constant SVG strings in icons.ts (never user input). */
|
|
5444
|
-
function icon(svg) {
|
|
5445
|
-
const span = document.createElement("span");
|
|
5446
|
-
span.className = "icon";
|
|
5447
|
-
span.setAttribute("aria-hidden", "true");
|
|
5448
|
-
const parsed = new DOMParser().parseFromString(svg, "image/svg+xml");
|
|
5449
|
-
span.append(document.importNode(parsed.documentElement, true));
|
|
5450
|
-
return span;
|
|
5451
|
-
}
|
|
5452
|
-
function initials(name) {
|
|
5453
|
-
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
5454
|
-
return (parts.length > 1 ? parts[0][0] + parts[parts.length - 1][0] : (parts[0] ?? "?").slice(0, 2)).toUpperCase();
|
|
5455
|
-
}
|
|
5456
|
-
/** Stable pleasant color per author name. */
|
|
5457
|
-
function colorFor(name) {
|
|
5458
|
-
let hash = 0;
|
|
5459
|
-
for (const ch of name) hash = hash * 31 + ch.charCodeAt(0) >>> 0;
|
|
5460
|
-
return `oklch(0.5 0.17 ${hash % 360})`;
|
|
5461
|
-
}
|
|
5462
|
-
/** Copy text, falling back to execCommand where the async clipboard is off (plain http previews). */
|
|
5463
|
-
async function copyText(text) {
|
|
5464
|
-
try {
|
|
5465
|
-
await navigator.clipboard.writeText(text);
|
|
5466
|
-
return true;
|
|
5467
|
-
} catch {}
|
|
5468
|
-
const area = document.createElement("textarea");
|
|
5469
|
-
area.value = text;
|
|
5470
|
-
area.setAttribute("readonly", "");
|
|
5471
|
-
area.style.cssText = "position:fixed;top:0;left:0;opacity:0";
|
|
5472
|
-
document.body.appendChild(area);
|
|
5473
|
-
area.select();
|
|
5474
|
-
try {
|
|
5475
|
-
return document.execCommand("copy");
|
|
5476
|
-
} catch {
|
|
5477
|
-
return false;
|
|
5478
|
-
} finally {
|
|
5479
|
-
area.remove();
|
|
5480
|
-
}
|
|
5481
|
-
}
|
|
5482
|
-
/** Any CSS color as RGB, using the browser's own parser. */
|
|
5483
|
-
function parseColor(color) {
|
|
5484
|
-
const probe = document.createElement("span");
|
|
5485
|
-
probe.style.color = color;
|
|
5486
|
-
if (!probe.style.color) return null;
|
|
5487
|
-
probe.style.display = "none";
|
|
5488
|
-
document.documentElement.appendChild(probe);
|
|
5489
|
-
const computed = getComputedStyle(probe).color;
|
|
5490
|
-
probe.remove();
|
|
5491
|
-
if (/^rgba?\(/.test(computed)) {
|
|
5492
|
-
const parts = computed.match(/[\d.]+/g)?.map(Number);
|
|
5493
|
-
if (parts && parts.length >= 3) return [
|
|
5494
|
-
parts[0],
|
|
5495
|
-
parts[1],
|
|
5496
|
-
parts[2]
|
|
5497
|
-
];
|
|
5498
|
-
}
|
|
5499
|
-
try {
|
|
5500
|
-
const canvas = document.createElement("canvas");
|
|
5501
|
-
canvas.width = canvas.height = 1;
|
|
5502
|
-
const ctx = canvas.getContext("2d");
|
|
5503
|
-
if (!ctx) return null;
|
|
5504
|
-
ctx.fillStyle = color;
|
|
5505
|
-
ctx.fillRect(0, 0, 1, 1);
|
|
5506
|
-
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
5507
|
-
return [
|
|
5508
|
-
r,
|
|
5509
|
-
g,
|
|
5510
|
-
b
|
|
5511
|
-
];
|
|
5512
|
-
} catch {
|
|
5513
|
-
return null;
|
|
5514
|
-
}
|
|
5515
|
-
}
|
|
5516
|
-
/** Black or white text, whichever reads better on the color (WCAG). */
|
|
5517
|
-
function readableOn([r, g, b]) {
|
|
5518
|
-
const channel = (c) => {
|
|
5519
|
-
const v = c / 255;
|
|
5520
|
-
return v <= .03928 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4;
|
|
5521
|
-
};
|
|
5522
|
-
const l = .2126 * channel(r) + .7152 * channel(g) + .0722 * channel(b);
|
|
5523
|
-
return 1.05 / (l + .05) >= (l + .05) / .05 ? "#ffffff" : "#111111";
|
|
5524
|
-
}
|
|
5525
|
-
//#endregion
|
|
5526
5483
|
//#region src/i18n.ts
|
|
5527
5484
|
/**
|
|
5528
5485
|
* Every word the widget shows. Override any of them with the `messages`
|
|
@@ -5550,11 +5507,18 @@ const EN = {
|
|
|
5550
5507
|
close: "Close",
|
|
5551
5508
|
tabOpen: "Open ({count})",
|
|
5552
5509
|
tabResolved: "Resolved ({count})",
|
|
5510
|
+
tabYours: "Yours ({count})",
|
|
5511
|
+
yours: "Yours",
|
|
5512
|
+
noYours: "Comments you leave on any page of this site show here.",
|
|
5513
|
+
thisPage: "This page",
|
|
5514
|
+
statusOpen: "Open",
|
|
5553
5515
|
loading: "Loading…",
|
|
5554
5516
|
noOpen: "No open comments on this page.",
|
|
5555
5517
|
noOpenKey: "No open comments on this page. Press {key} to add one.",
|
|
5556
5518
|
noResolved: "Nothing resolved yet.",
|
|
5557
5519
|
notFoundHere: "Couldn't find on this page",
|
|
5520
|
+
searchComments: "Search comments",
|
|
5521
|
+
noMatches: "No comments match “{query}”.",
|
|
5558
5522
|
otherPages: "Other pages",
|
|
5559
5523
|
openOnPage: "{count} open",
|
|
5560
5524
|
replyCount: {
|
|
@@ -5576,6 +5540,25 @@ const EN = {
|
|
|
5576
5540
|
one: "{count} character left",
|
|
5577
5541
|
other: "{count} characters left"
|
|
5578
5542
|
},
|
|
5543
|
+
suggestEdit: "Suggest an edit",
|
|
5544
|
+
suggestedEdit: "Suggested edit",
|
|
5545
|
+
removeSuggestion: "Remove",
|
|
5546
|
+
newText: "New text",
|
|
5547
|
+
addNote: "Add a note (optional)",
|
|
5548
|
+
addImage: "Add an image",
|
|
5549
|
+
markUpScreenshot: "Mark up a screenshot",
|
|
5550
|
+
preparingScreenshot: "Taking the screenshot…",
|
|
5551
|
+
markUpTitle: "Mark up the screenshot",
|
|
5552
|
+
markUpBox: "Box",
|
|
5553
|
+
markUpArrow: "Arrow",
|
|
5554
|
+
markUpPen: "Pen",
|
|
5555
|
+
markUpUndo: "Undo",
|
|
5556
|
+
markUpDone: "Attach",
|
|
5557
|
+
markUpFailed: "Couldn't save the drawing as an image. Try again.",
|
|
5558
|
+
imagesPublic: "Images are shown to everyone who can see this comment.",
|
|
5559
|
+
imageN: "Image {n}",
|
|
5560
|
+
removeImage: "Remove image {n}",
|
|
5561
|
+
openImage: "Open the image",
|
|
5579
5562
|
pinLabel: "Comment by {name}: {body}",
|
|
5580
5563
|
commentBy: "Comment by {name}",
|
|
5581
5564
|
openScreenshot: "Open the screenshot",
|
|
@@ -5589,6 +5572,15 @@ const EN = {
|
|
|
5589
5572
|
resolve: "Resolve",
|
|
5590
5573
|
reopen: "Reopen",
|
|
5591
5574
|
copyForAgent: "Copy for agent",
|
|
5575
|
+
copyNewText: "Copy new text",
|
|
5576
|
+
movePin: "Move pin",
|
|
5577
|
+
movePinHint: "Click the element this comment is about",
|
|
5578
|
+
pinMoved: "Pin moved",
|
|
5579
|
+
closeOutdated: "Close as outdated",
|
|
5580
|
+
closedOutdated: "Closed as outdated",
|
|
5581
|
+
outdated: "Outdated",
|
|
5582
|
+
lostHint: "This element isn't on the page any more. Move the pin to where it is now, or close the comment if it no longer applies.",
|
|
5583
|
+
newTextCopied: "New text copied",
|
|
5592
5584
|
copyForAgentHint: "Copy for your coding agent",
|
|
5593
5585
|
loadingContext: "Loading page context…",
|
|
5594
5586
|
edit: "Edit",
|
|
@@ -5615,6 +5607,12 @@ const EN = {
|
|
|
5615
5607
|
linkGone: "That comment was deleted or is on another page",
|
|
5616
5608
|
enterName: "Enter your name",
|
|
5617
5609
|
writeFirst: "Write a comment first",
|
|
5610
|
+
changeTextFirst: "Change the text to suggest an edit",
|
|
5611
|
+
tooManyImages: "Add up to {count} images",
|
|
5612
|
+
imageUnreadable: "Attach a PNG, JPEG or WebP image",
|
|
5613
|
+
imageFailed: "Couldn't attach an image",
|
|
5614
|
+
noScreenshot: "Couldn't take a screenshot of this element",
|
|
5615
|
+
suggestionTooLong: "Suggestions are limited to {count} characters",
|
|
5618
5616
|
commentTooLong: "Comments are limited to {count} characters",
|
|
5619
5617
|
replyTooLong: "Replies are limited to {count} characters",
|
|
5620
5618
|
postFailed: "Couldn't post the comment",
|
|
@@ -5690,7 +5688,10 @@ const ICONS = {
|
|
|
5690
5688
|
smile: s("<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01\"/>"),
|
|
5691
5689
|
user: s("<circle cx=\"12\" cy=\"8\" r=\"4\"/><path d=\"M4 21a8 8 0 0 1 16 0\"/>"),
|
|
5692
5690
|
arrowUp: s("<path d=\"M12 19V5M5 12l7-7 7 7\"/>"),
|
|
5691
|
+
search: s("<circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/>"),
|
|
5693
5692
|
target: s("<circle cx=\"12\" cy=\"12\" r=\"9\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>"),
|
|
5693
|
+
image: s("<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><circle cx=\"9\" cy=\"9\" r=\"2\"/><path d=\"m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21\"/>"),
|
|
5694
|
+
pen: s("<path d=\"m12 19 7-7 3 3-7 7z\"/><path d=\"m18 13-1.5-7.5L2 2l3.5 14.5L13 18z\"/><path d=\"m2 2 7.6 7.6\"/><circle cx=\"11\" cy=\"11\" r=\"2\"/>"),
|
|
5694
5695
|
github: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12 .5a11.5 11.5 0 0 0-3.6 22.4c.6.1.8-.3.8-.6v-2c-3.2.7-3.9-1.5-3.9-1.5-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2a11 11 0 0 1 5.8 0c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.9 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A11.5 11.5 0 0 0 12 .5z"/></svg>`,
|
|
5695
5696
|
link: s("<path d=\"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\"/>")
|
|
5696
5697
|
};
|
|
@@ -5945,7 +5946,11 @@ function write(key, value) {
|
|
|
5945
5946
|
const KEYS = {
|
|
5946
5947
|
name: "nuni:name",
|
|
5947
5948
|
secret: "nuni:author-secret",
|
|
5948
|
-
session: (project) => `nuni:session:${project}
|
|
5949
|
+
session: (project) => `nuni:session:${project}`,
|
|
5950
|
+
/** Where this visitor dragged the toolbar. */
|
|
5951
|
+
position: (project) => `nuni:position:${project}`,
|
|
5952
|
+
/** The comment this visitor opened last, marked in the panel. */
|
|
5953
|
+
lastViewed: (project) => `nuni:last-viewed:${project}`
|
|
5949
5954
|
};
|
|
5950
5955
|
//#endregion
|
|
5951
5956
|
//#region src/styles.ts
|
|
@@ -6022,7 +6027,18 @@ button { cursor: pointer; }
|
|
|
6022
6027
|
border: 1px solid var(--n-border);
|
|
6023
6028
|
box-shadow: var(--n-shadow);
|
|
6024
6029
|
pointer-events: auto;
|
|
6025
|
-
|
|
6030
|
+
cursor: grab;
|
|
6031
|
+
touch-action: none;
|
|
6032
|
+
user-select: none;
|
|
6033
|
+
-webkit-user-select: none;
|
|
6034
|
+
}
|
|
6035
|
+
.toolbar[data-dragging] {
|
|
6036
|
+
cursor: grabbing;
|
|
6037
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.08), 0 16px 40px rgba(0,0,0,0.2);
|
|
6038
|
+
}
|
|
6039
|
+
/* While dragging, the host covers the page so no move is lost to it. */
|
|
6040
|
+
:host([data-dragging]) { pointer-events: auto; cursor: grabbing; }
|
|
6041
|
+
:host([data-dragging]) .tb-btn { pointer-events: none; }
|
|
6026
6042
|
.tb-btn {
|
|
6027
6043
|
display: inline-flex;
|
|
6028
6044
|
align-items: center;
|
|
@@ -6179,7 +6195,6 @@ button { cursor: pointer; }
|
|
|
6179
6195
|
gap: 12px;
|
|
6180
6196
|
}
|
|
6181
6197
|
.confirm p { margin: 0; }
|
|
6182
|
-
.btn-danger-solid { background: var(--n-danger); color: #fff; border-color: transparent; }
|
|
6183
6198
|
|
|
6184
6199
|
/* Pins */
|
|
6185
6200
|
.pin {
|
|
@@ -6454,6 +6469,10 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
|
|
|
6454
6469
|
.btn-ghost { border-color: transparent; background: transparent; }
|
|
6455
6470
|
.btn-icon { width: 32px; padding: 0; justify-content: center; }
|
|
6456
6471
|
.btn-danger { color: var(--n-danger); }
|
|
6472
|
+
/* After .btn, which would otherwise paint over the red with the card's background. */
|
|
6473
|
+
.btn-danger-solid,
|
|
6474
|
+
.btn-danger-solid:hover { background: var(--n-danger); border-color: var(--n-danger); color: #fff; }
|
|
6475
|
+
.btn-danger-solid:hover { filter: brightness(1.08); }
|
|
6457
6476
|
.error { color: var(--n-danger); font-size: 12px; }
|
|
6458
6477
|
.divider { height: 1px; background: var(--n-border); }
|
|
6459
6478
|
|
|
@@ -6490,7 +6509,90 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
|
|
|
6490
6509
|
white-space: nowrap;
|
|
6491
6510
|
}
|
|
6492
6511
|
.cmp-link:hover { color: var(--n-fg); }
|
|
6493
|
-
.composer .quote { margin: 6px 14px 0; }
|
|
6512
|
+
.composer .quote, .composer .suggest { margin: 6px 14px 0; }
|
|
6513
|
+
.suggest {
|
|
6514
|
+
display: grid;
|
|
6515
|
+
gap: 4px;
|
|
6516
|
+
padding: 8px 10px;
|
|
6517
|
+
border-radius: 8px;
|
|
6518
|
+
background: var(--n-bg-subtle);
|
|
6519
|
+
white-space: pre-wrap;
|
|
6520
|
+
overflow-wrap: anywhere;
|
|
6521
|
+
}
|
|
6522
|
+
.suggest-label {
|
|
6523
|
+
display: flex;
|
|
6524
|
+
align-items: center;
|
|
6525
|
+
gap: 6px;
|
|
6526
|
+
min-height: 20px;
|
|
6527
|
+
color: var(--n-fg-muted);
|
|
6528
|
+
font-size: 12px;
|
|
6529
|
+
font-weight: 550;
|
|
6530
|
+
}
|
|
6531
|
+
.suggest .cmp-link { margin-left: auto; padding: 0 4px; }
|
|
6532
|
+
.suggest del, .item-suggest del { color: var(--n-fg-muted); text-decoration-color: var(--n-danger); }
|
|
6533
|
+
.suggest ins, .item-suggest ins {
|
|
6534
|
+
color: var(--n-fg);
|
|
6535
|
+
text-decoration: none;
|
|
6536
|
+
background: var(--n-accent-soft);
|
|
6537
|
+
border-radius: 4px;
|
|
6538
|
+
}
|
|
6539
|
+
.suggest ins { padding: 1px 3px; justify-self: start; }
|
|
6540
|
+
.cmp-suggest {
|
|
6541
|
+
display: block;
|
|
6542
|
+
width: 100%;
|
|
6543
|
+
max-height: 160px;
|
|
6544
|
+
padding: 4px 6px;
|
|
6545
|
+
border: 1px solid var(--n-border);
|
|
6546
|
+
border-radius: 6px;
|
|
6547
|
+
background: var(--n-bg);
|
|
6548
|
+
outline: none;
|
|
6549
|
+
resize: none;
|
|
6550
|
+
line-height: 1.4;
|
|
6551
|
+
}
|
|
6552
|
+
.cmp-suggest:focus { border-color: var(--n-accent); }
|
|
6553
|
+
.cmp-suggest-btn { display: inline-flex; align-items: center; gap: 4px; }
|
|
6554
|
+
.lost {
|
|
6555
|
+
display: grid;
|
|
6556
|
+
gap: 8px;
|
|
6557
|
+
padding: 10px;
|
|
6558
|
+
border: 1px dashed var(--n-border);
|
|
6559
|
+
border-radius: 8px;
|
|
6560
|
+
color: var(--n-fg-muted);
|
|
6561
|
+
font-size: 13px;
|
|
6562
|
+
}
|
|
6563
|
+
.lost p { margin: 0; }
|
|
6564
|
+
.cmp-images { display: grid; gap: 4px; padding: 6px 14px 0; }
|
|
6565
|
+
.cmp-thumbs { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
6566
|
+
.cmp-thumb { position: relative; }
|
|
6567
|
+
.cmp-thumb img, .images img {
|
|
6568
|
+
display: block;
|
|
6569
|
+
width: 64px;
|
|
6570
|
+
height: 48px;
|
|
6571
|
+
object-fit: cover;
|
|
6572
|
+
border: 1px solid var(--n-border);
|
|
6573
|
+
border-radius: 6px;
|
|
6574
|
+
background: var(--n-bg-subtle);
|
|
6575
|
+
}
|
|
6576
|
+
.cmp-thumb-remove {
|
|
6577
|
+
position: absolute;
|
|
6578
|
+
top: -6px;
|
|
6579
|
+
right: -6px;
|
|
6580
|
+
display: grid;
|
|
6581
|
+
place-items: center;
|
|
6582
|
+
width: 20px;
|
|
6583
|
+
height: 20px;
|
|
6584
|
+
padding: 0;
|
|
6585
|
+
border: 1px solid var(--n-border);
|
|
6586
|
+
border-radius: 999px;
|
|
6587
|
+
background: var(--n-bg);
|
|
6588
|
+
color: var(--n-fg-muted);
|
|
6589
|
+
}
|
|
6590
|
+
.cmp-thumb-remove .icon { width: 12px; height: 12px; }
|
|
6591
|
+
.cmp-images-note { margin: 0; color: var(--n-fg-muted); font-size: 12px; }
|
|
6592
|
+
.images { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
6593
|
+
.images img { width: 88px; height: 64px; }
|
|
6594
|
+
.lost .row { gap: 6px; flex-wrap: wrap; }
|
|
6595
|
+
.cmp-suggest-btn .icon { width: 13px; height: 13px; }
|
|
6494
6596
|
.cmp-text {
|
|
6495
6597
|
display: block;
|
|
6496
6598
|
width: 100%;
|
|
@@ -6559,7 +6661,7 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
|
|
|
6559
6661
|
}
|
|
6560
6662
|
.panel-head { display: flex; align-items: center; gap: 8px; padding: 12px; border-bottom: 1px solid var(--n-border); }
|
|
6561
6663
|
.panel-title { font-weight: 650; flex: 1; }
|
|
6562
|
-
.tabs { display: flex; gap: 4px; padding:
|
|
6664
|
+
.tabs { display: flex; gap: 4px; padding: 0 12px; margin-top: 8px; }
|
|
6563
6665
|
.tab {
|
|
6564
6666
|
border: 0;
|
|
6565
6667
|
background: transparent;
|
|
@@ -6582,6 +6684,11 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
|
|
|
6582
6684
|
border-radius: 10px;
|
|
6583
6685
|
}
|
|
6584
6686
|
.item:hover { background: var(--n-bg-subtle); }
|
|
6687
|
+
/* The comment opened last, so it's easy to find your place again. */
|
|
6688
|
+
.item[aria-current="true"] { background: var(--n-accent-soft); box-shadow: inset 3px 0 0 var(--n-accent); }
|
|
6689
|
+
.panel-search { position: relative; padding: 0 12px; margin-top: 8px; }
|
|
6690
|
+
.panel-search .icon { position: absolute; left: 22px; top: 50%; transform: translateY(-50%); color: var(--n-fg-muted); pointer-events: none; }
|
|
6691
|
+
.panel-search .field { padding-left: 32px; }
|
|
6585
6692
|
.item-text { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; }
|
|
6586
6693
|
.section-label { padding: 12px 10px 4px; font-size: 11px; font-weight: 650; letter-spacing: 0.04em; text-transform: uppercase; color: var(--n-fg-muted); }
|
|
6587
6694
|
.empty { padding: 32px 16px; text-align: center; color: var(--n-fg-muted); }
|
|
@@ -6625,8 +6732,10 @@ a.item { color: inherit; text-decoration: none; }
|
|
|
6625
6732
|
* { transition: none !important; animation: none !important; }
|
|
6626
6733
|
}
|
|
6627
6734
|
|
|
6628
|
-
/* The toolbar's
|
|
6735
|
+
/* The toolbar's spot (the position option, or where it was dragged) */
|
|
6629
6736
|
:host([data-position$="left"]) .toolbar { right: auto; left: 16px; }
|
|
6737
|
+
:host([data-position$="center"]) .toolbar { left: 0; right: 0; width: max-content; margin-inline: auto; }
|
|
6738
|
+
:host([data-position="top-center"]) .pick-hint { top: auto; bottom: 12px; }
|
|
6630
6739
|
:host([data-position^="top"]) .toolbar { bottom: auto; top: 16px; }
|
|
6631
6740
|
:host([data-position$="left"]) .panel { right: auto; left: 12px; }
|
|
6632
6741
|
:host([data-position^="top"]) .panel { top: 72px; bottom: 12px; }
|
|
@@ -6637,7 +6746,35 @@ a.item { color: inherit; text-decoration: none; }
|
|
|
6637
6746
|
}
|
|
6638
6747
|
`;
|
|
6639
6748
|
//#endregion
|
|
6749
|
+
//#region src/suggestion.ts
|
|
6750
|
+
/**
|
|
6751
|
+
* The text a comment can suggest new words for: the selected words, or the
|
|
6752
|
+
* whole text of a small element (a button, a link, a heading) with no other
|
|
6753
|
+
* text inside it. Null for areas, form fields and larger blocks.
|
|
6754
|
+
*/
|
|
6755
|
+
function suggestableText(anchor, element) {
|
|
6756
|
+
if (anchor.quote) return anchor.quote.exact;
|
|
6757
|
+
if (anchor.region || !anchor.text) return null;
|
|
6758
|
+
if (/^(INPUT|TEXTAREA|SELECT)$/.test(element.tagName)) return null;
|
|
6759
|
+
if (anchor.text.length >= LIMITS.anchorTextMaxLength) return null;
|
|
6760
|
+
for (const child of element.querySelectorAll("*")) if (elementText(child)) return null;
|
|
6761
|
+
return anchor.text;
|
|
6762
|
+
}
|
|
6763
|
+
/** The old words struck through and the new ones, under a small label. */
|
|
6764
|
+
function renderSuggestion(suggestion, label, action) {
|
|
6765
|
+
return h("div", { class: "suggest" }, h("div", { class: "suggest-label" }, label, action), h("del", {}, suggestion.before), h("ins", {}, suggestion.after));
|
|
6766
|
+
}
|
|
6767
|
+
/** One line for the panel: old → new. */
|
|
6768
|
+
function renderSuggestionLine(suggestion) {
|
|
6769
|
+
return h("div", { class: "item-quote item-suggest" }, h("del", {}, suggestion.before), " → ", h("ins", {}, suggestion.after));
|
|
6770
|
+
}
|
|
6771
|
+
//#endregion
|
|
6640
6772
|
//#region src/widget.ts
|
|
6773
|
+
const PANEL_TABS = [
|
|
6774
|
+
"open",
|
|
6775
|
+
"resolved",
|
|
6776
|
+
"mine"
|
|
6777
|
+
];
|
|
6641
6778
|
const PICKING_CLASS = "nuni-picking";
|
|
6642
6779
|
const PICK_EVENTS = [
|
|
6643
6780
|
"pointerdown",
|
|
@@ -6680,6 +6817,14 @@ function frameDocuments(doc, depth = 0) {
|
|
|
6680
6817
|
return out;
|
|
6681
6818
|
}
|
|
6682
6819
|
const MOBILE_QUERY = "(max-width: 640px)";
|
|
6820
|
+
/** How long pins have to stay settled before lost ones are reported. */
|
|
6821
|
+
const PIN_REPORT_DELAY_MS = 3e3;
|
|
6822
|
+
/** Drawn on screenshots: stands out on most pages, whatever the accent. */
|
|
6823
|
+
const MARKUP_COLOR = "#e5484d";
|
|
6824
|
+
/** The previews of a comment box's images are no longer shown. */
|
|
6825
|
+
function revokeImages(draft) {
|
|
6826
|
+
for (const image of draft.images ?? []) URL.revokeObjectURL(image.url);
|
|
6827
|
+
}
|
|
6683
6828
|
function isTypingTarget(target) {
|
|
6684
6829
|
if (!target || target.nodeType !== 1) return false;
|
|
6685
6830
|
const el = target;
|
|
@@ -6699,6 +6844,17 @@ function describeElement(el) {
|
|
|
6699
6844
|
return `${tag}${id}${cls}${text ? ` "${text}"` : ""}`;
|
|
6700
6845
|
}
|
|
6701
6846
|
/**
|
|
6847
|
+
* A link to one comment on another page of this site: the page, opened
|
|
6848
|
+
* with that comment focused. Null when the page can't be linked to.
|
|
6849
|
+
*/
|
|
6850
|
+
function commentHref(c, loc) {
|
|
6851
|
+
const page = otherPageHref(c.page.path, loc);
|
|
6852
|
+
if (page === null) return null;
|
|
6853
|
+
const url = new URL(page);
|
|
6854
|
+
url.searchParams.set("nuni", c._id);
|
|
6855
|
+
return url.toString();
|
|
6856
|
+
}
|
|
6857
|
+
/**
|
|
6702
6858
|
* Build an "Other pages" link target, or null when it would leave the
|
|
6703
6859
|
* current origin. The stored path comes from anonymous comment submissions;
|
|
6704
6860
|
* one that does not start with "/" (".evil.com/x", "@evil.com/") would
|
|
@@ -6724,6 +6880,8 @@ var NuniWidget = class {
|
|
|
6724
6880
|
_defineProperty(this, "overlayLayer", void 0);
|
|
6725
6881
|
_defineProperty(this, "selectButton", void 0);
|
|
6726
6882
|
_defineProperty(this, "uiLayer", void 0);
|
|
6883
|
+
_defineProperty(this, "modalLayer", void 0);
|
|
6884
|
+
_defineProperty(this, "markingUp", false);
|
|
6727
6885
|
_defineProperty(this, "api", void 0);
|
|
6728
6886
|
_defineProperty(this, "cleanups", []);
|
|
6729
6887
|
_defineProperty(this, "pageUnsub", null);
|
|
@@ -6738,6 +6896,13 @@ var NuniWidget = class {
|
|
|
6738
6896
|
_defineProperty(this, "picking", false);
|
|
6739
6897
|
_defineProperty(this, "panelOpen", false);
|
|
6740
6898
|
_defineProperty(this, "tab", "open");
|
|
6899
|
+
_defineProperty(this, "mine", null);
|
|
6900
|
+
_defineProperty(this, "mineUnsub", null);
|
|
6901
|
+
_defineProperty(this, "repinId", null);
|
|
6902
|
+
_defineProperty(this, "pinReported", /* @__PURE__ */ new Map());
|
|
6903
|
+
_defineProperty(this, "pinReportTimer", 0);
|
|
6904
|
+
_defineProperty(this, "search", "");
|
|
6905
|
+
_defineProperty(this, "lastViewedId", void 0);
|
|
6741
6906
|
_defineProperty(this, "card", null);
|
|
6742
6907
|
_defineProperty(this, "activeId", null);
|
|
6743
6908
|
_defineProperty(this, "hoverEl", null);
|
|
@@ -6775,6 +6940,10 @@ var NuniWidget = class {
|
|
|
6775
6940
|
_defineProperty(this, "resolveTimer", 0);
|
|
6776
6941
|
_defineProperty(this, "shownPlacements", "");
|
|
6777
6942
|
_defineProperty(this, "i18n", void 0);
|
|
6943
|
+
_defineProperty(this, "position", void 0);
|
|
6944
|
+
_defineProperty(this, "toolbarDrag", null);
|
|
6945
|
+
_defineProperty(this, "endToolbarDrag", null);
|
|
6946
|
+
_defineProperty(this, "toolbarDropped", false);
|
|
6778
6947
|
_defineProperty(this, "isIgnored", (el) => el === this.host || this.host.contains(el));
|
|
6779
6948
|
_defineProperty(this, "onFrameKey", (e) => this.onKeyDown(e));
|
|
6780
6949
|
_defineProperty(this, "swallow", (e) => {
|
|
@@ -6854,6 +7023,8 @@ var NuniWidget = class {
|
|
|
6854
7023
|
}
|
|
6855
7024
|
this.secret = secret;
|
|
6856
7025
|
this.ownerToken = read(KEYS.session(config.project));
|
|
7026
|
+
this.lastViewedId = read(KEYS.lastViewed(config.project));
|
|
7027
|
+
this.position = oneOf(read(KEYS.position(config.project)) ?? void 0, POSITIONS, config.position);
|
|
6857
7028
|
this.host = document.createElement("div");
|
|
6858
7029
|
this.host.id = "nuni-root";
|
|
6859
7030
|
this.host.setAttribute("data-nuni", "");
|
|
@@ -6868,6 +7039,7 @@ var NuniWidget = class {
|
|
|
6868
7039
|
this.pinLayer = h("div", { class: "pins" });
|
|
6869
7040
|
this.overlayLayer = h("div", { class: "overlay" });
|
|
6870
7041
|
this.uiLayer = h("div", { class: "ui" });
|
|
7042
|
+
this.modalLayer = h("div", { class: "modal" });
|
|
6871
7043
|
this.selectButton = h("button", {
|
|
6872
7044
|
class: "select-btn",
|
|
6873
7045
|
type: "button",
|
|
@@ -6882,8 +7054,13 @@ var NuniWidget = class {
|
|
|
6882
7054
|
role: "status",
|
|
6883
7055
|
"aria-live": "polite"
|
|
6884
7056
|
});
|
|
6885
|
-
this.root.append(style, this.markLayer, this.pinLayer, this.overlayLayer, this.uiLayer, this.selectButton, this.live);
|
|
7057
|
+
this.root.append(style, this.markLayer, this.pinLayer, this.overlayLayer, this.uiLayer, this.modalLayer, this.selectButton, this.live);
|
|
6886
7058
|
this.uiLayer.addEventListener("keydown", (e) => this.trapFocus(e));
|
|
7059
|
+
this.root.addEventListener("click", (e) => {
|
|
7060
|
+
if (!this.toolbarDropped) return;
|
|
7061
|
+
e.preventDefault();
|
|
7062
|
+
e.stopPropagation();
|
|
7063
|
+
}, true);
|
|
6887
7064
|
this.api = new NuniApi(config);
|
|
6888
7065
|
}
|
|
6889
7066
|
t(key, vars) {
|
|
@@ -6896,8 +7073,8 @@ var NuniWidget = class {
|
|
|
6896
7073
|
}
|
|
6897
7074
|
/** Position, theme, brand color and stacking order from the options. */
|
|
6898
7075
|
applyAppearance() {
|
|
6899
|
-
const {
|
|
6900
|
-
this.host.dataset.position = position;
|
|
7076
|
+
const { theme, accentColor, zIndex } = this.config;
|
|
7077
|
+
this.host.dataset.position = this.position;
|
|
6901
7078
|
this.host.dataset.theme = theme;
|
|
6902
7079
|
if (zIndex !== null) this.host.style.zIndex = String(zIndex);
|
|
6903
7080
|
const rgb = accentColor ? parseColor(accentColor) : null;
|
|
@@ -6944,6 +7121,7 @@ var NuniWidget = class {
|
|
|
6944
7121
|
if (e.composedPath().includes(this.host)) return;
|
|
6945
7122
|
this.pointerDown = true;
|
|
6946
7123
|
this.scheduleSelection();
|
|
7124
|
+
this.closeCardFromOutside(e);
|
|
6947
7125
|
};
|
|
6948
7126
|
const onUp = () => {
|
|
6949
7127
|
this.pointerDown = false;
|
|
@@ -6991,10 +7169,13 @@ var NuniWidget = class {
|
|
|
6991
7169
|
}
|
|
6992
7170
|
destroy() {
|
|
6993
7171
|
this.setPicking(false);
|
|
7172
|
+
this.endToolbarDrag?.();
|
|
6994
7173
|
this.pageUnsub?.();
|
|
6995
7174
|
this.sessionUnsub?.();
|
|
6996
7175
|
this.ownerDetailUnsub?.();
|
|
6997
7176
|
this.threadUnsub?.();
|
|
7177
|
+
this.mineUnsub?.();
|
|
7178
|
+
clearTimeout(this.pinReportTimer);
|
|
6998
7179
|
this.resolveRun++;
|
|
6999
7180
|
for (const stop of this.scopeWatchers.values()) stop();
|
|
7000
7181
|
this.scopeWatchers.clear();
|
|
@@ -7018,6 +7199,8 @@ var NuniWidget = class {
|
|
|
7018
7199
|
this.extraComment = null;
|
|
7019
7200
|
this.loaded = false;
|
|
7020
7201
|
this.placements.clear();
|
|
7202
|
+
this.pinReported.clear();
|
|
7203
|
+
clearTimeout(this.pinReportTimer);
|
|
7021
7204
|
this.card = null;
|
|
7022
7205
|
this.activeId = null;
|
|
7023
7206
|
this.pageUnsub = this.api.onPage(key, (comments) => {
|
|
@@ -7134,11 +7317,39 @@ var NuniWidget = class {
|
|
|
7134
7317
|
this.watchScopes();
|
|
7135
7318
|
this.renderPins();
|
|
7136
7319
|
this.layout();
|
|
7137
|
-
if (queue.length)
|
|
7138
|
-
|
|
7320
|
+
if (queue.length) {
|
|
7321
|
+
whenIdle(slice);
|
|
7322
|
+
return;
|
|
7323
|
+
}
|
|
7324
|
+
this.schedulePinReport();
|
|
7325
|
+
if ((onlyStale || run > 1) && this.panelOpen && this.placementSummary() !== this.shownPlacements) this.render();
|
|
7139
7326
|
};
|
|
7140
7327
|
slice();
|
|
7141
7328
|
}
|
|
7329
|
+
/**
|
|
7330
|
+
* Once the pins have settled for a few seconds, report the ones that
|
|
7331
|
+
* couldn't be found (or were found again), so the owner sees them in the
|
|
7332
|
+
* dashboard. Only changes are sent.
|
|
7333
|
+
*/
|
|
7334
|
+
schedulePinReport() {
|
|
7335
|
+
clearTimeout(this.pinReportTimer);
|
|
7336
|
+
this.pinReportTimer = window.setTimeout(() => {
|
|
7337
|
+
if (!this.loaded || document.visibilityState !== "visible") return;
|
|
7338
|
+
const lost = [];
|
|
7339
|
+
const found = [];
|
|
7340
|
+
for (const c of this.comments) {
|
|
7341
|
+
if (c.status !== "open" || c.page.path !== this.pageKey) continue;
|
|
7342
|
+
const placement = this.placements.get(c._id);
|
|
7343
|
+
if (!placement) continue;
|
|
7344
|
+
const isLost = !placement.element;
|
|
7345
|
+
if (isLost === (this.pinReported.get(c._id) ?? Boolean(c.pinLost))) continue;
|
|
7346
|
+
if (lost.length + found.length >= LIMITS.pinReportMax) break;
|
|
7347
|
+
this.pinReported.set(c._id, isLost);
|
|
7348
|
+
(isLost ? lost : found).push(c._id);
|
|
7349
|
+
}
|
|
7350
|
+
if (lost.length || found.length) this.api.reportPins(this.pageKey, lost, found);
|
|
7351
|
+
}, PIN_REPORT_DELAY_MS);
|
|
7352
|
+
}
|
|
7142
7353
|
/** What the panel and cards show from the placements: found, approximate or lost. */
|
|
7143
7354
|
placementSummary() {
|
|
7144
7355
|
return this.comments.map((c) => {
|
|
@@ -7296,6 +7507,7 @@ var NuniWidget = class {
|
|
|
7296
7507
|
this.placeSelectButton();
|
|
7297
7508
|
}
|
|
7298
7509
|
setPicking(on) {
|
|
7510
|
+
if (!on) this.repinId = null;
|
|
7299
7511
|
if (this.picking === on) return;
|
|
7300
7512
|
this.picking = on;
|
|
7301
7513
|
const html = document.documentElement;
|
|
@@ -7417,10 +7629,16 @@ var NuniWidget = class {
|
|
|
7417
7629
|
}
|
|
7418
7630
|
openComposer(draft) {
|
|
7419
7631
|
const { element } = draft;
|
|
7632
|
+
const repinId = this.repinId;
|
|
7420
7633
|
this.setPicking(false);
|
|
7634
|
+
if (repinId) {
|
|
7635
|
+
this.movePin(repinId, draft);
|
|
7636
|
+
return;
|
|
7637
|
+
}
|
|
7421
7638
|
if (this.config.capture.dom) try {
|
|
7422
7639
|
draft.dom = domContext(element);
|
|
7423
7640
|
} catch {}
|
|
7641
|
+
draft.suggestFrom = suggestableText(draft.anchor, element);
|
|
7424
7642
|
this.card = {
|
|
7425
7643
|
kind: "composer",
|
|
7426
7644
|
draft
|
|
@@ -7430,14 +7648,18 @@ var NuniWidget = class {
|
|
|
7430
7648
|
this.focusComposer();
|
|
7431
7649
|
draft.screenshot = this.takeScreenshot(draft);
|
|
7432
7650
|
}
|
|
7651
|
+
/** An iframe's document can't be rendered from the page. */
|
|
7652
|
+
canScreenshot(draft) {
|
|
7653
|
+
return Boolean(this.runtime.loadScreenshot) && draft.element.ownerDocument === document;
|
|
7654
|
+
}
|
|
7433
7655
|
/** Capture while the person types, so posting stays instant. */
|
|
7434
7656
|
async takeScreenshot(draft) {
|
|
7435
7657
|
const load = this.runtime.loadScreenshot;
|
|
7436
|
-
if (!load || draft
|
|
7658
|
+
if (!load || !this.canScreenshot(draft)) return null;
|
|
7437
7659
|
const accent = getComputedStyle(this.host).getPropertyValue("--n-accent").trim() || "#d6246e";
|
|
7438
7660
|
try {
|
|
7439
7661
|
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
7440
|
-
const
|
|
7662
|
+
const tools = await load();
|
|
7441
7663
|
const { range, anchor, element } = draft;
|
|
7442
7664
|
const focus = range ? () => range.getBoundingClientRect() : anchor.region ? () => {
|
|
7443
7665
|
const box = regionBox(anchor, element);
|
|
@@ -7448,7 +7670,7 @@ var NuniWidget = class {
|
|
|
7448
7670
|
bottom: box.top + box.height
|
|
7449
7671
|
};
|
|
7450
7672
|
} : void 0;
|
|
7451
|
-
return
|
|
7673
|
+
return tools ? await tools.capture(element, {
|
|
7452
7674
|
exclude: this.host,
|
|
7453
7675
|
accent,
|
|
7454
7676
|
focus
|
|
@@ -7473,19 +7695,93 @@ var NuniWidget = class {
|
|
|
7473
7695
|
if (image) await this.api.uploadScreenshot(id, this.secret, image);
|
|
7474
7696
|
} catch {}
|
|
7475
7697
|
}
|
|
7698
|
+
/** One after another, so they keep their order. */
|
|
7699
|
+
async attachImages(id, draft) {
|
|
7700
|
+
const images = draft.images ?? [];
|
|
7701
|
+
revokeImages(draft);
|
|
7702
|
+
try {
|
|
7703
|
+
for (const image of images) await this.api.uploadImage(id, this.secret, image.blob);
|
|
7704
|
+
} catch (error) {
|
|
7705
|
+
this.showToast(error instanceof NuniApiError ? error.message : this.t("imageFailed"));
|
|
7706
|
+
}
|
|
7707
|
+
}
|
|
7708
|
+
/** Whether this comment box can take images (claimed sites only). */
|
|
7709
|
+
canAttach(draft) {
|
|
7710
|
+
return this.config.images && Boolean(this.status?.claimed) && (draft.images?.length ?? 0) < LIMITS.imagesPerComment;
|
|
7711
|
+
}
|
|
7712
|
+
async addImages(draft, files) {
|
|
7713
|
+
draft.error = void 0;
|
|
7714
|
+
for (const file of files) {
|
|
7715
|
+
if ((draft.images?.length ?? 0) >= LIMITS.imagesPerComment) {
|
|
7716
|
+
draft.error = this.t("tooManyImages", { count: LIMITS.imagesPerComment });
|
|
7717
|
+
break;
|
|
7718
|
+
}
|
|
7719
|
+
const blob = await imageFromFile(file);
|
|
7720
|
+
if (!blob) {
|
|
7721
|
+
draft.error = this.t("imageUnreadable");
|
|
7722
|
+
continue;
|
|
7723
|
+
}
|
|
7724
|
+
(draft.images ?? (draft.images = [])).push({
|
|
7725
|
+
blob,
|
|
7726
|
+
url: URL.createObjectURL(blob)
|
|
7727
|
+
});
|
|
7728
|
+
}
|
|
7729
|
+
this.render();
|
|
7730
|
+
this.positionCard();
|
|
7731
|
+
}
|
|
7732
|
+
/** Draw on the screenshot, then attach the result for everyone to see. */
|
|
7733
|
+
async markUpScreenshot(draft) {
|
|
7734
|
+
const load = this.runtime.loadScreenshot;
|
|
7735
|
+
if (!load || !draft.screenshot || draft.preparingMarkup) return;
|
|
7736
|
+
draft.preparingMarkup = true;
|
|
7737
|
+
draft.error = void 0;
|
|
7738
|
+
this.render();
|
|
7739
|
+
const [tools, shot] = await Promise.all([load(), draft.screenshot]);
|
|
7740
|
+
draft.preparingMarkup = false;
|
|
7741
|
+
if (!tools || !shot) {
|
|
7742
|
+
this.setDraftError(draft, this.t("noScreenshot"));
|
|
7743
|
+
return;
|
|
7744
|
+
}
|
|
7745
|
+
this.markingUp = true;
|
|
7746
|
+
try {
|
|
7747
|
+
const marked = await tools.markUp(shot, {
|
|
7748
|
+
container: this.modalLayer,
|
|
7749
|
+
color: MARKUP_COLOR,
|
|
7750
|
+
labels: {
|
|
7751
|
+
title: this.t("markUpTitle"),
|
|
7752
|
+
box: this.t("markUpBox"),
|
|
7753
|
+
arrow: this.t("markUpArrow"),
|
|
7754
|
+
pen: this.t("markUpPen"),
|
|
7755
|
+
undo: this.t("markUpUndo"),
|
|
7756
|
+
cancel: this.t("cancel"),
|
|
7757
|
+
done: this.t("markUpDone"),
|
|
7758
|
+
failed: this.t("markUpFailed")
|
|
7759
|
+
}
|
|
7760
|
+
});
|
|
7761
|
+
if (marked) (draft.images ?? (draft.images = [])).push({
|
|
7762
|
+
blob: marked,
|
|
7763
|
+
url: URL.createObjectURL(marked)
|
|
7764
|
+
});
|
|
7765
|
+
} finally {
|
|
7766
|
+
this.markingUp = false;
|
|
7767
|
+
}
|
|
7768
|
+
this.render();
|
|
7769
|
+
this.positionCard();
|
|
7770
|
+
}
|
|
7476
7771
|
copyForAgent(comment) {
|
|
7477
7772
|
const detail = this.ownerDetail?._id === comment._id ? this.ownerDetail : null;
|
|
7478
7773
|
const replies = this.threadId === comment._id ? this.thread?.replies : void 0;
|
|
7479
|
-
|
|
7774
|
+
const prompt = buildCommentPrompt(detail ?? {
|
|
7480
7775
|
...comment,
|
|
7481
7776
|
replies
|
|
7482
|
-
}, { includeContext: Boolean(detail) })
|
|
7777
|
+
}, { includeContext: Boolean(detail) });
|
|
7778
|
+
copyText(prompt).then((ok) => this.showToast(ok ? this.t("copied") : this.t("copyFailed")));
|
|
7483
7779
|
}
|
|
7484
7780
|
renderHighlight() {
|
|
7485
7781
|
this.overlayLayer.replaceChildren();
|
|
7486
7782
|
if (!this.picking) return;
|
|
7487
7783
|
const mobile = window.matchMedia(MOBILE_QUERY).matches;
|
|
7488
|
-
this.overlayLayer.append(h("div", { class: "pick-hint" }, mobile ? this.t("pickHintMobile") : [h("span", {}, this.t("pickHint")), h("span", { class: "pick-keys" }, this.t("pickHintKeys"))]));
|
|
7784
|
+
this.overlayLayer.append(h("div", { class: "pick-hint" }, this.repinId ? this.t("movePinHint") : mobile ? this.t("pickHintMobile") : [h("span", {}, this.t("pickHint")), h("span", { class: "pick-keys" }, this.t("pickHintKeys"))]));
|
|
7489
7785
|
if (this.dragBox) {
|
|
7490
7786
|
const box = h("div", { class: "highlight drag-box" });
|
|
7491
7787
|
Object.assign(box.style, {
|
|
@@ -7510,12 +7806,16 @@ var NuniWidget = class {
|
|
|
7510
7806
|
this.overlayLayer.append(box);
|
|
7511
7807
|
}
|
|
7512
7808
|
onKeyDown(e) {
|
|
7809
|
+
if (this.markingUp) return;
|
|
7513
7810
|
const inWidget = e.composedPath().includes(this.host);
|
|
7514
7811
|
if (e.key === "Escape") {
|
|
7515
7812
|
if (this.confirming) this.answer(false);
|
|
7516
7813
|
else if (this.picking) this.setPicking(false);
|
|
7517
7814
|
else if (this.card) this.closeCard();
|
|
7518
|
-
else if (this.panelOpen
|
|
7815
|
+
else if (this.panelOpen && this.search) {
|
|
7816
|
+
this.search = "";
|
|
7817
|
+
this.render();
|
|
7818
|
+
} else if (this.panelOpen) this.togglePanel(false);
|
|
7519
7819
|
else return;
|
|
7520
7820
|
e.stopPropagation();
|
|
7521
7821
|
return;
|
|
@@ -7743,7 +8043,15 @@ var NuniWidget = class {
|
|
|
7743
8043
|
const cleanName = name.trim().slice(0, LIMITS.nameMaxLength);
|
|
7744
8044
|
const cleanBody = body.trim();
|
|
7745
8045
|
if (!cleanName) return this.setDraftError(draft, this.t("enterName"));
|
|
7746
|
-
|
|
8046
|
+
const before = draft.suggesting ? draft.suggestFrom : null;
|
|
8047
|
+
const after = (draft.suggestion ?? before ?? "").trim();
|
|
8048
|
+
if (before != null && (!after || after === before)) return this.setDraftError(draft, this.t("changeTextFirst"));
|
|
8049
|
+
if (after.length > LIMITS.quoteMaxLength) return this.setDraftError(draft, this.t("suggestionTooLong", { count: LIMITS.quoteMaxLength }));
|
|
8050
|
+
const suggestion = before != null ? {
|
|
8051
|
+
before,
|
|
8052
|
+
after
|
|
8053
|
+
} : void 0;
|
|
8054
|
+
if (!cleanBody && !suggestion) return this.setDraftError(draft, this.t("writeFirst"));
|
|
7747
8055
|
if (cleanBody.length > LIMITS.bodyMaxLength) return this.setDraftError(draft, this.t("commentTooLong", { count: LIMITS.bodyMaxLength }));
|
|
7748
8056
|
this.name = cleanName;
|
|
7749
8057
|
write(KEYS.name, cleanName);
|
|
@@ -7755,6 +8063,7 @@ var NuniWidget = class {
|
|
|
7755
8063
|
try {
|
|
7756
8064
|
const id = await this.api.createComment({
|
|
7757
8065
|
body: cleanBody,
|
|
8066
|
+
suggestion,
|
|
7758
8067
|
authorName: cleanName,
|
|
7759
8068
|
authorSecret: this.secret,
|
|
7760
8069
|
page: {
|
|
@@ -7771,6 +8080,7 @@ var NuniWidget = class {
|
|
|
7771
8080
|
context: this.collectContext(draft)
|
|
7772
8081
|
});
|
|
7773
8082
|
if (shot) this.attachScreenshot(id, shot);
|
|
8083
|
+
if (draft.images?.length) this.attachImages(id, draft);
|
|
7774
8084
|
this.placements.set(id, {
|
|
7775
8085
|
element: draft.element,
|
|
7776
8086
|
confidence: "exact",
|
|
@@ -7826,6 +8136,41 @@ var NuniWidget = class {
|
|
|
7826
8136
|
if (!token) return;
|
|
7827
8137
|
this.act(c._id, () => this.api.reopen(c._id, token), this.t("reopened"));
|
|
7828
8138
|
}
|
|
8139
|
+
/** Closed because what it was about is gone, not because it was fixed. */
|
|
8140
|
+
closeAsOutdated(c) {
|
|
8141
|
+
const token = this.ownerToken;
|
|
8142
|
+
if (!token) return;
|
|
8143
|
+
this.act(c._id, () => this.api.resolve(c._id, token, "outdated"), this.t("closedOutdated")).then((ok) => {
|
|
8144
|
+
if (ok && this.card?.kind === "thread" && this.card.id === c._id) this.closeCard();
|
|
8145
|
+
});
|
|
8146
|
+
}
|
|
8147
|
+
/** Pick the element the comment is about now; the pin moves there. */
|
|
8148
|
+
startMovePin(c) {
|
|
8149
|
+
if (!this.ownerToken) return;
|
|
8150
|
+
this.setPicking(true);
|
|
8151
|
+
this.repinId = c._id;
|
|
8152
|
+
this.render();
|
|
8153
|
+
}
|
|
8154
|
+
async movePin(id, draft) {
|
|
8155
|
+
const token = this.ownerToken;
|
|
8156
|
+
if (!token) return;
|
|
8157
|
+
const previous = this.placements.get(id);
|
|
8158
|
+
this.placements.set(id, {
|
|
8159
|
+
element: draft.element,
|
|
8160
|
+
confidence: "exact",
|
|
8161
|
+
range: draft.range
|
|
8162
|
+
});
|
|
8163
|
+
this.pinReported.set(id, false);
|
|
8164
|
+
try {
|
|
8165
|
+
await this.api.repin(id, draft.anchor, token);
|
|
8166
|
+
this.showToast(this.t("pinMoved"));
|
|
8167
|
+
} catch (error) {
|
|
8168
|
+
if (previous) this.placements.set(id, previous);
|
|
8169
|
+
else this.placements.delete(id);
|
|
8170
|
+
this.showToast(error instanceof NuniApiError ? error.message : this.t("failed"));
|
|
8171
|
+
}
|
|
8172
|
+
this.focusComment(id);
|
|
8173
|
+
}
|
|
7829
8174
|
async deleteComment(c) {
|
|
7830
8175
|
if (!await this.ask(this.t("confirmDeleteComment"), this.t("delete"))) return;
|
|
7831
8176
|
const token = this.ownerToken;
|
|
@@ -7903,8 +8248,16 @@ var NuniWidget = class {
|
|
|
7903
8248
|
}
|
|
7904
8249
|
});
|
|
7905
8250
|
}
|
|
7906
|
-
saveEdit(c, body) {
|
|
7907
|
-
this.act(c._id, () => this.api.editOwn(c._id, this.secret, body), this.t("saved"));
|
|
8251
|
+
saveEdit(c, body, suggestion) {
|
|
8252
|
+
this.act(c._id, () => this.api.editOwn(c._id, this.secret, body, suggestion), this.t("saved"));
|
|
8253
|
+
}
|
|
8254
|
+
/** For the owner: the suggested words, ready to paste into the code. */
|
|
8255
|
+
copyNewTextButton(text) {
|
|
8256
|
+
return h("button", {
|
|
8257
|
+
class: "cmp-link",
|
|
8258
|
+
type: "button",
|
|
8259
|
+
onclick: () => void copyText(text).then((ok) => this.showToast(this.t(ok ? "newTextCopied" : "copyFailed")))
|
|
8260
|
+
}, this.t("copyNewText"));
|
|
7908
8261
|
}
|
|
7909
8262
|
showToast(message) {
|
|
7910
8263
|
this.toast = message;
|
|
@@ -7918,15 +8271,33 @@ var NuniWidget = class {
|
|
|
7918
8271
|
togglePanel(open = !this.panelOpen) {
|
|
7919
8272
|
if (open === this.panelOpen) return;
|
|
7920
8273
|
this.panelOpen = open;
|
|
8274
|
+
if (open && !this.mineUnsub) this.mineUnsub = this.api.onMine(this.secret, (mine) => {
|
|
8275
|
+
this.mine = mine;
|
|
8276
|
+
this.render();
|
|
8277
|
+
});
|
|
7921
8278
|
if (open) {
|
|
7922
8279
|
this.keepFocus();
|
|
7923
8280
|
this.focusNext = ".panel [role=\"tab\"][aria-selected=\"true\"]";
|
|
7924
8281
|
if (window.matchMedia(MOBILE_QUERY).matches) this.card = null;
|
|
7925
|
-
}
|
|
8282
|
+
} else this.search = "";
|
|
7926
8283
|
this.render();
|
|
8284
|
+
if (open) this.uiLayer.querySelector(".panel .item[aria-current=\"true\"]")?.scrollIntoView({ block: "nearest" });
|
|
7927
8285
|
if (!open && !this.card) this.restoreFocus();
|
|
7928
8286
|
}
|
|
8287
|
+
/**
|
|
8288
|
+
* A press on the page closes the open card, like Figma, unless the card
|
|
8289
|
+
* holds words that haven't been sent yet.
|
|
8290
|
+
*/
|
|
8291
|
+
closeCardFromOutside(e) {
|
|
8292
|
+
if (!this.card || this.picking || this.confirming || !e.isPrimary) return;
|
|
8293
|
+
if (this.markingUp) return;
|
|
8294
|
+
if (onScrollbar(e)) return;
|
|
8295
|
+
if ([...this.uiLayer.querySelector("[data-card]")?.querySelectorAll("textarea") ?? []].some((t) => t.value.trim()) || this.card.kind === "composer" && Boolean(this.card.draft.images?.length)) return;
|
|
8296
|
+
if (!this.panelOpen) this.returnFocus = null;
|
|
8297
|
+
this.closeCard();
|
|
8298
|
+
}
|
|
7929
8299
|
closeCard() {
|
|
8300
|
+
if (this.card?.kind === "composer") revokeImages(this.card.draft);
|
|
7930
8301
|
const had = this.card !== null;
|
|
7931
8302
|
this.card = null;
|
|
7932
8303
|
this.activeId = null;
|
|
@@ -7937,6 +8308,8 @@ var NuniWidget = class {
|
|
|
7937
8308
|
openThread(id) {
|
|
7938
8309
|
this.keepFocus();
|
|
7939
8310
|
this.activeId = id;
|
|
8311
|
+
this.lastViewedId = id;
|
|
8312
|
+
write(KEYS.lastViewed(this.config.project), id);
|
|
7940
8313
|
this.card = {
|
|
7941
8314
|
kind: "thread",
|
|
7942
8315
|
id
|
|
@@ -7982,7 +8355,7 @@ var NuniWidget = class {
|
|
|
7982
8355
|
pin.style.background = colorFor(comment.authorName);
|
|
7983
8356
|
pin.setAttribute("aria-label", this.t("pinLabel", {
|
|
7984
8357
|
name: comment.authorName,
|
|
7985
|
-
body: comment.body.slice(0, 80)
|
|
8358
|
+
body: (comment.body || comment.suggestion?.after || "").slice(0, 80)
|
|
7986
8359
|
}));
|
|
7987
8360
|
if (comment.replyCount) pin.dataset.replies = String(comment.replyCount);
|
|
7988
8361
|
else delete pin.dataset.replies;
|
|
@@ -8035,10 +8408,14 @@ var NuniWidget = class {
|
|
|
8035
8408
|
}
|
|
8036
8409
|
renderToolbar() {
|
|
8037
8410
|
const open = this.comments.filter((c) => c.status === "open").length;
|
|
8411
|
+
const drag = this.toolbarDrag?.moving ? this.toolbarDrag : null;
|
|
8038
8412
|
return h("div", {
|
|
8039
8413
|
class: "toolbar",
|
|
8040
8414
|
role: "toolbar",
|
|
8041
|
-
"aria-label": this.t("toolbar")
|
|
8415
|
+
"aria-label": this.t("toolbar"),
|
|
8416
|
+
"data-dragging": !!drag,
|
|
8417
|
+
style: drag && `transform: translate(${drag.dx}px, ${drag.dy}px)`,
|
|
8418
|
+
onpointerdown: (e) => this.startToolbarDrag(e)
|
|
8042
8419
|
}, h("button", {
|
|
8043
8420
|
class: "tb-btn",
|
|
8044
8421
|
type: "button",
|
|
@@ -8057,16 +8434,113 @@ var NuniWidget = class {
|
|
|
8057
8434
|
onclick: () => this.togglePanel()
|
|
8058
8435
|
}, icon(ICONS.list), h("span", { class: "tb-count" }, String(open))));
|
|
8059
8436
|
}
|
|
8437
|
+
/**
|
|
8438
|
+
* Drag the toolbar anywhere; on release it glides to the nearest corner or
|
|
8439
|
+
* edge middle, which is remembered for this visitor.
|
|
8440
|
+
*/
|
|
8441
|
+
startToolbarDrag(e) {
|
|
8442
|
+
if (e.button !== 0 || !e.isPrimary) return;
|
|
8443
|
+
this.endToolbarDrag?.();
|
|
8444
|
+
const drag = {
|
|
8445
|
+
id: e.pointerId,
|
|
8446
|
+
x: e.clientX,
|
|
8447
|
+
y: e.clientY,
|
|
8448
|
+
dx: 0,
|
|
8449
|
+
dy: 0,
|
|
8450
|
+
moving: false
|
|
8451
|
+
};
|
|
8452
|
+
this.toolbarDrag = drag;
|
|
8453
|
+
const toolbar = () => this.uiLayer.querySelector(".toolbar");
|
|
8454
|
+
const onMove = (ev) => {
|
|
8455
|
+
if (ev.pointerId !== drag.id) return;
|
|
8456
|
+
drag.dx = ev.clientX - drag.x;
|
|
8457
|
+
drag.dy = ev.clientY - drag.y;
|
|
8458
|
+
if (!drag.moving && Math.hypot(drag.dx, drag.dy) < 5) return;
|
|
8459
|
+
if (!drag.moving) {
|
|
8460
|
+
drag.moving = true;
|
|
8461
|
+
this.host.dataset.dragging = "";
|
|
8462
|
+
toolbar()?.setAttribute("data-dragging", "");
|
|
8463
|
+
}
|
|
8464
|
+
ev.preventDefault();
|
|
8465
|
+
toolbar()?.style.setProperty("transform", `translate(${drag.dx}px, ${drag.dy}px)`);
|
|
8466
|
+
};
|
|
8467
|
+
const onUp = (ev) => {
|
|
8468
|
+
if (ev.pointerId !== drag.id) return;
|
|
8469
|
+
this.endToolbarDrag?.();
|
|
8470
|
+
if (drag.moving) this.dropToolbar(toolbar(), true);
|
|
8471
|
+
};
|
|
8472
|
+
const onCancel = (ev) => {
|
|
8473
|
+
if (ev instanceof PointerEvent && ev.pointerId !== drag.id) return;
|
|
8474
|
+
this.endToolbarDrag?.();
|
|
8475
|
+
if (drag.moving) this.dropToolbar(toolbar(), false);
|
|
8476
|
+
};
|
|
8477
|
+
window.addEventListener("pointermove", onMove, true);
|
|
8478
|
+
window.addEventListener("pointerup", onUp, true);
|
|
8479
|
+
window.addEventListener("pointercancel", onCancel, true);
|
|
8480
|
+
window.addEventListener("blur", onCancel);
|
|
8481
|
+
this.endToolbarDrag = () => {
|
|
8482
|
+
window.removeEventListener("pointermove", onMove, true);
|
|
8483
|
+
window.removeEventListener("pointerup", onUp, true);
|
|
8484
|
+
window.removeEventListener("pointercancel", onCancel, true);
|
|
8485
|
+
window.removeEventListener("blur", onCancel);
|
|
8486
|
+
delete this.host.dataset.dragging;
|
|
8487
|
+
this.toolbarDrag = null;
|
|
8488
|
+
this.endToolbarDrag = null;
|
|
8489
|
+
};
|
|
8490
|
+
}
|
|
8491
|
+
/** Glide the toolbar to the spot nearest where it was let go, or back. */
|
|
8492
|
+
dropToolbar(el, snap) {
|
|
8493
|
+
this.toolbarDropped = true;
|
|
8494
|
+
setTimeout(() => this.toolbarDropped = false);
|
|
8495
|
+
if (!el) return;
|
|
8496
|
+
const from = el.getBoundingClientRect();
|
|
8497
|
+
if (snap) {
|
|
8498
|
+
const position = nearestPosition(from.left + from.width / 2, from.top + from.height / 2);
|
|
8499
|
+
this.position = position;
|
|
8500
|
+
this.host.dataset.position = position;
|
|
8501
|
+
write(KEYS.position(this.config.project), position === this.config.position ? null : position);
|
|
8502
|
+
}
|
|
8503
|
+
el.removeAttribute("data-dragging");
|
|
8504
|
+
el.style.removeProperty("transform");
|
|
8505
|
+
const to = el.getBoundingClientRect();
|
|
8506
|
+
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
8507
|
+
el.animate([{ transform: `translate(${from.left - to.left}px, ${from.top - to.top}px)` }, { transform: "none" }], {
|
|
8508
|
+
duration: 320,
|
|
8509
|
+
easing: "cubic-bezier(0.2, 0.9, 0.3, 1.15)"
|
|
8510
|
+
});
|
|
8511
|
+
}
|
|
8060
8512
|
renderPanel() {
|
|
8061
8513
|
const open = this.comments.filter((c) => c.status === "open");
|
|
8062
8514
|
const resolved = this.comments.filter((c) => c.status === "resolved");
|
|
8063
|
-
const
|
|
8515
|
+
const query = this.search.trim().toLocaleLowerCase();
|
|
8516
|
+
const matches = (...texts) => !query || texts.some((t) => t?.toLocaleLowerCase().includes(query));
|
|
8517
|
+
const mine = this.mine ?? [];
|
|
8518
|
+
const list = (this.tab === "open" ? open : this.tab === "resolved" ? resolved : mine).filter((c) => matches(c.body, c.authorName, c.anchor.quote?.exact, c.suggestion?.after, this.tab === "mine" ? c.page.path : void 0));
|
|
8064
8519
|
const placed = list.filter((c) => this.placements.get(c._id)?.element);
|
|
8065
8520
|
const lost = list.filter((c) => !this.placements.get(c._id)?.element);
|
|
8066
|
-
const otherPages = this.pages.filter((p) => p.path !== this.pageKey);
|
|
8521
|
+
const otherPages = this.pages.filter((p) => p.path !== this.pageKey && matches(p.path));
|
|
8522
|
+
const content = (c) => [c.suggestion ? renderSuggestionLine(c.suggestion) : c.anchor.quote ? h("div", { class: "item-quote" }, c.anchor.quote.exact) : null, c.body ? h("div", { class: "item-text" }, c.body) : null];
|
|
8523
|
+
const mineItem = (c) => {
|
|
8524
|
+
const here = c.page.path === this.pageKey;
|
|
8525
|
+
const href = here ? null : commentHref(c, location);
|
|
8526
|
+
const resolvedBadge = c.status === "resolved";
|
|
8527
|
+
return h(here ? "button" : href === null ? "div" : "a", here ? {
|
|
8528
|
+
class: "item",
|
|
8529
|
+
type: "button",
|
|
8530
|
+
"data-focus-key": `item-${c._id}`,
|
|
8531
|
+
onclick: () => {
|
|
8532
|
+
this.deepLinkId = c._id;
|
|
8533
|
+
this.openDeepLink();
|
|
8534
|
+
}
|
|
8535
|
+
} : {
|
|
8536
|
+
class: "item",
|
|
8537
|
+
href
|
|
8538
|
+
}, h("div", { class: "row" }, h("span", { class: "author" }, here ? this.t("thisPage") : c.page.path), h("span", { class: "meta" }, this.i18n.timeAgo(c.createdAt)), h("span", { class: "spacer" }), h("span", { class: resolvedBadge ? "badge badge-ok" : "badge" }, resolvedBadge ? icon(ICONS.check) : null, this.t(!resolvedBadge ? "statusOpen" : c.resolution === "outdated" ? "outdated" : "resolved"))), ...content(c));
|
|
8539
|
+
};
|
|
8067
8540
|
const item = (c) => h("button", {
|
|
8068
8541
|
class: "item",
|
|
8069
8542
|
type: "button",
|
|
8543
|
+
"aria-current": c._id === this.lastViewedId ? "true" : null,
|
|
8070
8544
|
"data-focus-key": `item-${c._id}`,
|
|
8071
8545
|
onclick: () => this.focusComment(c._id)
|
|
8072
8546
|
}, h("div", { class: "row" }, h("span", {
|
|
@@ -8075,9 +8549,14 @@ var NuniWidget = class {
|
|
|
8075
8549
|
}, initials(c.authorName)), h("span", { class: "author" }, c.authorName), h("span", { class: "meta" }, this.i18n.timeAgo(c.createdAt) + (c.replyCount ? ` · ${this.t("replyCount", { count: c.replyCount })}` : "")), c.page.origin !== location.origin ? h("span", {
|
|
8076
8550
|
class: "badge",
|
|
8077
8551
|
title: c.page.origin
|
|
8078
|
-
}, new URL(c.page.origin).host) : null
|
|
8552
|
+
}, new URL(c.page.origin).host) : null, c.resolution === "outdated" ? h("span", { class: "badge" }, this.t("outdated")) : null), ...content(c));
|
|
8079
8553
|
const children = [];
|
|
8080
|
-
if (
|
|
8554
|
+
if (this.tab === "mine") {
|
|
8555
|
+
if (!this.mine) children.push(h("div", { class: "empty" }, this.t("loading")));
|
|
8556
|
+
else if (!list.length) children.push(h("div", { class: "empty" }, query ? this.t("noMatches", { query: this.search.trim() }) : this.t("noYours")));
|
|
8557
|
+
else children.push(...list.map(mineItem));
|
|
8558
|
+
} else if (!this.loaded) children.push(h("div", { class: "empty" }, this.t("loading")));
|
|
8559
|
+
else if (!list.length && query) children.push(h("div", { class: "empty" }, this.t("noMatches", { query: this.search.trim() })));
|
|
8081
8560
|
else if (!list.length) children.push(h("div", { class: "empty" }, this.tab === "open" ? this.withKey("noOpen", "noOpenKey") : this.t("noResolved")));
|
|
8082
8561
|
else {
|
|
8083
8562
|
children.push(...placed.map(item));
|
|
@@ -8086,7 +8565,7 @@ var NuniWidget = class {
|
|
|
8086
8565
|
children.push(...lost.map(item));
|
|
8087
8566
|
}
|
|
8088
8567
|
}
|
|
8089
|
-
if (otherPages.length) {
|
|
8568
|
+
if (otherPages.length && this.tab !== "mine") {
|
|
8090
8569
|
children.push(h("div", { class: "section-label" }, this.t("otherPages")));
|
|
8091
8570
|
for (const p of otherPages.slice(0, 20)) {
|
|
8092
8571
|
const href = otherPageHref(p.path, location);
|
|
@@ -8105,7 +8584,19 @@ var NuniWidget = class {
|
|
|
8105
8584
|
type: "button",
|
|
8106
8585
|
"aria-label": this.t("close"),
|
|
8107
8586
|
onclick: () => this.togglePanel(false)
|
|
8108
|
-
}, icon(ICONS.close))), h("div", {
|
|
8587
|
+
}, icon(ICONS.close))), this.comments.length || this.mine?.length ? h("div", { class: "panel-search" }, icon(ICONS.search), h("input", {
|
|
8588
|
+
class: "field",
|
|
8589
|
+
type: "search",
|
|
8590
|
+
value: this.search,
|
|
8591
|
+
placeholder: this.t("searchComments"),
|
|
8592
|
+
"aria-label": this.t("searchComments"),
|
|
8593
|
+
"aria-controls": "nuni-panel-list",
|
|
8594
|
+
"data-focus-key": "panel-search",
|
|
8595
|
+
oninput: (e) => {
|
|
8596
|
+
this.search = e.target.value;
|
|
8597
|
+
this.render();
|
|
8598
|
+
}
|
|
8599
|
+
})) : null, h("div", {
|
|
8109
8600
|
class: "tabs",
|
|
8110
8601
|
role: "tablist",
|
|
8111
8602
|
"aria-label": this.t("comments"),
|
|
@@ -8118,11 +8609,13 @@ var NuniWidget = class {
|
|
|
8118
8609
|
"End"
|
|
8119
8610
|
].includes(key)) return;
|
|
8120
8611
|
e.preventDefault();
|
|
8121
|
-
|
|
8612
|
+
const at = PANEL_TABS.indexOf(this.tab);
|
|
8613
|
+
const last = PANEL_TABS.length - 1;
|
|
8614
|
+
this.tab = PANEL_TABS[key === "Home" ? 0 : key === "End" ? last : key === "ArrowRight" ? (at + 1) % PANEL_TABS.length : (at + last) % PANEL_TABS.length];
|
|
8122
8615
|
this.focusNext = `[data-focus-key="tab-${this.tab}"]`;
|
|
8123
8616
|
this.render();
|
|
8124
8617
|
}
|
|
8125
|
-
},
|
|
8618
|
+
}, PANEL_TABS.map((tab) => h("button", {
|
|
8126
8619
|
class: "tab",
|
|
8127
8620
|
role: "tab",
|
|
8128
8621
|
type: "button",
|
|
@@ -8135,7 +8628,7 @@ var NuniWidget = class {
|
|
|
8135
8628
|
this.tab = tab;
|
|
8136
8629
|
this.render();
|
|
8137
8630
|
}
|
|
8138
|
-
}, tab === "open" ? this.t("tabOpen", { count: open.length }) : this.t("tabResolved", { count: resolved.length })))), h("div", {
|
|
8631
|
+
}, tab === "open" ? this.t("tabOpen", { count: open.length }) : tab === "resolved" ? this.t("tabResolved", { count: resolved.length }) : this.mine ? this.t("tabYours", { count: this.mine.length }) : this.t("yours")))), h("div", {
|
|
8139
8632
|
class: "panel-list",
|
|
8140
8633
|
id: "nuni-panel-list",
|
|
8141
8634
|
role: "tabpanel",
|
|
@@ -8175,14 +8668,25 @@ var NuniWidget = class {
|
|
|
8175
8668
|
"data-focus-key": "name",
|
|
8176
8669
|
"aria-label": this.t("yourName")
|
|
8177
8670
|
});
|
|
8671
|
+
const before = draft.suggestFrom ?? null;
|
|
8672
|
+
const suggesting = Boolean(draft.suggesting) && before !== null;
|
|
8178
8673
|
const textarea = h("textarea", {
|
|
8179
8674
|
class: "cmp-text",
|
|
8180
|
-
placeholder: this.t("leaveComment"),
|
|
8675
|
+
placeholder: this.t(suggesting ? "addNote" : "leaveComment"),
|
|
8181
8676
|
maxlength: LIMITS.bodyMaxLength,
|
|
8182
8677
|
rows: 3,
|
|
8183
8678
|
"data-focus-key": "body",
|
|
8184
8679
|
"aria-label": this.t("comment")
|
|
8185
8680
|
});
|
|
8681
|
+
const suggestInput = suggesting ? h("textarea", {
|
|
8682
|
+
class: "cmp-suggest",
|
|
8683
|
+
maxlength: LIMITS.quoteMaxLength,
|
|
8684
|
+
rows: 1,
|
|
8685
|
+
"data-focus-key": "suggestion",
|
|
8686
|
+
"aria-label": this.t("newText")
|
|
8687
|
+
}) : null;
|
|
8688
|
+
if (suggestInput) suggestInput.value = draft.suggestion ?? before ?? "";
|
|
8689
|
+
const canPost = () => Boolean(textarea.value.trim()) || Boolean(suggestInput && suggestInput.value.trim() && suggestInput.value.trim() !== before);
|
|
8186
8690
|
const prev = this.uiLayer.querySelector("[data-focus-key=\"body\"]");
|
|
8187
8691
|
if (prev) textarea.value = prev.value;
|
|
8188
8692
|
const prevName = this.uiLayer.querySelector("[data-focus-key=\"name\"]");
|
|
@@ -8196,31 +8700,68 @@ var NuniWidget = class {
|
|
|
8196
8700
|
const post = h("button", {
|
|
8197
8701
|
class: "cmp-post",
|
|
8198
8702
|
type: "submit",
|
|
8199
|
-
disabled: Boolean(draft.sending) || !
|
|
8703
|
+
disabled: Boolean(draft.sending) || !canPost()
|
|
8200
8704
|
}, draft.sending ? this.t("posting") : this.t("post"), icon(ICONS.arrowUp));
|
|
8201
8705
|
const update = () => {
|
|
8202
|
-
post.disabled = Boolean(draft.sending) || !
|
|
8706
|
+
post.disabled = Boolean(draft.sending) || !canPost();
|
|
8203
8707
|
const left = LIMITS.bodyMaxLength - textarea.value.length;
|
|
8204
8708
|
counter.textContent = left <= LIMITS.bodyMaxLength * .1 ? this.t("charsLeft", { count: left }) : "";
|
|
8205
|
-
textarea
|
|
8206
|
-
|
|
8709
|
+
for (const field of [textarea, suggestInput]) {
|
|
8710
|
+
if (!field) continue;
|
|
8711
|
+
field.style.height = "auto";
|
|
8712
|
+
field.style.height = `${field.scrollHeight + 2}px`;
|
|
8713
|
+
}
|
|
8207
8714
|
};
|
|
8208
8715
|
textarea.addEventListener("input", () => {
|
|
8209
8716
|
update();
|
|
8210
8717
|
this.positionCard();
|
|
8211
8718
|
});
|
|
8719
|
+
suggestInput?.addEventListener("input", () => {
|
|
8720
|
+
draft.suggestion = suggestInput.value;
|
|
8721
|
+
update();
|
|
8722
|
+
this.positionCard();
|
|
8723
|
+
});
|
|
8212
8724
|
requestAnimationFrame(() => {
|
|
8213
8725
|
if (!textarea.isConnected) return;
|
|
8214
8726
|
update();
|
|
8215
8727
|
this.positionCard();
|
|
8216
8728
|
});
|
|
8217
8729
|
const submit = () => void this.submitDraft(draft, textarea.value, needsName ? nameInput.value : this.name);
|
|
8218
|
-
textarea
|
|
8730
|
+
for (const field of [textarea, suggestInput]) field?.addEventListener("keydown", (e) => {
|
|
8219
8731
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
8220
8732
|
e.preventDefault();
|
|
8221
8733
|
submit();
|
|
8222
8734
|
}
|
|
8223
8735
|
});
|
|
8736
|
+
const attach = this.canAttach(draft);
|
|
8737
|
+
const fileInput = attach ? h("input", {
|
|
8738
|
+
type: "file",
|
|
8739
|
+
accept: IMAGE_TYPES.join(","),
|
|
8740
|
+
multiple: true,
|
|
8741
|
+
hidden: true,
|
|
8742
|
+
tabindex: -1,
|
|
8743
|
+
"aria-hidden": "true",
|
|
8744
|
+
onchange: (e) => {
|
|
8745
|
+
const input = e.target;
|
|
8746
|
+
const files = [...input.files ?? []];
|
|
8747
|
+
input.value = "";
|
|
8748
|
+
this.addImages(draft, files);
|
|
8749
|
+
}
|
|
8750
|
+
}) : null;
|
|
8751
|
+
if (attach) textarea.addEventListener("paste", (e) => {
|
|
8752
|
+
const files = [...e.clipboardData?.files ?? []].filter((f) => IMAGE_TYPES.includes(f.type));
|
|
8753
|
+
if (!files.length) return;
|
|
8754
|
+
e.preventDefault();
|
|
8755
|
+
this.addImages(draft, files);
|
|
8756
|
+
});
|
|
8757
|
+
const setSuggesting = (on) => {
|
|
8758
|
+
draft.suggesting = on;
|
|
8759
|
+
draft.error = void 0;
|
|
8760
|
+
this.render();
|
|
8761
|
+
const field = this.uiLayer.querySelector(`[data-focus-key="${on ? "suggestion" : "body"}"]`);
|
|
8762
|
+
field?.focus();
|
|
8763
|
+
if (on) field?.select();
|
|
8764
|
+
};
|
|
8224
8765
|
return h("form", {
|
|
8225
8766
|
class: `${cls} composer`,
|
|
8226
8767
|
"data-card": "composer",
|
|
@@ -8236,13 +8777,48 @@ var NuniWidget = class {
|
|
|
8236
8777
|
this.render();
|
|
8237
8778
|
this.focusComposer();
|
|
8238
8779
|
}
|
|
8239
|
-
}, this.t("notYou"))),
|
|
8780
|
+
}, this.t("notYou"))), suggesting ? h("div", { class: "suggest" }, h("div", { class: "suggest-label" }, this.t("suggestedEdit"), h("span", { class: "spacer" }), h("button", {
|
|
8781
|
+
class: "cmp-link",
|
|
8782
|
+
type: "button",
|
|
8783
|
+
onclick: () => setSuggesting(false)
|
|
8784
|
+
}, this.t("removeSuggestion"))), h("del", {}, before), suggestInput) : draft.anchor.quote ? h("blockquote", { class: "quote" }, draft.anchor.quote.exact) : null, textarea, draft.images?.length ? h("div", { class: "cmp-images" }, h("div", { class: "cmp-thumbs" }, ...draft.images.map((image, i) => h("div", { class: "cmp-thumb" }, h("img", {
|
|
8785
|
+
src: image.url,
|
|
8786
|
+
alt: this.t("imageN", { n: i + 1 })
|
|
8787
|
+
}), h("button", {
|
|
8788
|
+
class: "cmp-thumb-remove",
|
|
8789
|
+
type: "button",
|
|
8790
|
+
"aria-label": this.t("removeImage", { n: i + 1 }),
|
|
8791
|
+
title: this.t("removeImage", { n: i + 1 }),
|
|
8792
|
+
onclick: () => {
|
|
8793
|
+
URL.revokeObjectURL(image.url);
|
|
8794
|
+
draft.images?.splice(i, 1);
|
|
8795
|
+
this.render();
|
|
8796
|
+
this.positionCard();
|
|
8797
|
+
}
|
|
8798
|
+
}, icon(ICONS.close))))), h("p", { class: "cmp-images-note" }, this.t("imagesPublic"))) : null, draft.error ? h("div", {
|
|
8240
8799
|
class: "error cmp-error",
|
|
8241
8800
|
role: "alert"
|
|
8242
8801
|
}, draft.error) : null, h("div", { class: "cmp-foot" }, h("span", {
|
|
8243
8802
|
class: "cmp-keys",
|
|
8244
8803
|
title: this.t("send", { key: this.i18n.mod })
|
|
8245
|
-
}, h("kbd", {}, this.i18n.mod), h("kbd", {}, "↵")),
|
|
8804
|
+
}, h("kbd", {}, this.i18n.mod), h("kbd", {}, "↵")), before !== null && !suggesting ? h("button", {
|
|
8805
|
+
class: "cmp-link cmp-suggest-btn",
|
|
8806
|
+
type: "button",
|
|
8807
|
+
onclick: () => setSuggesting(true)
|
|
8808
|
+
}, icon(ICONS.edit), this.t("suggestEdit")) : null, attach ? h("button", {
|
|
8809
|
+
class: "icon-btn",
|
|
8810
|
+
type: "button",
|
|
8811
|
+
"aria-label": this.t("addImage"),
|
|
8812
|
+
title: this.t("addImage"),
|
|
8813
|
+
onclick: () => fileInput?.click()
|
|
8814
|
+
}, icon(ICONS.image)) : null, attach && this.canScreenshot(draft) ? h("button", {
|
|
8815
|
+
class: "icon-btn",
|
|
8816
|
+
type: "button",
|
|
8817
|
+
"aria-label": this.t("markUpScreenshot"),
|
|
8818
|
+
title: this.t(draft.preparingMarkup ? "preparingScreenshot" : "markUpScreenshot"),
|
|
8819
|
+
disabled: Boolean(draft.preparingMarkup),
|
|
8820
|
+
onclick: () => void this.markUpScreenshot(draft)
|
|
8821
|
+
}, icon(ICONS.pen)) : null, fileInput, counter, h("span", { class: "spacer" }), post), h("button", {
|
|
8246
8822
|
class: "icon-btn cmp-close",
|
|
8247
8823
|
type: "button",
|
|
8248
8824
|
"aria-label": this.t("cancel"),
|
|
@@ -8265,11 +8841,24 @@ var NuniWidget = class {
|
|
|
8265
8841
|
const textarea = h("textarea", {
|
|
8266
8842
|
class: "field",
|
|
8267
8843
|
maxlength: LIMITS.bodyMaxLength,
|
|
8268
|
-
"
|
|
8844
|
+
placeholder: comment.suggestion ? this.t("addNote") : null,
|
|
8845
|
+
"data-focus-key": "edit",
|
|
8846
|
+
"aria-label": this.t("comment")
|
|
8269
8847
|
});
|
|
8270
8848
|
const prev = this.uiLayer.querySelector("[data-focus-key=\"edit\"]");
|
|
8271
8849
|
textarea.value = prev ? prev.value : comment.body;
|
|
8272
|
-
|
|
8850
|
+
const suggestField = comment.suggestion ? h("textarea", {
|
|
8851
|
+
class: "field",
|
|
8852
|
+
rows: 2,
|
|
8853
|
+
maxlength: LIMITS.quoteMaxLength,
|
|
8854
|
+
"data-focus-key": "edit-suggestion",
|
|
8855
|
+
"aria-label": this.t("newText")
|
|
8856
|
+
}) : null;
|
|
8857
|
+
if (suggestField && comment.suggestion) {
|
|
8858
|
+
const prevSuggestion = this.uiLayer.querySelector("[data-focus-key=\"edit-suggestion\"]");
|
|
8859
|
+
suggestField.value = prevSuggestion ? prevSuggestion.value : comment.suggestion.after;
|
|
8860
|
+
}
|
|
8861
|
+
body = h("div", { class: "card-body" }, comment.suggestion && suggestField ? h("div", { class: "suggest" }, h("div", { class: "suggest-label" }, this.t("suggestedEdit")), h("del", {}, comment.suggestion.before), suggestField) : null, textarea, h("div", { class: "row" }, h("span", { class: "spacer" }), h("button", {
|
|
8273
8862
|
class: "btn btn-ghost",
|
|
8274
8863
|
type: "button",
|
|
8275
8864
|
onclick: () => {
|
|
@@ -8280,11 +8869,11 @@ var NuniWidget = class {
|
|
|
8280
8869
|
class: "btn btn-primary",
|
|
8281
8870
|
type: "button",
|
|
8282
8871
|
disabled: Boolean(card.busy),
|
|
8283
|
-
onclick: () => this.saveEdit(comment, textarea.value)
|
|
8872
|
+
onclick: () => this.saveEdit(comment, textarea.value, suggestField?.value)
|
|
8284
8873
|
}, this.t("save"))));
|
|
8285
8874
|
} else {
|
|
8286
8875
|
const badges = [
|
|
8287
|
-
comment.status === "resolved" ? h("span", { class: "badge badge-ok" }, icon(ICONS.check), this.t("resolved")) : null,
|
|
8876
|
+
comment.status === "resolved" ? h("span", { class: "badge badge-ok" }, icon(ICONS.check), this.t(comment.resolution === "outdated" ? "outdated" : "resolved")) : null,
|
|
8288
8877
|
comment.page.origin !== location.origin ? h("span", {
|
|
8289
8878
|
class: "badge",
|
|
8290
8879
|
title: this.t("leftOn", { origin: comment.page.origin })
|
|
@@ -8313,7 +8902,16 @@ var NuniWidget = class {
|
|
|
8313
8902
|
"aria-label": this.t("close"),
|
|
8314
8903
|
title: this.t("close"),
|
|
8315
8904
|
onclick: () => this.closeCard()
|
|
8316
|
-
}, icon(ICONS.close))), h("div", { class: "msg-main" }, comment.anchor.quote ? h("blockquote", { class: "quote" }, comment.anchor.quote.exact) : null, h("div", { class: "comment-body" }, comment.body), this.
|
|
8905
|
+
}, icon(ICONS.close))), h("div", { class: "msg-main" }, comment.suggestion ? renderSuggestion(comment.suggestion, this.t("suggestedEdit"), owner ? this.copyNewTextButton(comment.suggestion.after) : null) : comment.anchor.quote ? h("blockquote", { class: "quote" }, comment.anchor.quote.exact) : null, comment.body ? h("div", { class: "comment-body" }, comment.body) : null, this.threadId === comment._id && this.thread?.images?.length ? h("div", { class: "images" }, ...this.thread.images.map((url, i) => h("a", {
|
|
8906
|
+
href: url,
|
|
8907
|
+
target: "_blank",
|
|
8908
|
+
rel: "noreferrer",
|
|
8909
|
+
title: this.t("openImage")
|
|
8910
|
+
}, h("img", {
|
|
8911
|
+
src: url,
|
|
8912
|
+
alt: this.t("imageN", { n: i + 1 }),
|
|
8913
|
+
loading: "lazy"
|
|
8914
|
+
})))) : null, this.ownerDetail?._id === comment._id && this.ownerDetail.screenshotUrl ? h("a", {
|
|
8317
8915
|
class: "shot",
|
|
8318
8916
|
href: this.ownerDetail.screenshotUrl,
|
|
8319
8917
|
target: "_blank",
|
|
@@ -8322,14 +8920,29 @@ var NuniWidget = class {
|
|
|
8322
8920
|
}, h("img", {
|
|
8323
8921
|
src: this.ownerDetail.screenshotUrl,
|
|
8324
8922
|
alt: this.t("screenshotTaken")
|
|
8325
|
-
})) : null, badges.length ? h("div", { class: "badges" }, ...badges) : null, h("div", { class: "
|
|
8923
|
+
})) : null, badges.length ? h("div", { class: "badges" }, ...badges) : null, owner && !resolved && !placement?.element ? h("div", { class: "lost" }, h("p", {}, this.t("lostHint")), h("div", { class: "row" }, h("button", {
|
|
8924
|
+
class: "btn",
|
|
8925
|
+
type: "button",
|
|
8926
|
+
onclick: () => this.startMovePin(comment)
|
|
8927
|
+
}, icon(ICONS.target), this.t("movePin")), h("button", {
|
|
8928
|
+
class: "btn btn-ghost",
|
|
8929
|
+
type: "button",
|
|
8930
|
+
disabled: Boolean(card.busy),
|
|
8931
|
+
onclick: () => this.closeAsOutdated(comment)
|
|
8932
|
+
}, this.t("closeOutdated")))) : null, h("div", { class: "msg-foot" }, this.renderReactions(comment, comment._id), h("span", { class: "spacer" }), h("button", {
|
|
8326
8933
|
class: "icon-btn",
|
|
8327
8934
|
type: "button",
|
|
8328
8935
|
"aria-label": this.t("copyForAgent"),
|
|
8329
8936
|
title: contextPending ? this.t("loadingContext") : this.t("copyForAgentHint"),
|
|
8330
8937
|
disabled: contextPending,
|
|
8331
8938
|
onclick: () => this.copyForAgent(comment)
|
|
8332
|
-
}, icon(ICONS.bot)),
|
|
8939
|
+
}, icon(ICONS.bot)), owner && placement?.element ? h("button", {
|
|
8940
|
+
class: "icon-btn",
|
|
8941
|
+
type: "button",
|
|
8942
|
+
"aria-label": this.t("movePin"),
|
|
8943
|
+
title: this.t("movePin"),
|
|
8944
|
+
onclick: () => this.startMovePin(comment)
|
|
8945
|
+
}, icon(ICONS.target)) : null, mine ? h("button", {
|
|
8333
8946
|
class: "icon-btn",
|
|
8334
8947
|
type: "button",
|
|
8335
8948
|
"aria-label": this.t("edit"),
|
|
@@ -8543,8 +9156,8 @@ var NuniWidget = class {
|
|
|
8543
9156
|
top = point.y - 28;
|
|
8544
9157
|
if (left + width > vw - 12) left = point.x - width - 12;
|
|
8545
9158
|
} else {
|
|
8546
|
-
const { position } = this
|
|
8547
|
-
left = position.endsWith("left") ? 16 : vw - width - 16;
|
|
9159
|
+
const { position } = this;
|
|
9160
|
+
left = position.endsWith("left") ? 16 : position.endsWith("center") ? (vw - width) / 2 : vw - width - 16;
|
|
8548
9161
|
top = position.startsWith("top") ? 72 : vh - height - 72;
|
|
8549
9162
|
}
|
|
8550
9163
|
left = Math.max(12, Math.min(left, vw - width - 12));
|
|
@@ -8558,6 +9171,22 @@ var NuniWidget = class {
|
|
|
8558
9171
|
});
|
|
8559
9172
|
}
|
|
8560
9173
|
};
|
|
9174
|
+
/** The toolbar spot nearest a point: thirds across the screen, halves down. */
|
|
9175
|
+
function nearestPosition(x, y) {
|
|
9176
|
+
const width = window.innerWidth;
|
|
9177
|
+
const across = x < width / 3 ? "left" : x > width * 2 / 3 ? "right" : "center";
|
|
9178
|
+
return `${y < window.innerHeight / 2 ? "top" : "bottom"}-${across}`;
|
|
9179
|
+
}
|
|
9180
|
+
/** A press on a scrollbar, which scrolls rather than points at the page. */
|
|
9181
|
+
function onScrollbar(e) {
|
|
9182
|
+
const el = e.composedPath()[0];
|
|
9183
|
+
if (!(el instanceof Element)) return false;
|
|
9184
|
+
const root = document.documentElement;
|
|
9185
|
+
if (el === root) return e.clientX >= root.clientWidth || e.clientY >= root.clientHeight;
|
|
9186
|
+
if (!(el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)) return false;
|
|
9187
|
+
const box = el.getBoundingClientRect();
|
|
9188
|
+
return e.clientX - box.left - el.clientLeft >= el.clientWidth || e.clientY - box.top - el.clientTop >= el.clientHeight;
|
|
9189
|
+
}
|
|
8561
9190
|
//#endregion
|
|
8562
9191
|
//#region src/mount.ts
|
|
8563
9192
|
/** Nuni's own requests are not the page's problem. */
|
|
@@ -8640,7 +9269,7 @@ function unmount(state) {
|
|
|
8640
9269
|
* down the widget it was created for.
|
|
8641
9270
|
*/
|
|
8642
9271
|
function init(options) {
|
|
8643
|
-
return mount(options, { loadScreenshot: () => import("./
|
|
9272
|
+
return mount(options, { loadScreenshot: () => import("./tools-gIAPu66h.js").then((m) => m.screenshotTools) });
|
|
8644
9273
|
}
|
|
8645
9274
|
//#endregion
|
|
8646
9275
|
export { VERSION, init, EN as messages };
|