@designfever/web-review-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +241 -0
- package/dist/chunk-U5K2YGGL.js +3486 -0
- package/dist/chunk-U5K2YGGL.js.map +1 -0
- package/dist/index.cjs +3524 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/react-shell.cjs +8841 -0
- package/dist/react-shell.cjs.map +1 -0
- package/dist/react-shell.d.cts +156 -0
- package/dist/react-shell.d.ts +156 -0
- package/dist/react-shell.js +5844 -0
- package/dist/react-shell.js.map +1 -0
- package/dist/types-D_mNjOHx.d.cts +183 -0
- package/dist/types-D_mNjOHx.d.ts +183 -0
- package/docs/README.md +37 -0
- package/docs/adapter-handoff.md +146 -0
- package/docs/concept.md +102 -0
- package/docs/df-sheet-adapter.md +336 -0
- package/docs/df-sheet-next.md +222 -0
- package/docs/initial-plan.md +226 -0
- package/docs/installation.md +222 -0
- package/docs/package-split-checkpoint.md +79 -0
- package/docs/presence-handoff.md +138 -0
- package/docs/review-feedback-2026-06-20.md +267 -0
- package/docs/smoke-baseline-2026-06-20.md +41 -0
- package/docs/stabilize-ui-work-guide.md +243 -0
- package/docs/supabase-presence.md +198 -0
- package/docs/supabase-review-items.md +365 -0
- package/docs/supabase.md +205 -0
- package/package.json +61 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3524 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_REVIEW_VIEWPORTS: () => DEFAULT_REVIEW_VIEWPORTS,
|
|
24
|
+
DF_SHEET_REVIEW_SOURCE: () => DF_SHEET_REVIEW_SOURCE,
|
|
25
|
+
REVIEW_WORKFLOW_STATUS_OPTIONS: () => REVIEW_WORKFLOW_STATUS_OPTIONS,
|
|
26
|
+
createWebReviewKit: () => createWebReviewKit,
|
|
27
|
+
dfSheetAdapter: () => dfSheetAdapter,
|
|
28
|
+
findReviewViewportPreset: () => findReviewViewportPreset,
|
|
29
|
+
getNumberedReviewItems: () => getNumberedReviewItems,
|
|
30
|
+
getReviewItemScope: () => getReviewItemScope,
|
|
31
|
+
getReviewItemScopeLabel: () => getReviewItemScopeLabel,
|
|
32
|
+
getReviewViewportScope: () => getReviewViewportScope,
|
|
33
|
+
localAdapter: () => localAdapter,
|
|
34
|
+
normalizeReviewItemStatus: () => normalizeReviewItemStatus,
|
|
35
|
+
supabaseAdapter: () => supabaseAdapter
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// src/route.ts
|
|
40
|
+
function getItemRouteKey(item) {
|
|
41
|
+
return item.routeKey || normalizeRoutePath(item.normalizedPath);
|
|
42
|
+
}
|
|
43
|
+
function normalizeRoutePath(pathname) {
|
|
44
|
+
const [pathWithoutQuery] = pathname.split(/[?#]/);
|
|
45
|
+
const path = (pathWithoutQuery || "/").replace(/\/index\.html$/, "/");
|
|
46
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/status.ts
|
|
50
|
+
var REVIEW_WORKFLOW_STATUS_OPTIONS = [
|
|
51
|
+
{ value: "todo", label: "\uC791\uC5C5\uC804" },
|
|
52
|
+
{ value: "doing", label: "\uC791\uC5C5\uC911" },
|
|
53
|
+
{ value: "review", label: "\uAC80\uD1A0 \uD544\uC694" },
|
|
54
|
+
{ value: "hold", label: "\uBCF4\uB958" },
|
|
55
|
+
{ value: "done", label: "\uC644\uB8CC" }
|
|
56
|
+
];
|
|
57
|
+
function normalizeReviewItemStatus(status) {
|
|
58
|
+
if (status === "resolved") return "done";
|
|
59
|
+
if (status === "open" || !status) return "todo";
|
|
60
|
+
return status;
|
|
61
|
+
}
|
|
62
|
+
function matchesReviewItemStatus(itemStatus, queryStatus) {
|
|
63
|
+
return normalizeReviewItemStatus(itemStatus) === normalizeReviewItemStatus(queryStatus);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/adapters/local.ts
|
|
67
|
+
var DEFAULT_STORAGE_KEY = "df-web-review-kit:items";
|
|
68
|
+
function localAdapter(options = {}) {
|
|
69
|
+
const storageKey = options.storageKey ?? DEFAULT_STORAGE_KEY;
|
|
70
|
+
const write = (items) => {
|
|
71
|
+
window.localStorage.setItem(storageKey, JSON.stringify(items));
|
|
72
|
+
};
|
|
73
|
+
const read = () => {
|
|
74
|
+
if (typeof window === "undefined") return [];
|
|
75
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
76
|
+
if (!raw) return [];
|
|
77
|
+
try {
|
|
78
|
+
const value = JSON.parse(raw);
|
|
79
|
+
if (!Array.isArray(value)) return [];
|
|
80
|
+
let changed = false;
|
|
81
|
+
const items = value.flatMap((item) => {
|
|
82
|
+
const normalized = normalizeStoredReviewItem(item);
|
|
83
|
+
if (!normalized || normalized !== item) changed = true;
|
|
84
|
+
return normalized ? [normalized] : [];
|
|
85
|
+
});
|
|
86
|
+
const numberedItems = ensureStoredReviewNumbers(items);
|
|
87
|
+
if (numberedItems !== items) changed = true;
|
|
88
|
+
if (changed) write(numberedItems);
|
|
89
|
+
return numberedItems;
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
async get(id) {
|
|
96
|
+
return read().find((item) => item.id === id) ?? null;
|
|
97
|
+
},
|
|
98
|
+
async list(query) {
|
|
99
|
+
return read().filter((item) => {
|
|
100
|
+
if (item.projectId !== query.projectId) return false;
|
|
101
|
+
const queryRouteKey = query.routeKey ?? query.normalizedPath;
|
|
102
|
+
if (queryRouteKey && getItemRouteKey(item) !== queryRouteKey) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (query.status && !matchesReviewItemStatus(item.status, query.status)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
async create(item) {
|
|
112
|
+
const items = read();
|
|
113
|
+
items.unshift(item);
|
|
114
|
+
write(items);
|
|
115
|
+
return item;
|
|
116
|
+
},
|
|
117
|
+
async update(id, patch) {
|
|
118
|
+
const items = read();
|
|
119
|
+
const index = items.findIndex((item) => item.id === id);
|
|
120
|
+
if (index < 0) {
|
|
121
|
+
throw new Error(`Review item not found: ${id}`);
|
|
122
|
+
}
|
|
123
|
+
const next = {
|
|
124
|
+
...items[index],
|
|
125
|
+
...patch,
|
|
126
|
+
id,
|
|
127
|
+
createdAt: items[index].createdAt,
|
|
128
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
129
|
+
};
|
|
130
|
+
items[index] = next;
|
|
131
|
+
write(items);
|
|
132
|
+
return next;
|
|
133
|
+
},
|
|
134
|
+
async remove(id) {
|
|
135
|
+
write(read().filter((item) => item.id !== id));
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function normalizeStoredReviewItem(value) {
|
|
140
|
+
if (!value || typeof value !== "object") return void 0;
|
|
141
|
+
const raw = value;
|
|
142
|
+
const kind = raw.kind === "text" ? "note" : raw.kind === "capture" ? "area" : raw.kind;
|
|
143
|
+
if (kind !== "note" && kind !== "area") return void 0;
|
|
144
|
+
const { screenshot: _screenshot, ...item } = raw;
|
|
145
|
+
if (kind === raw.kind && _screenshot === void 0) {
|
|
146
|
+
return raw;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
...item,
|
|
150
|
+
kind
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function ensureStoredReviewNumbers(items) {
|
|
154
|
+
const usedNumbers = /* @__PURE__ */ new Set();
|
|
155
|
+
let maxNumber = 0;
|
|
156
|
+
let changed = false;
|
|
157
|
+
items.forEach((item) => {
|
|
158
|
+
const number = normalizeReviewNumber(item.reviewNumber);
|
|
159
|
+
if (!number) {
|
|
160
|
+
changed = true;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (usedNumbers.has(number)) {
|
|
164
|
+
changed = true;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
usedNumbers.add(number);
|
|
168
|
+
maxNumber = Math.max(maxNumber, number);
|
|
169
|
+
});
|
|
170
|
+
if (!changed) return items;
|
|
171
|
+
let nextNumber = maxNumber + 1;
|
|
172
|
+
const assignedNumbers = /* @__PURE__ */ new Set();
|
|
173
|
+
const numberById = /* @__PURE__ */ new Map();
|
|
174
|
+
[...items].sort((a, b) => {
|
|
175
|
+
const createdOrder = a.createdAt.localeCompare(b.createdAt);
|
|
176
|
+
if (createdOrder !== 0) return createdOrder;
|
|
177
|
+
return a.id.localeCompare(b.id);
|
|
178
|
+
}).forEach((item) => {
|
|
179
|
+
const storedNumber = normalizeReviewNumber(item.reviewNumber);
|
|
180
|
+
const reviewNumber = storedNumber && !assignedNumbers.has(storedNumber) ? storedNumber : nextNumber++;
|
|
181
|
+
assignedNumbers.add(reviewNumber);
|
|
182
|
+
numberById.set(item.id, reviewNumber);
|
|
183
|
+
});
|
|
184
|
+
return items.map((item) => {
|
|
185
|
+
const reviewNumber = numberById.get(item.id);
|
|
186
|
+
return item.reviewNumber === reviewNumber ? item : { ...item, reviewNumber };
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function normalizeReviewNumber(value) {
|
|
190
|
+
if (typeof value !== "number") return void 0;
|
|
191
|
+
if (!Number.isInteger(value) || value < 1) return void 0;
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/adapters/df-sheet.ts
|
|
196
|
+
var DF_SHEET_REVIEW_SOURCE = "df-web-review-kit";
|
|
197
|
+
var REVIEW_METADATA_VERSION = 1;
|
|
198
|
+
function dfSheetAdapter(options) {
|
|
199
|
+
return {
|
|
200
|
+
async get(id) {
|
|
201
|
+
const issue = await requestDfSheet(
|
|
202
|
+
`/api/issues/${encodeURIComponent(id)}`,
|
|
203
|
+
options
|
|
204
|
+
);
|
|
205
|
+
return issueToReviewItem(issue, options);
|
|
206
|
+
},
|
|
207
|
+
async list(query) {
|
|
208
|
+
const params = new URLSearchParams();
|
|
209
|
+
params.set("project_id", options.projectId);
|
|
210
|
+
params.set("page_id", options.pageId);
|
|
211
|
+
params.set("review_source", options.source ?? DF_SHEET_REVIEW_SOURCE);
|
|
212
|
+
if (query.routeKey ?? query.normalizedPath) {
|
|
213
|
+
params.set("review_route_key", query.routeKey ?? query.normalizedPath ?? "");
|
|
214
|
+
}
|
|
215
|
+
const issues = await requestDfSheet(
|
|
216
|
+
`/api/issues?${params.toString()}`,
|
|
217
|
+
options
|
|
218
|
+
);
|
|
219
|
+
return issues.flatMap((issue) => {
|
|
220
|
+
const item = issueToReviewItem(issue, options);
|
|
221
|
+
return item ? [item] : [];
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
async create(item) {
|
|
225
|
+
const metadata = createReviewMetadata(item, options);
|
|
226
|
+
const draftDescription = buildIssueDescription(item, metadata);
|
|
227
|
+
const created = await requestDfSheet("/api/issues", options, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
body: JSON.stringify({
|
|
230
|
+
project_id: options.projectId,
|
|
231
|
+
page_id: options.pageId,
|
|
232
|
+
title: buildIssueTitle(item),
|
|
233
|
+
description: draftDescription,
|
|
234
|
+
type: options.issueType ?? "task",
|
|
235
|
+
types: [options.issueType ?? "task"],
|
|
236
|
+
status: "todo",
|
|
237
|
+
priority: options.priority ?? "medium",
|
|
238
|
+
links: metadata.reviewUrl ?? null,
|
|
239
|
+
review_metadata: metadata
|
|
240
|
+
})
|
|
241
|
+
});
|
|
242
|
+
const externalIssueId = created.id;
|
|
243
|
+
const externalIssueUrl = buildIssueUrl(options, externalIssueId);
|
|
244
|
+
const finalMetadata = {
|
|
245
|
+
...metadata,
|
|
246
|
+
externalIssueId,
|
|
247
|
+
externalIssueUrl,
|
|
248
|
+
reviewUrl: buildReviewUrl(item, options, externalIssueId),
|
|
249
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
250
|
+
submitStatus: "submitted"
|
|
251
|
+
};
|
|
252
|
+
const patched = await requestDfSheet(
|
|
253
|
+
`/api/issues/${encodeURIComponent(externalIssueId)}`,
|
|
254
|
+
options,
|
|
255
|
+
{
|
|
256
|
+
method: "PATCH",
|
|
257
|
+
body: JSON.stringify({
|
|
258
|
+
description: buildIssueDescription(item, finalMetadata),
|
|
259
|
+
links: finalMetadata.reviewUrl,
|
|
260
|
+
review_metadata: finalMetadata
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
return issueToReviewItem(patched, options) ?? {
|
|
265
|
+
...item,
|
|
266
|
+
id: externalIssueId,
|
|
267
|
+
externalIssueId,
|
|
268
|
+
externalIssueUrl,
|
|
269
|
+
submittedAt: finalMetadata.submittedAt,
|
|
270
|
+
submitStatus: "submitted",
|
|
271
|
+
updatedAt: finalMetadata.submittedAt ?? item.updatedAt
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
async update() {
|
|
275
|
+
throw new Error("df-sheet review items are read-only in review-kit.");
|
|
276
|
+
},
|
|
277
|
+
async remove() {
|
|
278
|
+
throw new Error("df-sheet review items are read-only in review-kit.");
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function createReviewMetadata(item, options) {
|
|
283
|
+
return {
|
|
284
|
+
source: options.source ?? DF_SHEET_REVIEW_SOURCE,
|
|
285
|
+
schema: DF_SHEET_REVIEW_SOURCE,
|
|
286
|
+
version: REVIEW_METADATA_VERSION,
|
|
287
|
+
reviewProjectId: options.reviewProjectId ?? item.projectId,
|
|
288
|
+
id: item.id,
|
|
289
|
+
reviewNumber: item.reviewNumber,
|
|
290
|
+
projectId: item.projectId,
|
|
291
|
+
routeKey: item.routeKey,
|
|
292
|
+
pageUrl: item.pageUrl,
|
|
293
|
+
originalUrl: item.originalUrl,
|
|
294
|
+
normalizedPath: item.normalizedPath,
|
|
295
|
+
scope: item.scope,
|
|
296
|
+
kind: item.kind,
|
|
297
|
+
title: item.title,
|
|
298
|
+
comment: item.comment,
|
|
299
|
+
status: item.status,
|
|
300
|
+
viewport: item.viewport,
|
|
301
|
+
devicePixelRatio: item.devicePixelRatio,
|
|
302
|
+
scroll: item.scroll,
|
|
303
|
+
anchor: item.anchor,
|
|
304
|
+
marker: item.marker,
|
|
305
|
+
selection: item.selection,
|
|
306
|
+
createdAt: item.createdAt,
|
|
307
|
+
updatedAt: item.updatedAt,
|
|
308
|
+
reviewUrl: buildReviewUrl(item, options)
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function issueToReviewItem(issue, options) {
|
|
312
|
+
const metadata = getReviewMetadata(issue);
|
|
313
|
+
if (!metadata) return null;
|
|
314
|
+
const routeKey = metadata.routeKey ?? metadata.normalizedPath ?? "/";
|
|
315
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
316
|
+
return {
|
|
317
|
+
id: issue.id,
|
|
318
|
+
reviewNumber: metadata.reviewNumber,
|
|
319
|
+
projectId: metadata.reviewProjectId ?? metadata.projectId ?? options.reviewProjectId ?? options.projectId,
|
|
320
|
+
routeKey,
|
|
321
|
+
pageUrl: metadata.pageUrl ?? metadata.originalUrl ?? toAbsoluteReviewUrl(routeKey),
|
|
322
|
+
originalUrl: metadata.originalUrl,
|
|
323
|
+
normalizedPath: metadata.normalizedPath ?? routeKey,
|
|
324
|
+
scope: metadata.scope,
|
|
325
|
+
kind: normalizeKind(metadata.kind),
|
|
326
|
+
title: metadata.title ?? issue.title,
|
|
327
|
+
comment: metadata.comment ?? issue.title,
|
|
328
|
+
status: mapIssueStatus(issue.status ?? metadata.status),
|
|
329
|
+
viewport: metadata.viewport ?? { width: 390, height: 720 },
|
|
330
|
+
devicePixelRatio: metadata.devicePixelRatio,
|
|
331
|
+
scroll: metadata.scroll,
|
|
332
|
+
anchor: metadata.anchor,
|
|
333
|
+
marker: metadata.marker,
|
|
334
|
+
selection: metadata.selection,
|
|
335
|
+
externalIssueId: issue.id,
|
|
336
|
+
externalIssueUrl: metadata.externalIssueUrl ?? buildIssueUrl(options, issue.id),
|
|
337
|
+
submittedAt: metadata.submittedAt ?? issue.created_at,
|
|
338
|
+
submitStatus: "submitted",
|
|
339
|
+
createdAt: metadata.createdAt ?? issue.created_at ?? now,
|
|
340
|
+
updatedAt: issue.updated_at ?? metadata.updatedAt ?? issue.created_at ?? now
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function getReviewMetadata(issue) {
|
|
344
|
+
const metadata = issue.review_metadata;
|
|
345
|
+
if (!metadata || typeof metadata !== "object") return null;
|
|
346
|
+
const value = metadata;
|
|
347
|
+
if (value.source !== DF_SHEET_REVIEW_SOURCE) return null;
|
|
348
|
+
return value;
|
|
349
|
+
}
|
|
350
|
+
async function requestDfSheet(path, options, init = {}) {
|
|
351
|
+
const url = new URL(path, getDfSheetBaseUrl(options)).toString();
|
|
352
|
+
const headers = new Headers(init.headers);
|
|
353
|
+
headers.set("Accept", "application/json");
|
|
354
|
+
if (init.body && !headers.has("Content-Type")) {
|
|
355
|
+
headers.set("Content-Type", "application/json");
|
|
356
|
+
}
|
|
357
|
+
if (options.token) {
|
|
358
|
+
headers.set("Authorization", `Bearer ${options.token}`);
|
|
359
|
+
}
|
|
360
|
+
const response = await fetch(url, {
|
|
361
|
+
...init,
|
|
362
|
+
headers,
|
|
363
|
+
credentials: "include"
|
|
364
|
+
});
|
|
365
|
+
const json = await response.json().catch(() => null);
|
|
366
|
+
if (!response.ok || !json?.success || json.data === void 0) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
json?.message ?? json?.error ?? `df-sheet request failed: ${response.status}`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return json.data;
|
|
372
|
+
}
|
|
373
|
+
function getDfSheetBaseUrl(options) {
|
|
374
|
+
const baseUrl = options.baseUrl?.trim();
|
|
375
|
+
if (baseUrl) return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
376
|
+
if (typeof window !== "undefined") return window.location.origin;
|
|
377
|
+
return "http://localhost";
|
|
378
|
+
}
|
|
379
|
+
function buildIssueTitle(item) {
|
|
380
|
+
const reviewId = formatReviewItemIndexId(item);
|
|
381
|
+
const idPrefix = reviewId ? `${reviewId} ` : "";
|
|
382
|
+
const prefix = item.kind === "area" ? "[Area]" : "[Note]";
|
|
383
|
+
const summary = item.title || item.comment.split("\n")[0] || "Review item";
|
|
384
|
+
return `${idPrefix}${prefix} ${summary}`.slice(0, 120);
|
|
385
|
+
}
|
|
386
|
+
function buildIssueDescription(item, metadata) {
|
|
387
|
+
const reviewId = formatReviewItemIndexId(item);
|
|
388
|
+
return [
|
|
389
|
+
"df-web-review-kit issue",
|
|
390
|
+
"",
|
|
391
|
+
reviewId ? `Review ID: ${reviewId}` : null,
|
|
392
|
+
`Kind: ${item.kind}`,
|
|
393
|
+
`Page: ${item.routeKey || item.normalizedPath || "/"}`,
|
|
394
|
+
`Viewport: ${Math.round(item.viewport.width)}x${Math.round(item.viewport.height)}`,
|
|
395
|
+
item.scroll ? `Scroll: ${Math.round(item.scroll.x)},${Math.round(item.scroll.y)}` : null,
|
|
396
|
+
item.anchor?.selector ? `Selector: ${item.anchor.selector}` : null,
|
|
397
|
+
item.marker?.viewport ? `Point: ${Math.round(item.marker.viewport.x)},${Math.round(item.marker.viewport.y)}` : null,
|
|
398
|
+
item.selection?.viewport ? `Area: ${Math.round(item.selection.viewport.x)},${Math.round(item.selection.viewport.y)},${Math.round(item.selection.viewport.width)},${Math.round(item.selection.viewport.height)}` : null,
|
|
399
|
+
metadata.reviewUrl ? `Review link: ${metadata.reviewUrl}` : null,
|
|
400
|
+
"",
|
|
401
|
+
"Note:",
|
|
402
|
+
item.comment
|
|
403
|
+
].filter((line) => line !== null).join("\n");
|
|
404
|
+
}
|
|
405
|
+
function formatReviewItemIndexId(item) {
|
|
406
|
+
if (typeof item.reviewNumber === "number" && item.reviewNumber > 0) {
|
|
407
|
+
return `#${item.reviewNumber}`;
|
|
408
|
+
}
|
|
409
|
+
return /^\d+$/.test(item.id) ? `#${item.id}` : void 0;
|
|
410
|
+
}
|
|
411
|
+
function buildReviewUrl(item, options, issueId) {
|
|
412
|
+
if (typeof window === "undefined") return void 0;
|
|
413
|
+
const prefix = options.reviewPathPrefix ?? "/review";
|
|
414
|
+
const url = new URL(prefix, window.location.origin);
|
|
415
|
+
url.searchParams.set("source", "df-sheet");
|
|
416
|
+
url.searchParams.set("target", item.routeKey || item.normalizedPath || "/");
|
|
417
|
+
url.searchParams.set("w", String(Math.round(item.viewport.width)));
|
|
418
|
+
url.searchParams.set("h", String(Math.round(item.viewport.height)));
|
|
419
|
+
if (issueId) url.searchParams.set("item", issueId);
|
|
420
|
+
return url.toString();
|
|
421
|
+
}
|
|
422
|
+
function buildIssueUrl(options, issueId) {
|
|
423
|
+
const path = `/projects/${options.projectId}/issues/${issueId}`;
|
|
424
|
+
return options.baseUrl ? new URL(path, getDfSheetBaseUrl(options)).toString() : path;
|
|
425
|
+
}
|
|
426
|
+
function toAbsoluteReviewUrl(path) {
|
|
427
|
+
if (typeof window === "undefined") return path;
|
|
428
|
+
return new URL(path, window.location.origin).toString();
|
|
429
|
+
}
|
|
430
|
+
function normalizeKind(value) {
|
|
431
|
+
return value === "area" ? "area" : "note";
|
|
432
|
+
}
|
|
433
|
+
function mapIssueStatus(value) {
|
|
434
|
+
if (value === "done" || value === "review" || value === "todo") return value;
|
|
435
|
+
if (value === "in_progress") return "doing";
|
|
436
|
+
if (value === "on_hold") return "hold";
|
|
437
|
+
return "todo";
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/adapters/supabase.ts
|
|
441
|
+
var DEFAULT_SUPABASE_REVIEW_TABLE = "review_items";
|
|
442
|
+
var DEFAULT_SUPABASE_REVIEW_SOURCE = "supabase";
|
|
443
|
+
var DEFAULT_SUPABASE_CREATE_REVIEW_ITEM_RPC = "create_review_item";
|
|
444
|
+
function supabaseAdapter(options) {
|
|
445
|
+
const tableName = options.table ?? DEFAULT_SUPABASE_REVIEW_TABLE;
|
|
446
|
+
const source = options.source ?? DEFAULT_SUPABASE_REVIEW_SOURCE;
|
|
447
|
+
const fromTable = () => options.client.from(tableName);
|
|
448
|
+
return {
|
|
449
|
+
async get(id) {
|
|
450
|
+
const row = await unwrapResponse(
|
|
451
|
+
fromTable().select("*").eq("id", id).maybeSingle(),
|
|
452
|
+
"supabase get review item"
|
|
453
|
+
);
|
|
454
|
+
return row ? rowToReviewItem(row, options) : null;
|
|
455
|
+
},
|
|
456
|
+
async list(query) {
|
|
457
|
+
let request = fromTable().select("*").eq("project_id", query.projectId).eq("source", query.source ?? source);
|
|
458
|
+
const routeKey = query.routeKey ?? query.normalizedPath;
|
|
459
|
+
if (routeKey) {
|
|
460
|
+
request = request.eq("route_key", routeKey);
|
|
461
|
+
}
|
|
462
|
+
if (query.status) {
|
|
463
|
+
request = request.eq(
|
|
464
|
+
"status",
|
|
465
|
+
normalizeReviewItemStatus(query.status)
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
const rows = await unwrapResponse(
|
|
469
|
+
request.order("updated_at", { ascending: false }),
|
|
470
|
+
"supabase list review items"
|
|
471
|
+
);
|
|
472
|
+
return (rows ?? []).flatMap((row) => {
|
|
473
|
+
const item = rowToReviewItem(row, options);
|
|
474
|
+
return item ? [item] : [];
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
async create(item) {
|
|
478
|
+
const nextItem = normalizeItemForSupabaseCreate(item, source, options);
|
|
479
|
+
if (options.unsafeClientReviewNumberFallback) {
|
|
480
|
+
return createItemWithClientReviewNumber(
|
|
481
|
+
nextItem,
|
|
482
|
+
source,
|
|
483
|
+
options,
|
|
484
|
+
fromTable
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return createItemWithRpc(nextItem, source, options);
|
|
488
|
+
},
|
|
489
|
+
async update(id, patch) {
|
|
490
|
+
const current = await this.get(id);
|
|
491
|
+
if (!current) throw new Error(`Review item not found: ${id}`);
|
|
492
|
+
const nextStatus = patch.status ? normalizeReviewItemStatus(patch.status) : current.status;
|
|
493
|
+
const nextItem = {
|
|
494
|
+
...current,
|
|
495
|
+
...patch,
|
|
496
|
+
id,
|
|
497
|
+
status: nextStatus,
|
|
498
|
+
createdAt: current.createdAt,
|
|
499
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
500
|
+
};
|
|
501
|
+
const patchRow = itemToRowPatch(nextItem, source, options);
|
|
502
|
+
const updated = await unwrapResponse(
|
|
503
|
+
fromTable().update(patchRow).eq("id", id).select("*").single(),
|
|
504
|
+
"supabase update review item"
|
|
505
|
+
);
|
|
506
|
+
return rowToReviewItem(updated, options) ?? nextItem;
|
|
507
|
+
},
|
|
508
|
+
async remove(id) {
|
|
509
|
+
await unwrapResponse(
|
|
510
|
+
fromTable().delete().eq("id", id),
|
|
511
|
+
"supabase delete review item"
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function normalizeItemForSupabaseCreate(item, source, options) {
|
|
517
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
518
|
+
const id = createSupabaseReviewItemId();
|
|
519
|
+
const normalizedStatus = normalizeReviewItemStatus(item.status);
|
|
520
|
+
const routeKey = item.routeKey || item.normalizedPath || "/";
|
|
521
|
+
const viewport = item.viewport ?? { width: 390, height: 720 };
|
|
522
|
+
const nextItem = {
|
|
523
|
+
...item,
|
|
524
|
+
id,
|
|
525
|
+
reviewNumber: void 0,
|
|
526
|
+
projectId: options.projectId,
|
|
527
|
+
routeKey,
|
|
528
|
+
normalizedPath: item.normalizedPath || routeKey,
|
|
529
|
+
viewport,
|
|
530
|
+
status: normalizedStatus,
|
|
531
|
+
externalIssueId: id,
|
|
532
|
+
externalIssueUrl: buildSupabaseReviewUrl(
|
|
533
|
+
{ routeKey, normalizedPath: item.normalizedPath || routeKey, viewport },
|
|
534
|
+
source,
|
|
535
|
+
options,
|
|
536
|
+
id
|
|
537
|
+
),
|
|
538
|
+
submittedAt: item.submittedAt ?? now,
|
|
539
|
+
submitStatus: item.submitStatus ?? "submitted",
|
|
540
|
+
createdAt: now,
|
|
541
|
+
updatedAt: now
|
|
542
|
+
};
|
|
543
|
+
return {
|
|
544
|
+
...nextItem,
|
|
545
|
+
externalIssueUrl: nextItem.externalIssueUrl ?? buildSupabaseReviewUrl(nextItem, source, options)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
async function createItemWithRpc(item, source, options) {
|
|
549
|
+
const rpcName = options.createRpc ?? DEFAULT_SUPABASE_CREATE_REVIEW_ITEM_RPC;
|
|
550
|
+
if (!options.client.rpc) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`supabase create review item: ${rpcName} rpc is required`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
const row = await unwrapResponse(
|
|
556
|
+
options.client.rpc(rpcName, {
|
|
557
|
+
p_id: item.id,
|
|
558
|
+
p_project_id: options.projectId,
|
|
559
|
+
p_route_key: item.routeKey || item.normalizedPath || "/",
|
|
560
|
+
p_source: source,
|
|
561
|
+
p_status: normalizeReviewItemStatus(item.status),
|
|
562
|
+
p_item: item
|
|
563
|
+
}),
|
|
564
|
+
`supabase create review item rpc ${rpcName}`
|
|
565
|
+
);
|
|
566
|
+
return rowToReviewItem(row, options) ?? item;
|
|
567
|
+
}
|
|
568
|
+
async function createItemWithClientReviewNumber(item, source, options, fromTable) {
|
|
569
|
+
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
570
|
+
const reviewNumber = await getNextReviewNumber(
|
|
571
|
+
options.projectId,
|
|
572
|
+
source,
|
|
573
|
+
fromTable
|
|
574
|
+
);
|
|
575
|
+
const nextItem = { ...item, reviewNumber };
|
|
576
|
+
const row = itemToRow(nextItem, source, options);
|
|
577
|
+
const created = await fromTable().insert(row).select("*").single();
|
|
578
|
+
if (!created.error) {
|
|
579
|
+
return rowToReviewItem(created.data, options) ?? nextItem;
|
|
580
|
+
}
|
|
581
|
+
if (created.error.code === "23505" && attempt < 4) {
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
throw new Error(
|
|
585
|
+
`supabase create review item: ${created.error.message ?? created.error.code ?? "failed"}`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
throw new Error("supabase create review item: failed");
|
|
589
|
+
}
|
|
590
|
+
async function getNextReviewNumber(projectId, source, fromTable) {
|
|
591
|
+
const rows = await unwrapResponse(
|
|
592
|
+
fromTable().select("review_number").eq("project_id", projectId).eq("source", source).order("review_number", { ascending: false }).limit(1),
|
|
593
|
+
"supabase get next review number"
|
|
594
|
+
);
|
|
595
|
+
const maxNumber = normalizeReviewNumber2(rows?.[0]?.review_number) ?? 0;
|
|
596
|
+
return maxNumber + 1;
|
|
597
|
+
}
|
|
598
|
+
function itemToRow(item, source, options) {
|
|
599
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
600
|
+
const updatedAt = item.updatedAt || now;
|
|
601
|
+
return {
|
|
602
|
+
id: item.id,
|
|
603
|
+
project_id: options.projectId,
|
|
604
|
+
route_key: item.routeKey || item.normalizedPath || "/",
|
|
605
|
+
source,
|
|
606
|
+
review_number: item.reviewNumber ?? null,
|
|
607
|
+
status: normalizeReviewItemStatus(item.status),
|
|
608
|
+
item: {
|
|
609
|
+
...item,
|
|
610
|
+
projectId: options.projectId,
|
|
611
|
+
status: normalizeReviewItemStatus(item.status),
|
|
612
|
+
updatedAt
|
|
613
|
+
},
|
|
614
|
+
created_at: item.createdAt || now,
|
|
615
|
+
updated_at: updatedAt
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function itemToRowPatch(item, source, options) {
|
|
619
|
+
const row = itemToRow(item, source, options);
|
|
620
|
+
return {
|
|
621
|
+
route_key: row.route_key,
|
|
622
|
+
review_number: row.review_number,
|
|
623
|
+
status: row.status,
|
|
624
|
+
item: row.item,
|
|
625
|
+
updated_at: row.updated_at
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function rowToReviewItem(row, options) {
|
|
629
|
+
if (!row.item || typeof row.item !== "object") return null;
|
|
630
|
+
const item = row.item;
|
|
631
|
+
const status = normalizeReviewItemStatus(
|
|
632
|
+
row.status || item.status || "todo"
|
|
633
|
+
);
|
|
634
|
+
const routeKey = row.route_key || item.routeKey || item.normalizedPath || "/";
|
|
635
|
+
const viewport = item.viewport ?? { width: 390, height: 720 };
|
|
636
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
637
|
+
return {
|
|
638
|
+
...item,
|
|
639
|
+
id: row.id,
|
|
640
|
+
reviewNumber: row.review_number ?? item.reviewNumber,
|
|
641
|
+
projectId: row.project_id || item.projectId || options.projectId,
|
|
642
|
+
routeKey,
|
|
643
|
+
pageUrl: item.pageUrl || toAbsoluteReviewUrl2(routeKey),
|
|
644
|
+
normalizedPath: item.normalizedPath || routeKey,
|
|
645
|
+
kind: item.kind === "area" ? "area" : "note",
|
|
646
|
+
comment: item.comment || "",
|
|
647
|
+
status,
|
|
648
|
+
viewport,
|
|
649
|
+
externalIssueId: item.externalIssueId ?? row.id,
|
|
650
|
+
externalIssueUrl: item.externalIssueUrl ?? buildSupabaseReviewUrl(
|
|
651
|
+
{ routeKey, normalizedPath: routeKey, viewport },
|
|
652
|
+
row.source,
|
|
653
|
+
options,
|
|
654
|
+
row.id
|
|
655
|
+
),
|
|
656
|
+
submittedAt: item.submittedAt ?? row.created_at,
|
|
657
|
+
submitStatus: item.submitStatus ?? "submitted",
|
|
658
|
+
createdAt: item.createdAt ?? row.created_at ?? now,
|
|
659
|
+
updatedAt: row.updated_at ?? item.updatedAt ?? now
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async function unwrapResponse(request, label) {
|
|
663
|
+
const { data, error } = await request;
|
|
664
|
+
if (error) {
|
|
665
|
+
throw new Error(`${label}: ${error.message ?? error.code ?? "failed"}`);
|
|
666
|
+
}
|
|
667
|
+
return data;
|
|
668
|
+
}
|
|
669
|
+
function buildSupabaseReviewUrl(item, source, options, itemId) {
|
|
670
|
+
if (typeof window === "undefined") return void 0;
|
|
671
|
+
const prefix = options.reviewPathPrefix ?? "/review";
|
|
672
|
+
const url = new URL(prefix, window.location.origin);
|
|
673
|
+
url.searchParams.set("source", source);
|
|
674
|
+
url.searchParams.set("target", item.routeKey || item.normalizedPath || "/");
|
|
675
|
+
url.searchParams.set("w", String(Math.round(item.viewport.width)));
|
|
676
|
+
url.searchParams.set("h", String(Math.round(item.viewport.height)));
|
|
677
|
+
if (itemId) url.searchParams.set("item", itemId);
|
|
678
|
+
return url.toString();
|
|
679
|
+
}
|
|
680
|
+
function toAbsoluteReviewUrl2(path) {
|
|
681
|
+
if (typeof window === "undefined") return path;
|
|
682
|
+
return new URL(path, window.location.origin).toString();
|
|
683
|
+
}
|
|
684
|
+
function normalizeReviewNumber2(value) {
|
|
685
|
+
if (typeof value !== "number") return void 0;
|
|
686
|
+
if (!Number.isInteger(value) || value < 1) return void 0;
|
|
687
|
+
return value;
|
|
688
|
+
}
|
|
689
|
+
function createSupabaseReviewItemId() {
|
|
690
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
691
|
+
return crypto.randomUUID();
|
|
692
|
+
}
|
|
693
|
+
return `review-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/core/overlay-style.ts
|
|
697
|
+
function createStyleElement() {
|
|
698
|
+
const style = document.createElement("style");
|
|
699
|
+
style.textContent = `
|
|
700
|
+
:host {
|
|
701
|
+
color-scheme: dark;
|
|
702
|
+
--df-review-font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
703
|
+
--df-review-font-size-2xs: 10px;
|
|
704
|
+
--df-review-font-size-xs: 11px;
|
|
705
|
+
--df-review-font-size-sm: 12px;
|
|
706
|
+
--df-review-font-size-md: 13px;
|
|
707
|
+
--df-review-font-size-xl: 15px;
|
|
708
|
+
--df-review-space-1: 4px;
|
|
709
|
+
--df-review-space-1-5: 6px;
|
|
710
|
+
--df-review-space-2: 8px;
|
|
711
|
+
--df-review-space-2-5: 10px;
|
|
712
|
+
--df-review-space-3: 12px;
|
|
713
|
+
--df-review-space-3-5: 14px;
|
|
714
|
+
--df-review-space-4: 16px;
|
|
715
|
+
--df-review-control-height-sm: 32px;
|
|
716
|
+
--df-review-control-height-md: 34px;
|
|
717
|
+
--df-review-radius-xs: 3px;
|
|
718
|
+
--df-review-radius-sm: 6px;
|
|
719
|
+
--df-review-radius-md: 8px;
|
|
720
|
+
--df-review-radius-pill: 999px;
|
|
721
|
+
--df-review-color-canvas: #111820;
|
|
722
|
+
--df-review-color-panel: #1f2428;
|
|
723
|
+
--df-review-color-panel-strong: #15191d;
|
|
724
|
+
--df-review-color-control: #2c3338;
|
|
725
|
+
--df-review-color-control-hover: #3b444b;
|
|
726
|
+
--df-review-color-border: rgba(255, 255, 255, 0.14);
|
|
727
|
+
--df-review-color-border-strong: rgba(255, 255, 255, 0.18);
|
|
728
|
+
--df-review-color-text: #f7f7f2;
|
|
729
|
+
--df-review-color-text-muted: rgba(247, 247, 242, 0.62);
|
|
730
|
+
--df-review-color-text-subtle: rgba(247, 247, 242, 0.46);
|
|
731
|
+
--df-review-color-accent: #d7ff5f;
|
|
732
|
+
--df-review-color-accent-contrast: #171b1e;
|
|
733
|
+
--df-review-color-accent-soft: rgba(215, 255, 95, 0.16);
|
|
734
|
+
--df-review-color-accent-ring: rgba(215, 255, 95, 0.6);
|
|
735
|
+
--df-review-color-area: #63d7c7;
|
|
736
|
+
--df-review-color-error: #ffb7a7;
|
|
737
|
+
--df-review-shadow-panel: 0 18px 48px rgba(0, 0, 0, 0.34);
|
|
738
|
+
--df-review-shadow-popover: 0 16px 38px rgba(0, 0, 0, 0.32);
|
|
739
|
+
--df-review-shadow-highlight: 0 10px 30px rgba(0, 0, 0, 0.22);
|
|
740
|
+
font-family: var(--df-review-font-sans);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
* {
|
|
744
|
+
box-sizing: border-box;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.dfwr-shell {
|
|
748
|
+
display: none;
|
|
749
|
+
position: fixed;
|
|
750
|
+
inset: 0;
|
|
751
|
+
z-index: 500;
|
|
752
|
+
pointer-events: none;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.dfwr-shell.is-open {
|
|
756
|
+
display: block;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.dfwr-panel {
|
|
760
|
+
position: fixed;
|
|
761
|
+
right: 16px;
|
|
762
|
+
top: 16px;
|
|
763
|
+
z-index: 3;
|
|
764
|
+
width: min(380px, calc(100vw - 32px));
|
|
765
|
+
max-height: calc(100vh - 32px);
|
|
766
|
+
overflow: auto;
|
|
767
|
+
pointer-events: auto;
|
|
768
|
+
color: var(--df-review-color-text);
|
|
769
|
+
background: var(--df-review-color-panel);
|
|
770
|
+
border: 1px solid var(--df-review-color-border);
|
|
771
|
+
border-radius: var(--df-review-radius-md);
|
|
772
|
+
box-shadow: var(--df-review-shadow-panel);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.dfwr-header {
|
|
776
|
+
display: flex;
|
|
777
|
+
align-items: flex-start;
|
|
778
|
+
justify-content: space-between;
|
|
779
|
+
gap: 16px;
|
|
780
|
+
padding: 14px 14px 10px;
|
|
781
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.dfwr-title {
|
|
785
|
+
font-size: 15px;
|
|
786
|
+
font-weight: 700;
|
|
787
|
+
line-height: 1.25;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
.dfwr-meta {
|
|
791
|
+
max-width: 292px;
|
|
792
|
+
margin-top: 4px;
|
|
793
|
+
overflow: hidden;
|
|
794
|
+
color: rgba(247, 247, 242, 0.56);
|
|
795
|
+
font-size: var(--df-review-font-size-xs);
|
|
796
|
+
line-height: 1.35;
|
|
797
|
+
text-overflow: ellipsis;
|
|
798
|
+
white-space: nowrap;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.dfwr-toolbar,
|
|
802
|
+
.dfwr-actions {
|
|
803
|
+
display: flex;
|
|
804
|
+
flex-wrap: wrap;
|
|
805
|
+
gap: 8px;
|
|
806
|
+
padding: 12px 14px;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.dfwr-body,
|
|
810
|
+
.dfwr-list {
|
|
811
|
+
padding: 0 14px 14px;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.dfwr-list {
|
|
815
|
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
816
|
+
padding-top: 12px;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
.dfwr-button,
|
|
820
|
+
.dfwr-icon-button {
|
|
821
|
+
appearance: none;
|
|
822
|
+
border: 1px solid var(--df-review-color-border-strong);
|
|
823
|
+
background: var(--df-review-color-control);
|
|
824
|
+
color: var(--df-review-color-text);
|
|
825
|
+
cursor: pointer;
|
|
826
|
+
font: inherit;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.dfwr-button {
|
|
830
|
+
min-height: var(--df-review-control-height-md);
|
|
831
|
+
padding: 0 12px;
|
|
832
|
+
border-radius: var(--df-review-radius-sm);
|
|
833
|
+
font-size: var(--df-review-font-size-sm);
|
|
834
|
+
font-weight: 650;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
.dfwr-button:hover,
|
|
838
|
+
.dfwr-icon-button:hover,
|
|
839
|
+
.dfwr-button.is-active {
|
|
840
|
+
border-color: rgba(255, 255, 255, 0.4);
|
|
841
|
+
background: var(--df-review-color-control-hover);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
.dfwr-button.is-primary {
|
|
845
|
+
border-color: var(--df-review-color-accent);
|
|
846
|
+
background: var(--df-review-color-accent);
|
|
847
|
+
color: var(--df-review-color-accent-contrast);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.dfwr-icon-button {
|
|
851
|
+
display: inline-flex;
|
|
852
|
+
align-items: center;
|
|
853
|
+
justify-content: center;
|
|
854
|
+
min-width: 32px;
|
|
855
|
+
height: var(--df-review-control-height-sm);
|
|
856
|
+
padding: 0 8px;
|
|
857
|
+
border-radius: var(--df-review-radius-sm);
|
|
858
|
+
font-size: var(--df-review-font-size-xs);
|
|
859
|
+
font-weight: 700;
|
|
860
|
+
line-height: 1;
|
|
861
|
+
text-transform: uppercase;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.dfwr-marker-layer {
|
|
865
|
+
position: fixed;
|
|
866
|
+
inset: 0;
|
|
867
|
+
z-index: 1;
|
|
868
|
+
pointer-events: none;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.dfwr-area-preview-layer {
|
|
872
|
+
position: fixed;
|
|
873
|
+
inset: 0;
|
|
874
|
+
z-index: 3;
|
|
875
|
+
pointer-events: none;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
.dfwr-selection-highlight {
|
|
879
|
+
position: fixed;
|
|
880
|
+
z-index: 1;
|
|
881
|
+
border: 2px solid #d7ff5f;
|
|
882
|
+
border-radius: var(--df-review-radius-xs);
|
|
883
|
+
background: rgba(215, 255, 95, 0.08);
|
|
884
|
+
box-shadow:
|
|
885
|
+
0 0 0 1px rgba(31, 36, 40, 0.72),
|
|
886
|
+
0 0 0 9999px rgba(0, 0, 0, 0.12),
|
|
887
|
+
0 10px 30px rgba(0, 0, 0, 0.22);
|
|
888
|
+
animation: dfwr-selection-pulse 900ms ease 0s 2;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.dfwr-selection-highlight.is-draft {
|
|
892
|
+
border-color: #63d7c7;
|
|
893
|
+
background: rgba(99, 215, 199, 0.1);
|
|
894
|
+
box-shadow:
|
|
895
|
+
0 0 0 1px rgba(31, 36, 40, 0.72),
|
|
896
|
+
0 0 0 9999px rgba(0, 0, 0, 0.08),
|
|
897
|
+
0 10px 30px rgba(0, 0, 0, 0.2);
|
|
898
|
+
animation: none;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.dfwr-dom-hover {
|
|
902
|
+
position: fixed;
|
|
903
|
+
z-index: 2;
|
|
904
|
+
border: 1px solid #d7ff5f;
|
|
905
|
+
border-radius: var(--df-review-radius-xs);
|
|
906
|
+
background: rgba(215, 255, 95, 0.1);
|
|
907
|
+
box-shadow:
|
|
908
|
+
0 0 0 1px rgba(31, 36, 40, 0.72),
|
|
909
|
+
0 0 0 9999px rgba(0, 0, 0, 0.08);
|
|
910
|
+
pointer-events: none;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
.dfwr-dom-hover[hidden] {
|
|
914
|
+
display: none;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
.dfwr-bound-marker,
|
|
918
|
+
.dfwr-item-scope {
|
|
919
|
+
--dfwr-scope: #7cc7ff;
|
|
920
|
+
--dfwr-scope-rgb: 124, 199, 255;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.dfwr-bound-marker.is-scope-tablet,
|
|
924
|
+
.dfwr-item-scope.is-scope-tablet {
|
|
925
|
+
--dfwr-scope: #63d7c7;
|
|
926
|
+
--dfwr-scope-rgb: 99, 215, 199;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
.dfwr-bound-marker.is-scope-desktop,
|
|
930
|
+
.dfwr-item-scope.is-scope-desktop {
|
|
931
|
+
--dfwr-scope: #f3b75f;
|
|
932
|
+
--dfwr-scope-rgb: 243, 183, 95;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.dfwr-bound-marker.is-scope-wide,
|
|
936
|
+
.dfwr-item-scope.is-scope-wide {
|
|
937
|
+
--dfwr-scope: #c99cff;
|
|
938
|
+
--dfwr-scope-rgb: 201, 156, 255;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.dfwr-bound-marker.is-scope-dom,
|
|
942
|
+
.dfwr-item-scope.is-scope-dom {
|
|
943
|
+
--dfwr-scope: #ff8f61;
|
|
944
|
+
--dfwr-scope-rgb: 255, 143, 97;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.dfwr-item-target-highlight,
|
|
948
|
+
.dfwr-item-target-label {
|
|
949
|
+
--dfwr-item-color: #7cc7ff;
|
|
950
|
+
--dfwr-item-color-rgb: 124, 199, 255;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.dfwr-item-target-highlight.is-mode-area,
|
|
954
|
+
.dfwr-item-target-label.is-mode-area {
|
|
955
|
+
--dfwr-item-color: #63d7c7;
|
|
956
|
+
--dfwr-item-color-rgb: 99, 215, 199;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.dfwr-item-target-highlight.is-mode-dom,
|
|
960
|
+
.dfwr-item-target-label.is-mode-dom {
|
|
961
|
+
--dfwr-item-color: #ff8f61;
|
|
962
|
+
--dfwr-item-color-rgb: 255, 143, 97;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.dfwr-item-target-highlight {
|
|
966
|
+
position: fixed;
|
|
967
|
+
z-index: 2;
|
|
968
|
+
border: 2px solid var(--dfwr-item-color);
|
|
969
|
+
border-radius: 4px;
|
|
970
|
+
background: rgba(var(--dfwr-item-color-rgb), 0.08);
|
|
971
|
+
box-shadow:
|
|
972
|
+
0 0 0 1px rgba(31, 36, 40, 0.78),
|
|
973
|
+
0 0 0 9999px rgba(0, 0, 0, 0.08),
|
|
974
|
+
0 12px 30px rgba(0, 0, 0, 0.24);
|
|
975
|
+
pointer-events: none;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.dfwr-item-target-highlight.is-fallback {
|
|
979
|
+
border-style: dashed;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.dfwr-item-target-highlight.is-highlighted {
|
|
983
|
+
border-width: 3px;
|
|
984
|
+
background: rgba(var(--dfwr-item-color-rgb), 0.12);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
.dfwr-item-target-highlight.is-highlighted::after {
|
|
988
|
+
content: "";
|
|
989
|
+
position: absolute;
|
|
990
|
+
inset: -6px;
|
|
991
|
+
border: 2px solid var(--dfwr-item-color);
|
|
992
|
+
border-radius: var(--df-review-radius-md);
|
|
993
|
+
opacity: 0;
|
|
994
|
+
animation: dfwr-target-ring-blink 1000ms ease-in-out infinite;
|
|
995
|
+
pointer-events: none;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.dfwr-item-target-label {
|
|
999
|
+
position: fixed;
|
|
1000
|
+
z-index: 3;
|
|
1001
|
+
display: inline-flex;
|
|
1002
|
+
align-items: center;
|
|
1003
|
+
min-width: 24px;
|
|
1004
|
+
height: 20px;
|
|
1005
|
+
padding: 0 7px;
|
|
1006
|
+
border: 1px solid var(--dfwr-item-color);
|
|
1007
|
+
border-radius: 4px;
|
|
1008
|
+
background: var(--dfwr-item-color);
|
|
1009
|
+
box-shadow:
|
|
1010
|
+
0 0 0 3px rgba(var(--dfwr-item-color-rgb), 0.2),
|
|
1011
|
+
0 8px 18px rgba(0, 0, 0, 0.28);
|
|
1012
|
+
color: #111820;
|
|
1013
|
+
font-size: var(--df-review-font-size-2xs);
|
|
1014
|
+
font-weight: 900;
|
|
1015
|
+
line-height: 1;
|
|
1016
|
+
pointer-events: none;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
.dfwr-item-target-label.is-highlighted {
|
|
1020
|
+
animation: dfwr-selected-blink 1000ms ease-in-out infinite;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.dfwr-bound-marker {
|
|
1024
|
+
position: fixed;
|
|
1025
|
+
z-index: 2;
|
|
1026
|
+
display: inline-flex;
|
|
1027
|
+
align-items: center;
|
|
1028
|
+
justify-content: center;
|
|
1029
|
+
gap: 4px;
|
|
1030
|
+
min-width: 28px;
|
|
1031
|
+
height: 22px;
|
|
1032
|
+
padding: 0 6px;
|
|
1033
|
+
transform: translate(-50%, -50%);
|
|
1034
|
+
border: 1px solid var(--dfwr-scope);
|
|
1035
|
+
border-radius: var(--df-review-radius-pill);
|
|
1036
|
+
background: var(--df-review-color-panel);
|
|
1037
|
+
box-shadow: 0 0 0 4px rgba(var(--dfwr-scope-rgb), 0.18);
|
|
1038
|
+
color: var(--dfwr-scope);
|
|
1039
|
+
font-size: var(--df-review-font-size-2xs);
|
|
1040
|
+
font-weight: 800;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.dfwr-bound-marker.is-highlighted {
|
|
1044
|
+
min-width: 32px;
|
|
1045
|
+
height: 26px;
|
|
1046
|
+
border-width: 2px;
|
|
1047
|
+
box-shadow:
|
|
1048
|
+
0 0 0 5px rgba(var(--dfwr-scope-rgb), 0.22),
|
|
1049
|
+
0 12px 26px rgba(0, 0, 0, 0.34);
|
|
1050
|
+
animation: dfwr-selected-blink 1000ms ease-in-out infinite;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.dfwr-bound-marker.is-fallback {
|
|
1054
|
+
border-style: dashed;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.dfwr-bound-marker.is-note-callout,
|
|
1058
|
+
.dfwr-bound-marker.is-note-callout.is-highlighted {
|
|
1059
|
+
--dfwr-scope: #7cc7ff;
|
|
1060
|
+
--dfwr-scope-rgb: 124, 199, 255;
|
|
1061
|
+
min-width: 0;
|
|
1062
|
+
width: 0;
|
|
1063
|
+
height: 0;
|
|
1064
|
+
padding: 0;
|
|
1065
|
+
transform: none;
|
|
1066
|
+
border: 0;
|
|
1067
|
+
border-radius: 0;
|
|
1068
|
+
background: transparent;
|
|
1069
|
+
box-shadow: none;
|
|
1070
|
+
color: var(--dfwr-scope);
|
|
1071
|
+
animation: none;
|
|
1072
|
+
overflow: visible;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.dfwr-bound-marker.is-note-callout::before {
|
|
1076
|
+
content: "";
|
|
1077
|
+
position: absolute;
|
|
1078
|
+
left: 0;
|
|
1079
|
+
top: 0;
|
|
1080
|
+
width: 8px;
|
|
1081
|
+
height: 8px;
|
|
1082
|
+
transform: translate(-50%, -50%);
|
|
1083
|
+
border: 2px solid #111820;
|
|
1084
|
+
border-radius: var(--df-review-radius-pill);
|
|
1085
|
+
background: var(--dfwr-scope);
|
|
1086
|
+
box-shadow:
|
|
1087
|
+
0 0 0 3px rgba(var(--dfwr-scope-rgb), 0.22),
|
|
1088
|
+
0 6px 16px rgba(0, 0, 0, 0.28);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
.dfwr-bound-marker.is-note-callout.is-highlighted::before {
|
|
1092
|
+
animation: dfwr-note-dot-pulse 1000ms ease-in-out infinite;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon {
|
|
1096
|
+
position: absolute;
|
|
1097
|
+
left: 0;
|
|
1098
|
+
top: 0;
|
|
1099
|
+
width: 31px;
|
|
1100
|
+
height: 2px;
|
|
1101
|
+
transform: rotate(-42deg);
|
|
1102
|
+
transform-origin: left center;
|
|
1103
|
+
border-radius: var(--df-review-radius-pill);
|
|
1104
|
+
background: currentColor;
|
|
1105
|
+
opacity: 1;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
.dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon::before,
|
|
1109
|
+
.dfwr-bound-marker.is-note-callout .dfwr-bound-marker-icon::after {
|
|
1110
|
+
display: none;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.dfwr-bound-marker.is-note-callout .dfwr-bound-marker-number {
|
|
1114
|
+
position: absolute;
|
|
1115
|
+
left: 24px;
|
|
1116
|
+
top: -41px;
|
|
1117
|
+
display: inline-flex;
|
|
1118
|
+
align-items: center;
|
|
1119
|
+
justify-content: center;
|
|
1120
|
+
min-width: 28px;
|
|
1121
|
+
height: 20px;
|
|
1122
|
+
padding: 0 7px;
|
|
1123
|
+
border: 1px solid var(--dfwr-scope);
|
|
1124
|
+
border-radius: 4px;
|
|
1125
|
+
background: var(--dfwr-scope);
|
|
1126
|
+
box-shadow:
|
|
1127
|
+
0 0 0 3px rgba(var(--dfwr-scope-rgb), 0.18),
|
|
1128
|
+
0 8px 18px rgba(0, 0, 0, 0.28);
|
|
1129
|
+
color: #111820;
|
|
1130
|
+
text-align: center;
|
|
1131
|
+
line-height: 1;
|
|
1132
|
+
white-space: nowrap;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.dfwr-bound-marker.is-note-callout.is-highlighted .dfwr-bound-marker-icon,
|
|
1136
|
+
.dfwr-bound-marker.is-note-callout.is-highlighted .dfwr-bound-marker-number {
|
|
1137
|
+
animation: dfwr-selected-blink 1000ms ease-in-out infinite;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.dfwr-area-preview-layer .dfwr-bound-marker {
|
|
1141
|
+
border-color: #63d7c7;
|
|
1142
|
+
background: var(--df-review-color-panel);
|
|
1143
|
+
box-shadow:
|
|
1144
|
+
0 0 0 5px rgba(99, 215, 199, 0.2),
|
|
1145
|
+
0 12px 26px rgba(0, 0, 0, 0.3);
|
|
1146
|
+
color: #63d7c7;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.dfwr-bound-marker-icon {
|
|
1150
|
+
position: relative;
|
|
1151
|
+
display: inline-block;
|
|
1152
|
+
width: 10px;
|
|
1153
|
+
height: 10px;
|
|
1154
|
+
flex: 0 0 auto;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.dfwr-bound-marker-icon::before,
|
|
1158
|
+
.dfwr-bound-marker-icon::after {
|
|
1159
|
+
content: "";
|
|
1160
|
+
position: absolute;
|
|
1161
|
+
display: block;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
.dfwr-bound-marker-icon::before {
|
|
1165
|
+
inset: 1px 2px;
|
|
1166
|
+
border: 1.5px solid currentColor;
|
|
1167
|
+
border-radius: 2px;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.dfwr-bound-marker.is-scope-mobile .dfwr-bound-marker-icon::before {
|
|
1171
|
+
inset: 0 2.5px;
|
|
1172
|
+
border-radius: 2px;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.dfwr-bound-marker.is-scope-tablet .dfwr-bound-marker-icon::before {
|
|
1176
|
+
inset: 0.5px 1.5px;
|
|
1177
|
+
border-radius: 2px;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
.dfwr-bound-marker.is-scope-desktop .dfwr-bound-marker-icon::before {
|
|
1181
|
+
inset: 1px 0 3px;
|
|
1182
|
+
border-radius: 1px;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.dfwr-bound-marker.is-scope-desktop .dfwr-bound-marker-icon::after {
|
|
1186
|
+
left: 3px;
|
|
1187
|
+
right: 3px;
|
|
1188
|
+
bottom: 0;
|
|
1189
|
+
height: 1.5px;
|
|
1190
|
+
background: currentColor;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.dfwr-bound-marker.is-scope-wide .dfwr-bound-marker-icon::before {
|
|
1194
|
+
inset: 2px 0;
|
|
1195
|
+
border-radius: 1px;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.dfwr-bound-marker.is-scope-dom .dfwr-bound-marker-icon::before {
|
|
1199
|
+
inset: 2px;
|
|
1200
|
+
border-radius: 1px;
|
|
1201
|
+
transform: rotate(45deg);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.dfwr-bound-marker-number {
|
|
1205
|
+
min-width: 6px;
|
|
1206
|
+
text-align: center;
|
|
1207
|
+
line-height: 1;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.dfwr-note-draft {
|
|
1211
|
+
position: fixed;
|
|
1212
|
+
inset: 0;
|
|
1213
|
+
z-index: 4;
|
|
1214
|
+
pointer-events: none;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.dfwr-note-pin {
|
|
1218
|
+
appearance: none;
|
|
1219
|
+
position: fixed;
|
|
1220
|
+
z-index: 5;
|
|
1221
|
+
width: 18px;
|
|
1222
|
+
height: 18px;
|
|
1223
|
+
padding: 0;
|
|
1224
|
+
transform: translate(-50%, -50%);
|
|
1225
|
+
border: 2px solid #1f2428;
|
|
1226
|
+
border-radius: var(--df-review-radius-pill);
|
|
1227
|
+
background: var(--df-review-color-accent);
|
|
1228
|
+
box-shadow:
|
|
1229
|
+
0 0 0 4px rgba(215, 255, 95, 0.22),
|
|
1230
|
+
0 8px 18px rgba(0, 0, 0, 0.28);
|
|
1231
|
+
cursor: grab;
|
|
1232
|
+
pointer-events: auto;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
.dfwr-note-pin:active {
|
|
1236
|
+
cursor: grabbing;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.dfwr-note-popover {
|
|
1240
|
+
position: fixed;
|
|
1241
|
+
z-index: 4;
|
|
1242
|
+
width: min(320px, calc(100vw - 24px));
|
|
1243
|
+
padding: 12px;
|
|
1244
|
+
pointer-events: auto;
|
|
1245
|
+
color: var(--df-review-color-text);
|
|
1246
|
+
background: var(--df-review-color-panel);
|
|
1247
|
+
border: 1px solid rgba(215, 255, 95, 0.56);
|
|
1248
|
+
border-radius: var(--df-review-radius-md);
|
|
1249
|
+
box-shadow: var(--df-review-shadow-popover);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
.dfwr-area-draft {
|
|
1253
|
+
position: fixed;
|
|
1254
|
+
right: 16px;
|
|
1255
|
+
top: 16px;
|
|
1256
|
+
z-index: 4;
|
|
1257
|
+
width: min(360px, calc(100vw - 32px));
|
|
1258
|
+
max-height: calc(100vh - 32px);
|
|
1259
|
+
overflow: auto;
|
|
1260
|
+
padding: 12px;
|
|
1261
|
+
pointer-events: auto;
|
|
1262
|
+
color: var(--df-review-color-text);
|
|
1263
|
+
background: var(--df-review-color-panel);
|
|
1264
|
+
border: 1px solid rgba(215, 255, 95, 0.56);
|
|
1265
|
+
border-radius: var(--df-review-radius-md);
|
|
1266
|
+
box-shadow: var(--df-review-shadow-popover);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
.dfwr-note-popover .dfwr-actions {
|
|
1270
|
+
padding: 0;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
.dfwr-area-draft .dfwr-actions {
|
|
1274
|
+
padding: 0;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.dfwr-form {
|
|
1278
|
+
display: grid;
|
|
1279
|
+
gap: 10px;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
.dfwr-textarea {
|
|
1283
|
+
width: 100%;
|
|
1284
|
+
min-height: 92px;
|
|
1285
|
+
resize: vertical;
|
|
1286
|
+
border: 1px solid rgba(255, 255, 255, 0.16);
|
|
1287
|
+
border-radius: var(--df-review-radius-sm);
|
|
1288
|
+
padding: 10px;
|
|
1289
|
+
color: var(--df-review-color-text);
|
|
1290
|
+
background: var(--df-review-color-panel-strong);
|
|
1291
|
+
font: inherit;
|
|
1292
|
+
font-size: var(--df-review-font-size-md);
|
|
1293
|
+
line-height: 1.45;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
.dfwr-textarea:focus {
|
|
1297
|
+
outline: 2px solid var(--df-review-color-accent-ring);
|
|
1298
|
+
outline-offset: 1px;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.dfwr-empty,
|
|
1302
|
+
.dfwr-error {
|
|
1303
|
+
margin: 0;
|
|
1304
|
+
color: rgba(247, 247, 242, 0.62);
|
|
1305
|
+
font-size: var(--df-review-font-size-sm);
|
|
1306
|
+
line-height: 1.45;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
.dfwr-error {
|
|
1310
|
+
color: var(--df-review-color-error);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
.dfwr-preview,
|
|
1314
|
+
.dfwr-thumb {
|
|
1315
|
+
display: block;
|
|
1316
|
+
width: 100%;
|
|
1317
|
+
border: 1px solid var(--df-review-color-border);
|
|
1318
|
+
border-radius: var(--df-review-radius-sm);
|
|
1319
|
+
object-fit: cover;
|
|
1320
|
+
background: var(--df-review-color-canvas);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
.dfwr-preview {
|
|
1324
|
+
max-height: 180px;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.dfwr-thumb {
|
|
1328
|
+
max-height: 120px;
|
|
1329
|
+
margin-top: 10px;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.dfwr-list-heading {
|
|
1333
|
+
margin-bottom: 10px;
|
|
1334
|
+
color: rgba(247, 247, 242, 0.74);
|
|
1335
|
+
font-size: var(--df-review-font-size-sm);
|
|
1336
|
+
font-weight: 700;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
.dfwr-item {
|
|
1340
|
+
display: flex;
|
|
1341
|
+
gap: 12px;
|
|
1342
|
+
justify-content: space-between;
|
|
1343
|
+
padding: 12px 0;
|
|
1344
|
+
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
1345
|
+
cursor: pointer;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.dfwr-item:first-of-type {
|
|
1349
|
+
border-top: 0;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
.dfwr-item:focus-visible {
|
|
1353
|
+
outline: 2px solid rgba(215, 255, 95, 0.72);
|
|
1354
|
+
outline-offset: 4px;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
.dfwr-item-body {
|
|
1358
|
+
min-width: 0;
|
|
1359
|
+
flex: 1;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
.dfwr-item-badges {
|
|
1363
|
+
display: flex;
|
|
1364
|
+
align-items: center;
|
|
1365
|
+
gap: 6px;
|
|
1366
|
+
flex-wrap: wrap;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
.dfwr-item-scope,
|
|
1370
|
+
.dfwr-item-kind {
|
|
1371
|
+
display: inline-flex;
|
|
1372
|
+
align-items: center;
|
|
1373
|
+
min-height: 20px;
|
|
1374
|
+
border-radius: var(--df-review-radius-pill);
|
|
1375
|
+
padding: 0 7px;
|
|
1376
|
+
font-size: var(--df-review-font-size-2xs);
|
|
1377
|
+
font-weight: 800;
|
|
1378
|
+
line-height: 1;
|
|
1379
|
+
letter-spacing: 0;
|
|
1380
|
+
text-transform: uppercase;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.dfwr-item-scope {
|
|
1384
|
+
border: 1px solid rgba(var(--dfwr-scope-rgb), 0.38);
|
|
1385
|
+
background: rgba(var(--dfwr-scope-rgb), 0.12);
|
|
1386
|
+
color: var(--dfwr-scope);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
.dfwr-item-kind {
|
|
1390
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
1391
|
+
background: rgba(255, 255, 255, 0.05);
|
|
1392
|
+
color: rgba(247, 247, 242, 0.64);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
.dfwr-item-comment {
|
|
1396
|
+
margin: 4px 0;
|
|
1397
|
+
color: var(--df-review-color-text);
|
|
1398
|
+
font-size: var(--df-review-font-size-md);
|
|
1399
|
+
line-height: 1.42;
|
|
1400
|
+
overflow-wrap: anywhere;
|
|
1401
|
+
white-space: pre-wrap;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
.dfwr-item-date {
|
|
1405
|
+
color: rgba(247, 247, 242, 0.46);
|
|
1406
|
+
font-size: var(--df-review-font-size-xs);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
.dfwr-item-actions {
|
|
1410
|
+
display: flex;
|
|
1411
|
+
flex-direction: column;
|
|
1412
|
+
gap: 6px;
|
|
1413
|
+
flex: 0 0 auto;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
.dfwr-text-layer,
|
|
1417
|
+
.dfwr-element-layer,
|
|
1418
|
+
.dfwr-area-layer {
|
|
1419
|
+
position: fixed;
|
|
1420
|
+
inset: 0;
|
|
1421
|
+
z-index: 1;
|
|
1422
|
+
pointer-events: auto;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.dfwr-text-layer {
|
|
1426
|
+
cursor: crosshair;
|
|
1427
|
+
background: rgba(0, 0, 0, 0.06);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
.dfwr-element-layer {
|
|
1431
|
+
cursor: cell;
|
|
1432
|
+
background: rgba(0, 0, 0, 0.06);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.dfwr-area-layer {
|
|
1436
|
+
cursor: crosshair;
|
|
1437
|
+
background: rgba(0, 0, 0, 0.22);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
.dfwr-area-box {
|
|
1441
|
+
position: fixed;
|
|
1442
|
+
z-index: 2;
|
|
1443
|
+
width: 0;
|
|
1444
|
+
height: 0;
|
|
1445
|
+
border: 1px solid #d7ff5f;
|
|
1446
|
+
background: rgba(215, 255, 95, 0.16);
|
|
1447
|
+
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.18);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
@keyframes dfwr-marker-pulse {
|
|
1451
|
+
0% {
|
|
1452
|
+
transform: translate(-50%, -50%) scale(0.92);
|
|
1453
|
+
}
|
|
1454
|
+
45% {
|
|
1455
|
+
transform: translate(-50%, -50%) scale(1.1);
|
|
1456
|
+
}
|
|
1457
|
+
100% {
|
|
1458
|
+
transform: translate(-50%, -50%) scale(1);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
@keyframes dfwr-note-dot-pulse {
|
|
1463
|
+
0% {
|
|
1464
|
+
transform: translate(-50%, -50%) scale(0.88);
|
|
1465
|
+
}
|
|
1466
|
+
45% {
|
|
1467
|
+
transform: translate(-50%, -50%) scale(1.3);
|
|
1468
|
+
}
|
|
1469
|
+
100% {
|
|
1470
|
+
transform: translate(-50%, -50%) scale(1);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
@keyframes dfwr-selected-blink {
|
|
1475
|
+
0%,
|
|
1476
|
+
100% {
|
|
1477
|
+
opacity: 0.78;
|
|
1478
|
+
}
|
|
1479
|
+
50% {
|
|
1480
|
+
opacity: 1;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
@keyframes dfwr-target-ring-blink {
|
|
1485
|
+
0%,
|
|
1486
|
+
100% {
|
|
1487
|
+
opacity: 0;
|
|
1488
|
+
transform: scale(0.98);
|
|
1489
|
+
}
|
|
1490
|
+
50% {
|
|
1491
|
+
opacity: 0.82;
|
|
1492
|
+
transform: scale(1);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
@keyframes dfwr-selection-pulse {
|
|
1497
|
+
0% {
|
|
1498
|
+
opacity: 0.72;
|
|
1499
|
+
}
|
|
1500
|
+
45% {
|
|
1501
|
+
opacity: 1;
|
|
1502
|
+
}
|
|
1503
|
+
100% {
|
|
1504
|
+
opacity: 0.86;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
@media (max-width: 520px) {
|
|
1509
|
+
.dfwr-panel {
|
|
1510
|
+
right: 8px;
|
|
1511
|
+
top: 8px;
|
|
1512
|
+
width: calc(100vw - 16px);
|
|
1513
|
+
max-height: calc(100vh - 16px);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
`;
|
|
1517
|
+
return style;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/core/review-scope.ts
|
|
1521
|
+
var DEFAULT_REVIEW_VIEWPORTS = [
|
|
1522
|
+
{ label: "Mobile", width: 390, height: 720, scope: "mobile" },
|
|
1523
|
+
{ label: "Tablet", width: 768, height: 1024, scope: "tablet" },
|
|
1524
|
+
{ label: "Desktop", width: 1440, height: 900, scope: "desktop" },
|
|
1525
|
+
{ label: "Wide", width: 1980, height: 1080, scope: "wide" }
|
|
1526
|
+
];
|
|
1527
|
+
var REVIEW_SCOPE_LABELS = {
|
|
1528
|
+
mobile: "Mobile",
|
|
1529
|
+
tablet: "Tablet",
|
|
1530
|
+
desktop: "Desktop",
|
|
1531
|
+
wide: "Wide",
|
|
1532
|
+
dom: "Element"
|
|
1533
|
+
};
|
|
1534
|
+
var normalizeReviewItemScope = (value) => {
|
|
1535
|
+
if (value === "element") return "dom";
|
|
1536
|
+
if (value === "mobile" || value === "tablet" || value === "desktop" || value === "wide" || value === "dom") {
|
|
1537
|
+
return value;
|
|
1538
|
+
}
|
|
1539
|
+
return void 0;
|
|
1540
|
+
};
|
|
1541
|
+
var getViewportPresetDistance = (preset, viewport) => Math.abs(preset.width - viewport.width) + Math.abs(preset.height - viewport.height);
|
|
1542
|
+
var inferViewportScope = (preset) => {
|
|
1543
|
+
if (preset.scope) return preset.scope;
|
|
1544
|
+
const label = preset.label.toLowerCase();
|
|
1545
|
+
if (label.includes("mobile") || label.includes("phone")) return "mobile";
|
|
1546
|
+
if (label.includes("tablet") || label.includes("pad")) return "tablet";
|
|
1547
|
+
if (label.includes("wide") || label.includes("1980") || label.includes("1940") || label.includes("1920")) {
|
|
1548
|
+
return "wide";
|
|
1549
|
+
}
|
|
1550
|
+
if (label.includes("desktop")) return "desktop";
|
|
1551
|
+
if (preset.width >= 1800) return "wide";
|
|
1552
|
+
if (preset.width >= 1e3) return "desktop";
|
|
1553
|
+
if (preset.width >= 700) return "tablet";
|
|
1554
|
+
return "mobile";
|
|
1555
|
+
};
|
|
1556
|
+
function findReviewViewportPreset(viewport, presets = DEFAULT_REVIEW_VIEWPORTS) {
|
|
1557
|
+
const fallback = presets[0] ?? DEFAULT_REVIEW_VIEWPORTS[0];
|
|
1558
|
+
const exact = presets.find(
|
|
1559
|
+
(preset) => preset.width === viewport.width && preset.height === viewport.height
|
|
1560
|
+
);
|
|
1561
|
+
if (exact) return exact;
|
|
1562
|
+
return presets.reduce((closest, preset) => {
|
|
1563
|
+
const closestDistance = getViewportPresetDistance(closest, viewport);
|
|
1564
|
+
const presetDistance = getViewportPresetDistance(preset, viewport);
|
|
1565
|
+
return presetDistance < closestDistance ? preset : closest;
|
|
1566
|
+
}, fallback);
|
|
1567
|
+
}
|
|
1568
|
+
function getReviewViewportScope(viewport, presets = DEFAULT_REVIEW_VIEWPORTS) {
|
|
1569
|
+
return inferViewportScope(findReviewViewportPreset(viewport, presets));
|
|
1570
|
+
}
|
|
1571
|
+
function getReviewItemScope(item, presets = DEFAULT_REVIEW_VIEWPORTS) {
|
|
1572
|
+
const scope = normalizeReviewItemScope(item.scope);
|
|
1573
|
+
if (scope && scope !== "dom") return scope;
|
|
1574
|
+
return getReviewViewportScope(item.viewport, presets);
|
|
1575
|
+
}
|
|
1576
|
+
function getReviewItemScopeLabel(item, presets = DEFAULT_REVIEW_VIEWPORTS) {
|
|
1577
|
+
const scope = getReviewItemScope(item, presets);
|
|
1578
|
+
if (scope === "dom") return REVIEW_SCOPE_LABELS.dom;
|
|
1579
|
+
const preset = findReviewViewportPreset(item.viewport, presets);
|
|
1580
|
+
return preset.label || REVIEW_SCOPE_LABELS[scope];
|
|
1581
|
+
}
|
|
1582
|
+
function getNumberedReviewItems(items, presets = DEFAULT_REVIEW_VIEWPORTS) {
|
|
1583
|
+
const numbers = /* @__PURE__ */ new Map();
|
|
1584
|
+
const usedNumbers = /* @__PURE__ */ new Set();
|
|
1585
|
+
let nextNumber = getNextReviewItemNumber(items);
|
|
1586
|
+
[...items].sort((a, b) => {
|
|
1587
|
+
const createdOrder = a.createdAt.localeCompare(b.createdAt);
|
|
1588
|
+
if (createdOrder !== 0) return createdOrder;
|
|
1589
|
+
return a.id.localeCompare(b.id);
|
|
1590
|
+
}).forEach((item) => {
|
|
1591
|
+
const storedNumber = getReviewItemNumber(item);
|
|
1592
|
+
const number = storedNumber && !usedNumbers.has(storedNumber) ? storedNumber : nextNumber++;
|
|
1593
|
+
usedNumbers.add(number);
|
|
1594
|
+
numbers.set(item.id, number);
|
|
1595
|
+
});
|
|
1596
|
+
return items.map((item) => {
|
|
1597
|
+
const scope = getReviewItemScope(item, presets);
|
|
1598
|
+
const label = getReviewItemScopeLabel(item, presets);
|
|
1599
|
+
const number = numbers.get(item.id) ?? 0;
|
|
1600
|
+
return {
|
|
1601
|
+
item,
|
|
1602
|
+
scope,
|
|
1603
|
+
label,
|
|
1604
|
+
number,
|
|
1605
|
+
displayLabel: `#${number}`
|
|
1606
|
+
};
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
function getReviewItemNumber(item) {
|
|
1610
|
+
return normalizeReviewNumber3(item.reviewNumber);
|
|
1611
|
+
}
|
|
1612
|
+
function getNextReviewItemNumber(items) {
|
|
1613
|
+
const maxNumber = items.reduce((max, item) => {
|
|
1614
|
+
const number = getReviewItemNumber(item);
|
|
1615
|
+
return number ? Math.max(max, number) : max;
|
|
1616
|
+
}, 0);
|
|
1617
|
+
return maxNumber + 1;
|
|
1618
|
+
}
|
|
1619
|
+
function normalizeReviewNumber3(value) {
|
|
1620
|
+
if (typeof value !== "number") return void 0;
|
|
1621
|
+
if (!Number.isInteger(value) || value < 1) return void 0;
|
|
1622
|
+
return value;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// src/core/web-review-kit-app.ts
|
|
1626
|
+
var ROOT_ID = "df-web-review-kit-root";
|
|
1627
|
+
var INTERNAL_QUERY_PARAMS = ["__dfwr_target"];
|
|
1628
|
+
function createWebReviewKit(options) {
|
|
1629
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1630
|
+
return createNoopController();
|
|
1631
|
+
}
|
|
1632
|
+
const app = new WebReviewKitApp(options);
|
|
1633
|
+
app.mount();
|
|
1634
|
+
return {
|
|
1635
|
+
open: () => app.open(),
|
|
1636
|
+
close: () => app.close(),
|
|
1637
|
+
toggle: () => app.toggle(),
|
|
1638
|
+
setMode: (mode) => app.setMode(mode),
|
|
1639
|
+
getMode: () => app.getMode(),
|
|
1640
|
+
highlightItem: (itemId) => app.highlightItem(itemId),
|
|
1641
|
+
setHiddenItemIds: (itemIds) => app.setHiddenItemIds(itemIds),
|
|
1642
|
+
reload: () => app.reload(),
|
|
1643
|
+
getItems: () => app.getItems(),
|
|
1644
|
+
destroy: () => app.destroy()
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
var WebReviewKitApp = class {
|
|
1648
|
+
constructor(options) {
|
|
1649
|
+
this.options = options;
|
|
1650
|
+
this.isOpen = false;
|
|
1651
|
+
this.mode = "idle";
|
|
1652
|
+
this.items = [];
|
|
1653
|
+
this.isSelectingArea = false;
|
|
1654
|
+
this.handleKeyDown = (event) => {
|
|
1655
|
+
if (event.key === "Escape" && this.cancelMode()) {
|
|
1656
|
+
event.preventDefault();
|
|
1657
|
+
event.stopPropagation();
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
if (!isHotkey(event, this.hotkey)) return;
|
|
1661
|
+
event.preventDefault();
|
|
1662
|
+
event.stopPropagation();
|
|
1663
|
+
this.toggle();
|
|
1664
|
+
};
|
|
1665
|
+
this.handleViewportChange = () => {
|
|
1666
|
+
if (!this.isOpen || this.renderFrame) return;
|
|
1667
|
+
this.renderFrame = window.requestAnimationFrame(() => {
|
|
1668
|
+
this.renderFrame = void 0;
|
|
1669
|
+
this.render();
|
|
1670
|
+
});
|
|
1671
|
+
};
|
|
1672
|
+
this.adapter = options.adapter ?? localAdapter();
|
|
1673
|
+
this.hotkey = options.hotkeys?.qa ?? "Shift+Q";
|
|
1674
|
+
}
|
|
1675
|
+
mount() {
|
|
1676
|
+
if (this.root) return;
|
|
1677
|
+
const existing = document.getElementById(ROOT_ID);
|
|
1678
|
+
if (existing) existing.remove();
|
|
1679
|
+
this.root = document.createElement("div");
|
|
1680
|
+
this.root.id = ROOT_ID;
|
|
1681
|
+
this.root.style.display = "contents";
|
|
1682
|
+
this.shadow = this.root.attachShadow({ mode: "open" });
|
|
1683
|
+
document.body.appendChild(this.root);
|
|
1684
|
+
document.addEventListener("keydown", this.handleKeyDown, true);
|
|
1685
|
+
window.addEventListener("scroll", this.handleViewportChange, true);
|
|
1686
|
+
window.addEventListener("resize", this.handleViewportChange);
|
|
1687
|
+
this.render();
|
|
1688
|
+
}
|
|
1689
|
+
destroy() {
|
|
1690
|
+
document.removeEventListener("keydown", this.handleKeyDown, true);
|
|
1691
|
+
window.removeEventListener("scroll", this.handleViewportChange, true);
|
|
1692
|
+
window.removeEventListener("resize", this.handleViewportChange);
|
|
1693
|
+
if (this.renderFrame) {
|
|
1694
|
+
window.cancelAnimationFrame(this.renderFrame);
|
|
1695
|
+
this.renderFrame = void 0;
|
|
1696
|
+
}
|
|
1697
|
+
this.root?.remove();
|
|
1698
|
+
this.root = void 0;
|
|
1699
|
+
this.shadow = void 0;
|
|
1700
|
+
}
|
|
1701
|
+
open() {
|
|
1702
|
+
if (this.isOpen) return;
|
|
1703
|
+
this.isOpen = true;
|
|
1704
|
+
void this.reload();
|
|
1705
|
+
}
|
|
1706
|
+
close() {
|
|
1707
|
+
this.isOpen = false;
|
|
1708
|
+
this.setModeState("idle");
|
|
1709
|
+
this.noteDraft = void 0;
|
|
1710
|
+
this.areaDraft = void 0;
|
|
1711
|
+
this.isSelectingArea = false;
|
|
1712
|
+
this.render();
|
|
1713
|
+
}
|
|
1714
|
+
toggle() {
|
|
1715
|
+
if (this.isOpen) {
|
|
1716
|
+
this.close();
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
this.open();
|
|
1720
|
+
}
|
|
1721
|
+
setMode(mode) {
|
|
1722
|
+
if (!this.isOpen) {
|
|
1723
|
+
this.isOpen = true;
|
|
1724
|
+
}
|
|
1725
|
+
this.setModeState(this.mode === mode ? "idle" : mode);
|
|
1726
|
+
this.noteDraft = void 0;
|
|
1727
|
+
this.areaDraft = void 0;
|
|
1728
|
+
this.render();
|
|
1729
|
+
}
|
|
1730
|
+
getMode() {
|
|
1731
|
+
return this.mode;
|
|
1732
|
+
}
|
|
1733
|
+
getItems() {
|
|
1734
|
+
return this.items;
|
|
1735
|
+
}
|
|
1736
|
+
highlightItem(itemId) {
|
|
1737
|
+
if (!itemId) {
|
|
1738
|
+
this.clearHighlightedItem();
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
if (!this.isOpen) {
|
|
1742
|
+
this.isOpen = true;
|
|
1743
|
+
}
|
|
1744
|
+
this.highlightedItemId = itemId;
|
|
1745
|
+
this.render();
|
|
1746
|
+
}
|
|
1747
|
+
setHiddenItemIds(itemIds) {
|
|
1748
|
+
this.hiddenItemIds = itemIds ? new Set(itemIds) : void 0;
|
|
1749
|
+
this.updateHiddenItemsStyle();
|
|
1750
|
+
}
|
|
1751
|
+
clearHighlightedItem() {
|
|
1752
|
+
if (!this.highlightedItemId) return;
|
|
1753
|
+
this.highlightedItemId = void 0;
|
|
1754
|
+
this.render();
|
|
1755
|
+
}
|
|
1756
|
+
createHiddenItemsStyleElement() {
|
|
1757
|
+
const style = document.createElement("style");
|
|
1758
|
+
style.dataset.dfwrHiddenItems = "true";
|
|
1759
|
+
style.textContent = this.getHiddenItemsCss();
|
|
1760
|
+
return style;
|
|
1761
|
+
}
|
|
1762
|
+
updateHiddenItemsStyle() {
|
|
1763
|
+
if (!this.shadow) return;
|
|
1764
|
+
let style = this.shadow.querySelector(
|
|
1765
|
+
'style[data-dfwr-hidden-items="true"]'
|
|
1766
|
+
);
|
|
1767
|
+
if (!style) {
|
|
1768
|
+
style = this.createHiddenItemsStyleElement();
|
|
1769
|
+
this.shadow.prepend(style);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
style.textContent = this.getHiddenItemsCss();
|
|
1773
|
+
}
|
|
1774
|
+
getHiddenItemsCss() {
|
|
1775
|
+
if (!this.hiddenItemIds?.size) return "";
|
|
1776
|
+
return Array.from(this.hiddenItemIds).map(
|
|
1777
|
+
(itemId) => `[data-review-item-id="${cssEscape(itemId)}"] { display: none !important; }`
|
|
1778
|
+
).join("\n");
|
|
1779
|
+
}
|
|
1780
|
+
setModeState(mode) {
|
|
1781
|
+
if (this.mode === mode) return;
|
|
1782
|
+
this.mode = mode;
|
|
1783
|
+
this.options.onModeChange?.(mode);
|
|
1784
|
+
}
|
|
1785
|
+
cancelMode() {
|
|
1786
|
+
if (this.mode === "idle" && !this.noteDraft && !this.areaDraft && !this.isSelectingArea) {
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
this.setModeState("idle");
|
|
1790
|
+
this.noteDraft = void 0;
|
|
1791
|
+
this.areaDraft = void 0;
|
|
1792
|
+
this.isSelectingArea = false;
|
|
1793
|
+
this.render();
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1796
|
+
getEnvironment() {
|
|
1797
|
+
const target = typeof this.options.target === "function" ? this.options.target() : this.options.target;
|
|
1798
|
+
if (!target) {
|
|
1799
|
+
return {
|
|
1800
|
+
window,
|
|
1801
|
+
document,
|
|
1802
|
+
viewportRect: {
|
|
1803
|
+
left: 0,
|
|
1804
|
+
top: 0,
|
|
1805
|
+
width: window.innerWidth,
|
|
1806
|
+
height: window.innerHeight
|
|
1807
|
+
},
|
|
1808
|
+
overlayRect: {
|
|
1809
|
+
left: 0,
|
|
1810
|
+
top: 0,
|
|
1811
|
+
width: window.innerWidth,
|
|
1812
|
+
height: window.innerHeight
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
try {
|
|
1817
|
+
const rect = target.getViewportRect?.() ?? {
|
|
1818
|
+
left: 0,
|
|
1819
|
+
top: 0,
|
|
1820
|
+
width: target.window.innerWidth,
|
|
1821
|
+
height: target.window.innerHeight
|
|
1822
|
+
};
|
|
1823
|
+
const overlayRect = target.getOverlayRect?.() ?? rect;
|
|
1824
|
+
return {
|
|
1825
|
+
window: target.window,
|
|
1826
|
+
document: target.document,
|
|
1827
|
+
viewportRect: {
|
|
1828
|
+
left: rect.left,
|
|
1829
|
+
top: rect.top,
|
|
1830
|
+
width: rect.width,
|
|
1831
|
+
height: rect.height
|
|
1832
|
+
},
|
|
1833
|
+
overlayRect: {
|
|
1834
|
+
left: overlayRect.left,
|
|
1835
|
+
top: overlayRect.top,
|
|
1836
|
+
width: overlayRect.width,
|
|
1837
|
+
height: overlayRect.height
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
} catch {
|
|
1841
|
+
return void 0;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
async reload() {
|
|
1845
|
+
const environment = this.getEnvironment();
|
|
1846
|
+
if (!environment) return this.items;
|
|
1847
|
+
this.items = await this.adapter.list({
|
|
1848
|
+
projectId: this.options.projectId,
|
|
1849
|
+
routeKey: getRouteKey(environment)
|
|
1850
|
+
});
|
|
1851
|
+
this.options.onItemsChange?.(this.items);
|
|
1852
|
+
if (this.isOpen) {
|
|
1853
|
+
this.render();
|
|
1854
|
+
}
|
|
1855
|
+
return this.items;
|
|
1856
|
+
}
|
|
1857
|
+
render() {
|
|
1858
|
+
if (!this.shadow) return;
|
|
1859
|
+
this.shadow.replaceChildren();
|
|
1860
|
+
this.shadow.append(createStyleElement());
|
|
1861
|
+
this.shadow.append(this.createHiddenItemsStyleElement());
|
|
1862
|
+
const shell = document.createElement("div");
|
|
1863
|
+
shell.className = `dfwr-shell${this.isOpen ? " is-open" : ""}`;
|
|
1864
|
+
shell.setAttribute("aria-hidden", this.isOpen ? "false" : "true");
|
|
1865
|
+
if (this.options.ui?.panel !== false) {
|
|
1866
|
+
this.panel = document.createElement("div");
|
|
1867
|
+
this.panel.className = "dfwr-panel";
|
|
1868
|
+
this.panel.setAttribute("role", "dialog");
|
|
1869
|
+
this.panel.setAttribute("aria-label", "Web review kit");
|
|
1870
|
+
this.panel.append(
|
|
1871
|
+
this.createHeader(),
|
|
1872
|
+
this.createToolbar(),
|
|
1873
|
+
this.createBody(),
|
|
1874
|
+
this.createList()
|
|
1875
|
+
);
|
|
1876
|
+
shell.append(this.panel);
|
|
1877
|
+
} else {
|
|
1878
|
+
this.panel = void 0;
|
|
1879
|
+
}
|
|
1880
|
+
shell.append(this.createMarkerLayer());
|
|
1881
|
+
if (this.isOpen && (this.mode === "note" || this.mode === "element")) {
|
|
1882
|
+
shell.append(
|
|
1883
|
+
this.noteDraft ? this.createNotePopover(this.noteDraft) : this.mode === "element" ? this.createElementLayer() : this.createNoteLayer()
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
if (this.isOpen && this.mode === "area" && !this.areaDraft) {
|
|
1887
|
+
shell.append(this.createAreaLayer());
|
|
1888
|
+
}
|
|
1889
|
+
if (this.isOpen && this.mode === "area" && this.areaDraft && this.options.ui?.panel === false) {
|
|
1890
|
+
if (this.areaDraft.selection) {
|
|
1891
|
+
shell.append(this.createAreaDraftOverlay(this.areaDraft));
|
|
1892
|
+
}
|
|
1893
|
+
shell.append(this.createAreaDraftPopover(this.areaDraft));
|
|
1894
|
+
}
|
|
1895
|
+
this.shadow.append(shell);
|
|
1896
|
+
}
|
|
1897
|
+
createHeader() {
|
|
1898
|
+
const header = document.createElement("div");
|
|
1899
|
+
header.className = "dfwr-header";
|
|
1900
|
+
const title = document.createElement("div");
|
|
1901
|
+
title.className = "dfwr-title";
|
|
1902
|
+
title.textContent = "Review Kit";
|
|
1903
|
+
const meta = document.createElement("div");
|
|
1904
|
+
meta.className = "dfwr-meta";
|
|
1905
|
+
meta.textContent = getRouteKey(this.getEnvironment());
|
|
1906
|
+
const titleGroup = document.createElement("div");
|
|
1907
|
+
titleGroup.append(title, meta);
|
|
1908
|
+
const close = document.createElement("button");
|
|
1909
|
+
close.className = "dfwr-icon-button";
|
|
1910
|
+
close.type = "button";
|
|
1911
|
+
close.textContent = "x";
|
|
1912
|
+
close.setAttribute("aria-label", "Close");
|
|
1913
|
+
close.addEventListener("click", () => this.close());
|
|
1914
|
+
header.append(titleGroup, close);
|
|
1915
|
+
return header;
|
|
1916
|
+
}
|
|
1917
|
+
createToolbar() {
|
|
1918
|
+
const toolbar = document.createElement("div");
|
|
1919
|
+
toolbar.className = "dfwr-toolbar";
|
|
1920
|
+
toolbar.append(
|
|
1921
|
+
this.createToolbarButton("Note", this.mode === "note", () => {
|
|
1922
|
+
this.setModeState(this.mode === "note" ? "idle" : "note");
|
|
1923
|
+
this.noteDraft = void 0;
|
|
1924
|
+
this.areaDraft = void 0;
|
|
1925
|
+
this.render();
|
|
1926
|
+
}),
|
|
1927
|
+
this.createToolbarButton("Element", this.mode === "element", () => {
|
|
1928
|
+
this.setModeState(this.mode === "element" ? "idle" : "element");
|
|
1929
|
+
this.noteDraft = void 0;
|
|
1930
|
+
this.areaDraft = void 0;
|
|
1931
|
+
this.render();
|
|
1932
|
+
}),
|
|
1933
|
+
this.createToolbarButton(
|
|
1934
|
+
this.isSelectingArea ? "Selecting" : "Area",
|
|
1935
|
+
this.mode === "area",
|
|
1936
|
+
() => {
|
|
1937
|
+
this.setModeState(this.mode === "area" ? "idle" : "area");
|
|
1938
|
+
this.noteDraft = void 0;
|
|
1939
|
+
this.areaDraft = void 0;
|
|
1940
|
+
this.render();
|
|
1941
|
+
}
|
|
1942
|
+
),
|
|
1943
|
+
this.createToolbarButton("Refresh", false, () => {
|
|
1944
|
+
void this.reload();
|
|
1945
|
+
})
|
|
1946
|
+
);
|
|
1947
|
+
return toolbar;
|
|
1948
|
+
}
|
|
1949
|
+
createToolbarButton(label, active, onClick) {
|
|
1950
|
+
const button = document.createElement("button");
|
|
1951
|
+
button.className = `dfwr-button${active ? " is-active" : ""}`;
|
|
1952
|
+
button.type = "button";
|
|
1953
|
+
button.textContent = label;
|
|
1954
|
+
button.addEventListener("click", onClick);
|
|
1955
|
+
return button;
|
|
1956
|
+
}
|
|
1957
|
+
createBody() {
|
|
1958
|
+
const body = document.createElement("div");
|
|
1959
|
+
body.className = "dfwr-body";
|
|
1960
|
+
if (this.mode === "idle") {
|
|
1961
|
+
const empty = document.createElement("p");
|
|
1962
|
+
empty.className = "dfwr-empty";
|
|
1963
|
+
empty.textContent = "Add a note or mark an area.";
|
|
1964
|
+
body.append(empty);
|
|
1965
|
+
return body;
|
|
1966
|
+
}
|
|
1967
|
+
if (this.mode === "note" || this.mode === "element") {
|
|
1968
|
+
body.append(this.createNoteBody());
|
|
1969
|
+
return body;
|
|
1970
|
+
}
|
|
1971
|
+
body.append(this.createAreaForm());
|
|
1972
|
+
return body;
|
|
1973
|
+
}
|
|
1974
|
+
createNoteBody() {
|
|
1975
|
+
const empty = document.createElement("p");
|
|
1976
|
+
empty.className = "dfwr-empty";
|
|
1977
|
+
empty.textContent = this.noteDraft ? "Write the note in the page box." : this.mode === "element" ? "Click an element to add QA." : "Click on the page to place a note.";
|
|
1978
|
+
return empty;
|
|
1979
|
+
}
|
|
1980
|
+
createNotePopover(draft) {
|
|
1981
|
+
const environment = this.getEnvironment();
|
|
1982
|
+
const group = document.createElement("div");
|
|
1983
|
+
group.className = "dfwr-note-draft";
|
|
1984
|
+
if (!environment) return group;
|
|
1985
|
+
const hostPoint = toHostPoint(draft.marker.viewport, environment);
|
|
1986
|
+
if (draft.selection) {
|
|
1987
|
+
group.append(
|
|
1988
|
+
this.createSelectionHighlight(
|
|
1989
|
+
toViewportSelection(draft.selection.viewport),
|
|
1990
|
+
environment,
|
|
1991
|
+
true
|
|
1992
|
+
)
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
const pin = document.createElement("button");
|
|
1996
|
+
pin.className = "dfwr-note-pin";
|
|
1997
|
+
pin.type = "button";
|
|
1998
|
+
pin.setAttribute("aria-label", "Move note point");
|
|
1999
|
+
pin.style.left = `${hostPoint.x}px`;
|
|
2000
|
+
pin.style.top = `${hostPoint.y}px`;
|
|
2001
|
+
const popover = document.createElement("div");
|
|
2002
|
+
const position = getPopoverPosition(hostPoint, environment);
|
|
2003
|
+
popover.className = "dfwr-note-popover";
|
|
2004
|
+
popover.style.left = `${position.left}px`;
|
|
2005
|
+
popover.style.top = `${position.top}px`;
|
|
2006
|
+
const form = document.createElement("form");
|
|
2007
|
+
form.className = "dfwr-form";
|
|
2008
|
+
const meta = document.createElement("div");
|
|
2009
|
+
meta.className = "dfwr-item-date";
|
|
2010
|
+
meta.textContent = formatNoteDraftMeta(draft);
|
|
2011
|
+
const textarea = document.createElement("textarea");
|
|
2012
|
+
textarea.className = "dfwr-textarea";
|
|
2013
|
+
textarea.placeholder = "Review comment";
|
|
2014
|
+
textarea.rows = 4;
|
|
2015
|
+
textarea.value = draft.comment ?? "";
|
|
2016
|
+
textarea.addEventListener("input", () => {
|
|
2017
|
+
if (!this.noteDraft) return;
|
|
2018
|
+
this.noteDraft = {
|
|
2019
|
+
...this.noteDraft,
|
|
2020
|
+
comment: textarea.value
|
|
2021
|
+
};
|
|
2022
|
+
});
|
|
2023
|
+
const actions = this.createFormActions("Save note", () => {
|
|
2024
|
+
const comment = textarea.value.trim();
|
|
2025
|
+
if (!comment) return;
|
|
2026
|
+
void this.createItem({
|
|
2027
|
+
kind: "note",
|
|
2028
|
+
comment,
|
|
2029
|
+
viewport: draft.viewport,
|
|
2030
|
+
anchor: draft.anchor,
|
|
2031
|
+
marker: draft.marker,
|
|
2032
|
+
selection: draft.selection
|
|
2033
|
+
});
|
|
2034
|
+
});
|
|
2035
|
+
form.append(meta, textarea, actions);
|
|
2036
|
+
popover.append(form);
|
|
2037
|
+
group.append(pin, popover);
|
|
2038
|
+
this.attachDraftPinDrag(pin, popover, meta, textarea);
|
|
2039
|
+
window.setTimeout(() => textarea.focus(), 0);
|
|
2040
|
+
return group;
|
|
2041
|
+
}
|
|
2042
|
+
createAreaForm() {
|
|
2043
|
+
const form = document.createElement("form");
|
|
2044
|
+
form.className = "dfwr-form";
|
|
2045
|
+
if (!this.areaDraft) {
|
|
2046
|
+
const empty = document.createElement("p");
|
|
2047
|
+
empty.className = "dfwr-empty";
|
|
2048
|
+
empty.textContent = "Drag on the page to select an area.";
|
|
2049
|
+
form.append(empty);
|
|
2050
|
+
return form;
|
|
2051
|
+
}
|
|
2052
|
+
const meta = document.createElement("div");
|
|
2053
|
+
meta.className = "dfwr-item-date";
|
|
2054
|
+
meta.textContent = formatAreaDraftMeta(this.areaDraft);
|
|
2055
|
+
form.append(meta);
|
|
2056
|
+
const textarea = document.createElement("textarea");
|
|
2057
|
+
textarea.className = "dfwr-textarea";
|
|
2058
|
+
textarea.placeholder = "Area comment";
|
|
2059
|
+
textarea.rows = 4;
|
|
2060
|
+
const actions = this.createFormActions("Save area", () => {
|
|
2061
|
+
const comment = textarea.value.trim();
|
|
2062
|
+
if (!comment || !this.areaDraft) return;
|
|
2063
|
+
void this.createItem({
|
|
2064
|
+
kind: "area",
|
|
2065
|
+
comment,
|
|
2066
|
+
viewport: this.areaDraft.viewport,
|
|
2067
|
+
anchor: this.areaDraft.anchor,
|
|
2068
|
+
marker: this.areaDraft.marker,
|
|
2069
|
+
selection: this.areaDraft.selection
|
|
2070
|
+
});
|
|
2071
|
+
});
|
|
2072
|
+
form.append(textarea, actions);
|
|
2073
|
+
return form;
|
|
2074
|
+
}
|
|
2075
|
+
createAreaDraftOverlay(draft) {
|
|
2076
|
+
const layer = document.createElement("div");
|
|
2077
|
+
layer.className = "dfwr-area-preview-layer";
|
|
2078
|
+
const environment = this.getEnvironment();
|
|
2079
|
+
if (!environment || !draft.selection) return layer;
|
|
2080
|
+
const selection = toViewportSelection(draft.selection.viewport);
|
|
2081
|
+
layer.append(this.createSelectionHighlight(selection, environment, true));
|
|
2082
|
+
if (draft.marker) {
|
|
2083
|
+
const hostPoint = toHostPoint(draft.marker.viewport, environment);
|
|
2084
|
+
layer.append(
|
|
2085
|
+
this.createMarkerElement(
|
|
2086
|
+
void 0,
|
|
2087
|
+
hostPoint,
|
|
2088
|
+
"\u2022",
|
|
2089
|
+
getReviewViewportScope(
|
|
2090
|
+
draft.viewport,
|
|
2091
|
+
this.options.viewports?.presets
|
|
2092
|
+
),
|
|
2093
|
+
true,
|
|
2094
|
+
true
|
|
2095
|
+
)
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
return layer;
|
|
2099
|
+
}
|
|
2100
|
+
createAreaDraftPopover(draft) {
|
|
2101
|
+
const environment = this.getEnvironment();
|
|
2102
|
+
const popover = document.createElement("div");
|
|
2103
|
+
popover.className = "dfwr-area-draft";
|
|
2104
|
+
if (environment && draft.selection) {
|
|
2105
|
+
const selection = toHostSelection(
|
|
2106
|
+
toViewportSelection(draft.selection.viewport),
|
|
2107
|
+
environment
|
|
2108
|
+
);
|
|
2109
|
+
const position = getAreaPopoverPosition(selection, environment);
|
|
2110
|
+
popover.style.left = `${position.left}px`;
|
|
2111
|
+
popover.style.top = `${position.top}px`;
|
|
2112
|
+
popover.style.right = "auto";
|
|
2113
|
+
}
|
|
2114
|
+
popover.append(this.createAreaForm());
|
|
2115
|
+
return popover;
|
|
2116
|
+
}
|
|
2117
|
+
createFormActions(saveLabel, onSave) {
|
|
2118
|
+
const actions = document.createElement("div");
|
|
2119
|
+
actions.className = "dfwr-actions";
|
|
2120
|
+
const save = document.createElement("button");
|
|
2121
|
+
save.className = "dfwr-button is-primary";
|
|
2122
|
+
save.type = "button";
|
|
2123
|
+
save.textContent = saveLabel;
|
|
2124
|
+
save.addEventListener("click", onSave);
|
|
2125
|
+
const cancel = document.createElement("button");
|
|
2126
|
+
cancel.className = "dfwr-button";
|
|
2127
|
+
cancel.type = "button";
|
|
2128
|
+
cancel.textContent = "Cancel";
|
|
2129
|
+
cancel.addEventListener("click", () => {
|
|
2130
|
+
this.setModeState("idle");
|
|
2131
|
+
this.noteDraft = void 0;
|
|
2132
|
+
this.areaDraft = void 0;
|
|
2133
|
+
this.render();
|
|
2134
|
+
});
|
|
2135
|
+
actions.append(save, cancel);
|
|
2136
|
+
return actions;
|
|
2137
|
+
}
|
|
2138
|
+
createList() {
|
|
2139
|
+
const section = document.createElement("div");
|
|
2140
|
+
section.className = "dfwr-list";
|
|
2141
|
+
const heading = document.createElement("div");
|
|
2142
|
+
heading.className = "dfwr-list-heading";
|
|
2143
|
+
heading.textContent = `Review items (${this.items.length})`;
|
|
2144
|
+
section.append(heading);
|
|
2145
|
+
if (this.items.length === 0) {
|
|
2146
|
+
const empty = document.createElement("p");
|
|
2147
|
+
empty.className = "dfwr-empty";
|
|
2148
|
+
empty.textContent = "No review items on this page.";
|
|
2149
|
+
section.append(empty);
|
|
2150
|
+
return section;
|
|
2151
|
+
}
|
|
2152
|
+
for (const numberedItem of getNumberedReviewItems(
|
|
2153
|
+
this.items,
|
|
2154
|
+
this.options.viewports?.presets
|
|
2155
|
+
)) {
|
|
2156
|
+
section.append(this.createListItem(numberedItem));
|
|
2157
|
+
}
|
|
2158
|
+
return section;
|
|
2159
|
+
}
|
|
2160
|
+
createListItem(numberedItem) {
|
|
2161
|
+
const { item } = numberedItem;
|
|
2162
|
+
const row = document.createElement("article");
|
|
2163
|
+
row.className = "dfwr-item";
|
|
2164
|
+
row.tabIndex = 0;
|
|
2165
|
+
row.setAttribute("role", "button");
|
|
2166
|
+
row.setAttribute(
|
|
2167
|
+
"aria-label",
|
|
2168
|
+
`Restore review item: ${item.title ?? item.comment}`
|
|
2169
|
+
);
|
|
2170
|
+
row.addEventListener("click", () => {
|
|
2171
|
+
void this.restoreItem(item);
|
|
2172
|
+
});
|
|
2173
|
+
row.addEventListener("keydown", (event) => {
|
|
2174
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
2175
|
+
event.preventDefault();
|
|
2176
|
+
void this.restoreItem(item);
|
|
2177
|
+
});
|
|
2178
|
+
const body = document.createElement("div");
|
|
2179
|
+
body.className = "dfwr-item-body";
|
|
2180
|
+
const badges = document.createElement("div");
|
|
2181
|
+
badges.className = "dfwr-item-badges";
|
|
2182
|
+
const scope = document.createElement("div");
|
|
2183
|
+
scope.className = `dfwr-item-scope is-scope-${numberedItem.scope}`;
|
|
2184
|
+
scope.textContent = numberedItem.displayLabel;
|
|
2185
|
+
const kind = document.createElement("div");
|
|
2186
|
+
kind.className = "dfwr-item-kind";
|
|
2187
|
+
kind.textContent = item.kind;
|
|
2188
|
+
badges.append(scope, kind);
|
|
2189
|
+
const comment = document.createElement("p");
|
|
2190
|
+
comment.className = "dfwr-item-comment";
|
|
2191
|
+
comment.textContent = item.comment;
|
|
2192
|
+
const date = document.createElement("time");
|
|
2193
|
+
date.className = "dfwr-item-date";
|
|
2194
|
+
date.dateTime = item.createdAt;
|
|
2195
|
+
date.textContent = formatItemMeta(item);
|
|
2196
|
+
body.append(badges, comment, date);
|
|
2197
|
+
const actions = document.createElement("div");
|
|
2198
|
+
actions.className = "dfwr-item-actions";
|
|
2199
|
+
actions.addEventListener("click", (event) => event.stopPropagation());
|
|
2200
|
+
actions.addEventListener("keydown", (event) => event.stopPropagation());
|
|
2201
|
+
const remove = document.createElement("button");
|
|
2202
|
+
remove.className = "dfwr-icon-button";
|
|
2203
|
+
remove.type = "button";
|
|
2204
|
+
remove.textContent = "x";
|
|
2205
|
+
remove.setAttribute("aria-label", "Delete");
|
|
2206
|
+
remove.addEventListener("click", (event) => {
|
|
2207
|
+
event.stopPropagation();
|
|
2208
|
+
void this.adapter.remove(item.id).then(() => this.reload());
|
|
2209
|
+
});
|
|
2210
|
+
actions.append(remove);
|
|
2211
|
+
row.append(body, actions);
|
|
2212
|
+
return row;
|
|
2213
|
+
}
|
|
2214
|
+
createMarkerLayer() {
|
|
2215
|
+
const layer = document.createElement("div");
|
|
2216
|
+
layer.className = "dfwr-marker-layer";
|
|
2217
|
+
const environment = this.getEnvironment();
|
|
2218
|
+
if (!environment) return layer;
|
|
2219
|
+
const currentScope = getReviewViewportScope(
|
|
2220
|
+
getViewportSize(environment),
|
|
2221
|
+
this.options.viewports?.presets
|
|
2222
|
+
);
|
|
2223
|
+
getNumberedReviewItems(
|
|
2224
|
+
this.items,
|
|
2225
|
+
this.options.viewports?.presets
|
|
2226
|
+
).forEach((numberedItem) => {
|
|
2227
|
+
const { item, scope, number, displayLabel } = numberedItem;
|
|
2228
|
+
if (!shouldShowMarkerForScope(scope, currentScope)) {
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
const isHighlighted = item.id === this.highlightedItemId;
|
|
2232
|
+
const highlightMode = getReviewItemHighlightMode(item);
|
|
2233
|
+
if (highlightMode !== "note") {
|
|
2234
|
+
const selection = getItemHighlightSelection(item, environment);
|
|
2235
|
+
if (selection) {
|
|
2236
|
+
layer.append(
|
|
2237
|
+
...this.createItemHighlightElements(
|
|
2238
|
+
selection.viewport,
|
|
2239
|
+
environment,
|
|
2240
|
+
item,
|
|
2241
|
+
String(number),
|
|
2242
|
+
selection.isBound,
|
|
2243
|
+
isHighlighted
|
|
2244
|
+
)
|
|
2245
|
+
);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
const point = getBoundMarkerPoint(item, environment);
|
|
2250
|
+
if (!point || !isPointInViewport(point.viewport, environment)) {
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
const hostPoint = toHostPoint(point.viewport, environment);
|
|
2254
|
+
const marker = this.createMarkerElement(
|
|
2255
|
+
item.id,
|
|
2256
|
+
hostPoint,
|
|
2257
|
+
String(number),
|
|
2258
|
+
scope,
|
|
2259
|
+
point.isBound,
|
|
2260
|
+
isHighlighted,
|
|
2261
|
+
highlightMode === "note" ? "note" : "default"
|
|
2262
|
+
);
|
|
2263
|
+
marker.title = `${displayLabel} / ${item.comment}
|
|
2264
|
+
${formatItemMeta(item)}`;
|
|
2265
|
+
layer.append(marker);
|
|
2266
|
+
});
|
|
2267
|
+
return layer;
|
|
2268
|
+
}
|
|
2269
|
+
createItemHighlightElements(selection, environment, item, label, isBound, isHighlighted) {
|
|
2270
|
+
const rect = toHostSelection(selection, environment);
|
|
2271
|
+
const mode = getReviewItemHighlightMode(item);
|
|
2272
|
+
const highlight = document.createElement("div");
|
|
2273
|
+
highlight.className = [
|
|
2274
|
+
"dfwr-item-target-highlight",
|
|
2275
|
+
`is-mode-${mode}`,
|
|
2276
|
+
isBound ? "is-bound" : "is-fallback",
|
|
2277
|
+
isHighlighted ? "is-highlighted" : ""
|
|
2278
|
+
].filter(Boolean).join(" ");
|
|
2279
|
+
highlight.style.left = `${rect.left}px`;
|
|
2280
|
+
highlight.style.top = `${rect.top}px`;
|
|
2281
|
+
highlight.style.width = `${rect.width}px`;
|
|
2282
|
+
highlight.style.height = `${rect.height}px`;
|
|
2283
|
+
highlight.dataset.reviewItemId = item.id;
|
|
2284
|
+
const labelElement = document.createElement("div");
|
|
2285
|
+
labelElement.className = [
|
|
2286
|
+
"dfwr-item-target-label",
|
|
2287
|
+
`is-mode-${mode}`,
|
|
2288
|
+
isHighlighted ? "is-highlighted" : ""
|
|
2289
|
+
].filter(Boolean).join(" ");
|
|
2290
|
+
labelElement.textContent = `#${label}`;
|
|
2291
|
+
labelElement.style.left = `${Math.max(4, rect.left)}px`;
|
|
2292
|
+
labelElement.style.top = `${Math.max(4, rect.top - 24)}px`;
|
|
2293
|
+
labelElement.dataset.reviewItemId = item.id;
|
|
2294
|
+
return [highlight, labelElement];
|
|
2295
|
+
}
|
|
2296
|
+
createSelectionHighlight(selection, environment, isDraft) {
|
|
2297
|
+
const rect = toHostSelection(selection, environment);
|
|
2298
|
+
const highlight = document.createElement("div");
|
|
2299
|
+
highlight.className = `dfwr-selection-highlight${isDraft ? " is-draft" : ""}`;
|
|
2300
|
+
highlight.style.left = `${rect.left}px`;
|
|
2301
|
+
highlight.style.top = `${rect.top}px`;
|
|
2302
|
+
highlight.style.width = `${rect.width}px`;
|
|
2303
|
+
highlight.style.height = `${rect.height}px`;
|
|
2304
|
+
return highlight;
|
|
2305
|
+
}
|
|
2306
|
+
createMarkerElement(itemId, hostPoint, label, scope, isBound, isHighlighted, variant = "default") {
|
|
2307
|
+
const isNoteCallout = variant === "note";
|
|
2308
|
+
const marker = document.createElement("div");
|
|
2309
|
+
marker.className = [
|
|
2310
|
+
"dfwr-bound-marker",
|
|
2311
|
+
isNoteCallout ? "is-note-callout" : "",
|
|
2312
|
+
`is-scope-${scope}`,
|
|
2313
|
+
isBound ? "is-bound" : "is-fallback",
|
|
2314
|
+
isHighlighted ? "is-highlighted" : ""
|
|
2315
|
+
].filter(Boolean).join(" ");
|
|
2316
|
+
marker.style.left = `${hostPoint.x}px`;
|
|
2317
|
+
marker.style.top = `${hostPoint.y}px`;
|
|
2318
|
+
marker.dataset.scope = scope;
|
|
2319
|
+
if (itemId) {
|
|
2320
|
+
marker.dataset.reviewItemId = itemId;
|
|
2321
|
+
}
|
|
2322
|
+
const iconElement = document.createElement("span");
|
|
2323
|
+
iconElement.className = "dfwr-bound-marker-icon";
|
|
2324
|
+
iconElement.setAttribute("aria-hidden", "true");
|
|
2325
|
+
const labelElement = document.createElement("span");
|
|
2326
|
+
labelElement.className = "dfwr-bound-marker-number";
|
|
2327
|
+
labelElement.textContent = isNoteCallout ? `#${label}` : label;
|
|
2328
|
+
marker.append(iconElement, labelElement);
|
|
2329
|
+
return marker;
|
|
2330
|
+
}
|
|
2331
|
+
attachDraftPinDrag(pin, popover, meta, textarea) {
|
|
2332
|
+
let isDragging = false;
|
|
2333
|
+
const moveDraftUi = (hostPoint) => {
|
|
2334
|
+
const environment = this.getEnvironment();
|
|
2335
|
+
if (!environment) return;
|
|
2336
|
+
const nextPoint = clampPoint(toTargetPoint(hostPoint, environment), environment);
|
|
2337
|
+
const nextHostPoint = toHostPoint(nextPoint, environment);
|
|
2338
|
+
const position = getPopoverPosition(nextHostPoint, environment);
|
|
2339
|
+
pin.style.left = `${nextHostPoint.x}px`;
|
|
2340
|
+
pin.style.top = `${nextHostPoint.y}px`;
|
|
2341
|
+
popover.style.left = `${position.left}px`;
|
|
2342
|
+
popover.style.top = `${position.top}px`;
|
|
2343
|
+
if (!this.noteDraft) return;
|
|
2344
|
+
this.noteDraft = {
|
|
2345
|
+
...this.noteDraft,
|
|
2346
|
+
marker: {
|
|
2347
|
+
...this.noteDraft.marker,
|
|
2348
|
+
viewport: roundPoint(nextPoint)
|
|
2349
|
+
},
|
|
2350
|
+
comment: textarea.value
|
|
2351
|
+
};
|
|
2352
|
+
meta.textContent = formatNoteDraftMeta(this.noteDraft);
|
|
2353
|
+
};
|
|
2354
|
+
pin.addEventListener("pointerdown", (event) => {
|
|
2355
|
+
if (event.button !== 0) return;
|
|
2356
|
+
event.preventDefault();
|
|
2357
|
+
event.stopPropagation();
|
|
2358
|
+
isDragging = true;
|
|
2359
|
+
pin.setPointerCapture(event.pointerId);
|
|
2360
|
+
});
|
|
2361
|
+
pin.addEventListener("pointermove", (event) => {
|
|
2362
|
+
if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
|
|
2363
|
+
event.preventDefault();
|
|
2364
|
+
moveDraftUi({
|
|
2365
|
+
x: event.clientX,
|
|
2366
|
+
y: event.clientY
|
|
2367
|
+
});
|
|
2368
|
+
});
|
|
2369
|
+
pin.addEventListener("pointerup", (event) => {
|
|
2370
|
+
if (!isDragging || !pin.hasPointerCapture(event.pointerId)) return;
|
|
2371
|
+
event.preventDefault();
|
|
2372
|
+
event.stopPropagation();
|
|
2373
|
+
isDragging = false;
|
|
2374
|
+
pin.releasePointerCapture(event.pointerId);
|
|
2375
|
+
const nextPoint = toTargetPointFromHostEvent(event, this.getEnvironment());
|
|
2376
|
+
void (this.mode === "element" ? this.bindElementDraftToPoint(nextPoint, textarea.value) : this.bindNoteDraftToPoint(nextPoint, textarea.value));
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
createNoteLayer() {
|
|
2380
|
+
const layer = document.createElement("div");
|
|
2381
|
+
layer.className = "dfwr-text-layer";
|
|
2382
|
+
const environment = this.getEnvironment();
|
|
2383
|
+
if (environment) {
|
|
2384
|
+
placeLayerOverTarget(layer, environment);
|
|
2385
|
+
}
|
|
2386
|
+
layer.addEventListener("pointerdown", (event) => {
|
|
2387
|
+
if (event.button !== 0) return;
|
|
2388
|
+
event.preventDefault();
|
|
2389
|
+
void this.createNoteDraft(
|
|
2390
|
+
toTargetPointFromHostEvent(event, this.getEnvironment())
|
|
2391
|
+
);
|
|
2392
|
+
});
|
|
2393
|
+
return layer;
|
|
2394
|
+
}
|
|
2395
|
+
createElementLayer() {
|
|
2396
|
+
const layer = document.createElement("div");
|
|
2397
|
+
layer.className = "dfwr-element-layer";
|
|
2398
|
+
const environment = this.getEnvironment();
|
|
2399
|
+
const hover = document.createElement("div");
|
|
2400
|
+
hover.className = "dfwr-dom-hover";
|
|
2401
|
+
hover.hidden = true;
|
|
2402
|
+
layer.append(hover);
|
|
2403
|
+
if (environment) {
|
|
2404
|
+
placeLayerOverTarget(layer, environment);
|
|
2405
|
+
}
|
|
2406
|
+
const updateHover = (point) => {
|
|
2407
|
+
const nextEnvironment = this.getEnvironment();
|
|
2408
|
+
if (!nextEnvironment) return;
|
|
2409
|
+
const anchor = getDomAnchorFromPoint(
|
|
2410
|
+
clampPoint(point, nextEnvironment),
|
|
2411
|
+
this.options.anchors?.attribute,
|
|
2412
|
+
nextEnvironment
|
|
2413
|
+
);
|
|
2414
|
+
const selection = anchor ? getElementViewportSelection(anchor, nextEnvironment) : void 0;
|
|
2415
|
+
if (!selection) {
|
|
2416
|
+
hover.hidden = true;
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
2419
|
+
const rect = toHostSelection(selection, nextEnvironment);
|
|
2420
|
+
hover.hidden = false;
|
|
2421
|
+
hover.style.left = `${rect.left}px`;
|
|
2422
|
+
hover.style.top = `${rect.top}px`;
|
|
2423
|
+
hover.style.width = `${rect.width}px`;
|
|
2424
|
+
hover.style.height = `${rect.height}px`;
|
|
2425
|
+
};
|
|
2426
|
+
layer.addEventListener("pointermove", (event) => {
|
|
2427
|
+
updateHover(toTargetPointFromHostEvent(event, this.getEnvironment()));
|
|
2428
|
+
});
|
|
2429
|
+
layer.addEventListener("pointerleave", () => {
|
|
2430
|
+
hover.hidden = true;
|
|
2431
|
+
});
|
|
2432
|
+
layer.addEventListener("pointerdown", (event) => {
|
|
2433
|
+
if (event.button !== 0) return;
|
|
2434
|
+
event.preventDefault();
|
|
2435
|
+
void this.createElementDraft(
|
|
2436
|
+
toTargetPointFromHostEvent(event, this.getEnvironment())
|
|
2437
|
+
);
|
|
2438
|
+
});
|
|
2439
|
+
return layer;
|
|
2440
|
+
}
|
|
2441
|
+
async createNoteDraft(point) {
|
|
2442
|
+
await this.bindNoteDraftToPoint(point);
|
|
2443
|
+
}
|
|
2444
|
+
async createElementDraft(point) {
|
|
2445
|
+
await this.bindElementDraftToPoint(point);
|
|
2446
|
+
}
|
|
2447
|
+
async bindNoteDraftToPoint(point, comment) {
|
|
2448
|
+
const environment = this.getEnvironment();
|
|
2449
|
+
if (!environment) return;
|
|
2450
|
+
const viewport = getViewportSize(environment);
|
|
2451
|
+
const nextPoint = clampPoint(point, environment);
|
|
2452
|
+
const draft = await this.withOverlayHidden(() => {
|
|
2453
|
+
const selection = getPointSelection(nextPoint);
|
|
2454
|
+
const anchor = getDomAnchor(
|
|
2455
|
+
selection,
|
|
2456
|
+
this.options.anchors?.attribute,
|
|
2457
|
+
environment
|
|
2458
|
+
);
|
|
2459
|
+
const marker = {
|
|
2460
|
+
viewport: roundPoint(nextPoint),
|
|
2461
|
+
relative: anchor ? getRelativePoint(nextPoint, anchor, environment) : void 0
|
|
2462
|
+
};
|
|
2463
|
+
return {
|
|
2464
|
+
viewport,
|
|
2465
|
+
anchor,
|
|
2466
|
+
marker,
|
|
2467
|
+
comment
|
|
2468
|
+
};
|
|
2469
|
+
});
|
|
2470
|
+
this.noteDraft = draft;
|
|
2471
|
+
this.render();
|
|
2472
|
+
}
|
|
2473
|
+
async bindElementDraftToPoint(point, comment) {
|
|
2474
|
+
const environment = this.getEnvironment();
|
|
2475
|
+
if (!environment) return;
|
|
2476
|
+
const viewport = getViewportSize(environment);
|
|
2477
|
+
const nextPoint = clampPoint(point, environment);
|
|
2478
|
+
const draft = await this.withOverlayHidden(() => {
|
|
2479
|
+
const anchor = getDomAnchorFromPoint(
|
|
2480
|
+
nextPoint,
|
|
2481
|
+
this.options.anchors?.attribute,
|
|
2482
|
+
environment
|
|
2483
|
+
);
|
|
2484
|
+
const elementSelection = anchor ? getElementViewportSelection(anchor, environment) : void 0;
|
|
2485
|
+
const selection = elementSelection ?? getPointSelection(nextPoint);
|
|
2486
|
+
const markerPoint = getSelectionCenter(selection);
|
|
2487
|
+
const reviewSelection = elementSelection ? {
|
|
2488
|
+
viewport: toPublicSelection(elementSelection),
|
|
2489
|
+
relative: getRelativeSelection(
|
|
2490
|
+
elementSelection,
|
|
2491
|
+
anchor,
|
|
2492
|
+
environment
|
|
2493
|
+
)
|
|
2494
|
+
} : void 0;
|
|
2495
|
+
const marker = {
|
|
2496
|
+
viewport: roundPoint(markerPoint),
|
|
2497
|
+
relative: anchor ? getRelativePoint(markerPoint, anchor, environment) : void 0
|
|
2498
|
+
};
|
|
2499
|
+
return {
|
|
2500
|
+
viewport,
|
|
2501
|
+
anchor,
|
|
2502
|
+
marker,
|
|
2503
|
+
selection: reviewSelection,
|
|
2504
|
+
comment
|
|
2505
|
+
};
|
|
2506
|
+
});
|
|
2507
|
+
this.noteDraft = draft;
|
|
2508
|
+
this.render();
|
|
2509
|
+
}
|
|
2510
|
+
createAreaLayer() {
|
|
2511
|
+
const layer = document.createElement("div");
|
|
2512
|
+
layer.className = "dfwr-area-layer";
|
|
2513
|
+
const environment = this.getEnvironment();
|
|
2514
|
+
if (environment) {
|
|
2515
|
+
placeLayerOverTarget(layer, environment);
|
|
2516
|
+
}
|
|
2517
|
+
const box = document.createElement("div");
|
|
2518
|
+
box.className = "dfwr-area-box";
|
|
2519
|
+
layer.append(box);
|
|
2520
|
+
let startX = 0;
|
|
2521
|
+
let startY = 0;
|
|
2522
|
+
let selection;
|
|
2523
|
+
const updateBox = (event) => {
|
|
2524
|
+
const nextPoint = toTargetPointFromHostEvent(
|
|
2525
|
+
event,
|
|
2526
|
+
this.getEnvironment()
|
|
2527
|
+
);
|
|
2528
|
+
const left = Math.min(startX, nextPoint.x);
|
|
2529
|
+
const top = Math.min(startY, nextPoint.y);
|
|
2530
|
+
const width = Math.abs(nextPoint.x - startX);
|
|
2531
|
+
const height = Math.abs(nextPoint.y - startY);
|
|
2532
|
+
const hostPoint = toHostPoint(
|
|
2533
|
+
{ x: left, y: top },
|
|
2534
|
+
this.getEnvironment()
|
|
2535
|
+
);
|
|
2536
|
+
selection = { left, top, width, height };
|
|
2537
|
+
box.style.left = `${hostPoint.x}px`;
|
|
2538
|
+
box.style.top = `${hostPoint.y}px`;
|
|
2539
|
+
box.style.width = `${width}px`;
|
|
2540
|
+
box.style.height = `${height}px`;
|
|
2541
|
+
};
|
|
2542
|
+
layer.addEventListener("pointerdown", (event) => {
|
|
2543
|
+
event.preventDefault();
|
|
2544
|
+
layer.setPointerCapture(event.pointerId);
|
|
2545
|
+
const startPoint = toTargetPointFromHostEvent(
|
|
2546
|
+
event,
|
|
2547
|
+
this.getEnvironment()
|
|
2548
|
+
);
|
|
2549
|
+
startX = startPoint.x;
|
|
2550
|
+
startY = startPoint.y;
|
|
2551
|
+
updateBox(event);
|
|
2552
|
+
});
|
|
2553
|
+
layer.addEventListener("pointermove", (event) => {
|
|
2554
|
+
if (!layer.hasPointerCapture(event.pointerId)) return;
|
|
2555
|
+
updateBox(event);
|
|
2556
|
+
});
|
|
2557
|
+
layer.addEventListener("pointerup", (event) => {
|
|
2558
|
+
if (!layer.hasPointerCapture(event.pointerId)) return;
|
|
2559
|
+
layer.releasePointerCapture(event.pointerId);
|
|
2560
|
+
updateBox(event);
|
|
2561
|
+
if (!selection || selection.width < 8 || selection.height < 8) return;
|
|
2562
|
+
this.isSelectingArea = true;
|
|
2563
|
+
this.render();
|
|
2564
|
+
void this.createAreaDraft(selection);
|
|
2565
|
+
});
|
|
2566
|
+
return layer;
|
|
2567
|
+
}
|
|
2568
|
+
async createAreaDraft(selection) {
|
|
2569
|
+
const environment = this.getEnvironment();
|
|
2570
|
+
if (!environment) return;
|
|
2571
|
+
const viewport = getViewportSize(environment);
|
|
2572
|
+
this.areaDraft = await this.withOverlayHidden(() => {
|
|
2573
|
+
const anchor = getDomAnchor(
|
|
2574
|
+
selection,
|
|
2575
|
+
this.options.anchors?.attribute,
|
|
2576
|
+
environment
|
|
2577
|
+
);
|
|
2578
|
+
const relativeSelection = anchor ? getRelativeSelection(selection, anchor, environment) : void 0;
|
|
2579
|
+
const marker = createSelectionCenterMarker(
|
|
2580
|
+
selection,
|
|
2581
|
+
anchor,
|
|
2582
|
+
environment
|
|
2583
|
+
);
|
|
2584
|
+
const reviewSelection = {
|
|
2585
|
+
viewport: toPublicSelection(selection),
|
|
2586
|
+
relative: relativeSelection
|
|
2587
|
+
};
|
|
2588
|
+
return {
|
|
2589
|
+
viewport,
|
|
2590
|
+
anchor,
|
|
2591
|
+
marker,
|
|
2592
|
+
selection: reviewSelection
|
|
2593
|
+
};
|
|
2594
|
+
});
|
|
2595
|
+
this.isSelectingArea = false;
|
|
2596
|
+
this.setModeState("area");
|
|
2597
|
+
this.render();
|
|
2598
|
+
}
|
|
2599
|
+
async withOverlayHidden(callback) {
|
|
2600
|
+
if (!this.root) return callback();
|
|
2601
|
+
const previousDisplay = this.root.style.display;
|
|
2602
|
+
this.root.style.display = "none";
|
|
2603
|
+
try {
|
|
2604
|
+
return await callback();
|
|
2605
|
+
} finally {
|
|
2606
|
+
this.root.style.display = previousDisplay;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
async createItem(input) {
|
|
2610
|
+
const environment = this.getEnvironment();
|
|
2611
|
+
if (!environment) return;
|
|
2612
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2613
|
+
const routeKey = getRouteKey(environment);
|
|
2614
|
+
const viewport = input.viewport ?? getViewportSize(environment);
|
|
2615
|
+
const reviewNumber = await this.getNextReviewNumber();
|
|
2616
|
+
const item = {
|
|
2617
|
+
id: createId(),
|
|
2618
|
+
reviewNumber,
|
|
2619
|
+
projectId: this.options.projectId,
|
|
2620
|
+
routeKey,
|
|
2621
|
+
pageUrl: getPageUrl(environment),
|
|
2622
|
+
originalUrl: getOriginalUrl(environment),
|
|
2623
|
+
normalizedPath: routeKey,
|
|
2624
|
+
scope: input.scope ?? getReviewViewportScope(viewport, this.options.viewports?.presets),
|
|
2625
|
+
kind: input.kind,
|
|
2626
|
+
title: input.comment.split("\n")[0]?.slice(0, 80),
|
|
2627
|
+
comment: input.comment,
|
|
2628
|
+
status: "todo",
|
|
2629
|
+
viewport,
|
|
2630
|
+
devicePixelRatio: environment.window.devicePixelRatio || 1,
|
|
2631
|
+
scroll: {
|
|
2632
|
+
x: environment.window.scrollX,
|
|
2633
|
+
y: environment.window.scrollY
|
|
2634
|
+
},
|
|
2635
|
+
anchor: input.anchor,
|
|
2636
|
+
marker: input.marker,
|
|
2637
|
+
selection: input.selection,
|
|
2638
|
+
createdAt: now,
|
|
2639
|
+
updatedAt: now
|
|
2640
|
+
};
|
|
2641
|
+
await this.adapter.create(item);
|
|
2642
|
+
this.setModeState("idle");
|
|
2643
|
+
this.noteDraft = void 0;
|
|
2644
|
+
this.areaDraft = void 0;
|
|
2645
|
+
this.highlightItem(item.id);
|
|
2646
|
+
await this.reload();
|
|
2647
|
+
}
|
|
2648
|
+
async getNextReviewNumber() {
|
|
2649
|
+
const items = await this.adapter.list({
|
|
2650
|
+
projectId: this.options.projectId
|
|
2651
|
+
});
|
|
2652
|
+
return getNextReviewItemNumber(items);
|
|
2653
|
+
}
|
|
2654
|
+
async restoreItem(item) {
|
|
2655
|
+
this.setModeState("idle");
|
|
2656
|
+
this.noteDraft = void 0;
|
|
2657
|
+
this.areaDraft = void 0;
|
|
2658
|
+
if (this.options.onRestoreItem) {
|
|
2659
|
+
await this.options.onRestoreItem(item);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
const environment = this.getEnvironment();
|
|
2663
|
+
if (!environment) return;
|
|
2664
|
+
const scroll = item.scroll;
|
|
2665
|
+
if (scroll) {
|
|
2666
|
+
runWithAutoScrollBehavior(environment.document, () => {
|
|
2667
|
+
setDocumentScrollInstantly(environment, scroll);
|
|
2668
|
+
});
|
|
2669
|
+
await waitForNextFrame(environment);
|
|
2670
|
+
}
|
|
2671
|
+
this.highlightItem(item.id);
|
|
2672
|
+
this.render();
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
function rectanglesIntersect(a, b) {
|
|
2676
|
+
return a.left < b.left + b.width && a.left + a.width > b.left && a.top < b.top + b.height && a.top + a.height > b.top;
|
|
2677
|
+
}
|
|
2678
|
+
function waitForNextFrame(environment) {
|
|
2679
|
+
return new Promise((resolve) => {
|
|
2680
|
+
(environment?.window ?? window).requestAnimationFrame(() => resolve());
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
function getDomAnchor(selection, configuredAttribute = "data-qa-id", environment) {
|
|
2684
|
+
const x = selection.left + selection.width / 2;
|
|
2685
|
+
const y = selection.top + selection.height / 2;
|
|
2686
|
+
return getDomAnchorFromPoint({ x, y }, configuredAttribute, environment);
|
|
2687
|
+
}
|
|
2688
|
+
function getDomAnchorFromPoint(point, configuredAttribute = "data-qa-id", environment) {
|
|
2689
|
+
const target = environment.document.elementFromPoint(point.x, point.y);
|
|
2690
|
+
if (!target) return void 0;
|
|
2691
|
+
const candidates = createAnchorCandidates(target, configuredAttribute);
|
|
2692
|
+
const primary = candidates[0];
|
|
2693
|
+
if (!primary) return void 0;
|
|
2694
|
+
return {
|
|
2695
|
+
...primary,
|
|
2696
|
+
candidates,
|
|
2697
|
+
htmlSnippet: getElementHtmlSnippet(
|
|
2698
|
+
getAnchorSourceElement(target, primary, configuredAttribute) ?? target
|
|
2699
|
+
),
|
|
2700
|
+
source: getDomSourceHint(target)
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
function getElementViewportSelection(anchor, environment) {
|
|
2704
|
+
const element = getAnchorElement(anchor, environment);
|
|
2705
|
+
if (!element) return void 0;
|
|
2706
|
+
const rect = element.getBoundingClientRect();
|
|
2707
|
+
if (rect.width <= 0 || rect.height <= 0) return void 0;
|
|
2708
|
+
return {
|
|
2709
|
+
left: rect.left,
|
|
2710
|
+
top: rect.top,
|
|
2711
|
+
width: rect.width,
|
|
2712
|
+
height: rect.height
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
function createAnchorCandidates(target, configuredAttribute) {
|
|
2716
|
+
const candidates = [];
|
|
2717
|
+
const anchoredByAttribute = target.closest(`[${configuredAttribute}]`);
|
|
2718
|
+
if (anchoredByAttribute) {
|
|
2719
|
+
const value = anchoredByAttribute.getAttribute(configuredAttribute);
|
|
2720
|
+
if (value) {
|
|
2721
|
+
candidates.push({
|
|
2722
|
+
selector: `[${configuredAttribute}="${cssEscape(value)}"]`,
|
|
2723
|
+
strategy: "configured-attribute",
|
|
2724
|
+
confidence: 0.98,
|
|
2725
|
+
textFingerprint: getTextFingerprint(anchoredByAttribute)
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
if (isMeaningfulId(target.id)) {
|
|
2730
|
+
candidates.push({
|
|
2731
|
+
selector: `#${cssEscape(target.id)}`,
|
|
2732
|
+
strategy: "id",
|
|
2733
|
+
confidence: 0.94,
|
|
2734
|
+
textFingerprint: getTextFingerprint(target)
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
const targetClassName = getMeaningfulClassName(target);
|
|
2738
|
+
if (targetClassName) {
|
|
2739
|
+
candidates.push({
|
|
2740
|
+
selector: `${target.tagName.toLowerCase()}.${cssEscape(targetClassName)}`,
|
|
2741
|
+
strategy: "class",
|
|
2742
|
+
confidence: 0.82,
|
|
2743
|
+
textFingerprint: getTextFingerprint(target)
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
candidates.push({
|
|
2747
|
+
selector: getDomPath(target),
|
|
2748
|
+
strategy: "dom-path",
|
|
2749
|
+
confidence: 0.9,
|
|
2750
|
+
textFingerprint: getTextFingerprint(target)
|
|
2751
|
+
});
|
|
2752
|
+
const parent = target.parentElement;
|
|
2753
|
+
const anchoredById = parent ? findClosest(parent, (element) => isMeaningfulId(element.id)) : void 0;
|
|
2754
|
+
if (anchoredById?.id) {
|
|
2755
|
+
candidates.push({
|
|
2756
|
+
selector: `#${cssEscape(anchoredById.id)}`,
|
|
2757
|
+
strategy: "id",
|
|
2758
|
+
confidence: 0.72,
|
|
2759
|
+
textFingerprint: getTextFingerprint(anchoredById)
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
const anchoredByClass = parent ? findClosest(parent, (element) => Boolean(getMeaningfulClassName(element))) : void 0;
|
|
2763
|
+
const className = anchoredByClass ? getMeaningfulClassName(anchoredByClass) : void 0;
|
|
2764
|
+
if (anchoredByClass && className) {
|
|
2765
|
+
candidates.push({
|
|
2766
|
+
selector: `${anchoredByClass.tagName.toLowerCase()}.${cssEscape(
|
|
2767
|
+
className
|
|
2768
|
+
)}`,
|
|
2769
|
+
strategy: "class",
|
|
2770
|
+
confidence: 0.58,
|
|
2771
|
+
textFingerprint: getTextFingerprint(anchoredByClass)
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
return dedupeAnchorCandidates(candidates);
|
|
2775
|
+
}
|
|
2776
|
+
function getAnchorSourceElement(target, candidate, configuredAttribute) {
|
|
2777
|
+
if (candidate.strategy === "configured-attribute") {
|
|
2778
|
+
return target.closest(`[${configuredAttribute}]`);
|
|
2779
|
+
}
|
|
2780
|
+
if (candidate.strategy === "dom-path") return target;
|
|
2781
|
+
try {
|
|
2782
|
+
return target.closest(candidate.selector);
|
|
2783
|
+
} catch {
|
|
2784
|
+
return target;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
function getElementHtmlSnippet(element, maxLength = 1e3) {
|
|
2788
|
+
const html = decodeHtmlEntities(element.outerHTML.replace(/\s+/g, " ").trim());
|
|
2789
|
+
if (html.length <= maxLength) return html;
|
|
2790
|
+
return `${html.slice(0, maxLength - 3)}...`;
|
|
2791
|
+
}
|
|
2792
|
+
function decodeHtmlEntities(value) {
|
|
2793
|
+
return value.replace(
|
|
2794
|
+
/&(#\d+|#x[\da-f]+|lt|gt|quot|apos|amp);/gi,
|
|
2795
|
+
(match, entity) => {
|
|
2796
|
+
const normalized = entity.toLowerCase();
|
|
2797
|
+
if (normalized === "lt") return "<";
|
|
2798
|
+
if (normalized === "gt") return ">";
|
|
2799
|
+
if (normalized === "quot") return '"';
|
|
2800
|
+
if (normalized === "apos") return "'";
|
|
2801
|
+
if (normalized === "amp") return "&";
|
|
2802
|
+
const codePoint = normalized.startsWith("#x") ? Number.parseInt(normalized.slice(2), 16) : Number.parseInt(normalized.slice(1), 10);
|
|
2803
|
+
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
|
|
2804
|
+
}
|
|
2805
|
+
);
|
|
2806
|
+
}
|
|
2807
|
+
function getDomSourceHint(target) {
|
|
2808
|
+
const sourceElement = target.closest(
|
|
2809
|
+
"[data-file], [data-component], [data-section-index], [data-section-id]"
|
|
2810
|
+
);
|
|
2811
|
+
if (!sourceElement) return void 0;
|
|
2812
|
+
const dataset = sourceElement.dataset;
|
|
2813
|
+
const source = {
|
|
2814
|
+
component: dataset.component,
|
|
2815
|
+
file: dataset.file,
|
|
2816
|
+
sectionId: dataset.sectionId,
|
|
2817
|
+
sectionIndex: dataset.sectionIndex
|
|
2818
|
+
};
|
|
2819
|
+
return Object.values(source).some(Boolean) ? source : void 0;
|
|
2820
|
+
}
|
|
2821
|
+
function getRelativeSelection(selection, anchor, environment) {
|
|
2822
|
+
const element = getAnchorElement(anchor, environment);
|
|
2823
|
+
if (!element) return void 0;
|
|
2824
|
+
const rect = element.getBoundingClientRect();
|
|
2825
|
+
if (rect.width === 0 || rect.height === 0) return void 0;
|
|
2826
|
+
return {
|
|
2827
|
+
x: roundRatio((selection.left - rect.left) / rect.width),
|
|
2828
|
+
y: roundRatio((selection.top - rect.top) / rect.height),
|
|
2829
|
+
width: roundRatio(selection.width / rect.width),
|
|
2830
|
+
height: roundRatio(selection.height / rect.height)
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
function getRelativePoint(point, anchor, environment) {
|
|
2834
|
+
const element = getAnchorElement(anchor, environment);
|
|
2835
|
+
if (!element) return void 0;
|
|
2836
|
+
const rect = element.getBoundingClientRect();
|
|
2837
|
+
if (rect.width === 0 || rect.height === 0) return void 0;
|
|
2838
|
+
return {
|
|
2839
|
+
x: roundRatio((point.x - rect.left) / rect.width),
|
|
2840
|
+
y: roundRatio((point.y - rect.top) / rect.height)
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
function getBoundMarkerPoint(item, environment) {
|
|
2844
|
+
const marker = getItemMarker(item);
|
|
2845
|
+
if (!marker) return void 0;
|
|
2846
|
+
if (item.anchor && marker.relative) {
|
|
2847
|
+
const resolved = resolveAnchorElement(item.anchor, environment);
|
|
2848
|
+
const element = resolved?.element;
|
|
2849
|
+
if (element) {
|
|
2850
|
+
const rect = element.getBoundingClientRect();
|
|
2851
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
2852
|
+
return {
|
|
2853
|
+
viewport: roundPoint({
|
|
2854
|
+
x: rect.left + rect.width * marker.relative.x,
|
|
2855
|
+
y: rect.top + rect.height * marker.relative.y
|
|
2856
|
+
}),
|
|
2857
|
+
isBound: true,
|
|
2858
|
+
confidence: resolved.confidence,
|
|
2859
|
+
selector: resolved.candidate.selector
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
const sourceScroll = item.scroll ?? { x: 0, y: 0 };
|
|
2865
|
+
return {
|
|
2866
|
+
viewport: roundPoint({
|
|
2867
|
+
x: marker.viewport.x + sourceScroll.x - environment.window.scrollX,
|
|
2868
|
+
y: marker.viewport.y + sourceScroll.y - environment.window.scrollY
|
|
2869
|
+
}),
|
|
2870
|
+
isBound: false,
|
|
2871
|
+
confidence: 0
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
function getBoundSelection(item, environment) {
|
|
2875
|
+
const selection = getItemSelection(item);
|
|
2876
|
+
if (!selection?.viewport) return void 0;
|
|
2877
|
+
if (item.anchor && selection.relative) {
|
|
2878
|
+
const resolved = resolveAnchorElement(item.anchor, environment);
|
|
2879
|
+
const element = resolved?.element;
|
|
2880
|
+
if (element) {
|
|
2881
|
+
const rect = element.getBoundingClientRect();
|
|
2882
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
2883
|
+
return {
|
|
2884
|
+
viewport: {
|
|
2885
|
+
left: rect.left + rect.width * selection.relative.x,
|
|
2886
|
+
top: rect.top + rect.height * selection.relative.y,
|
|
2887
|
+
width: rect.width * selection.relative.width,
|
|
2888
|
+
height: rect.height * selection.relative.height
|
|
2889
|
+
},
|
|
2890
|
+
isBound: true,
|
|
2891
|
+
confidence: resolved.confidence,
|
|
2892
|
+
selector: resolved.candidate.selector
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
const sourceScroll = item.scroll ?? { x: 0, y: 0 };
|
|
2898
|
+
const viewportSelection = toViewportSelection(selection.viewport);
|
|
2899
|
+
return {
|
|
2900
|
+
viewport: {
|
|
2901
|
+
left: viewportSelection.left + sourceScroll.x - environment.window.scrollX,
|
|
2902
|
+
top: viewportSelection.top + sourceScroll.y - environment.window.scrollY,
|
|
2903
|
+
width: viewportSelection.width,
|
|
2904
|
+
height: viewportSelection.height
|
|
2905
|
+
},
|
|
2906
|
+
isBound: false,
|
|
2907
|
+
confidence: 0
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
function getItemHighlightSelection(item, environment) {
|
|
2911
|
+
if (item.kind === "area") {
|
|
2912
|
+
return getVisibleHighlightSelection(
|
|
2913
|
+
[
|
|
2914
|
+
getBoundSelection(item, environment),
|
|
2915
|
+
getAnchorHighlightSelection(item, environment),
|
|
2916
|
+
getPointHighlightSelection(item, environment)
|
|
2917
|
+
],
|
|
2918
|
+
environment
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
if (isDomReviewItem(item)) {
|
|
2922
|
+
return getVisibleHighlightSelection(
|
|
2923
|
+
[
|
|
2924
|
+
getAnchorHighlightSelection(item, environment),
|
|
2925
|
+
getBoundSelection(item, environment),
|
|
2926
|
+
getPointHighlightSelection(item, environment)
|
|
2927
|
+
],
|
|
2928
|
+
environment
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
return getVisibleHighlightSelection(
|
|
2932
|
+
[
|
|
2933
|
+
getAnchorHighlightSelection(item, environment),
|
|
2934
|
+
getBoundSelection(item, environment),
|
|
2935
|
+
getPointHighlightSelection(item, environment)
|
|
2936
|
+
],
|
|
2937
|
+
environment
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
function getAnchorHighlightSelection(item, environment) {
|
|
2941
|
+
if (!item.anchor) return void 0;
|
|
2942
|
+
const viewport = getElementViewportSelection(item.anchor, environment);
|
|
2943
|
+
if (!viewport) return void 0;
|
|
2944
|
+
return {
|
|
2945
|
+
viewport,
|
|
2946
|
+
isBound: true
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
function getPointHighlightSelection(item, environment) {
|
|
2950
|
+
const point = getBoundMarkerPoint(item, environment);
|
|
2951
|
+
if (!point) return void 0;
|
|
2952
|
+
const size = 16;
|
|
2953
|
+
return {
|
|
2954
|
+
viewport: {
|
|
2955
|
+
left: point.viewport.x - size / 2,
|
|
2956
|
+
top: point.viewport.y - size / 2,
|
|
2957
|
+
width: size,
|
|
2958
|
+
height: size
|
|
2959
|
+
},
|
|
2960
|
+
isBound: point.isBound
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
function getVisibleHighlightSelection(candidates, environment) {
|
|
2964
|
+
return candidates.find(
|
|
2965
|
+
(candidate) => Boolean(candidate && isSelectionInViewport(candidate.viewport, environment))
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
function getReviewItemHighlightMode(item) {
|
|
2969
|
+
if (isDomReviewItem(item)) return "dom";
|
|
2970
|
+
if (item.kind === "area") return "area";
|
|
2971
|
+
return "note";
|
|
2972
|
+
}
|
|
2973
|
+
function isDomReviewItem(item) {
|
|
2974
|
+
return item.scope === "dom" || item.kind === "note" && Boolean(item.anchor && getItemSelection(item));
|
|
2975
|
+
}
|
|
2976
|
+
function getItemMarker(item) {
|
|
2977
|
+
if (item.marker) return item.marker;
|
|
2978
|
+
const selection = getItemSelection(item);
|
|
2979
|
+
if (!selection?.viewport) return void 0;
|
|
2980
|
+
return {
|
|
2981
|
+
viewport: roundPoint(getSelectionCenter(selection.viewport)),
|
|
2982
|
+
relative: selection.relative ? roundPoint(getSelectionCenter(selection.relative)) : void 0
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
function getItemSelection(item) {
|
|
2986
|
+
const value = item.selection;
|
|
2987
|
+
if (!value) return void 0;
|
|
2988
|
+
if ("viewport" in value && isRelativeSelection(value.viewport)) {
|
|
2989
|
+
return value;
|
|
2990
|
+
}
|
|
2991
|
+
if (isRelativeSelection(value)) {
|
|
2992
|
+
return {
|
|
2993
|
+
viewport: value
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
return void 0;
|
|
2997
|
+
}
|
|
2998
|
+
function shouldShowMarkerForScope(scope, currentScope) {
|
|
2999
|
+
return scope === currentScope;
|
|
3000
|
+
}
|
|
3001
|
+
function createSelectionCenterMarker(selection, anchor, environment) {
|
|
3002
|
+
const centerPoint = getSelectionCenter(selection);
|
|
3003
|
+
return {
|
|
3004
|
+
viewport: roundPoint(centerPoint),
|
|
3005
|
+
relative: anchor ? getRelativePoint(centerPoint, anchor, environment) : void 0
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
function getAnchorElement(anchor, environment) {
|
|
3009
|
+
return typeof anchor === "string" ? queryAnchorElement(anchor, environment) : resolveAnchorElement(anchor, environment)?.element;
|
|
3010
|
+
}
|
|
3011
|
+
function resolveAnchorElement(anchor, environment) {
|
|
3012
|
+
const matches = getAnchorCandidates(anchor).flatMap((candidate) => {
|
|
3013
|
+
const match = queryBestAnchorCandidate(
|
|
3014
|
+
candidate,
|
|
3015
|
+
candidate.textFingerprint ?? anchor.textFingerprint,
|
|
3016
|
+
environment
|
|
3017
|
+
);
|
|
3018
|
+
if (!match) return [];
|
|
3019
|
+
const confidence = roundRatio(
|
|
3020
|
+
(candidate.confidence ?? 0.5) * match.score
|
|
3021
|
+
);
|
|
3022
|
+
return [{
|
|
3023
|
+
element: match.element,
|
|
3024
|
+
candidate,
|
|
3025
|
+
confidence
|
|
3026
|
+
}];
|
|
3027
|
+
});
|
|
3028
|
+
return matches.sort((a, b) => b.confidence - a.confidence)[0];
|
|
3029
|
+
}
|
|
3030
|
+
function getAnchorCandidates(anchor) {
|
|
3031
|
+
return dedupeAnchorCandidates([
|
|
3032
|
+
anchor,
|
|
3033
|
+
...anchor.candidates ?? []
|
|
3034
|
+
]);
|
|
3035
|
+
}
|
|
3036
|
+
function dedupeAnchorCandidates(candidates) {
|
|
3037
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3038
|
+
return candidates.filter((candidate) => {
|
|
3039
|
+
const key = `${candidate.strategy}:${candidate.selector}`;
|
|
3040
|
+
if (seen.has(key)) return false;
|
|
3041
|
+
seen.add(key);
|
|
3042
|
+
return true;
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
function queryBestAnchorCandidate(candidate, textFingerprint, environment) {
|
|
3046
|
+
const elements = queryAnchorElements(candidate.selector, environment);
|
|
3047
|
+
if (elements.length === 0) return void 0;
|
|
3048
|
+
if (!textFingerprint) {
|
|
3049
|
+
return {
|
|
3050
|
+
element: elements[0],
|
|
3051
|
+
score: 1
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
return elements.map((element) => ({
|
|
3055
|
+
element,
|
|
3056
|
+
score: getTextFingerprintScore(
|
|
3057
|
+
textFingerprint,
|
|
3058
|
+
getTextFingerprint(element)
|
|
3059
|
+
)
|
|
3060
|
+
})).sort((a, b) => b.score - a.score)[0];
|
|
3061
|
+
}
|
|
3062
|
+
function queryAnchorElement(selector, environment) {
|
|
3063
|
+
return queryAnchorElements(selector, environment)[0];
|
|
3064
|
+
}
|
|
3065
|
+
function queryAnchorElements(selector, environment) {
|
|
3066
|
+
try {
|
|
3067
|
+
return Array.from(environment.document.querySelectorAll(selector));
|
|
3068
|
+
} catch {
|
|
3069
|
+
return [];
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
function getPointSelection(point) {
|
|
3073
|
+
return {
|
|
3074
|
+
left: point.x,
|
|
3075
|
+
top: point.y,
|
|
3076
|
+
width: 1,
|
|
3077
|
+
height: 1
|
|
3078
|
+
};
|
|
3079
|
+
}
|
|
3080
|
+
function toViewportSelection(selection) {
|
|
3081
|
+
return {
|
|
3082
|
+
left: selection.x,
|
|
3083
|
+
top: selection.y,
|
|
3084
|
+
width: selection.width,
|
|
3085
|
+
height: selection.height
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
function toPublicSelection(selection) {
|
|
3089
|
+
return {
|
|
3090
|
+
x: Math.round(selection.left),
|
|
3091
|
+
y: Math.round(selection.top),
|
|
3092
|
+
width: Math.round(selection.width),
|
|
3093
|
+
height: Math.round(selection.height)
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
function getSelectionCenter(selection) {
|
|
3097
|
+
if ("left" in selection) {
|
|
3098
|
+
return {
|
|
3099
|
+
x: selection.left + selection.width / 2,
|
|
3100
|
+
y: selection.top + selection.height / 2
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
return {
|
|
3104
|
+
x: selection.x + selection.width / 2,
|
|
3105
|
+
y: selection.y + selection.height / 2
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
function isRelativeSelection(value) {
|
|
3109
|
+
if (!value || typeof value !== "object") return false;
|
|
3110
|
+
const selection = value;
|
|
3111
|
+
return typeof selection.x === "number" && typeof selection.y === "number" && typeof selection.width === "number" && typeof selection.height === "number";
|
|
3112
|
+
}
|
|
3113
|
+
function findClosest(start, predicate) {
|
|
3114
|
+
let element = start;
|
|
3115
|
+
const root = start.ownerDocument.documentElement;
|
|
3116
|
+
while (element && element !== root) {
|
|
3117
|
+
if (predicate(element)) return element;
|
|
3118
|
+
element = element.parentElement;
|
|
3119
|
+
}
|
|
3120
|
+
return void 0;
|
|
3121
|
+
}
|
|
3122
|
+
function getDomPath(element) {
|
|
3123
|
+
const parts = [];
|
|
3124
|
+
let current = element;
|
|
3125
|
+
const ownerDocument = element.ownerDocument;
|
|
3126
|
+
while (current && current !== ownerDocument.body && current !== ownerDocument.documentElement) {
|
|
3127
|
+
const parent = current.parentElement;
|
|
3128
|
+
const tag = current.tagName.toLowerCase();
|
|
3129
|
+
if (!parent) {
|
|
3130
|
+
parts.unshift(tag);
|
|
3131
|
+
break;
|
|
3132
|
+
}
|
|
3133
|
+
const currentTagName = current.tagName;
|
|
3134
|
+
const siblings = Array.from(parent.children).filter(
|
|
3135
|
+
(child) => child.tagName === currentTagName
|
|
3136
|
+
);
|
|
3137
|
+
const index = siblings.indexOf(current) + 1;
|
|
3138
|
+
parts.unshift(`${tag}:nth-of-type(${index})`);
|
|
3139
|
+
current = parent;
|
|
3140
|
+
}
|
|
3141
|
+
return `body > ${parts.join(" > ")}`;
|
|
3142
|
+
}
|
|
3143
|
+
function getTextFingerprint(element) {
|
|
3144
|
+
const text = element.textContent?.replace(/\s+/g, " ").trim();
|
|
3145
|
+
return text ? text.slice(0, 120) : void 0;
|
|
3146
|
+
}
|
|
3147
|
+
function getTextFingerprintScore(expected, actual) {
|
|
3148
|
+
if (!expected) return 1;
|
|
3149
|
+
if (!actual) return 0.5;
|
|
3150
|
+
if (expected === actual) return 1;
|
|
3151
|
+
if (actual.includes(expected) || expected.includes(actual)) return 0.82;
|
|
3152
|
+
const expectedTokens = getFingerprintTokens(expected);
|
|
3153
|
+
const actualTokens = new Set(getFingerprintTokens(actual));
|
|
3154
|
+
if (expectedTokens.length === 0 || actualTokens.size === 0) return 0.5;
|
|
3155
|
+
const matches = expectedTokens.filter((token) => actualTokens.has(token));
|
|
3156
|
+
return clamp(matches.length / expectedTokens.length, 0.25, 0.76);
|
|
3157
|
+
}
|
|
3158
|
+
function getFingerprintTokens(value) {
|
|
3159
|
+
return value.toLowerCase().split(/[\s/|,.:;()[\]{}"'`~!?<>]+/).map((token) => token.trim()).filter((token) => token.length > 1);
|
|
3160
|
+
}
|
|
3161
|
+
function isMeaningfulId(value) {
|
|
3162
|
+
const normalized = value.trim().toLowerCase();
|
|
3163
|
+
if (normalized.length <= 1) return false;
|
|
3164
|
+
return ![
|
|
3165
|
+
"app",
|
|
3166
|
+
"main",
|
|
3167
|
+
"page",
|
|
3168
|
+
"root",
|
|
3169
|
+
"__next",
|
|
3170
|
+
"__nuxt"
|
|
3171
|
+
].includes(normalized);
|
|
3172
|
+
}
|
|
3173
|
+
function getMeaningfulClassName(element) {
|
|
3174
|
+
return Array.from(element.classList).find((name) => isMeaningfulClass(name));
|
|
3175
|
+
}
|
|
3176
|
+
function isMeaningfulClass(value) {
|
|
3177
|
+
const normalized = value.trim();
|
|
3178
|
+
if ([
|
|
3179
|
+
"absolute",
|
|
3180
|
+
"block",
|
|
3181
|
+
"contents",
|
|
3182
|
+
"fixed",
|
|
3183
|
+
"flex",
|
|
3184
|
+
"grid",
|
|
3185
|
+
"hidden",
|
|
3186
|
+
"relative",
|
|
3187
|
+
"sticky"
|
|
3188
|
+
].includes(normalized)) {
|
|
3189
|
+
return false;
|
|
3190
|
+
}
|
|
3191
|
+
return normalized.length > 2 && !normalized.includes(":") && !/^(aspect|basis|bg|border|bottom|col|content|delay|duration|ease|font|from|gap|grow|h|inset|items|justify|leading|left|m|max-h|max-w|mb|ml|mr|mt|mx|my|min-h|min-w|object|opacity|order|origin|overflow|p|pb|pl|place|pointer|pr|pt|px|py|right|rotate|rounded|row|scale|self|shadow|shrink|text|to|top|tracking|transition|translate|via|w|z)-/.test(
|
|
3192
|
+
normalized
|
|
3193
|
+
) && !normalized.startsWith("mq-");
|
|
3194
|
+
}
|
|
3195
|
+
function isHotkey(event, hotkey) {
|
|
3196
|
+
const parts = hotkey.split("+").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
3197
|
+
const key = parts.find(
|
|
3198
|
+
(part) => !["shift", "ctrl", "control", "alt", "meta", "cmd"].includes(part)
|
|
3199
|
+
);
|
|
3200
|
+
if (!key) return false;
|
|
3201
|
+
if (parts.includes("shift") !== event.shiftKey) return false;
|
|
3202
|
+
if ((parts.includes("ctrl") || parts.includes("control")) !== event.ctrlKey) {
|
|
3203
|
+
return false;
|
|
3204
|
+
}
|
|
3205
|
+
if (parts.includes("alt") !== event.altKey) return false;
|
|
3206
|
+
if ((parts.includes("meta") || parts.includes("cmd")) !== event.metaKey) {
|
|
3207
|
+
return false;
|
|
3208
|
+
}
|
|
3209
|
+
return isHotkeyKey(event, key);
|
|
3210
|
+
}
|
|
3211
|
+
function isHotkeyKey(event, key) {
|
|
3212
|
+
const normalizedKey = key.toLowerCase();
|
|
3213
|
+
if (event.key.toLowerCase() === normalizedKey) return true;
|
|
3214
|
+
if (getHotkeyCode(normalizedKey) === event.code) return true;
|
|
3215
|
+
const aliases = {
|
|
3216
|
+
q: ["\u3142", "\u3143"]
|
|
3217
|
+
};
|
|
3218
|
+
return aliases[normalizedKey]?.includes(event.key) ?? false;
|
|
3219
|
+
}
|
|
3220
|
+
function getHotkeyCode(key) {
|
|
3221
|
+
if (/^[a-z]$/.test(key)) return `Key${key.toUpperCase()}`;
|
|
3222
|
+
if (/^[0-9]$/.test(key)) return `Digit${key}`;
|
|
3223
|
+
return void 0;
|
|
3224
|
+
}
|
|
3225
|
+
function getPageUrl(environment) {
|
|
3226
|
+
const location = environment?.window.location ?? window.location;
|
|
3227
|
+
const search = getPublicSearch(location);
|
|
3228
|
+
return `${location.origin}${location.pathname}${search}${location.hash}`;
|
|
3229
|
+
}
|
|
3230
|
+
function getOriginalUrl(environment) {
|
|
3231
|
+
const location = environment?.window.location ?? window.location;
|
|
3232
|
+
const search = getPublicSearch(location);
|
|
3233
|
+
return `${location.origin}${location.pathname}${search}${location.hash}`;
|
|
3234
|
+
}
|
|
3235
|
+
function getRouteKey(environment) {
|
|
3236
|
+
const location = environment?.window.location ?? window.location;
|
|
3237
|
+
return normalizeRoutePath(location.pathname);
|
|
3238
|
+
}
|
|
3239
|
+
function getPublicSearch(location) {
|
|
3240
|
+
const params = new URLSearchParams(location.search);
|
|
3241
|
+
INTERNAL_QUERY_PARAMS.forEach((key) => params.delete(key));
|
|
3242
|
+
const value = params.toString();
|
|
3243
|
+
return value ? `?${value}` : "";
|
|
3244
|
+
}
|
|
3245
|
+
function createId() {
|
|
3246
|
+
if ("randomUUID" in crypto) return crypto.randomUUID();
|
|
3247
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
3248
|
+
}
|
|
3249
|
+
function formatDate(value) {
|
|
3250
|
+
const date = new Date(value);
|
|
3251
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
3252
|
+
return date.toLocaleString(void 0, {
|
|
3253
|
+
month: "2-digit",
|
|
3254
|
+
day: "2-digit",
|
|
3255
|
+
hour: "2-digit",
|
|
3256
|
+
minute: "2-digit"
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
3259
|
+
function formatAreaDraftMeta(draft) {
|
|
3260
|
+
const parts = [`viewport ${formatSize(draft.viewport)}`];
|
|
3261
|
+
if (draft.selection) {
|
|
3262
|
+
parts.push(`rect ${formatSelection(draft.selection.viewport)}`);
|
|
3263
|
+
}
|
|
3264
|
+
if (draft.marker) {
|
|
3265
|
+
parts.push(`point ${formatPoint(draft.marker.viewport)}`);
|
|
3266
|
+
}
|
|
3267
|
+
return parts.join(" / ");
|
|
3268
|
+
}
|
|
3269
|
+
function formatNoteDraftMeta(draft) {
|
|
3270
|
+
const parts = [
|
|
3271
|
+
`viewport ${formatSize(draft.viewport)}`,
|
|
3272
|
+
`point ${formatPoint(draft.marker.viewport)}`
|
|
3273
|
+
];
|
|
3274
|
+
if (draft.anchor) {
|
|
3275
|
+
parts.push(formatAnchorMeta(draft.anchor));
|
|
3276
|
+
}
|
|
3277
|
+
return parts.join(" / ");
|
|
3278
|
+
}
|
|
3279
|
+
function formatItemMeta(item) {
|
|
3280
|
+
const parts = [formatDate(item.createdAt)];
|
|
3281
|
+
const marker = getItemMarker(item);
|
|
3282
|
+
const selection = getItemSelection(item);
|
|
3283
|
+
if (item.viewport) {
|
|
3284
|
+
parts.push(`viewport ${formatSize(item.viewport)}`);
|
|
3285
|
+
}
|
|
3286
|
+
if (marker) {
|
|
3287
|
+
parts.push(`point ${formatPoint(marker.viewport)}`);
|
|
3288
|
+
}
|
|
3289
|
+
if (selection) {
|
|
3290
|
+
parts.push(`rect ${formatSelection(selection.viewport)}`);
|
|
3291
|
+
}
|
|
3292
|
+
if (item.anchor) {
|
|
3293
|
+
parts.push(formatAnchorMeta(item.anchor));
|
|
3294
|
+
}
|
|
3295
|
+
return parts.join(" / ");
|
|
3296
|
+
}
|
|
3297
|
+
function formatSize(size) {
|
|
3298
|
+
return `${Math.round(size.width)}x${Math.round(size.height)}`;
|
|
3299
|
+
}
|
|
3300
|
+
function formatPoint(point) {
|
|
3301
|
+
return `${Math.round(point.x)},${Math.round(point.y)}`;
|
|
3302
|
+
}
|
|
3303
|
+
function formatSelection(selection) {
|
|
3304
|
+
return [
|
|
3305
|
+
Math.round(selection.x),
|
|
3306
|
+
Math.round(selection.y),
|
|
3307
|
+
Math.round(selection.width),
|
|
3308
|
+
Math.round(selection.height)
|
|
3309
|
+
].join(",");
|
|
3310
|
+
}
|
|
3311
|
+
function formatAnchorMeta(anchor) {
|
|
3312
|
+
const parts = [`dom ${anchor.strategy}`];
|
|
3313
|
+
if (typeof anchor.confidence === "number") {
|
|
3314
|
+
parts.push(`${Math.round(anchor.confidence * 100)}%`);
|
|
3315
|
+
}
|
|
3316
|
+
const candidates = getAnchorCandidates(anchor);
|
|
3317
|
+
if (candidates.length > 1) {
|
|
3318
|
+
parts.push(`${candidates.length} candidates`);
|
|
3319
|
+
}
|
|
3320
|
+
return parts.join(" ");
|
|
3321
|
+
}
|
|
3322
|
+
function getViewportSize(environment) {
|
|
3323
|
+
const targetWindow = environment?.window ?? window;
|
|
3324
|
+
return {
|
|
3325
|
+
width: targetWindow.innerWidth,
|
|
3326
|
+
height: targetWindow.innerHeight
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
function roundPoint(point) {
|
|
3330
|
+
return {
|
|
3331
|
+
x: Math.round(point.x),
|
|
3332
|
+
y: Math.round(point.y)
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
function runWithAutoScrollBehavior(targetDocument, callback) {
|
|
3336
|
+
const elements = [
|
|
3337
|
+
targetDocument.documentElement,
|
|
3338
|
+
targetDocument.body
|
|
3339
|
+
].filter((element) => Boolean(element));
|
|
3340
|
+
const previousValues = elements.map((element) => element.style.scrollBehavior);
|
|
3341
|
+
elements.forEach((element) => {
|
|
3342
|
+
element.style.scrollBehavior = "auto";
|
|
3343
|
+
});
|
|
3344
|
+
try {
|
|
3345
|
+
callback();
|
|
3346
|
+
} finally {
|
|
3347
|
+
elements.forEach((element, index) => {
|
|
3348
|
+
element.style.scrollBehavior = previousValues[index] ?? "";
|
|
3349
|
+
});
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
function setDocumentScrollInstantly(environment, position) {
|
|
3353
|
+
const scrollElement = environment.document.scrollingElement;
|
|
3354
|
+
if (scrollElement) {
|
|
3355
|
+
scrollElement.scrollLeft = Math.max(0, Math.round(position.x));
|
|
3356
|
+
scrollElement.scrollTop = Math.max(0, Math.round(position.y));
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
environment.window.scrollTo(
|
|
3360
|
+
Math.max(0, Math.round(position.x)),
|
|
3361
|
+
Math.max(0, Math.round(position.y))
|
|
3362
|
+
);
|
|
3363
|
+
}
|
|
3364
|
+
function isPointInViewport(point, environment) {
|
|
3365
|
+
const viewport = getViewportSize(environment);
|
|
3366
|
+
return point.x >= 0 && point.y >= 0 && point.x <= viewport.width && point.y <= viewport.height;
|
|
3367
|
+
}
|
|
3368
|
+
function isSelectionInViewport(selection, environment) {
|
|
3369
|
+
const viewport = getViewportSize(environment);
|
|
3370
|
+
return rectanglesIntersect(selection, {
|
|
3371
|
+
left: 0,
|
|
3372
|
+
top: 0,
|
|
3373
|
+
width: viewport.width,
|
|
3374
|
+
height: viewport.height
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
function clampPoint(point, environment) {
|
|
3378
|
+
const viewport = getViewportSize(environment);
|
|
3379
|
+
return {
|
|
3380
|
+
x: clamp(point.x, 0, viewport.width),
|
|
3381
|
+
y: clamp(point.y, 0, viewport.height)
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
function getPopoverPosition(point, environment, options) {
|
|
3385
|
+
const bounds = getPopoverBounds(environment);
|
|
3386
|
+
const margin = 12;
|
|
3387
|
+
const width = Math.min(
|
|
3388
|
+
options?.width ?? 320,
|
|
3389
|
+
Math.max(240, bounds.width - margin * 2)
|
|
3390
|
+
);
|
|
3391
|
+
const estimatedHeight = options?.estimatedHeight ?? 178;
|
|
3392
|
+
const offset = options?.offset ?? 12;
|
|
3393
|
+
return {
|
|
3394
|
+
left: clamp(
|
|
3395
|
+
point.x + offset,
|
|
3396
|
+
bounds.left + margin,
|
|
3397
|
+
bounds.left + bounds.width - width - margin
|
|
3398
|
+
),
|
|
3399
|
+
top: clamp(
|
|
3400
|
+
point.y + offset,
|
|
3401
|
+
bounds.top + margin,
|
|
3402
|
+
bounds.top + bounds.height - estimatedHeight - margin
|
|
3403
|
+
)
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
function getAreaPopoverPosition(selection, environment) {
|
|
3407
|
+
return getPopoverPosition(
|
|
3408
|
+
{
|
|
3409
|
+
x: selection.left + selection.width,
|
|
3410
|
+
y: selection.top
|
|
3411
|
+
},
|
|
3412
|
+
environment,
|
|
3413
|
+
{
|
|
3414
|
+
width: 360,
|
|
3415
|
+
estimatedHeight: 206
|
|
3416
|
+
}
|
|
3417
|
+
);
|
|
3418
|
+
}
|
|
3419
|
+
function getPopoverBounds(environment) {
|
|
3420
|
+
if (!environment) {
|
|
3421
|
+
return {
|
|
3422
|
+
left: 0,
|
|
3423
|
+
top: 0,
|
|
3424
|
+
width: window.innerWidth,
|
|
3425
|
+
height: window.innerHeight
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
return environment.overlayRect;
|
|
3429
|
+
}
|
|
3430
|
+
function toHostPoint(point, environment) {
|
|
3431
|
+
if (!environment) return point;
|
|
3432
|
+
return {
|
|
3433
|
+
x: point.x + environment.viewportRect.left,
|
|
3434
|
+
y: point.y + environment.viewportRect.top
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
function toHostSelection(selection, environment) {
|
|
3438
|
+
return {
|
|
3439
|
+
left: selection.left + environment.viewportRect.left,
|
|
3440
|
+
top: selection.top + environment.viewportRect.top,
|
|
3441
|
+
width: selection.width,
|
|
3442
|
+
height: selection.height
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
function toTargetPoint(point, environment) {
|
|
3446
|
+
if (!environment) return point;
|
|
3447
|
+
return {
|
|
3448
|
+
x: point.x - environment.viewportRect.left,
|
|
3449
|
+
y: point.y - environment.viewportRect.top
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
function toTargetPointFromHostEvent(event, environment) {
|
|
3453
|
+
return toTargetPoint(
|
|
3454
|
+
{
|
|
3455
|
+
x: event.clientX,
|
|
3456
|
+
y: event.clientY
|
|
3457
|
+
},
|
|
3458
|
+
environment
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
function placeLayerOverTarget(layer, environment) {
|
|
3462
|
+
layer.style.left = `${environment.viewportRect.left}px`;
|
|
3463
|
+
layer.style.top = `${environment.viewportRect.top}px`;
|
|
3464
|
+
layer.style.width = `${environment.viewportRect.width}px`;
|
|
3465
|
+
layer.style.height = `${environment.viewportRect.height}px`;
|
|
3466
|
+
layer.style.right = "auto";
|
|
3467
|
+
layer.style.bottom = "auto";
|
|
3468
|
+
}
|
|
3469
|
+
function clamp(value, min, max) {
|
|
3470
|
+
return Math.min(Math.max(value, min), Math.max(min, max));
|
|
3471
|
+
}
|
|
3472
|
+
function roundRatio(value) {
|
|
3473
|
+
return Math.round(value * 1e4) / 1e4;
|
|
3474
|
+
}
|
|
3475
|
+
function cssEscape(value) {
|
|
3476
|
+
if ("CSS" in window && typeof window.CSS.escape === "function") {
|
|
3477
|
+
return window.CSS.escape(value);
|
|
3478
|
+
}
|
|
3479
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
3480
|
+
}
|
|
3481
|
+
function createNoopController() {
|
|
3482
|
+
return {
|
|
3483
|
+
open() {
|
|
3484
|
+
},
|
|
3485
|
+
close() {
|
|
3486
|
+
},
|
|
3487
|
+
toggle() {
|
|
3488
|
+
},
|
|
3489
|
+
setMode() {
|
|
3490
|
+
},
|
|
3491
|
+
getMode() {
|
|
3492
|
+
return "idle";
|
|
3493
|
+
},
|
|
3494
|
+
highlightItem() {
|
|
3495
|
+
},
|
|
3496
|
+
setHiddenItemIds() {
|
|
3497
|
+
},
|
|
3498
|
+
async reload() {
|
|
3499
|
+
return [];
|
|
3500
|
+
},
|
|
3501
|
+
getItems() {
|
|
3502
|
+
return [];
|
|
3503
|
+
},
|
|
3504
|
+
destroy() {
|
|
3505
|
+
}
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3509
|
+
0 && (module.exports = {
|
|
3510
|
+
DEFAULT_REVIEW_VIEWPORTS,
|
|
3511
|
+
DF_SHEET_REVIEW_SOURCE,
|
|
3512
|
+
REVIEW_WORKFLOW_STATUS_OPTIONS,
|
|
3513
|
+
createWebReviewKit,
|
|
3514
|
+
dfSheetAdapter,
|
|
3515
|
+
findReviewViewportPreset,
|
|
3516
|
+
getNumberedReviewItems,
|
|
3517
|
+
getReviewItemScope,
|
|
3518
|
+
getReviewItemScopeLabel,
|
|
3519
|
+
getReviewViewportScope,
|
|
3520
|
+
localAdapter,
|
|
3521
|
+
normalizeReviewItemStatus,
|
|
3522
|
+
supabaseAdapter
|
|
3523
|
+
});
|
|
3524
|
+
//# sourceMappingURL=index.cjs.map
|