@nuniapp/widget 0.1.8 → 0.1.10
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 +87 -6
- package/dist/index.js +606 -150
- package/dist/nuni-screenshot.global.js +41 -7
- package/dist/nuni.global.js +91 -8
- package/dist/{screenshot-DcJ_SzKF.js → screenshot-tools-Dk-xNV0c.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.10";
|
|
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,
|
|
@@ -5272,6 +5284,13 @@ var NuniApi = class {
|
|
|
5272
5284
|
path
|
|
5273
5285
|
}, (comments) => cb(comments), onError);
|
|
5274
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
|
+
}
|
|
5275
5294
|
onPages(cb) {
|
|
5276
5295
|
return this.client.onUpdate(api.comments.pagesWithComments, { publicId: this.config.project }, cb, () => {});
|
|
5277
5296
|
}
|
|
@@ -5340,9 +5359,16 @@ var NuniApi = class {
|
|
|
5340
5359
|
if (!response.ok || !data.id) throw new NuniApiError(data.message ?? "Couldn't post the reply", data.code ?? "error");
|
|
5341
5360
|
return data.id;
|
|
5342
5361
|
}
|
|
5343
|
-
/** Attach
|
|
5344
|
-
|
|
5345
|
-
|
|
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}`, {
|
|
5346
5372
|
method: "POST",
|
|
5347
5373
|
headers: {
|
|
5348
5374
|
"Content-Type": image.type,
|
|
@@ -5354,7 +5380,7 @@ var NuniApi = class {
|
|
|
5354
5380
|
});
|
|
5355
5381
|
if (!response.ok) {
|
|
5356
5382
|
const data = await response.json().catch(() => ({}));
|
|
5357
|
-
throw new NuniApiError(data.message ?? "Couldn't attach the
|
|
5383
|
+
throw new NuniApiError(data.message ?? "Couldn't attach the image", data.code ?? "error");
|
|
5358
5384
|
}
|
|
5359
5385
|
}
|
|
5360
5386
|
async run(promise) {
|
|
@@ -5364,11 +5390,12 @@ var NuniApi = class {
|
|
|
5364
5390
|
throw new NuniApiError(errorMessage(error), "error");
|
|
5365
5391
|
}
|
|
5366
5392
|
}
|
|
5367
|
-
editOwn(id, authorSecret, body) {
|
|
5393
|
+
editOwn(id, authorSecret, body, suggestion) {
|
|
5368
5394
|
return this.run(this.client.mutation(api.comments.editOwn, {
|
|
5369
5395
|
id,
|
|
5370
5396
|
authorSecret,
|
|
5371
|
-
body
|
|
5397
|
+
body,
|
|
5398
|
+
suggestion
|
|
5372
5399
|
}));
|
|
5373
5400
|
}
|
|
5374
5401
|
deleteOwn(id, authorSecret) {
|
|
@@ -5377,12 +5404,40 @@ var NuniApi = class {
|
|
|
5377
5404
|
authorSecret
|
|
5378
5405
|
}));
|
|
5379
5406
|
}
|
|
5380
|
-
resolve(id, sessionToken) {
|
|
5407
|
+
resolve(id, sessionToken, resolution) {
|
|
5381
5408
|
return this.run(this.client.mutation(api.comments.resolve, {
|
|
5382
5409
|
id,
|
|
5410
|
+
resolution,
|
|
5383
5411
|
sessionToken
|
|
5384
5412
|
}));
|
|
5385
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,
|
|
5419
|
+
sessionToken
|
|
5420
|
+
}));
|
|
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
|
+
}
|
|
5386
5441
|
reopen(id, sessionToken) {
|
|
5387
5442
|
return this.run(this.client.mutation(api.comments.reopen, {
|
|
5388
5443
|
id,
|
|
@@ -5425,106 +5480,6 @@ var NuniApi = class {
|
|
|
5425
5480
|
}
|
|
5426
5481
|
};
|
|
5427
5482
|
//#endregion
|
|
5428
|
-
//#region src/dom.ts
|
|
5429
|
-
/** Tiny element factory. Strings become text nodes (never HTML). */
|
|
5430
|
-
function h(tag, attrs = {}, ...children) {
|
|
5431
|
-
const el = document.createElement(tag);
|
|
5432
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
5433
|
-
if (value == null || value === false) continue;
|
|
5434
|
-
if (key.startsWith("on") && typeof value === "function") el.addEventListener(key.slice(2).toLowerCase(), value);
|
|
5435
|
-
else if (key === "class") el.className = String(value);
|
|
5436
|
-
else if (value === true) el.setAttribute(key, "");
|
|
5437
|
-
else el.setAttribute(key, String(value));
|
|
5438
|
-
}
|
|
5439
|
-
for (const child of children.flat()) {
|
|
5440
|
-
if (child == null || child === false) continue;
|
|
5441
|
-
el.append(typeof child === "string" ? document.createTextNode(child) : child);
|
|
5442
|
-
}
|
|
5443
|
-
return el;
|
|
5444
|
-
}
|
|
5445
|
-
/** Renders one of the constant SVG strings in icons.ts (never user input). */
|
|
5446
|
-
function icon(svg) {
|
|
5447
|
-
const span = document.createElement("span");
|
|
5448
|
-
span.className = "icon";
|
|
5449
|
-
span.setAttribute("aria-hidden", "true");
|
|
5450
|
-
const parsed = new DOMParser().parseFromString(svg, "image/svg+xml");
|
|
5451
|
-
span.append(document.importNode(parsed.documentElement, true));
|
|
5452
|
-
return span;
|
|
5453
|
-
}
|
|
5454
|
-
function initials(name) {
|
|
5455
|
-
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
5456
|
-
return (parts.length > 1 ? parts[0][0] + parts[parts.length - 1][0] : (parts[0] ?? "?").slice(0, 2)).toUpperCase();
|
|
5457
|
-
}
|
|
5458
|
-
/** Stable pleasant color per author name. */
|
|
5459
|
-
function colorFor(name) {
|
|
5460
|
-
let hash = 0;
|
|
5461
|
-
for (const ch of name) hash = hash * 31 + ch.charCodeAt(0) >>> 0;
|
|
5462
|
-
return `oklch(0.5 0.17 ${hash % 360})`;
|
|
5463
|
-
}
|
|
5464
|
-
/** Copy text, falling back to execCommand where the async clipboard is off (plain http previews). */
|
|
5465
|
-
async function copyText(text) {
|
|
5466
|
-
try {
|
|
5467
|
-
await navigator.clipboard.writeText(text);
|
|
5468
|
-
return true;
|
|
5469
|
-
} catch {}
|
|
5470
|
-
const area = document.createElement("textarea");
|
|
5471
|
-
area.value = text;
|
|
5472
|
-
area.setAttribute("readonly", "");
|
|
5473
|
-
area.style.cssText = "position:fixed;top:0;left:0;opacity:0";
|
|
5474
|
-
document.body.appendChild(area);
|
|
5475
|
-
area.select();
|
|
5476
|
-
try {
|
|
5477
|
-
return document.execCommand("copy");
|
|
5478
|
-
} catch {
|
|
5479
|
-
return false;
|
|
5480
|
-
} finally {
|
|
5481
|
-
area.remove();
|
|
5482
|
-
}
|
|
5483
|
-
}
|
|
5484
|
-
/** Any CSS color as RGB, using the browser's own parser. */
|
|
5485
|
-
function parseColor(color) {
|
|
5486
|
-
const probe = document.createElement("span");
|
|
5487
|
-
probe.style.color = color;
|
|
5488
|
-
if (!probe.style.color) return null;
|
|
5489
|
-
probe.style.display = "none";
|
|
5490
|
-
document.documentElement.appendChild(probe);
|
|
5491
|
-
const computed = getComputedStyle(probe).color;
|
|
5492
|
-
probe.remove();
|
|
5493
|
-
if (/^rgba?\(/.test(computed)) {
|
|
5494
|
-
const parts = computed.match(/[\d.]+/g)?.map(Number);
|
|
5495
|
-
if (parts && parts.length >= 3) return [
|
|
5496
|
-
parts[0],
|
|
5497
|
-
parts[1],
|
|
5498
|
-
parts[2]
|
|
5499
|
-
];
|
|
5500
|
-
}
|
|
5501
|
-
try {
|
|
5502
|
-
const canvas = document.createElement("canvas");
|
|
5503
|
-
canvas.width = canvas.height = 1;
|
|
5504
|
-
const ctx = canvas.getContext("2d");
|
|
5505
|
-
if (!ctx) return null;
|
|
5506
|
-
ctx.fillStyle = color;
|
|
5507
|
-
ctx.fillRect(0, 0, 1, 1);
|
|
5508
|
-
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
|
|
5509
|
-
return [
|
|
5510
|
-
r,
|
|
5511
|
-
g,
|
|
5512
|
-
b
|
|
5513
|
-
];
|
|
5514
|
-
} catch {
|
|
5515
|
-
return null;
|
|
5516
|
-
}
|
|
5517
|
-
}
|
|
5518
|
-
/** Black or white text, whichever reads better on the color (WCAG). */
|
|
5519
|
-
function readableOn([r, g, b]) {
|
|
5520
|
-
const channel = (c) => {
|
|
5521
|
-
const v = c / 255;
|
|
5522
|
-
return v <= .03928 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4;
|
|
5523
|
-
};
|
|
5524
|
-
const l = .2126 * channel(r) + .7152 * channel(g) + .0722 * channel(b);
|
|
5525
|
-
return 1.05 / (l + .05) >= (l + .05) / .05 ? "#ffffff" : "#111111";
|
|
5526
|
-
}
|
|
5527
|
-
//#endregion
|
|
5528
5483
|
//#region src/i18n.ts
|
|
5529
5484
|
/**
|
|
5530
5485
|
* Every word the widget shows. Override any of them with the `messages`
|
|
@@ -5552,6 +5507,11 @@ const EN = {
|
|
|
5552
5507
|
close: "Close",
|
|
5553
5508
|
tabOpen: "Open ({count})",
|
|
5554
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",
|
|
5555
5515
|
loading: "Loading…",
|
|
5556
5516
|
noOpen: "No open comments on this page.",
|
|
5557
5517
|
noOpenKey: "No open comments on this page. Press {key} to add one.",
|
|
@@ -5580,6 +5540,25 @@ const EN = {
|
|
|
5580
5540
|
one: "{count} character left",
|
|
5581
5541
|
other: "{count} characters left"
|
|
5582
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",
|
|
5583
5562
|
pinLabel: "Comment by {name}: {body}",
|
|
5584
5563
|
commentBy: "Comment by {name}",
|
|
5585
5564
|
openScreenshot: "Open the screenshot",
|
|
@@ -5593,6 +5572,15 @@ const EN = {
|
|
|
5593
5572
|
resolve: "Resolve",
|
|
5594
5573
|
reopen: "Reopen",
|
|
5595
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",
|
|
5596
5584
|
copyForAgentHint: "Copy for your coding agent",
|
|
5597
5585
|
loadingContext: "Loading page context…",
|
|
5598
5586
|
edit: "Edit",
|
|
@@ -5619,6 +5607,12 @@ const EN = {
|
|
|
5619
5607
|
linkGone: "That comment was deleted or is on another page",
|
|
5620
5608
|
enterName: "Enter your name",
|
|
5621
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",
|
|
5622
5616
|
commentTooLong: "Comments are limited to {count} characters",
|
|
5623
5617
|
replyTooLong: "Replies are limited to {count} characters",
|
|
5624
5618
|
postFailed: "Couldn't post the comment",
|
|
@@ -5696,6 +5690,8 @@ const ICONS = {
|
|
|
5696
5690
|
arrowUp: s("<path d=\"M12 19V5M5 12l7-7 7 7\"/>"),
|
|
5697
5691
|
search: s("<circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m21 21-4.3-4.3\"/>"),
|
|
5698
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\"/>"),
|
|
5699
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>`,
|
|
5700
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\"/>")
|
|
5701
5697
|
};
|
|
@@ -6513,7 +6509,90 @@ textarea.field { resize: vertical; min-height: 72px; max-height: 240px; }
|
|
|
6513
6509
|
white-space: nowrap;
|
|
6514
6510
|
}
|
|
6515
6511
|
.cmp-link:hover { color: var(--n-fg); }
|
|
6516
|
-
.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; }
|
|
6517
6596
|
.cmp-text {
|
|
6518
6597
|
display: block;
|
|
6519
6598
|
width: 100%;
|
|
@@ -6667,7 +6746,35 @@ a.item { color: inherit; text-decoration: none; }
|
|
|
6667
6746
|
}
|
|
6668
6747
|
`;
|
|
6669
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
|
|
6670
6772
|
//#region src/widget.ts
|
|
6773
|
+
const PANEL_TABS = [
|
|
6774
|
+
"open",
|
|
6775
|
+
"resolved",
|
|
6776
|
+
"mine"
|
|
6777
|
+
];
|
|
6671
6778
|
const PICKING_CLASS = "nuni-picking";
|
|
6672
6779
|
const PICK_EVENTS = [
|
|
6673
6780
|
"pointerdown",
|
|
@@ -6710,6 +6817,14 @@ function frameDocuments(doc, depth = 0) {
|
|
|
6710
6817
|
return out;
|
|
6711
6818
|
}
|
|
6712
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
|
+
}
|
|
6713
6828
|
function isTypingTarget(target) {
|
|
6714
6829
|
if (!target || target.nodeType !== 1) return false;
|
|
6715
6830
|
const el = target;
|
|
@@ -6729,6 +6844,17 @@ function describeElement(el) {
|
|
|
6729
6844
|
return `${tag}${id}${cls}${text ? ` "${text}"` : ""}`;
|
|
6730
6845
|
}
|
|
6731
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
|
+
/**
|
|
6732
6858
|
* Build an "Other pages" link target, or null when it would leave the
|
|
6733
6859
|
* current origin. The stored path comes from anonymous comment submissions;
|
|
6734
6860
|
* one that does not start with "/" (".evil.com/x", "@evil.com/") would
|
|
@@ -6754,6 +6880,8 @@ var NuniWidget = class {
|
|
|
6754
6880
|
_defineProperty(this, "overlayLayer", void 0);
|
|
6755
6881
|
_defineProperty(this, "selectButton", void 0);
|
|
6756
6882
|
_defineProperty(this, "uiLayer", void 0);
|
|
6883
|
+
_defineProperty(this, "modalLayer", void 0);
|
|
6884
|
+
_defineProperty(this, "markingUp", false);
|
|
6757
6885
|
_defineProperty(this, "api", void 0);
|
|
6758
6886
|
_defineProperty(this, "cleanups", []);
|
|
6759
6887
|
_defineProperty(this, "pageUnsub", null);
|
|
@@ -6768,6 +6896,11 @@ var NuniWidget = class {
|
|
|
6768
6896
|
_defineProperty(this, "picking", false);
|
|
6769
6897
|
_defineProperty(this, "panelOpen", false);
|
|
6770
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);
|
|
6771
6904
|
_defineProperty(this, "search", "");
|
|
6772
6905
|
_defineProperty(this, "lastViewedId", void 0);
|
|
6773
6906
|
_defineProperty(this, "card", null);
|
|
@@ -6906,6 +7039,7 @@ var NuniWidget = class {
|
|
|
6906
7039
|
this.pinLayer = h("div", { class: "pins" });
|
|
6907
7040
|
this.overlayLayer = h("div", { class: "overlay" });
|
|
6908
7041
|
this.uiLayer = h("div", { class: "ui" });
|
|
7042
|
+
this.modalLayer = h("div", { class: "modal" });
|
|
6909
7043
|
this.selectButton = h("button", {
|
|
6910
7044
|
class: "select-btn",
|
|
6911
7045
|
type: "button",
|
|
@@ -6920,7 +7054,7 @@ var NuniWidget = class {
|
|
|
6920
7054
|
role: "status",
|
|
6921
7055
|
"aria-live": "polite"
|
|
6922
7056
|
});
|
|
6923
|
-
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);
|
|
6924
7058
|
this.uiLayer.addEventListener("keydown", (e) => this.trapFocus(e));
|
|
6925
7059
|
this.root.addEventListener("click", (e) => {
|
|
6926
7060
|
if (!this.toolbarDropped) return;
|
|
@@ -7040,6 +7174,8 @@ var NuniWidget = class {
|
|
|
7040
7174
|
this.sessionUnsub?.();
|
|
7041
7175
|
this.ownerDetailUnsub?.();
|
|
7042
7176
|
this.threadUnsub?.();
|
|
7177
|
+
this.mineUnsub?.();
|
|
7178
|
+
clearTimeout(this.pinReportTimer);
|
|
7043
7179
|
this.resolveRun++;
|
|
7044
7180
|
for (const stop of this.scopeWatchers.values()) stop();
|
|
7045
7181
|
this.scopeWatchers.clear();
|
|
@@ -7063,6 +7199,8 @@ var NuniWidget = class {
|
|
|
7063
7199
|
this.extraComment = null;
|
|
7064
7200
|
this.loaded = false;
|
|
7065
7201
|
this.placements.clear();
|
|
7202
|
+
this.pinReported.clear();
|
|
7203
|
+
clearTimeout(this.pinReportTimer);
|
|
7066
7204
|
this.card = null;
|
|
7067
7205
|
this.activeId = null;
|
|
7068
7206
|
this.pageUnsub = this.api.onPage(key, (comments) => {
|
|
@@ -7179,11 +7317,39 @@ var NuniWidget = class {
|
|
|
7179
7317
|
this.watchScopes();
|
|
7180
7318
|
this.renderPins();
|
|
7181
7319
|
this.layout();
|
|
7182
|
-
if (queue.length)
|
|
7183
|
-
|
|
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();
|
|
7184
7326
|
};
|
|
7185
7327
|
slice();
|
|
7186
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
|
+
}
|
|
7187
7353
|
/** What the panel and cards show from the placements: found, approximate or lost. */
|
|
7188
7354
|
placementSummary() {
|
|
7189
7355
|
return this.comments.map((c) => {
|
|
@@ -7341,6 +7507,7 @@ var NuniWidget = class {
|
|
|
7341
7507
|
this.placeSelectButton();
|
|
7342
7508
|
}
|
|
7343
7509
|
setPicking(on) {
|
|
7510
|
+
if (!on) this.repinId = null;
|
|
7344
7511
|
if (this.picking === on) return;
|
|
7345
7512
|
this.picking = on;
|
|
7346
7513
|
const html = document.documentElement;
|
|
@@ -7462,10 +7629,16 @@ var NuniWidget = class {
|
|
|
7462
7629
|
}
|
|
7463
7630
|
openComposer(draft) {
|
|
7464
7631
|
const { element } = draft;
|
|
7632
|
+
const repinId = this.repinId;
|
|
7465
7633
|
this.setPicking(false);
|
|
7634
|
+
if (repinId) {
|
|
7635
|
+
this.movePin(repinId, draft);
|
|
7636
|
+
return;
|
|
7637
|
+
}
|
|
7466
7638
|
if (this.config.capture.dom) try {
|
|
7467
7639
|
draft.dom = domContext(element);
|
|
7468
7640
|
} catch {}
|
|
7641
|
+
draft.suggestFrom = suggestableText(draft.anchor, element);
|
|
7469
7642
|
this.card = {
|
|
7470
7643
|
kind: "composer",
|
|
7471
7644
|
draft
|
|
@@ -7475,14 +7648,18 @@ var NuniWidget = class {
|
|
|
7475
7648
|
this.focusComposer();
|
|
7476
7649
|
draft.screenshot = this.takeScreenshot(draft);
|
|
7477
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
|
+
}
|
|
7478
7655
|
/** Capture while the person types, so posting stays instant. */
|
|
7479
7656
|
async takeScreenshot(draft) {
|
|
7480
7657
|
const load = this.runtime.loadScreenshot;
|
|
7481
|
-
if (!load || draft
|
|
7658
|
+
if (!load || !this.canScreenshot(draft)) return null;
|
|
7482
7659
|
const accent = getComputedStyle(this.host).getPropertyValue("--n-accent").trim() || "#d6246e";
|
|
7483
7660
|
try {
|
|
7484
7661
|
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
7485
|
-
const
|
|
7662
|
+
const tools = await load();
|
|
7486
7663
|
const { range, anchor, element } = draft;
|
|
7487
7664
|
const focus = range ? () => range.getBoundingClientRect() : anchor.region ? () => {
|
|
7488
7665
|
const box = regionBox(anchor, element);
|
|
@@ -7493,7 +7670,7 @@ var NuniWidget = class {
|
|
|
7493
7670
|
bottom: box.top + box.height
|
|
7494
7671
|
};
|
|
7495
7672
|
} : void 0;
|
|
7496
|
-
return
|
|
7673
|
+
return tools ? await tools.capture(element, {
|
|
7497
7674
|
exclude: this.host,
|
|
7498
7675
|
accent,
|
|
7499
7676
|
focus
|
|
@@ -7518,19 +7695,93 @@ var NuniWidget = class {
|
|
|
7518
7695
|
if (image) await this.api.uploadScreenshot(id, this.secret, image);
|
|
7519
7696
|
} catch {}
|
|
7520
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
|
+
}
|
|
7521
7771
|
copyForAgent(comment) {
|
|
7522
7772
|
const detail = this.ownerDetail?._id === comment._id ? this.ownerDetail : null;
|
|
7523
7773
|
const replies = this.threadId === comment._id ? this.thread?.replies : void 0;
|
|
7524
|
-
|
|
7774
|
+
const prompt = buildCommentPrompt(detail ?? {
|
|
7525
7775
|
...comment,
|
|
7526
7776
|
replies
|
|
7527
|
-
}, { includeContext: Boolean(detail) })
|
|
7777
|
+
}, { includeContext: Boolean(detail) });
|
|
7778
|
+
copyText(prompt).then((ok) => this.showToast(ok ? this.t("copied") : this.t("copyFailed")));
|
|
7528
7779
|
}
|
|
7529
7780
|
renderHighlight() {
|
|
7530
7781
|
this.overlayLayer.replaceChildren();
|
|
7531
7782
|
if (!this.picking) return;
|
|
7532
7783
|
const mobile = window.matchMedia(MOBILE_QUERY).matches;
|
|
7533
|
-
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"))]));
|
|
7534
7785
|
if (this.dragBox) {
|
|
7535
7786
|
const box = h("div", { class: "highlight drag-box" });
|
|
7536
7787
|
Object.assign(box.style, {
|
|
@@ -7555,6 +7806,7 @@ var NuniWidget = class {
|
|
|
7555
7806
|
this.overlayLayer.append(box);
|
|
7556
7807
|
}
|
|
7557
7808
|
onKeyDown(e) {
|
|
7809
|
+
if (this.markingUp) return;
|
|
7558
7810
|
const inWidget = e.composedPath().includes(this.host);
|
|
7559
7811
|
if (e.key === "Escape") {
|
|
7560
7812
|
if (this.confirming) this.answer(false);
|
|
@@ -7791,7 +8043,15 @@ var NuniWidget = class {
|
|
|
7791
8043
|
const cleanName = name.trim().slice(0, LIMITS.nameMaxLength);
|
|
7792
8044
|
const cleanBody = body.trim();
|
|
7793
8045
|
if (!cleanName) return this.setDraftError(draft, this.t("enterName"));
|
|
7794
|
-
|
|
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"));
|
|
7795
8055
|
if (cleanBody.length > LIMITS.bodyMaxLength) return this.setDraftError(draft, this.t("commentTooLong", { count: LIMITS.bodyMaxLength }));
|
|
7796
8056
|
this.name = cleanName;
|
|
7797
8057
|
write(KEYS.name, cleanName);
|
|
@@ -7803,6 +8063,7 @@ var NuniWidget = class {
|
|
|
7803
8063
|
try {
|
|
7804
8064
|
const id = await this.api.createComment({
|
|
7805
8065
|
body: cleanBody,
|
|
8066
|
+
suggestion,
|
|
7806
8067
|
authorName: cleanName,
|
|
7807
8068
|
authorSecret: this.secret,
|
|
7808
8069
|
page: {
|
|
@@ -7819,6 +8080,7 @@ var NuniWidget = class {
|
|
|
7819
8080
|
context: this.collectContext(draft)
|
|
7820
8081
|
});
|
|
7821
8082
|
if (shot) this.attachScreenshot(id, shot);
|
|
8083
|
+
if (draft.images?.length) this.attachImages(id, draft);
|
|
7822
8084
|
this.placements.set(id, {
|
|
7823
8085
|
element: draft.element,
|
|
7824
8086
|
confidence: "exact",
|
|
@@ -7874,6 +8136,41 @@ var NuniWidget = class {
|
|
|
7874
8136
|
if (!token) return;
|
|
7875
8137
|
this.act(c._id, () => this.api.reopen(c._id, token), this.t("reopened"));
|
|
7876
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
|
+
}
|
|
7877
8174
|
async deleteComment(c) {
|
|
7878
8175
|
if (!await this.ask(this.t("confirmDeleteComment"), this.t("delete"))) return;
|
|
7879
8176
|
const token = this.ownerToken;
|
|
@@ -7951,8 +8248,16 @@ var NuniWidget = class {
|
|
|
7951
8248
|
}
|
|
7952
8249
|
});
|
|
7953
8250
|
}
|
|
7954
|
-
saveEdit(c, body) {
|
|
7955
|
-
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"));
|
|
7956
8261
|
}
|
|
7957
8262
|
showToast(message) {
|
|
7958
8263
|
this.toast = message;
|
|
@@ -7966,6 +8271,10 @@ var NuniWidget = class {
|
|
|
7966
8271
|
togglePanel(open = !this.panelOpen) {
|
|
7967
8272
|
if (open === this.panelOpen) return;
|
|
7968
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
|
+
});
|
|
7969
8278
|
if (open) {
|
|
7970
8279
|
this.keepFocus();
|
|
7971
8280
|
this.focusNext = ".panel [role=\"tab\"][aria-selected=\"true\"]";
|
|
@@ -7981,12 +8290,14 @@ var NuniWidget = class {
|
|
|
7981
8290
|
*/
|
|
7982
8291
|
closeCardFromOutside(e) {
|
|
7983
8292
|
if (!this.card || this.picking || this.confirming || !e.isPrimary) return;
|
|
8293
|
+
if (this.markingUp) return;
|
|
7984
8294
|
if (onScrollbar(e)) return;
|
|
7985
|
-
if ([...this.uiLayer.querySelector("[data-card]")?.querySelectorAll("textarea") ?? []].some((t) => t.value.trim())) 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;
|
|
7986
8296
|
if (!this.panelOpen) this.returnFocus = null;
|
|
7987
8297
|
this.closeCard();
|
|
7988
8298
|
}
|
|
7989
8299
|
closeCard() {
|
|
8300
|
+
if (this.card?.kind === "composer") revokeImages(this.card.draft);
|
|
7990
8301
|
const had = this.card !== null;
|
|
7991
8302
|
this.card = null;
|
|
7992
8303
|
this.activeId = null;
|
|
@@ -8044,7 +8355,7 @@ var NuniWidget = class {
|
|
|
8044
8355
|
pin.style.background = colorFor(comment.authorName);
|
|
8045
8356
|
pin.setAttribute("aria-label", this.t("pinLabel", {
|
|
8046
8357
|
name: comment.authorName,
|
|
8047
|
-
body: comment.body.slice(0, 80)
|
|
8358
|
+
body: (comment.body || comment.suggestion?.after || "").slice(0, 80)
|
|
8048
8359
|
}));
|
|
8049
8360
|
if (comment.replyCount) pin.dataset.replies = String(comment.replyCount);
|
|
8050
8361
|
else delete pin.dataset.replies;
|
|
@@ -8203,10 +8514,29 @@ var NuniWidget = class {
|
|
|
8203
8514
|
const resolved = this.comments.filter((c) => c.status === "resolved");
|
|
8204
8515
|
const query = this.search.trim().toLocaleLowerCase();
|
|
8205
8516
|
const matches = (...texts) => !query || texts.some((t) => t?.toLocaleLowerCase().includes(query));
|
|
8206
|
-
const
|
|
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));
|
|
8207
8519
|
const placed = list.filter((c) => this.placements.get(c._id)?.element);
|
|
8208
8520
|
const lost = list.filter((c) => !this.placements.get(c._id)?.element);
|
|
8209
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
|
+
};
|
|
8210
8540
|
const item = (c) => h("button", {
|
|
8211
8541
|
class: "item",
|
|
8212
8542
|
type: "button",
|
|
@@ -8219,9 +8549,13 @@ var NuniWidget = class {
|
|
|
8219
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", {
|
|
8220
8550
|
class: "badge",
|
|
8221
8551
|
title: c.page.origin
|
|
8222
|
-
}, 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));
|
|
8223
8553
|
const children = [];
|
|
8224
|
-
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")));
|
|
8225
8559
|
else if (!list.length && query) children.push(h("div", { class: "empty" }, this.t("noMatches", { query: this.search.trim() })));
|
|
8226
8560
|
else if (!list.length) children.push(h("div", { class: "empty" }, this.tab === "open" ? this.withKey("noOpen", "noOpenKey") : this.t("noResolved")));
|
|
8227
8561
|
else {
|
|
@@ -8231,7 +8565,7 @@ var NuniWidget = class {
|
|
|
8231
8565
|
children.push(...lost.map(item));
|
|
8232
8566
|
}
|
|
8233
8567
|
}
|
|
8234
|
-
if (otherPages.length) {
|
|
8568
|
+
if (otherPages.length && this.tab !== "mine") {
|
|
8235
8569
|
children.push(h("div", { class: "section-label" }, this.t("otherPages")));
|
|
8236
8570
|
for (const p of otherPages.slice(0, 20)) {
|
|
8237
8571
|
const href = otherPageHref(p.path, location);
|
|
@@ -8250,7 +8584,7 @@ var NuniWidget = class {
|
|
|
8250
8584
|
type: "button",
|
|
8251
8585
|
"aria-label": this.t("close"),
|
|
8252
8586
|
onclick: () => this.togglePanel(false)
|
|
8253
|
-
}, icon(ICONS.close))), this.comments.length ? h("div", { class: "panel-search" }, icon(ICONS.search), h("input", {
|
|
8587
|
+
}, icon(ICONS.close))), this.comments.length || this.mine?.length ? h("div", { class: "panel-search" }, icon(ICONS.search), h("input", {
|
|
8254
8588
|
class: "field",
|
|
8255
8589
|
type: "search",
|
|
8256
8590
|
value: this.search,
|
|
@@ -8275,11 +8609,13 @@ var NuniWidget = class {
|
|
|
8275
8609
|
"End"
|
|
8276
8610
|
].includes(key)) return;
|
|
8277
8611
|
e.preventDefault();
|
|
8278
|
-
|
|
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];
|
|
8279
8615
|
this.focusNext = `[data-focus-key="tab-${this.tab}"]`;
|
|
8280
8616
|
this.render();
|
|
8281
8617
|
}
|
|
8282
|
-
},
|
|
8618
|
+
}, PANEL_TABS.map((tab) => h("button", {
|
|
8283
8619
|
class: "tab",
|
|
8284
8620
|
role: "tab",
|
|
8285
8621
|
type: "button",
|
|
@@ -8292,7 +8628,7 @@ var NuniWidget = class {
|
|
|
8292
8628
|
this.tab = tab;
|
|
8293
8629
|
this.render();
|
|
8294
8630
|
}
|
|
8295
|
-
}, 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", {
|
|
8296
8632
|
class: "panel-list",
|
|
8297
8633
|
id: "nuni-panel-list",
|
|
8298
8634
|
role: "tabpanel",
|
|
@@ -8332,14 +8668,25 @@ var NuniWidget = class {
|
|
|
8332
8668
|
"data-focus-key": "name",
|
|
8333
8669
|
"aria-label": this.t("yourName")
|
|
8334
8670
|
});
|
|
8671
|
+
const before = draft.suggestFrom ?? null;
|
|
8672
|
+
const suggesting = Boolean(draft.suggesting) && before !== null;
|
|
8335
8673
|
const textarea = h("textarea", {
|
|
8336
8674
|
class: "cmp-text",
|
|
8337
|
-
placeholder: this.t("leaveComment"),
|
|
8675
|
+
placeholder: this.t(suggesting ? "addNote" : "leaveComment"),
|
|
8338
8676
|
maxlength: LIMITS.bodyMaxLength,
|
|
8339
8677
|
rows: 3,
|
|
8340
8678
|
"data-focus-key": "body",
|
|
8341
8679
|
"aria-label": this.t("comment")
|
|
8342
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);
|
|
8343
8690
|
const prev = this.uiLayer.querySelector("[data-focus-key=\"body\"]");
|
|
8344
8691
|
if (prev) textarea.value = prev.value;
|
|
8345
8692
|
const prevName = this.uiLayer.querySelector("[data-focus-key=\"name\"]");
|
|
@@ -8353,31 +8700,68 @@ var NuniWidget = class {
|
|
|
8353
8700
|
const post = h("button", {
|
|
8354
8701
|
class: "cmp-post",
|
|
8355
8702
|
type: "submit",
|
|
8356
|
-
disabled: Boolean(draft.sending) || !
|
|
8703
|
+
disabled: Boolean(draft.sending) || !canPost()
|
|
8357
8704
|
}, draft.sending ? this.t("posting") : this.t("post"), icon(ICONS.arrowUp));
|
|
8358
8705
|
const update = () => {
|
|
8359
|
-
post.disabled = Boolean(draft.sending) || !
|
|
8706
|
+
post.disabled = Boolean(draft.sending) || !canPost();
|
|
8360
8707
|
const left = LIMITS.bodyMaxLength - textarea.value.length;
|
|
8361
8708
|
counter.textContent = left <= LIMITS.bodyMaxLength * .1 ? this.t("charsLeft", { count: left }) : "";
|
|
8362
|
-
textarea
|
|
8363
|
-
|
|
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
|
+
}
|
|
8364
8714
|
};
|
|
8365
8715
|
textarea.addEventListener("input", () => {
|
|
8366
8716
|
update();
|
|
8367
8717
|
this.positionCard();
|
|
8368
8718
|
});
|
|
8719
|
+
suggestInput?.addEventListener("input", () => {
|
|
8720
|
+
draft.suggestion = suggestInput.value;
|
|
8721
|
+
update();
|
|
8722
|
+
this.positionCard();
|
|
8723
|
+
});
|
|
8369
8724
|
requestAnimationFrame(() => {
|
|
8370
8725
|
if (!textarea.isConnected) return;
|
|
8371
8726
|
update();
|
|
8372
8727
|
this.positionCard();
|
|
8373
8728
|
});
|
|
8374
8729
|
const submit = () => void this.submitDraft(draft, textarea.value, needsName ? nameInput.value : this.name);
|
|
8375
|
-
textarea
|
|
8730
|
+
for (const field of [textarea, suggestInput]) field?.addEventListener("keydown", (e) => {
|
|
8376
8731
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
8377
8732
|
e.preventDefault();
|
|
8378
8733
|
submit();
|
|
8379
8734
|
}
|
|
8380
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
|
+
};
|
|
8381
8765
|
return h("form", {
|
|
8382
8766
|
class: `${cls} composer`,
|
|
8383
8767
|
"data-card": "composer",
|
|
@@ -8393,13 +8777,48 @@ var NuniWidget = class {
|
|
|
8393
8777
|
this.render();
|
|
8394
8778
|
this.focusComposer();
|
|
8395
8779
|
}
|
|
8396
|
-
}, 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", {
|
|
8397
8799
|
class: "error cmp-error",
|
|
8398
8800
|
role: "alert"
|
|
8399
8801
|
}, draft.error) : null, h("div", { class: "cmp-foot" }, h("span", {
|
|
8400
8802
|
class: "cmp-keys",
|
|
8401
8803
|
title: this.t("send", { key: this.i18n.mod })
|
|
8402
|
-
}, 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", {
|
|
8403
8822
|
class: "icon-btn cmp-close",
|
|
8404
8823
|
type: "button",
|
|
8405
8824
|
"aria-label": this.t("cancel"),
|
|
@@ -8422,11 +8841,24 @@ var NuniWidget = class {
|
|
|
8422
8841
|
const textarea = h("textarea", {
|
|
8423
8842
|
class: "field",
|
|
8424
8843
|
maxlength: LIMITS.bodyMaxLength,
|
|
8425
|
-
"
|
|
8844
|
+
placeholder: comment.suggestion ? this.t("addNote") : null,
|
|
8845
|
+
"data-focus-key": "edit",
|
|
8846
|
+
"aria-label": this.t("comment")
|
|
8426
8847
|
});
|
|
8427
8848
|
const prev = this.uiLayer.querySelector("[data-focus-key=\"edit\"]");
|
|
8428
8849
|
textarea.value = prev ? prev.value : comment.body;
|
|
8429
|
-
|
|
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", {
|
|
8430
8862
|
class: "btn btn-ghost",
|
|
8431
8863
|
type: "button",
|
|
8432
8864
|
onclick: () => {
|
|
@@ -8437,11 +8869,11 @@ var NuniWidget = class {
|
|
|
8437
8869
|
class: "btn btn-primary",
|
|
8438
8870
|
type: "button",
|
|
8439
8871
|
disabled: Boolean(card.busy),
|
|
8440
|
-
onclick: () => this.saveEdit(comment, textarea.value)
|
|
8872
|
+
onclick: () => this.saveEdit(comment, textarea.value, suggestField?.value)
|
|
8441
8873
|
}, this.t("save"))));
|
|
8442
8874
|
} else {
|
|
8443
8875
|
const badges = [
|
|
8444
|
-
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,
|
|
8445
8877
|
comment.page.origin !== location.origin ? h("span", {
|
|
8446
8878
|
class: "badge",
|
|
8447
8879
|
title: this.t("leftOn", { origin: comment.page.origin })
|
|
@@ -8470,7 +8902,16 @@ var NuniWidget = class {
|
|
|
8470
8902
|
"aria-label": this.t("close"),
|
|
8471
8903
|
title: this.t("close"),
|
|
8472
8904
|
onclick: () => this.closeCard()
|
|
8473
|
-
}, 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", {
|
|
8474
8915
|
class: "shot",
|
|
8475
8916
|
href: this.ownerDetail.screenshotUrl,
|
|
8476
8917
|
target: "_blank",
|
|
@@ -8479,14 +8920,29 @@ var NuniWidget = class {
|
|
|
8479
8920
|
}, h("img", {
|
|
8480
8921
|
src: this.ownerDetail.screenshotUrl,
|
|
8481
8922
|
alt: this.t("screenshotTaken")
|
|
8482
|
-
})) : 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", {
|
|
8483
8933
|
class: "icon-btn",
|
|
8484
8934
|
type: "button",
|
|
8485
8935
|
"aria-label": this.t("copyForAgent"),
|
|
8486
8936
|
title: contextPending ? this.t("loadingContext") : this.t("copyForAgentHint"),
|
|
8487
8937
|
disabled: contextPending,
|
|
8488
8938
|
onclick: () => this.copyForAgent(comment)
|
|
8489
|
-
}, 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", {
|
|
8490
8946
|
class: "icon-btn",
|
|
8491
8947
|
type: "button",
|
|
8492
8948
|
"aria-label": this.t("edit"),
|
|
@@ -8813,7 +9269,7 @@ function unmount(state) {
|
|
|
8813
9269
|
* down the widget it was created for.
|
|
8814
9270
|
*/
|
|
8815
9271
|
function init(options) {
|
|
8816
|
-
return mount(options, { loadScreenshot: () => import("./screenshot-
|
|
9272
|
+
return mount(options, { loadScreenshot: () => import("./screenshot-tools-Dk-xNV0c.js").then((m) => m.screenshotTools) });
|
|
8817
9273
|
}
|
|
8818
9274
|
//#endregion
|
|
8819
9275
|
export { VERSION, init, EN as messages };
|