@siteping/widget 0.8.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/dist/index.js ADDED
@@ -0,0 +1,3689 @@
1
+ // ../../node_modules/.bun/@medv+finder@3.2.0/node_modules/@medv/finder/finder.js
2
+ var config;
3
+ var rootDocument;
4
+ var start;
5
+ function finder(input, options) {
6
+ start = /* @__PURE__ */ new Date();
7
+ if (input.nodeType !== Node.ELEMENT_NODE) {
8
+ throw new Error(`Can't generate CSS selector for non-element node type.`);
9
+ }
10
+ if ("html" === input.tagName.toLowerCase()) {
11
+ return "html";
12
+ }
13
+ const defaults = {
14
+ root: document.body,
15
+ idName: (name) => true,
16
+ className: (name) => true,
17
+ tagName: (name) => true,
18
+ attr: (name, value) => false,
19
+ seedMinLength: 1,
20
+ optimizedMinLength: 2,
21
+ threshold: 1e3,
22
+ maxNumberOfTries: 1e4,
23
+ timeoutMs: void 0
24
+ };
25
+ config = { ...defaults, ...options };
26
+ rootDocument = findRootDocument(config.root, defaults);
27
+ let path = bottomUpSearch(input, "all", () => bottomUpSearch(input, "two", () => bottomUpSearch(input, "one", () => bottomUpSearch(input, "none"))));
28
+ if (path) {
29
+ const optimized = sort(optimize(path, input));
30
+ if (optimized.length > 0) {
31
+ path = optimized[0];
32
+ }
33
+ return selector(path);
34
+ } else {
35
+ throw new Error(`Selector was not found.`);
36
+ }
37
+ }
38
+ function findRootDocument(rootNode, defaults) {
39
+ if (rootNode.nodeType === Node.DOCUMENT_NODE) {
40
+ return rootNode;
41
+ }
42
+ if (rootNode === defaults.root) {
43
+ return rootNode.ownerDocument;
44
+ }
45
+ return rootNode;
46
+ }
47
+ function bottomUpSearch(input, limit, fallback) {
48
+ let path = null;
49
+ let stack = [];
50
+ let current = input;
51
+ let i = 0;
52
+ while (current) {
53
+ const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - start.getTime();
54
+ if (config.timeoutMs !== void 0 && elapsedTime > config.timeoutMs) {
55
+ throw new Error(`Timeout: Can't find a unique selector after ${elapsedTime}ms`);
56
+ }
57
+ let level = maybe(id(current)) || maybe(...attr(current)) || maybe(...classNames(current)) || maybe(tagName(current)) || [any()];
58
+ const nth = index(current);
59
+ if (limit == "all") {
60
+ if (nth) {
61
+ level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth)));
62
+ }
63
+ } else if (limit == "two") {
64
+ level = level.slice(0, 1);
65
+ if (nth) {
66
+ level = level.concat(level.filter(dispensableNth).map((node) => nthChild(node, nth)));
67
+ }
68
+ } else if (limit == "one") {
69
+ const [node] = level = level.slice(0, 1);
70
+ if (nth && dispensableNth(node)) {
71
+ level = [nthChild(node, nth)];
72
+ }
73
+ } else if (limit == "none") {
74
+ level = [any()];
75
+ if (nth) {
76
+ level = [nthChild(level[0], nth)];
77
+ }
78
+ }
79
+ for (let node of level) {
80
+ node.level = i;
81
+ }
82
+ stack.push(level);
83
+ if (stack.length >= config.seedMinLength) {
84
+ path = findUniquePath(stack, fallback);
85
+ if (path) {
86
+ break;
87
+ }
88
+ }
89
+ current = current.parentElement;
90
+ i++;
91
+ }
92
+ if (!path) {
93
+ path = findUniquePath(stack, fallback);
94
+ }
95
+ if (!path && fallback) {
96
+ return fallback();
97
+ }
98
+ return path;
99
+ }
100
+ function findUniquePath(stack, fallback) {
101
+ const paths = sort(combinations(stack));
102
+ if (paths.length > config.threshold) {
103
+ return fallback ? fallback() : null;
104
+ }
105
+ for (let candidate of paths) {
106
+ if (unique(candidate)) {
107
+ return candidate;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+ function selector(path) {
113
+ let node = path[0];
114
+ let query = node.name;
115
+ for (let i = 1; i < path.length; i++) {
116
+ const level = path[i].level || 0;
117
+ if (node.level === level - 1) {
118
+ query = `${path[i].name} > ${query}`;
119
+ } else {
120
+ query = `${path[i].name} ${query}`;
121
+ }
122
+ node = path[i];
123
+ }
124
+ return query;
125
+ }
126
+ function penalty(path) {
127
+ return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0);
128
+ }
129
+ function unique(path) {
130
+ const css = selector(path);
131
+ switch (rootDocument.querySelectorAll(css).length) {
132
+ case 0:
133
+ throw new Error(`Can't select any node with this selector: ${css}`);
134
+ case 1:
135
+ return true;
136
+ default:
137
+ return false;
138
+ }
139
+ }
140
+ function id(input) {
141
+ const elementId = input.getAttribute("id");
142
+ if (elementId && config.idName(elementId)) {
143
+ return {
144
+ name: "#" + CSS.escape(elementId),
145
+ penalty: 0
146
+ };
147
+ }
148
+ return null;
149
+ }
150
+ function attr(input) {
151
+ const attrs = Array.from(input.attributes).filter((attr2) => config.attr(attr2.name, attr2.value));
152
+ return attrs.map((attr2) => ({
153
+ name: `[${CSS.escape(attr2.name)}="${CSS.escape(attr2.value)}"]`,
154
+ penalty: 0.5
155
+ }));
156
+ }
157
+ function classNames(input) {
158
+ const names = Array.from(input.classList).filter(config.className);
159
+ return names.map((name) => ({
160
+ name: "." + CSS.escape(name),
161
+ penalty: 1
162
+ }));
163
+ }
164
+ function tagName(input) {
165
+ const name = input.tagName.toLowerCase();
166
+ if (config.tagName(name)) {
167
+ return {
168
+ name,
169
+ penalty: 2
170
+ };
171
+ }
172
+ return null;
173
+ }
174
+ function any() {
175
+ return {
176
+ name: "*",
177
+ penalty: 3
178
+ };
179
+ }
180
+ function index(input) {
181
+ const parent = input.parentNode;
182
+ if (!parent) {
183
+ return null;
184
+ }
185
+ let child = parent.firstChild;
186
+ if (!child) {
187
+ return null;
188
+ }
189
+ let i = 0;
190
+ while (child) {
191
+ if (child.nodeType === Node.ELEMENT_NODE) {
192
+ i++;
193
+ }
194
+ if (child === input) {
195
+ break;
196
+ }
197
+ child = child.nextSibling;
198
+ }
199
+ return i;
200
+ }
201
+ function nthChild(node, i) {
202
+ return {
203
+ name: node.name + `:nth-child(${i})`,
204
+ penalty: node.penalty + 1
205
+ };
206
+ }
207
+ function dispensableNth(node) {
208
+ return node.name !== "html" && !node.name.startsWith("#");
209
+ }
210
+ function maybe(...level) {
211
+ const list = level.filter(notEmpty);
212
+ if (list.length > 0) {
213
+ return list;
214
+ }
215
+ return null;
216
+ }
217
+ function notEmpty(value) {
218
+ return value !== null && value !== void 0;
219
+ }
220
+ function* combinations(stack, path = []) {
221
+ if (stack.length > 0) {
222
+ for (let node of stack[0]) {
223
+ yield* combinations(stack.slice(1, stack.length), path.concat(node));
224
+ }
225
+ } else {
226
+ yield path;
227
+ }
228
+ }
229
+ function sort(paths) {
230
+ return [...paths].sort((a, b) => penalty(a) - penalty(b));
231
+ }
232
+ function* optimize(path, input, scope = {
233
+ counter: 0,
234
+ visited: /* @__PURE__ */ new Map()
235
+ }) {
236
+ if (path.length > 2 && path.length > config.optimizedMinLength) {
237
+ for (let i = 1; i < path.length - 1; i++) {
238
+ if (scope.counter > config.maxNumberOfTries) {
239
+ return;
240
+ }
241
+ scope.counter += 1;
242
+ const newPath = [...path];
243
+ newPath.splice(i, 1);
244
+ const newPathKey = selector(newPath);
245
+ if (scope.visited.has(newPathKey)) {
246
+ return;
247
+ }
248
+ if (unique(newPath) && same(newPath, input)) {
249
+ yield newPath;
250
+ scope.visited.set(newPathKey, true);
251
+ yield* optimize(newPath, input, scope);
252
+ }
253
+ }
254
+ }
255
+ }
256
+ function same(path, input) {
257
+ return rootDocument.querySelector(selector(path)) === input;
258
+ }
259
+
260
+ // src/dom/fingerprint.ts
261
+ var STABLE_ATTRS = ["role", "aria-label", "type", "name", "href", "src", "data-testid", "data-id"];
262
+ function djb2(str) {
263
+ let hash = 5381;
264
+ for (let i = 0; i < str.length; i++) {
265
+ hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
266
+ }
267
+ return (hash >>> 0).toString(36);
268
+ }
269
+ function generateFingerprint(element) {
270
+ const childCount = element.children.length;
271
+ let siblingIdx = 0;
272
+ const parent = element.parentElement;
273
+ if (parent) {
274
+ for (const child of parent.children) {
275
+ if (child === element) break;
276
+ if (child.tagName === element.tagName) siblingIdx++;
277
+ }
278
+ }
279
+ const attrs = [];
280
+ for (const attr2 of STABLE_ATTRS) {
281
+ const val = element.getAttribute(attr2);
282
+ if (val) attrs.push(`${attr2}=${val}`);
283
+ }
284
+ const attrHash = attrs.length > 0 ? djb2(attrs.join(",")) : "0";
285
+ return `${childCount}:${siblingIdx}:${attrHash}`;
286
+ }
287
+ function scoreFingerprint(candidate, storedFingerprint) {
288
+ const parts = storedFingerprint.split(":");
289
+ if (parts.length !== 3) return 0;
290
+ const [storedChildren, storedSibIdx, storedAttrHash] = parts;
291
+ const storedChildCount = Number(storedChildren);
292
+ const storedSibIndex = Number(storedSibIdx);
293
+ if (Number.isNaN(storedChildCount) || Number.isNaN(storedSibIndex)) return 0;
294
+ const candidateFp = generateFingerprint(candidate);
295
+ const [candChildren, candSibIdx, candAttrHash] = candidateFp.split(":");
296
+ let score = 0;
297
+ const childDiff = Math.abs(Number(candChildren) - storedChildCount);
298
+ if (childDiff === 0) score += 0.2;
299
+ else if (childDiff <= 2) score += 0.1;
300
+ else if (childDiff <= 5) score += 0.03;
301
+ const sibDiff = Math.abs(Number(candSibIdx) - storedSibIndex);
302
+ if (sibDiff === 0) score += 0.4;
303
+ else if (sibDiff === 1) score += 0.2;
304
+ else if (sibDiff <= 3) score += 0.08;
305
+ if (candAttrHash === storedAttrHash) score += 0.4;
306
+ return score;
307
+ }
308
+
309
+ // src/dom/text-context.ts
310
+ function adjacentText(element, direction) {
311
+ const prop = direction === "before" ? "previousElementSibling" : "nextElementSibling";
312
+ let sibling = element[prop];
313
+ let attempts = 3;
314
+ while (sibling && attempts > 0) {
315
+ const text = sibling.textContent?.trim();
316
+ if (text) {
317
+ return direction === "before" ? text.slice(-32) : text.slice(0, 32);
318
+ }
319
+ sibling = sibling[prop];
320
+ attempts--;
321
+ }
322
+ return "";
323
+ }
324
+ function neighborText(element) {
325
+ const prev = element.previousElementSibling?.textContent?.trim().slice(0, 40) ?? "";
326
+ const next = element.nextElementSibling?.textContent?.trim().slice(0, 40) ?? "";
327
+ return [prev, next].filter(Boolean).join(" | ");
328
+ }
329
+
330
+ // src/dom/xpath.ts
331
+ function generateXPath(element) {
332
+ if (element.id) {
333
+ const safeId = element.id.includes("'") ? `concat('${element.id.replace(/'/g, `',"'",'`)}')` : `'${element.id}'`;
334
+ return `//${element.localName}[@id=${safeId}]`;
335
+ }
336
+ const segments = [];
337
+ let current = element;
338
+ while (current && current !== document.body && segments.length < 6) {
339
+ const tag = current.localName;
340
+ const parent = current.parentElement;
341
+ if (current.id) {
342
+ const safeId = current.id.includes("'") ? `concat('${current.id.replace(/'/g, `',"'",'`)}')` : `'${current.id}'`;
343
+ segments.unshift(`/${tag}[@id=${safeId}]`);
344
+ return "/" + segments.join("");
345
+ }
346
+ let position = 1;
347
+ if (parent) {
348
+ for (const sibling of parent.children) {
349
+ if (sibling === current) break;
350
+ if (sibling.localName === tag) position++;
351
+ }
352
+ }
353
+ segments.unshift(`/${tag}[${position}]`);
354
+ current = parent;
355
+ }
356
+ return "/html/body" + segments.join("");
357
+ }
358
+
359
+ // src/dom/anchor.ts
360
+ function generateAnchor(element) {
361
+ const cssSelector = finder(element, {
362
+ // Filter out CSS-in-JS hashed class names
363
+ className: (name) => !/^(css|sc|emotion|styled)-/.test(name) && !/^[a-z]{1,3}[A-Za-z0-9]{4,8}$/.test(name),
364
+ // Prefer stable attributes
365
+ attr: (name) => ["data-testid", "data-id", "role", "aria-label"].includes(name),
366
+ // Exclude framework-generated dynamic IDs
367
+ idName: (name) => !name.startsWith("radix-") && !/^:r[0-9]+:$/.test(name),
368
+ seedMinLength: 3,
369
+ optimizedMinLength: 2
370
+ });
371
+ const xpath = generateXPath(element);
372
+ const rawText = element.textContent?.trim() ?? "";
373
+ const textSnippet = rawText.slice(0, 120);
374
+ const textPrefix = adjacentText(element, "before");
375
+ const textSuffix = adjacentText(element, "after");
376
+ const fingerprint = generateFingerprint(element);
377
+ const neighbor = neighborText(element);
378
+ return {
379
+ cssSelector,
380
+ xpath,
381
+ textSnippet,
382
+ textPrefix,
383
+ textSuffix,
384
+ fingerprint,
385
+ neighborText: neighbor,
386
+ elementTag: element.tagName,
387
+ elementId: element.id || void 0
388
+ };
389
+ }
390
+ function findAnchorElement(rect, root = document.documentElement) {
391
+ const centerX = rect.x + rect.width / 2;
392
+ const centerY = rect.y + rect.height / 2;
393
+ const elementAtCenter = document.elementFromPoint(centerX, centerY);
394
+ if (!elementAtCenter || elementAtCenter === root) return document.body;
395
+ let candidate = elementAtCenter;
396
+ let current = elementAtCenter;
397
+ while (current && current !== document.body) {
398
+ const bounds = current.getBoundingClientRect();
399
+ if (bounds.left <= rect.x && bounds.top <= rect.y && bounds.right >= rect.x + rect.width && bounds.bottom >= rect.y + rect.height) {
400
+ candidate = current;
401
+ break;
402
+ }
403
+ current = current.parentElement;
404
+ }
405
+ return candidate;
406
+ }
407
+ function rectToPercentages(rect, anchorBounds) {
408
+ if (anchorBounds.width <= 0 || anchorBounds.height <= 0) {
409
+ return { xPct: 0, yPct: 0, wPct: 1, hPct: 1 };
410
+ }
411
+ return {
412
+ xPct: (rect.x - anchorBounds.x) / anchorBounds.width,
413
+ yPct: (rect.y - anchorBounds.y) / anchorBounds.height,
414
+ wPct: rect.width / anchorBounds.width,
415
+ hPct: rect.height / anchorBounds.height
416
+ };
417
+ }
418
+
419
+ // src/dom-utils.ts
420
+ function parseSvg(svgString) {
421
+ const range = document.createRange();
422
+ const fragment = range.createContextualFragment(svgString);
423
+ const svg = fragment.firstElementChild;
424
+ if (!svg || svg.nodeName.toLowerCase() !== "svg") {
425
+ throw new Error("[siteping] Invalid SVG string");
426
+ }
427
+ return svg;
428
+ }
429
+ function el(tag, attrs) {
430
+ const element = document.createElement(tag);
431
+ if (attrs) {
432
+ for (const [key, value] of Object.entries(attrs)) {
433
+ if (key === "class") {
434
+ element.className = value;
435
+ } else if (key === "style") {
436
+ element.style.cssText = value;
437
+ } else {
438
+ element.setAttribute(key, value);
439
+ }
440
+ }
441
+ }
442
+ return element;
443
+ }
444
+ function setText(element, text) {
445
+ element.textContent = text;
446
+ }
447
+ function formatRelativeDate(isoString) {
448
+ const diff = Date.now() - new Date(isoString).getTime();
449
+ const minutes = Math.floor(diff / 6e4);
450
+ if (minutes < 1) return "maintenant";
451
+ if (minutes < 60) return `il y a ${minutes}min`;
452
+ const hours = Math.floor(minutes / 60);
453
+ if (hours < 24) return `il y a ${hours}h`;
454
+ const days = Math.floor(hours / 24);
455
+ if (days < 7) return `il y a ${days}j`;
456
+ return new Date(isoString).toLocaleDateString("fr-FR");
457
+ }
458
+
459
+ // src/icons.ts
460
+ var ICON_SITEPING = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><circle cx="12" cy="10" r="1" fill="currentColor" stroke="none"/><circle cx="8" cy="10" r="1" fill="currentColor" stroke="none"/><circle cx="16" cy="10" r="1" fill="currentColor" stroke="none"/></svg>`;
461
+ var ICON_CHAT = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
462
+ var ICON_ANNOTATE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 3v18"/></svg>`;
463
+ var ICON_EYE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`;
464
+ var ICON_EYE_OFF = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>`;
465
+ var ICON_CLOSE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
466
+ var ICON_SEARCH = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`;
467
+ var ICON_CHECK = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
468
+ var ICON_QUESTION = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`;
469
+ var ICON_CHANGE = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`;
470
+ var ICON_BUG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="6" width="8" height="14" rx="4"/><path d="M19 9h2"/><path d="M3 9h2"/><path d="M19 13h2"/><path d="M3 13h2"/><path d="M19 17h2"/><path d="M3 17h2"/><path d="M10 2h4"/></svg>`;
471
+ var ICON_OTHER = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>`;
472
+ var ICON_UNDO = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>`;
473
+ var ICON_TRASH = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>`;
474
+
475
+ // src/styles/theme.ts
476
+ var DEFAULT_ACCENT = "#0066ff";
477
+ var HEX6_RE = /^#[0-9a-fA-F]{6}$/;
478
+ var HEX3_RE = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$/;
479
+ var HEX8_RE = /^#[0-9a-fA-F]{8}$/;
480
+ function normalizeHex(color) {
481
+ if (HEX6_RE.test(color)) return color;
482
+ const short = HEX3_RE.test(color) ? color.match(HEX3_RE) : null;
483
+ if (short) return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`;
484
+ if (HEX8_RE.test(color)) return color.slice(0, 7);
485
+ return DEFAULT_ACCENT;
486
+ }
487
+ function darkenHex(hex, amount) {
488
+ const r = Math.max(0, Math.round(parseInt(hex.slice(1, 3), 16) * (1 - amount)));
489
+ const g = Math.max(0, Math.round(parseInt(hex.slice(3, 5), 16) * (1 - amount)));
490
+ const b = Math.max(0, Math.round(parseInt(hex.slice(5, 7), 16) * (1 - amount)));
491
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
492
+ }
493
+ function buildThemeColors(accent = DEFAULT_ACCENT) {
494
+ const hex = normalizeHex(accent);
495
+ const dark = darkenHex(hex, 0.15);
496
+ return {
497
+ accent: hex,
498
+ accentLight: hex + "14",
499
+ // 8% opacity
500
+ accentDark: dark,
501
+ accentGlow: hex + "33",
502
+ // 20% opacity
503
+ accentGradient: `linear-gradient(135deg, ${hex}, ${dark})`,
504
+ bg: "#ffffff",
505
+ bgHover: "#f8f9fb",
506
+ text: "#0f172a",
507
+ textSecondary: "#475569",
508
+ textTertiary: "#64748b",
509
+ border: "#e2e8f0",
510
+ shadow: "rgba(0, 0, 0, 0.06)",
511
+ // Glass tokens
512
+ glassBg: "rgba(255, 255, 255, 0.72)",
513
+ glassBgHeavy: "rgba(255, 255, 255, 0.85)",
514
+ glassBorder: "rgba(255, 255, 255, 0.35)",
515
+ glassBorderSubtle: "rgba(255, 255, 255, 0.18)",
516
+ // Vibrant type colors
517
+ typeQuestion: "#3b82f6",
518
+ typeChangement: "#f59e0b",
519
+ typeBug: "#ef4444",
520
+ typeAutre: "#64748b",
521
+ // Pastel backgrounds
522
+ typeQuestionBg: "#eff6ff",
523
+ typeChangementBg: "#fffbeb",
524
+ typeBugBg: "#fef2f2",
525
+ typeAutreBg: "#f8fafc"
526
+ };
527
+ }
528
+ function getTypeColor(type, colors) {
529
+ switch (type) {
530
+ case "question":
531
+ return colors.typeQuestion;
532
+ case "changement":
533
+ return colors.typeChangement;
534
+ case "bug":
535
+ return colors.typeBug;
536
+ default:
537
+ return colors.typeAutre;
538
+ }
539
+ }
540
+ function getTypeBgColor(type, colors) {
541
+ switch (type) {
542
+ case "question":
543
+ return colors.typeQuestionBg;
544
+ case "changement":
545
+ return colors.typeChangementBg;
546
+ case "bug":
547
+ return colors.typeBugBg;
548
+ default:
549
+ return colors.typeAutreBg;
550
+ }
551
+ }
552
+ function cssVariables(colors) {
553
+ return `
554
+ --sp-accent: ${colors.accent};
555
+ --sp-accent-light: ${colors.accentLight};
556
+ --sp-accent-dark: ${colors.accentDark};
557
+ --sp-accent-glow: ${colors.accentGlow};
558
+ --sp-accent-gradient: ${colors.accentGradient};
559
+ --sp-bg: ${colors.bg};
560
+ --sp-bg-hover: ${colors.bgHover};
561
+ --sp-text: ${colors.text};
562
+ --sp-text-secondary: ${colors.textSecondary};
563
+ --sp-text-tertiary: ${colors.textTertiary};
564
+ --sp-border: ${colors.border};
565
+ --sp-shadow: ${colors.shadow};
566
+ --sp-glass-bg: ${colors.glassBg};
567
+ --sp-glass-bg-heavy: ${colors.glassBgHeavy};
568
+ --sp-glass-border: ${colors.glassBorder};
569
+ --sp-glass-border-subtle: ${colors.glassBorderSubtle};
570
+ --sp-type-question: ${colors.typeQuestion};
571
+ --sp-type-changement: ${colors.typeChangement};
572
+ --sp-type-bug: ${colors.typeBug};
573
+ --sp-type-autre: ${colors.typeAutre};
574
+ --sp-type-question-bg: ${colors.typeQuestionBg};
575
+ --sp-type-changement-bg: ${colors.typeChangementBg};
576
+ --sp-type-bug-bg: ${colors.typeBugBg};
577
+ --sp-type-autre-bg: ${colors.typeAutreBg};
578
+ --sp-radius: 12px;
579
+ --sp-radius-lg: 16px;
580
+ --sp-radius-xl: 20px;
581
+ --sp-radius-full: 9999px;
582
+ --sp-blur: 20px;
583
+ --sp-blur-heavy: 32px;
584
+ --sp-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04);
585
+ --sp-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.04);
586
+ --sp-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04);
587
+ --sp-shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.04);
588
+ --sp-shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.12), 0 8px 16px rgba(0, 0, 0, 0.06);
589
+ --sp-font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
590
+ `;
591
+ }
592
+
593
+ // src/popup.ts
594
+ var TYPE_OPTIONS = [
595
+ { type: "question", label: "Question", icon: ICON_QUESTION },
596
+ { type: "changement", label: "Changement", icon: ICON_CHANGE },
597
+ { type: "bug", label: "Bug", icon: ICON_BUG },
598
+ { type: "autre", label: "Autre", icon: ICON_OTHER }
599
+ ];
600
+ var Popup = class {
601
+ constructor(colors) {
602
+ this.colors = colors;
603
+ this.root = el("div", {
604
+ style: `
605
+ position:fixed;
606
+ z-index:2147483647;
607
+ width:300px;
608
+ padding:16px;
609
+ border-radius:16px;
610
+ background:rgba(255, 255, 255, 0.82);
611
+ backdrop-filter:blur(24px);
612
+ -webkit-backdrop-filter:blur(24px);
613
+ border:1px solid rgba(255, 255, 255, 0.35);
614
+ box-shadow:0 8px 32px rgba(0,0,0,0.1), 0 2px 8px rgba(0,0,0,0.04);
615
+ font-family:"Inter",system-ui,-apple-system,sans-serif;
616
+ opacity:0;
617
+ transform:translateY(8px) scale(0.98);
618
+ transition:opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1),transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
619
+ display:none;
620
+ -webkit-font-smoothing:antialiased;
621
+ `
622
+ });
623
+ const typeRow = el("div", { style: "display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:12px;" });
624
+ for (const option of TYPE_OPTIONS) {
625
+ const btn = document.createElement("button");
626
+ btn.style.cssText = `
627
+ height:34px;
628
+ border-radius:9999px;border:1px solid #e2e8f0;
629
+ background:rgba(255,255,255,0.8);cursor:pointer;
630
+ display:flex;align-items:center;justify-content:center;gap:5px;
631
+ font-family:"Inter",system-ui,-apple-system,sans-serif;
632
+ font-size:12px;font-weight:500;color:#64748b;
633
+ transition:all 0.2s ease;
634
+ padding:0 10px;
635
+ `;
636
+ const icon = parseSvg(option.icon);
637
+ icon.setAttribute("style", "width:13px;height:13px;flex-shrink:0;");
638
+ btn.appendChild(icon);
639
+ const labelSpan = document.createElement("span");
640
+ setText(labelSpan, option.label);
641
+ btn.appendChild(labelSpan);
642
+ btn.dataset.type = option.type;
643
+ btn.addEventListener("click", () => {
644
+ this.selectType(option.type, typeRow);
645
+ });
646
+ btn.addEventListener("mouseenter", () => {
647
+ if (btn.dataset.type !== this.selectedType) {
648
+ const bgColor = getTypeBgColor(btn.dataset.type ?? "", this.colors);
649
+ btn.style.background = bgColor;
650
+ btn.style.borderColor = getTypeColor(btn.dataset.type ?? "", this.colors) + "40";
651
+ }
652
+ });
653
+ btn.addEventListener("mouseleave", () => {
654
+ if (btn.dataset.type !== this.selectedType) {
655
+ btn.style.background = "rgba(255,255,255,0.8)";
656
+ btn.style.borderColor = "#e2e8f0";
657
+ }
658
+ });
659
+ typeRow.appendChild(btn);
660
+ }
661
+ this.textarea = document.createElement("textarea");
662
+ this.textarea.style.cssText = `
663
+ width:100%;min-height:72px;max-height:152px;
664
+ padding:10px 12px;border-radius:12px;
665
+ border:1px solid #e2e8f0;
666
+ background:rgba(255,255,255,0.85);
667
+ color:#0f172a;font-family:"Inter",system-ui,-apple-system,sans-serif;
668
+ font-size:13px;line-height:1.5;resize:vertical;
669
+ outline:none;transition:all 0.2s ease;
670
+ box-sizing:border-box;
671
+ `;
672
+ this.textarea.placeholder = "D\xE9crivez votre retour...";
673
+ this.textarea.setAttribute("aria-label", "Message de feedback");
674
+ const hint = el("div", {
675
+ style: `
676
+ font-size:11px;color:#94a3b8;
677
+ text-align:right;margin-top:4px;
678
+ font-family:"Inter",system-ui,-apple-system,sans-serif;
679
+ letter-spacing:0.01em;
680
+ `
681
+ });
682
+ const isMac = navigator.platform.includes("Mac");
683
+ setText(hint, isMac ? "\u2318+Entr\xE9e pour envoyer" : "Ctrl+Entr\xE9e pour envoyer");
684
+ this.textarea.addEventListener("focus", () => {
685
+ this.textarea.style.borderColor = this.colors.accent;
686
+ this.textarea.style.boxShadow = `0 0 0 3px ${this.colors.accent}14`;
687
+ this.textarea.style.background = "#fff";
688
+ });
689
+ this.textarea.addEventListener("blur", () => {
690
+ this.textarea.style.borderColor = "#e2e8f0";
691
+ this.textarea.style.boxShadow = "none";
692
+ this.textarea.style.background = "rgba(255,255,255,0.85)";
693
+ });
694
+ this.textarea.addEventListener("input", () => {
695
+ this.updateSubmitState();
696
+ });
697
+ this.textarea.addEventListener("keydown", (e) => {
698
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
699
+ e.preventDefault();
700
+ this.submit();
701
+ }
702
+ if (e.key === "Escape") {
703
+ this.cancel();
704
+ }
705
+ });
706
+ const btnRow = el("div", { style: "display:flex;justify-content:flex-end;gap:8px;margin-top:12px;" });
707
+ const cancelBtn = document.createElement("button");
708
+ cancelBtn.style.cssText = `
709
+ height:34px;padding:0 16px;border-radius:9999px;
710
+ border:1px solid #e2e8f0;
711
+ background:rgba(255,255,255,0.8);
712
+ color:#64748b;font-family:"Inter",system-ui,-apple-system,sans-serif;
713
+ font-size:13px;font-weight:500;cursor:pointer;
714
+ transition:all 0.2s ease;
715
+ `;
716
+ setText(cancelBtn, "Annuler");
717
+ cancelBtn.addEventListener("click", () => this.cancel());
718
+ cancelBtn.addEventListener("mouseenter", () => {
719
+ cancelBtn.style.borderColor = this.colors.accent;
720
+ cancelBtn.style.color = this.colors.accent;
721
+ });
722
+ cancelBtn.addEventListener("mouseleave", () => {
723
+ cancelBtn.style.borderColor = "#e2e8f0";
724
+ cancelBtn.style.color = "#64748b";
725
+ });
726
+ this.submitBtn = document.createElement("button");
727
+ this.submitBtn.style.cssText = `
728
+ height:34px;padding:0 18px;border-radius:9999px;
729
+ border:none;background:${this.colors.accentGradient};
730
+ color:#fff;font-family:"Inter",system-ui,-apple-system,sans-serif;
731
+ font-size:13px;font-weight:600;cursor:pointer;
732
+ opacity:0.35;pointer-events:none;
733
+ transition:all 0.2s ease;
734
+ box-shadow:0 2px 8px ${this.colors.accentGlow};
735
+ `;
736
+ setText(this.submitBtn, "Envoyer");
737
+ this.submitBtn.addEventListener("click", () => this.submit());
738
+ btnRow.appendChild(cancelBtn);
739
+ btnRow.appendChild(this.submitBtn);
740
+ this.root.appendChild(typeRow);
741
+ this.root.appendChild(this.textarea);
742
+ this.root.appendChild(hint);
743
+ this.root.appendChild(btnRow);
744
+ document.body.appendChild(this.root);
745
+ }
746
+ colors;
747
+ root;
748
+ selectedType = null;
749
+ textarea;
750
+ submitBtn;
751
+ resolve = null;
752
+ /**
753
+ * Show the popup near a drawn rectangle and return the user's input.
754
+ * Returns null if cancelled.
755
+ */
756
+ show(rectBounds) {
757
+ return new Promise((resolve) => {
758
+ this.resolve = resolve;
759
+ this.selectedType = null;
760
+ this.textarea.value = "";
761
+ this.updateSubmitState();
762
+ this.resetTypeButtons();
763
+ let top = rectBounds.bottom + 8;
764
+ let left = rectBounds.left;
765
+ if (top + 220 > window.innerHeight) {
766
+ top = rectBounds.top - 220 - 8;
767
+ }
768
+ if (left + 300 > window.innerWidth) {
769
+ left = rectBounds.right - 300;
770
+ }
771
+ left = Math.max(8, left);
772
+ top = Math.max(8, top);
773
+ this.root.style.top = `${top}px`;
774
+ this.root.style.left = `${left}px`;
775
+ this.root.style.display = "block";
776
+ requestAnimationFrame(() => {
777
+ this.root.style.opacity = "1";
778
+ this.root.style.transform = "translateY(0) scale(1)";
779
+ this.textarea.focus();
780
+ });
781
+ });
782
+ }
783
+ selectType(type, container) {
784
+ this.selectedType = type;
785
+ const buttons = container.querySelectorAll("button");
786
+ for (const btn of buttons) {
787
+ const isActive = btn.dataset.type === type;
788
+ const color = getTypeColor(btn.dataset.type ?? "", this.colors);
789
+ const bgColor = getTypeBgColor(btn.dataset.type ?? "", this.colors);
790
+ btn.style.background = isActive ? bgColor : "rgba(255,255,255,0.8)";
791
+ btn.style.borderColor = isActive ? color + "60" : "#e2e8f0";
792
+ btn.style.color = isActive ? color : "#64748b";
793
+ btn.style.fontWeight = isActive ? "600" : "500";
794
+ }
795
+ this.updateSubmitState();
796
+ }
797
+ resetTypeButtons() {
798
+ const buttons = this.root.querySelectorAll("button[data-type]");
799
+ for (const btn of buttons) {
800
+ btn.style.background = "rgba(255,255,255,0.8)";
801
+ btn.style.borderColor = "#e2e8f0";
802
+ btn.style.color = "#64748b";
803
+ btn.style.fontWeight = "500";
804
+ }
805
+ }
806
+ updateSubmitState() {
807
+ const enabled = this.selectedType !== null && this.textarea.value.trim().length > 0;
808
+ this.submitBtn.style.opacity = enabled ? "1" : "0.35";
809
+ this.submitBtn.style.pointerEvents = enabled ? "auto" : "none";
810
+ }
811
+ submit() {
812
+ if (!this.selectedType || !this.textarea.value.trim()) return;
813
+ this.resolve?.({ type: this.selectedType, message: this.textarea.value.trim() });
814
+ this.resolve = null;
815
+ this.hideElement();
816
+ }
817
+ cancel() {
818
+ this.resolve?.(null);
819
+ this.resolve = null;
820
+ this.hideElement();
821
+ }
822
+ hideElement() {
823
+ this.root.style.opacity = "0";
824
+ this.root.style.transform = "translateY(8px) scale(0.98)";
825
+ setTimeout(() => {
826
+ this.root.style.display = "none";
827
+ }, 250);
828
+ }
829
+ destroy() {
830
+ this.root.remove();
831
+ }
832
+ };
833
+
834
+ // src/annotator.ts
835
+ var Annotator = class {
836
+ constructor(config2, colors, bus) {
837
+ this.config = config2;
838
+ this.colors = colors;
839
+ this.bus = bus;
840
+ this.popup = new Popup(colors);
841
+ this.bus.on("annotation:start", () => this.activate());
842
+ }
843
+ config;
844
+ colors;
845
+ bus;
846
+ overlay = null;
847
+ toolbar = null;
848
+ drawingRect = null;
849
+ startX = 0;
850
+ startY = 0;
851
+ isDrawing = false;
852
+ isActive = false;
853
+ popup;
854
+ savedOverflow = "";
855
+ activate() {
856
+ if (this.isActive) return;
857
+ this.isActive = true;
858
+ this.config.onAnnotationStart?.();
859
+ this.savedOverflow = document.body.style.overflow;
860
+ document.body.style.overflow = "hidden";
861
+ this.overlay = el("div", {
862
+ style: `
863
+ position:fixed;inset:0;
864
+ z-index:2147483646;
865
+ background:rgba(15, 23, 42, 0.04);
866
+ cursor:crosshair;
867
+ `
868
+ });
869
+ this.toolbar = el("div", {
870
+ style: `
871
+ position:fixed;top:0;left:0;right:0;
872
+ z-index:2147483647;
873
+ height:52px;
874
+ background:rgba(255, 255, 255, 0.82);
875
+ backdrop-filter:blur(24px);
876
+ -webkit-backdrop-filter:blur(24px);
877
+ border-bottom:1px solid rgba(255, 255, 255, 0.35);
878
+ display:flex;align-items:center;justify-content:center;gap:16px;
879
+ font-family:"Inter",system-ui,-apple-system,sans-serif;
880
+ font-size:14px;color:#0f172a;
881
+ box-shadow:0 4px 16px rgba(0,0,0,0.06);
882
+ -webkit-font-smoothing:antialiased;
883
+ `
884
+ });
885
+ const dot = el("span", {
886
+ style: `
887
+ width:8px;height:8px;border-radius:50%;
888
+ background:${this.colors.accent};
889
+ box-shadow:0 0 8px ${this.colors.accentGlow};
890
+ animation:pulse 1.5s ease-in-out infinite;
891
+ `
892
+ });
893
+ const style = document.createElement("style");
894
+ style.textContent = `@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}`;
895
+ this.toolbar.appendChild(style);
896
+ const instruction = el("span", { style: "font-weight:500;letter-spacing:-0.01em;" });
897
+ setText(instruction, "Tracez un rectangle sur la zone \xE0 commenter");
898
+ const cancelBtn = document.createElement("button");
899
+ cancelBtn.style.cssText = `
900
+ height:34px;padding:0 18px;border-radius:9999px;
901
+ border:1px solid #e2e8f0;
902
+ background:rgba(255,255,255,0.8);
903
+ color:#64748b;font-family:"Inter",system-ui,-apple-system,sans-serif;
904
+ font-size:13px;font-weight:500;cursor:pointer;
905
+ transition:all 0.2s ease;
906
+ `;
907
+ setText(cancelBtn, "Annuler");
908
+ cancelBtn.addEventListener("click", () => this.deactivate());
909
+ cancelBtn.addEventListener("mouseenter", () => {
910
+ cancelBtn.style.borderColor = "#ef4444";
911
+ cancelBtn.style.color = "#ef4444";
912
+ cancelBtn.style.background = "rgba(239,68,68,0.06)";
913
+ });
914
+ cancelBtn.addEventListener("mouseleave", () => {
915
+ cancelBtn.style.borderColor = "#e2e8f0";
916
+ cancelBtn.style.color = "#64748b";
917
+ cancelBtn.style.background = "rgba(255,255,255,0.8)";
918
+ });
919
+ this.toolbar.appendChild(dot);
920
+ this.toolbar.appendChild(instruction);
921
+ this.toolbar.appendChild(cancelBtn);
922
+ this.overlay.addEventListener("mousedown", this.onMouseDown);
923
+ this.overlay.addEventListener("mousemove", this.onMouseMove);
924
+ this.overlay.addEventListener("mouseup", this.onMouseUp);
925
+ document.addEventListener("keydown", this.onKeyDown);
926
+ document.body.appendChild(this.overlay);
927
+ document.body.appendChild(this.toolbar);
928
+ }
929
+ deactivate() {
930
+ if (!this.isActive) return;
931
+ this.isActive = false;
932
+ this.isDrawing = false;
933
+ document.body.style.overflow = this.savedOverflow;
934
+ document.removeEventListener("keydown", this.onKeyDown);
935
+ this.overlay?.remove();
936
+ this.toolbar?.remove();
937
+ this.drawingRect?.remove();
938
+ this.overlay = null;
939
+ this.toolbar = null;
940
+ this.drawingRect = null;
941
+ this.config.onAnnotationEnd?.();
942
+ this.bus.emit("annotation:end");
943
+ }
944
+ onKeyDown = (e) => {
945
+ if (e.key === "Escape") this.deactivate();
946
+ };
947
+ onMouseDown = (e) => {
948
+ this.isDrawing = true;
949
+ this.startX = e.clientX;
950
+ this.startY = e.clientY;
951
+ this.drawingRect?.remove();
952
+ this.drawingRect = el("div", {
953
+ style: `
954
+ position:fixed;
955
+ border:2px solid ${this.colors.accent};
956
+ background:${this.colors.accent}12;
957
+ pointer-events:none;
958
+ border-radius:8px;
959
+ box-shadow:0 0 16px ${this.colors.accentGlow};
960
+ transition:box-shadow 0.15s ease;
961
+ `
962
+ });
963
+ this.overlay?.appendChild(this.drawingRect);
964
+ };
965
+ onMouseMove = (e) => {
966
+ if (!this.isDrawing || !this.drawingRect) return;
967
+ const x = Math.min(e.clientX, this.startX);
968
+ const y = Math.min(e.clientY, this.startY);
969
+ const w = Math.abs(e.clientX - this.startX);
970
+ const h = Math.abs(e.clientY - this.startY);
971
+ this.drawingRect.style.left = `${x}px`;
972
+ this.drawingRect.style.top = `${y}px`;
973
+ this.drawingRect.style.width = `${w}px`;
974
+ this.drawingRect.style.height = `${h}px`;
975
+ };
976
+ onMouseUp = async (e) => {
977
+ if (!this.isDrawing || !this.drawingRect) return;
978
+ this.isDrawing = false;
979
+ const x = Math.min(e.clientX, this.startX);
980
+ const y = Math.min(e.clientY, this.startY);
981
+ const w = Math.abs(e.clientX - this.startX);
982
+ const h = Math.abs(e.clientY - this.startY);
983
+ if (w < 10 || h < 10) {
984
+ this.drawingRect.remove();
985
+ this.drawingRect = null;
986
+ return;
987
+ }
988
+ const rectBounds = new DOMRect(x, y, w, h);
989
+ const result = await this.popup.show(rectBounds);
990
+ if (!result) {
991
+ this.drawingRect?.remove();
992
+ this.drawingRect = null;
993
+ return;
994
+ }
995
+ const annotation = this.buildAnnotation(rectBounds);
996
+ this.drawingRect?.remove();
997
+ this.drawingRect = null;
998
+ this.deactivate();
999
+ this.bus.emit("annotation:complete", {
1000
+ annotation,
1001
+ type: result.type,
1002
+ message: result.message
1003
+ });
1004
+ };
1005
+ /**
1006
+ * Build an AnnotationPayload from a drawn rectangle.
1007
+ * Temporarily hides the overlay to access the real DOM underneath.
1008
+ */
1009
+ buildAnnotation(rectBounds) {
1010
+ if (this.overlay) this.overlay.style.pointerEvents = "none";
1011
+ const anchorElement = findAnchorElement(rectBounds);
1012
+ if (this.overlay) this.overlay.style.pointerEvents = "auto";
1013
+ const anchor = generateAnchor(anchorElement);
1014
+ const anchorBounds = anchorElement.getBoundingClientRect();
1015
+ const rect = rectToPercentages(rectBounds, anchorBounds);
1016
+ return {
1017
+ anchor,
1018
+ rect,
1019
+ scrollX: window.scrollX,
1020
+ scrollY: window.scrollY,
1021
+ viewportW: window.innerWidth,
1022
+ viewportH: window.innerHeight,
1023
+ devicePixelRatio: window.devicePixelRatio
1024
+ };
1025
+ }
1026
+ destroy() {
1027
+ this.deactivate();
1028
+ this.popup.destroy();
1029
+ }
1030
+ };
1031
+
1032
+ // src/api-client.ts
1033
+ var MAX_RETRIES = 3;
1034
+ var TIMEOUT_MS = 1e4;
1035
+ var RETRY_QUEUE_KEY = "siteping_retry_queue";
1036
+ async function resilientFetch(url, init, retries = MAX_RETRIES) {
1037
+ for (let attempt = 0; attempt <= retries; attempt++) {
1038
+ const controller = new AbortController();
1039
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
1040
+ try {
1041
+ const response = await fetch(url, {
1042
+ ...init,
1043
+ signal: controller.signal
1044
+ });
1045
+ clearTimeout(timeout);
1046
+ if (response.ok || response.status >= 400 && response.status < 500) {
1047
+ return response;
1048
+ }
1049
+ if (attempt === retries) return response;
1050
+ } catch (error) {
1051
+ clearTimeout(timeout);
1052
+ if (attempt === retries) throw error;
1053
+ }
1054
+ const baseDelay = 1e3 * 2 ** attempt;
1055
+ const jitter = Math.random() * 1e3 - 500;
1056
+ await new Promise((r) => setTimeout(r, baseDelay + jitter));
1057
+ }
1058
+ throw new Error("Max retries exceeded");
1059
+ }
1060
+ function queueForRetry(endpoint, payload) {
1061
+ try {
1062
+ const raw = localStorage.getItem(RETRY_QUEUE_KEY);
1063
+ const queue = raw ? JSON.parse(raw) : [];
1064
+ queue.push({ endpoint, payload });
1065
+ localStorage.setItem(RETRY_QUEUE_KEY, JSON.stringify(queue));
1066
+ } catch {
1067
+ }
1068
+ }
1069
+ async function flushRetryQueue(endpoint) {
1070
+ try {
1071
+ const raw = localStorage.getItem(RETRY_QUEUE_KEY);
1072
+ if (!raw) return;
1073
+ const queue = JSON.parse(raw);
1074
+ const toRetry = queue.filter((e) => e.endpoint === endpoint);
1075
+ if (toRetry.length === 0) return;
1076
+ const failed = [];
1077
+ for (const entry of toRetry) {
1078
+ try {
1079
+ const res = await fetch(endpoint, {
1080
+ method: "POST",
1081
+ headers: { "Content-Type": "application/json" },
1082
+ body: JSON.stringify(entry.payload)
1083
+ });
1084
+ if (!res.ok) failed.push(entry);
1085
+ } catch {
1086
+ failed.push(entry);
1087
+ }
1088
+ }
1089
+ const remaining = queue.filter((e) => e.endpoint !== endpoint).concat(failed);
1090
+ if (remaining.length > 0) {
1091
+ localStorage.setItem(RETRY_QUEUE_KEY, JSON.stringify(remaining));
1092
+ } else {
1093
+ localStorage.removeItem(RETRY_QUEUE_KEY);
1094
+ }
1095
+ } catch {
1096
+ }
1097
+ }
1098
+ var ApiClient = class {
1099
+ constructor(endpoint) {
1100
+ this.endpoint = endpoint;
1101
+ }
1102
+ endpoint;
1103
+ async sendFeedback(payload) {
1104
+ try {
1105
+ const response = await resilientFetch(this.endpoint, {
1106
+ method: "POST",
1107
+ headers: { "Content-Type": "application/json" },
1108
+ body: JSON.stringify(payload)
1109
+ });
1110
+ if (!response.ok) {
1111
+ const text = await response.text().catch(() => "Unknown error");
1112
+ throw new Error(`Failed to send feedback: ${response.status} ${text}`);
1113
+ }
1114
+ return await response.json();
1115
+ } catch (error) {
1116
+ queueForRetry(this.endpoint, payload);
1117
+ throw error;
1118
+ }
1119
+ }
1120
+ async getFeedbacks(projectName, options) {
1121
+ const params = new URLSearchParams({ projectName });
1122
+ if (options?.page) params.set("page", String(options.page));
1123
+ if (options?.limit) params.set("limit", String(options.limit));
1124
+ if (options?.type) params.set("type", options.type);
1125
+ if (options?.status) params.set("status", options.status);
1126
+ if (options?.search) params.set("search", options.search);
1127
+ const response = await resilientFetch(`${this.endpoint}?${params.toString()}`, { method: "GET" });
1128
+ if (!response.ok) {
1129
+ throw new Error(`Failed to fetch feedbacks: ${response.status}`);
1130
+ }
1131
+ return await response.json();
1132
+ }
1133
+ async resolveFeedback(id2, resolved) {
1134
+ const response = await resilientFetch(this.endpoint, {
1135
+ method: "PATCH",
1136
+ headers: { "Content-Type": "application/json" },
1137
+ body: JSON.stringify({ id: id2, status: resolved ? "resolved" : "open" })
1138
+ });
1139
+ if (!response.ok) {
1140
+ throw new Error(`Failed to update feedback: ${response.status}`);
1141
+ }
1142
+ return await response.json();
1143
+ }
1144
+ async deleteFeedback(id2) {
1145
+ const response = await resilientFetch(this.endpoint, {
1146
+ method: "DELETE",
1147
+ headers: { "Content-Type": "application/json" },
1148
+ body: JSON.stringify({ id: id2 })
1149
+ });
1150
+ if (!response.ok) {
1151
+ throw new Error(`Failed to delete feedback: ${response.status}`);
1152
+ }
1153
+ }
1154
+ async deleteAllFeedbacks(projectName) {
1155
+ const response = await resilientFetch(this.endpoint, {
1156
+ method: "DELETE",
1157
+ headers: { "Content-Type": "application/json" },
1158
+ body: JSON.stringify({ projectName, deleteAll: true })
1159
+ });
1160
+ if (!response.ok) {
1161
+ throw new Error(`Failed to delete all feedbacks: ${response.status}`);
1162
+ }
1163
+ }
1164
+ };
1165
+
1166
+ // src/events.ts
1167
+ var EventBus = class {
1168
+ listeners = /* @__PURE__ */ new Map();
1169
+ on(event, listener) {
1170
+ if (!this.listeners.has(event)) {
1171
+ this.listeners.set(event, /* @__PURE__ */ new Set());
1172
+ }
1173
+ const set = this.listeners.get(event);
1174
+ set.add(listener);
1175
+ return () => {
1176
+ set.delete(listener);
1177
+ };
1178
+ }
1179
+ emit(event, ...args) {
1180
+ const set = this.listeners.get(event);
1181
+ if (!set) return;
1182
+ for (const fn of set) {
1183
+ try {
1184
+ fn(...args);
1185
+ } catch (err) {
1186
+ console.error(`[siteping] Error in event listener for "${String(event)}":`, err);
1187
+ }
1188
+ }
1189
+ }
1190
+ removeAll() {
1191
+ this.listeners.clear();
1192
+ }
1193
+ };
1194
+
1195
+ // src/fab.ts
1196
+ var ITEM_GAP = 54;
1197
+ var Fab = class {
1198
+ constructor(shadowRoot, config2, bus) {
1199
+ this.bus = bus;
1200
+ const position = config2.position ?? "bottom-right";
1201
+ const isRight = position === "bottom-right";
1202
+ this.items = [
1203
+ { id: "chat", icon: ICON_CHAT, label: "Messages" },
1204
+ { id: "annotate", icon: ICON_ANNOTATE, label: "Annoter" },
1205
+ { id: "toggle-annotations", icon: ICON_EYE, iconAlt: ICON_EYE_OFF, label: "Annotations" }
1206
+ ];
1207
+ this.fab = document.createElement("button");
1208
+ this.fab.className = `sp-fab sp-fab--${position} sp-anim-fab-in`;
1209
+ this.fab.style.position = "fixed";
1210
+ this.fab.appendChild(parseSvg(ICON_SITEPING));
1211
+ this.fab.setAttribute("aria-label", "Siteping \u2014 Menu feedback");
1212
+ this.fab.setAttribute("aria-expanded", "false");
1213
+ this.fab.addEventListener("click", () => this.toggle());
1214
+ this.radialContainer = document.createElement("div");
1215
+ this.radialContainer.className = `sp-radial sp-radial--${position}`;
1216
+ this.radialContainer.setAttribute("role", "menu");
1217
+ for (let i = 0; i < this.items.length; i++) {
1218
+ const item = this.items[i];
1219
+ const btn = document.createElement("button");
1220
+ btn.className = "sp-radial-item";
1221
+ btn.style.setProperty("--sp-i", String(i));
1222
+ btn.appendChild(parseSvg(item.icon));
1223
+ btn.setAttribute("role", "menuitem");
1224
+ btn.setAttribute("aria-label", item.label);
1225
+ btn.dataset.itemId = item.id;
1226
+ btn.addEventListener("click", (e) => {
1227
+ e.stopPropagation();
1228
+ this.handleItemClick(item.id);
1229
+ });
1230
+ const label = document.createElement("span");
1231
+ label.className = "sp-radial-label";
1232
+ label.textContent = item.label;
1233
+ label.style.cssText = isRight ? "position:absolute; right:54px; top:50%; transform:translateY(-50%); white-space:nowrap;" : "position:absolute; left:54px; top:50%; transform:translateY(-50%); white-space:nowrap;";
1234
+ btn.appendChild(label);
1235
+ this.radialContainer.appendChild(btn);
1236
+ }
1237
+ this.root = document.createElement("div");
1238
+ this.root.appendChild(this.radialContainer);
1239
+ this.root.appendChild(this.fab);
1240
+ shadowRoot.appendChild(this.root);
1241
+ const host = shadowRoot.host;
1242
+ this.onDocumentClick = (e) => {
1243
+ if (this.isOpen && !e.composedPath().includes(host)) {
1244
+ this.close();
1245
+ }
1246
+ };
1247
+ document.addEventListener("click", this.onDocumentClick);
1248
+ this.fab.addEventListener("keydown", (e) => {
1249
+ if (e.key === "Escape" && this.isOpen) this.close();
1250
+ });
1251
+ }
1252
+ bus;
1253
+ root;
1254
+ fab;
1255
+ radialContainer;
1256
+ badgeEl = null;
1257
+ isOpen = false;
1258
+ annotationsVisible = true;
1259
+ items;
1260
+ onDocumentClick;
1261
+ /** Update the badge count. Pass 0 to hide. */
1262
+ updateBadge(count) {
1263
+ if (count <= 0) {
1264
+ this.badgeEl?.remove();
1265
+ this.badgeEl = null;
1266
+ return;
1267
+ }
1268
+ if (!this.badgeEl) {
1269
+ this.badgeEl = document.createElement("span");
1270
+ this.badgeEl.className = "sp-fab-badge";
1271
+ this.fab.appendChild(this.badgeEl);
1272
+ }
1273
+ setText(this.badgeEl, count > 99 ? "99+" : String(count));
1274
+ }
1275
+ toggle() {
1276
+ this.isOpen ? this.close() : this.open();
1277
+ }
1278
+ open() {
1279
+ this.isOpen = true;
1280
+ this.setFabIcon(ICON_CLOSE);
1281
+ this.fab.setAttribute("aria-expanded", "true");
1282
+ const buttons = this.radialContainer.querySelectorAll(".sp-radial-item");
1283
+ buttons.forEach((btn, i) => {
1284
+ const y = -(16 + ITEM_GAP * (i + 1));
1285
+ btn.style.transform = `translate(0px, ${y}px) scale(1)`;
1286
+ btn.classList.add("sp-radial-item--open");
1287
+ });
1288
+ }
1289
+ close() {
1290
+ this.isOpen = false;
1291
+ this.setFabIcon(ICON_SITEPING);
1292
+ this.fab.setAttribute("aria-expanded", "false");
1293
+ const buttons = this.radialContainer.querySelectorAll(".sp-radial-item");
1294
+ buttons.forEach((btn) => {
1295
+ btn.style.transform = "translate(0, 0) scale(0.8)";
1296
+ btn.classList.remove("sp-radial-item--open");
1297
+ });
1298
+ }
1299
+ setFabIcon(svgStr) {
1300
+ const badge = this.badgeEl;
1301
+ this.fab.replaceChildren(parseSvg(svgStr));
1302
+ if (badge) this.fab.appendChild(badge);
1303
+ }
1304
+ handleItemClick(id2) {
1305
+ this.close();
1306
+ switch (id2) {
1307
+ case "chat":
1308
+ this.bus.emit("panel:toggle", true);
1309
+ break;
1310
+ case "annotate":
1311
+ this.bus.emit("annotation:start");
1312
+ break;
1313
+ case "toggle-annotations": {
1314
+ this.annotationsVisible = !this.annotationsVisible;
1315
+ this.bus.emit("annotations:toggle", this.annotationsVisible);
1316
+ const btn = this.radialContainer.querySelector('[data-item-id="toggle-annotations"]');
1317
+ if (btn) {
1318
+ btn.replaceChildren(parseSvg(this.annotationsVisible ? ICON_EYE : ICON_EYE_OFF));
1319
+ }
1320
+ break;
1321
+ }
1322
+ }
1323
+ }
1324
+ destroy() {
1325
+ document.removeEventListener("click", this.onDocumentClick);
1326
+ this.root.remove();
1327
+ }
1328
+ };
1329
+
1330
+ // src/identity.ts
1331
+ var STORAGE_KEY = "siteping_identity";
1332
+ function getIdentity() {
1333
+ try {
1334
+ const raw = localStorage.getItem(STORAGE_KEY);
1335
+ if (!raw) return null;
1336
+ const parsed = JSON.parse(raw);
1337
+ if (parsed.name && parsed.email) return parsed;
1338
+ return null;
1339
+ } catch {
1340
+ return null;
1341
+ }
1342
+ }
1343
+ function saveIdentity(identity) {
1344
+ try {
1345
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(identity));
1346
+ } catch {
1347
+ }
1348
+ }
1349
+
1350
+ // src/dom/fuzzy.ts
1351
+ function editDistance(a, b) {
1352
+ if (a === b) return 0;
1353
+ if (a.length === 0) return b.length;
1354
+ if (b.length === 0) return a.length;
1355
+ if (a.length > b.length) {
1356
+ const t = a;
1357
+ a = b;
1358
+ b = t;
1359
+ }
1360
+ const aLen = a.length;
1361
+ const bLen = b.length;
1362
+ let prev = new Array(aLen + 1);
1363
+ for (let k = 0; k <= aLen; k++) prev[k] = k;
1364
+ let curr = new Array(aLen + 1);
1365
+ for (let j = 1; j <= bLen; j++) {
1366
+ curr[0] = j;
1367
+ for (let i = 1; i <= aLen; i++) {
1368
+ curr[i] = a[i - 1] === b[j - 1] ? prev[i - 1] : 1 + Math.min(prev[i - 1], prev[i], curr[i - 1]);
1369
+ }
1370
+ const tmp = prev;
1371
+ prev = curr;
1372
+ curr = tmp;
1373
+ }
1374
+ return prev[aLen];
1375
+ }
1376
+ function similarity(a, b) {
1377
+ if (a === b) return 1;
1378
+ const maxLen = Math.max(a.length, b.length);
1379
+ if (maxLen === 0) return 1;
1380
+ return 1 - editDistance(a, b) / maxLen;
1381
+ }
1382
+ function fuzzyIncludes(haystack, needle, minScore = 0.6) {
1383
+ if (!needle || !haystack) return 0;
1384
+ if (haystack.includes(needle)) return 1;
1385
+ const nLen = needle.length;
1386
+ if (nLen > haystack.length) {
1387
+ const score = similarity(haystack, needle);
1388
+ return score >= minScore ? score : 0;
1389
+ }
1390
+ let best = 0;
1391
+ const capped = haystack.length > 500 ? haystack.slice(0, 500) : haystack;
1392
+ const limit = capped.length - nLen;
1393
+ for (let i = 0; i <= limit; i++) {
1394
+ const window2 = capped.slice(i, i + nLen);
1395
+ const score = similarity(window2, needle);
1396
+ if (score > best) best = score;
1397
+ if (best >= 0.95) break;
1398
+ }
1399
+ return best >= minScore ? best : 0;
1400
+ }
1401
+
1402
+ // src/dom/resolver.ts
1403
+ var MAX_SCAN_CANDIDATES = 300;
1404
+ var TEXT_MATCH_THRESHOLD = 0.3;
1405
+ function textMatches(el2, anchor) {
1406
+ if (!anchor.textSnippet) return true;
1407
+ const text = (el2.textContent?.trim() ?? "").slice(0, 500);
1408
+ return fuzzyIncludes(text, anchor.textSnippet, 0.5) > TEXT_MATCH_THRESHOLD;
1409
+ }
1410
+ function resolveAnchor(anchor) {
1411
+ if (anchor.elementId) {
1412
+ const el2 = document.getElementById(anchor.elementId);
1413
+ if (el2 && el2.tagName === anchor.elementTag && textMatches(el2, anchor)) {
1414
+ return { element: el2, confidence: 1, strategy: "id" };
1415
+ }
1416
+ }
1417
+ try {
1418
+ const el2 = document.querySelector(anchor.cssSelector);
1419
+ if (el2 && el2.tagName === anchor.elementTag && textMatches(el2, anchor)) {
1420
+ return { element: el2, confidence: 0.95, strategy: "css" };
1421
+ }
1422
+ } catch {
1423
+ }
1424
+ try {
1425
+ const result = document.evaluate(anchor.xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
1426
+ const el2 = result.singleNodeValue;
1427
+ if (el2 instanceof Element && el2.tagName === anchor.elementTag && textMatches(el2, anchor)) {
1428
+ return { element: el2, confidence: 0.9, strategy: "xpath" };
1429
+ }
1430
+ } catch {
1431
+ }
1432
+ return smartScan(anchor);
1433
+ }
1434
+ function smartScan(anchor) {
1435
+ const tag = anchor.elementTag.toLowerCase();
1436
+ const candidates = document.querySelectorAll(tag);
1437
+ if (candidates.length === 0) return null;
1438
+ let bestElement = null;
1439
+ let bestScore = 0;
1440
+ const limit = Math.min(candidates.length, MAX_SCAN_CANDIDATES);
1441
+ for (let i = 0; i < limit; i++) {
1442
+ const el2 = candidates[i];
1443
+ const score = scoreCandidate(el2, anchor);
1444
+ if (score > bestScore) {
1445
+ bestScore = score;
1446
+ bestElement = el2;
1447
+ if (bestScore >= 0.85) break;
1448
+ }
1449
+ }
1450
+ if (!bestElement || bestScore < 0.4) return null;
1451
+ return {
1452
+ element: bestElement,
1453
+ confidence: Math.min(bestScore, 0.85),
1454
+ strategy: "scan"
1455
+ };
1456
+ }
1457
+ function scoreCandidate(candidate, anchor) {
1458
+ let score = 0;
1459
+ let totalWeight = 0;
1460
+ const candidateText = (candidate.textContent?.trim() ?? "").slice(0, 500);
1461
+ if (anchor.textSnippet) {
1462
+ totalWeight += 40;
1463
+ score += fuzzyIncludes(candidateText, anchor.textSnippet, 0.5) * 40;
1464
+ }
1465
+ if (anchor.fingerprint) {
1466
+ totalWeight += 20;
1467
+ score += scoreFingerprint(candidate, anchor.fingerprint) * 20;
1468
+ }
1469
+ if (anchor.textPrefix || anchor.textSuffix) {
1470
+ totalWeight += 20;
1471
+ let contextScore = 0;
1472
+ let contextParts = 0;
1473
+ if (anchor.textPrefix) {
1474
+ const prevText = adjacentText(candidate, "before");
1475
+ contextScore += prevText ? similarity(prevText, anchor.textPrefix) : 0;
1476
+ contextParts++;
1477
+ }
1478
+ if (anchor.textSuffix) {
1479
+ const nextText = adjacentText(candidate, "after");
1480
+ contextScore += nextText ? similarity(nextText, anchor.textSuffix) : 0;
1481
+ contextParts++;
1482
+ }
1483
+ if (contextParts > 0) {
1484
+ score += contextScore / contextParts * 20;
1485
+ }
1486
+ }
1487
+ if (anchor.neighborText) {
1488
+ totalWeight += 20;
1489
+ const candidateNeighbor = neighborText(candidate);
1490
+ score += candidateNeighbor ? similarity(candidateNeighbor, anchor.neighborText) * 20 : 0;
1491
+ }
1492
+ return totalWeight > 0 ? score / totalWeight : 0;
1493
+ }
1494
+ function resolveAnnotation(anchor, rect) {
1495
+ const resolution = resolveAnchor(anchor);
1496
+ if (!resolution) return null;
1497
+ const bounds = resolution.element.getBoundingClientRect();
1498
+ const absoluteRect = new DOMRect(
1499
+ bounds.x + rect.xPct * bounds.width,
1500
+ bounds.y + rect.yPct * bounds.height,
1501
+ rect.wPct * bounds.width,
1502
+ rect.hPct * bounds.height
1503
+ );
1504
+ return {
1505
+ element: resolution.element,
1506
+ rect: absoluteRect,
1507
+ confidence: resolution.confidence,
1508
+ strategy: resolution.strategy
1509
+ };
1510
+ }
1511
+
1512
+ // src/markers.ts
1513
+ function toAnchorData(a) {
1514
+ return {
1515
+ cssSelector: a.cssSelector,
1516
+ xpath: a.xpath,
1517
+ textSnippet: a.textSnippet,
1518
+ elementTag: a.elementTag,
1519
+ elementId: a.elementId ?? void 0,
1520
+ textPrefix: a.textPrefix,
1521
+ textSuffix: a.textSuffix,
1522
+ fingerprint: a.fingerprint,
1523
+ neighborText: a.neighborText
1524
+ };
1525
+ }
1526
+ function toRectData(a) {
1527
+ return { xPct: a.xPct, yPct: a.yPct, wPct: a.wPct, hPct: a.hPct };
1528
+ }
1529
+ var MARKER_OFFSET = 13;
1530
+ function markerPosition(rect) {
1531
+ return {
1532
+ top: rect.top + window.scrollY - MARKER_OFFSET,
1533
+ left: rect.right + window.scrollX - MARKER_OFFSET
1534
+ };
1535
+ }
1536
+ function clusterMarker(cluster, i) {
1537
+ return cluster.entries[i].elements[cluster.elementIndices[i]];
1538
+ }
1539
+ var HIGHLIGHT_FADE = 300;
1540
+ var REPOSITION_DEBOUNCE = 200;
1541
+ var LOW_CONFIDENCE_THRESHOLD = 0.7;
1542
+ var CLUSTER_DISTANCE = 28;
1543
+ var FAN_SPACING = 32;
1544
+ var MarkerManager = class {
1545
+ constructor(colors, tooltip, bus) {
1546
+ this.colors = colors;
1547
+ this.tooltip = tooltip;
1548
+ this.bus = bus;
1549
+ this.container = el("div", {
1550
+ style: "position:absolute;top:0;left:0;pointer-events:none;z-index:2147483646;"
1551
+ });
1552
+ this.container.id = "siteping-markers";
1553
+ document.body.appendChild(this.container);
1554
+ this.bus.on("annotations:toggle", (visible) => {
1555
+ this.container.style.display = visible ? "block" : "none";
1556
+ });
1557
+ this.resizeHandler = () => this.scheduleReposition();
1558
+ window.addEventListener("resize", this.resizeHandler, { passive: true });
1559
+ this.mutationObserver = new MutationObserver((mutations) => {
1560
+ const isWidgetMutation = mutations.every(
1561
+ (m) => this.container.contains(m.target) || this.tooltip.contains(m.target)
1562
+ );
1563
+ if (!isWidgetMutation) this.scheduleReposition();
1564
+ });
1565
+ this.mutationObserver.observe(document.body, {
1566
+ childList: true,
1567
+ subtree: true,
1568
+ attributes: false,
1569
+ characterData: false
1570
+ });
1571
+ this.onDocumentClickForClusters = (e) => {
1572
+ if (this.container.contains(e.target)) return;
1573
+ this.collapseAllClusters();
1574
+ };
1575
+ document.addEventListener("click", this.onDocumentClickForClusters);
1576
+ }
1577
+ colors;
1578
+ tooltip;
1579
+ bus;
1580
+ container;
1581
+ entries = [];
1582
+ highlightElements = [];
1583
+ pinnedFeedback = null;
1584
+ onDocumentClick = null;
1585
+ repositionTimer = null;
1586
+ mutationObserver = null;
1587
+ resizeHandler = null;
1588
+ clusters = [];
1589
+ onDocumentClickForClusters = null;
1590
+ get count() {
1591
+ return this.entries.length;
1592
+ }
1593
+ scheduleReposition() {
1594
+ if (this.repositionTimer) return;
1595
+ this.repositionTimer = setTimeout(() => {
1596
+ this.repositionTimer = null;
1597
+ this.repositionAll();
1598
+ }, REPOSITION_DEBOUNCE);
1599
+ }
1600
+ repositionAll() {
1601
+ for (const entry of this.entries) {
1602
+ for (let i = 0; i < entry.feedback.annotations.length; i++) {
1603
+ const markerEl = entry.elements[i];
1604
+ if (!markerEl) continue;
1605
+ const annotation = entry.feedback.annotations[i];
1606
+ const resolved = resolveAnnotation(toAnchorData(annotation), toRectData(annotation));
1607
+ if (!resolved) {
1608
+ markerEl.style.display = "none";
1609
+ continue;
1610
+ }
1611
+ const pos = markerPosition(resolved.rect);
1612
+ entry.baseTop = pos.top;
1613
+ entry.baseLeft = pos.left;
1614
+ markerEl.style.display = "flex";
1615
+ this.applyConfidenceStyle(markerEl, resolved.confidence, entry.feedback);
1616
+ }
1617
+ }
1618
+ this.applyClusterPositions();
1619
+ }
1620
+ applyClusterPositions() {
1621
+ for (const cluster of this.clusters) {
1622
+ if (cluster.expanded) {
1623
+ this.applyFanPositions(cluster);
1624
+ } else {
1625
+ this.applyStackPositions(cluster);
1626
+ }
1627
+ }
1628
+ }
1629
+ render(feedbacks) {
1630
+ this.clear();
1631
+ feedbacks.forEach((feedback, i) => {
1632
+ const entry = this.buildEntry(feedback, i + 1);
1633
+ this.entries.push(entry);
1634
+ });
1635
+ this.buildClusters();
1636
+ }
1637
+ addFeedback(feedback, index2) {
1638
+ const entry = this.buildEntry(feedback, index2);
1639
+ for (const m of entry.elements) {
1640
+ m.style.animation = "sp-marker-in 0.35s cubic-bezier(0.34,1.56,0.64,1) both";
1641
+ }
1642
+ this.entries.push(entry);
1643
+ this.buildClusters();
1644
+ }
1645
+ buildEntry(feedback, index2) {
1646
+ const entry = { feedback, elements: [], baseTop: 0, baseLeft: 0 };
1647
+ for (const annotation of feedback.annotations) {
1648
+ const resolved = resolveAnnotation(toAnchorData(annotation), toRectData(annotation));
1649
+ if (!resolved) continue;
1650
+ const pos = markerPosition(resolved.rect);
1651
+ entry.baseTop = pos.top;
1652
+ entry.baseLeft = pos.left;
1653
+ const marker = this.createMarker(index2, feedback, pos);
1654
+ this.applyConfidenceStyle(marker, resolved.confidence, feedback);
1655
+ this.container.appendChild(marker);
1656
+ entry.elements.push(marker);
1657
+ }
1658
+ return entry;
1659
+ }
1660
+ buildClusters() {
1661
+ for (const badge of this.container.querySelectorAll(".sp-cluster-badge")) {
1662
+ badge.remove();
1663
+ }
1664
+ const allItems = [];
1665
+ for (const entry of this.entries) {
1666
+ for (let i = 0; i < entry.elements.length; i++) {
1667
+ allItems.push({ entry, elIdx: i });
1668
+ }
1669
+ }
1670
+ const used = /* @__PURE__ */ new Set();
1671
+ this.clusters = [];
1672
+ for (let i = 0; i < allItems.length; i++) {
1673
+ if (used.has(i)) continue;
1674
+ const cluster = {
1675
+ entries: [allItems[i].entry],
1676
+ elementIndices: [allItems[i].elIdx],
1677
+ expanded: false
1678
+ };
1679
+ used.add(i);
1680
+ for (let j = i + 1; j < allItems.length; j++) {
1681
+ if (used.has(j)) continue;
1682
+ const a = allItems[i].entry;
1683
+ const b = allItems[j].entry;
1684
+ const dist = Math.sqrt((a.baseLeft - b.baseLeft) ** 2 + (a.baseTop - b.baseTop) ** 2);
1685
+ if (dist < CLUSTER_DISTANCE) {
1686
+ cluster.entries.push(b);
1687
+ cluster.elementIndices.push(allItems[j].elIdx);
1688
+ used.add(j);
1689
+ }
1690
+ }
1691
+ this.clusters.push(cluster);
1692
+ }
1693
+ for (const cluster of this.clusters) {
1694
+ if (cluster.entries.length <= 1) continue;
1695
+ this.applyStackPositions(cluster);
1696
+ this.addClusterBadge(cluster);
1697
+ }
1698
+ }
1699
+ applyStackPositions(cluster) {
1700
+ const { baseTop, baseLeft } = cluster.entries[0];
1701
+ const isSolo = cluster.entries.length <= 1;
1702
+ for (let i = 0; i < cluster.entries.length; i++) {
1703
+ const m = clusterMarker(cluster, i);
1704
+ if (!m) continue;
1705
+ m.style.top = `${baseTop + (isSolo ? 0 : i * 3)}px`;
1706
+ m.style.left = `${baseLeft + (isSolo ? 0 : i * 3)}px`;
1707
+ m.style.zIndex = String(i + 1);
1708
+ }
1709
+ }
1710
+ applyFanPositions(cluster) {
1711
+ const { baseTop, baseLeft } = cluster.entries[0];
1712
+ const count = cluster.entries.length;
1713
+ const totalWidth = (count - 1) * FAN_SPACING;
1714
+ const startLeft = baseLeft - totalWidth / 2;
1715
+ for (let i = 0; i < count; i++) {
1716
+ const m = clusterMarker(cluster, i);
1717
+ if (!m) continue;
1718
+ m.style.top = `${baseTop}px`;
1719
+ m.style.left = `${startLeft + i * FAN_SPACING}px`;
1720
+ m.style.zIndex = String(10 + i);
1721
+ }
1722
+ }
1723
+ addClusterBadge(cluster) {
1724
+ const topMarker = clusterMarker(cluster, cluster.entries.length - 1);
1725
+ if (!topMarker) return;
1726
+ const badge = el("div", {
1727
+ class: "sp-cluster-badge",
1728
+ style: `
1729
+ position:absolute;top:-6px;right:-6px;
1730
+ min-width:16px;height:16px;padding:0 4px;
1731
+ border-radius:9999px;
1732
+ background:${this.colors.accent};color:#fff;
1733
+ font-size:10px;font-weight:700;
1734
+ display:flex;align-items:center;justify-content:center;
1735
+ border:1.5px solid #fff;
1736
+ pointer-events:none;
1737
+ font-family:"Inter",system-ui,-apple-system,sans-serif;
1738
+ line-height:1;
1739
+ `
1740
+ });
1741
+ setText(badge, String(cluster.entries.length));
1742
+ topMarker.appendChild(badge);
1743
+ }
1744
+ setBadgesVisible(cluster, visible) {
1745
+ for (let i = 0; i < cluster.entries.length; i++) {
1746
+ const badge = clusterMarker(cluster, i)?.querySelector(".sp-cluster-badge");
1747
+ if (badge) badge.style.display = visible ? "flex" : "none";
1748
+ }
1749
+ }
1750
+ findCluster(marker) {
1751
+ for (const cluster of this.clusters) {
1752
+ if (cluster.entries.length <= 1) continue;
1753
+ for (let i = 0; i < cluster.entries.length; i++) {
1754
+ if (clusterMarker(cluster, i) === marker) return cluster;
1755
+ }
1756
+ }
1757
+ return null;
1758
+ }
1759
+ handleClusterClick(marker, e) {
1760
+ const cluster = this.findCluster(marker);
1761
+ if (!cluster) return false;
1762
+ if (!cluster.expanded) {
1763
+ e.stopPropagation();
1764
+ this.collapseAllClusters();
1765
+ cluster.expanded = true;
1766
+ this.applyFanPositions(cluster);
1767
+ this.setBadgesVisible(cluster, false);
1768
+ return true;
1769
+ }
1770
+ return false;
1771
+ }
1772
+ collapseCluster(cluster) {
1773
+ if (!cluster.expanded) return;
1774
+ cluster.expanded = false;
1775
+ this.applyStackPositions(cluster);
1776
+ this.setBadgesVisible(cluster, true);
1777
+ }
1778
+ collapseAllClusters() {
1779
+ for (const cluster of this.clusters) {
1780
+ this.collapseCluster(cluster);
1781
+ }
1782
+ }
1783
+ applyConfidenceStyle(marker, confidence, feedback) {
1784
+ const isResolved = feedback.status === "resolved";
1785
+ if (confidence < LOW_CONFIDENCE_THRESHOLD && !isResolved) {
1786
+ marker.style.borderStyle = "dashed";
1787
+ marker.style.opacity = "0.7";
1788
+ marker.title = `Position approximative (confiance : ${Math.round(confidence * 100)}%)`;
1789
+ } else {
1790
+ marker.style.borderStyle = "solid";
1791
+ marker.style.opacity = "1";
1792
+ marker.title = "";
1793
+ }
1794
+ }
1795
+ createMarker(number, feedback, pos) {
1796
+ const typeColor = getTypeColor(feedback.type, this.colors);
1797
+ const isResolved = feedback.status === "resolved";
1798
+ const marker = el("div", {
1799
+ style: `
1800
+ position:absolute;
1801
+ top:${pos.top}px;
1802
+ left:${pos.left}px;
1803
+ width:26px;height:26px;
1804
+ border-radius:50%;
1805
+ background:${isResolved ? "rgba(241,245,249,0.9)" : "rgba(255,255,255,0.92)"};
1806
+ backdrop-filter:blur(12px);
1807
+ -webkit-backdrop-filter:blur(12px);
1808
+ border:2px solid ${isResolved ? "#cbd5e1" : typeColor};
1809
+ display:flex;align-items:center;justify-content:center;
1810
+ font-family:"Inter",system-ui,-apple-system,sans-serif;
1811
+ font-size:11px;font-weight:700;
1812
+ color:${isResolved ? "#94a3b8" : typeColor};
1813
+ cursor:pointer;pointer-events:auto;
1814
+ box-shadow:${isResolved ? "0 2px 8px rgba(0,0,0,0.06)" : `0 2px 12px ${typeColor}25, 0 2px 6px rgba(0,0,0,0.06)`};
1815
+ transition:top 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), left 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.15s ease, box-shadow 0.15s ease;
1816
+ user-select:none;
1817
+ -webkit-font-smoothing:antialiased;
1818
+ `
1819
+ });
1820
+ marker.dataset.feedbackId = feedback.id;
1821
+ setText(marker, isResolved ? "\u2713" : String(number));
1822
+ marker.addEventListener("mouseenter", () => {
1823
+ marker.style.transform = "scale(1.2)";
1824
+ marker.style.boxShadow = isResolved ? "0 4px 16px rgba(0,0,0,0.1)" : `0 4px 20px ${typeColor}35, 0 4px 12px rgba(0,0,0,0.08)`;
1825
+ this.tooltip.show(feedback, marker.getBoundingClientRect());
1826
+ if (!this.pinnedFeedback) this.showHighlight(feedback);
1827
+ });
1828
+ marker.addEventListener("mouseleave", () => {
1829
+ marker.style.transform = "scale(1)";
1830
+ marker.style.boxShadow = isResolved ? "0 2px 8px rgba(0,0,0,0.06)" : `0 2px 12px ${typeColor}25, 0 2px 6px rgba(0,0,0,0.06)`;
1831
+ this.tooltip.scheduleHide();
1832
+ if (!this.pinnedFeedback) this.clearHighlight();
1833
+ });
1834
+ marker.addEventListener("click", (e) => {
1835
+ if (this.handleClusterClick(marker, e)) return;
1836
+ this.pinHighlight(feedback);
1837
+ this.bus.emit("panel:toggle", true);
1838
+ marker.dispatchEvent(
1839
+ new CustomEvent("sp-marker-click", {
1840
+ detail: { feedbackId: feedback.id },
1841
+ bubbles: true
1842
+ })
1843
+ );
1844
+ });
1845
+ return marker;
1846
+ }
1847
+ highlight(feedbackId) {
1848
+ for (const entry of this.entries) {
1849
+ if (entry.feedback.id === feedbackId) {
1850
+ for (const markerEl of entry.elements) {
1851
+ markerEl.style.animation = "sp-pulse-ring 0.7s ease-out";
1852
+ markerEl.addEventListener(
1853
+ "animationend",
1854
+ () => {
1855
+ markerEl.style.animation = "";
1856
+ },
1857
+ { once: true }
1858
+ );
1859
+ }
1860
+ }
1861
+ }
1862
+ }
1863
+ showHighlight(feedback) {
1864
+ this.removeHighlightElements();
1865
+ for (const annotation of feedback.annotations) {
1866
+ const resolved = resolveAnnotation(toAnchorData(annotation), toRectData(annotation));
1867
+ if (!resolved) continue;
1868
+ const typeColor = getTypeColor(feedback.type, this.colors);
1869
+ const rect = resolved.rect;
1870
+ const highlight = el("div", {
1871
+ style: `
1872
+ position:absolute;
1873
+ top:${rect.top + window.scrollY}px;
1874
+ left:${rect.left + window.scrollX}px;
1875
+ width:${rect.width}px;height:${rect.height}px;
1876
+ border:2px solid ${typeColor};
1877
+ background:${typeColor}0c;
1878
+ border-radius:8px;
1879
+ pointer-events:none;z-index:-1;
1880
+ opacity:0;
1881
+ box-shadow:0 0 16px ${typeColor}20;
1882
+ transition:opacity ${HIGHLIGHT_FADE}ms ease;
1883
+ `
1884
+ });
1885
+ this.container.appendChild(highlight);
1886
+ this.highlightElements.push(highlight);
1887
+ highlight.offsetHeight;
1888
+ highlight.style.opacity = "1";
1889
+ }
1890
+ }
1891
+ pinHighlight(feedback) {
1892
+ this.unpinHighlight();
1893
+ this.showHighlight(feedback);
1894
+ this.pinnedFeedback = feedback;
1895
+ this.onDocumentClick = (e) => {
1896
+ if (this.container.contains(e.target)) return;
1897
+ this.unpinHighlight();
1898
+ };
1899
+ document.addEventListener("click", this.onDocumentClick, { capture: true });
1900
+ }
1901
+ unpinHighlight() {
1902
+ if (this.onDocumentClick) {
1903
+ document.removeEventListener("click", this.onDocumentClick, { capture: true });
1904
+ this.onDocumentClick = null;
1905
+ }
1906
+ this.pinnedFeedback = null;
1907
+ this.clearHighlight();
1908
+ }
1909
+ clearHighlight() {
1910
+ for (const h of this.highlightElements) {
1911
+ h.style.opacity = "0";
1912
+ setTimeout(() => h.remove(), HIGHLIGHT_FADE);
1913
+ }
1914
+ this.highlightElements = [];
1915
+ }
1916
+ removeHighlightElements() {
1917
+ for (const h of this.highlightElements) h.remove();
1918
+ this.highlightElements = [];
1919
+ }
1920
+ clear() {
1921
+ this.unpinHighlight();
1922
+ this.container.replaceChildren();
1923
+ this.entries = [];
1924
+ this.clusters = [];
1925
+ }
1926
+ destroy() {
1927
+ this.unpinHighlight();
1928
+ if (this.repositionTimer) clearTimeout(this.repositionTimer);
1929
+ if (this.resizeHandler) window.removeEventListener("resize", this.resizeHandler);
1930
+ if (this.onDocumentClickForClusters) document.removeEventListener("click", this.onDocumentClickForClusters);
1931
+ this.mutationObserver?.disconnect();
1932
+ this.container.remove();
1933
+ }
1934
+ };
1935
+
1936
+ // src/panel.ts
1937
+ var TYPE_LABELS = {
1938
+ question: "Question",
1939
+ changement: "Changement",
1940
+ bug: "Bug",
1941
+ autre: "Autre"
1942
+ };
1943
+ var Panel = class {
1944
+ constructor(shadowRoot, colors, bus, apiClient, projectName, markers) {
1945
+ this.colors = colors;
1946
+ this.bus = bus;
1947
+ this.apiClient = apiClient;
1948
+ this.projectName = projectName;
1949
+ this.markers = markers;
1950
+ this.root = el("div", { class: "sp-panel" });
1951
+ const header = el("div", { class: "sp-panel-header" });
1952
+ const title = el("span", { class: "sp-panel-title" });
1953
+ setText(title, "Feedbacks");
1954
+ const closeBtn = document.createElement("button");
1955
+ closeBtn.className = "sp-panel-close";
1956
+ closeBtn.setAttribute("aria-label", "Fermer le panneau");
1957
+ closeBtn.appendChild(parseSvg(ICON_CLOSE));
1958
+ closeBtn.addEventListener("click", () => this.close());
1959
+ this.deleteAllBtn = document.createElement("button");
1960
+ this.deleteAllBtn.className = "sp-btn-delete-all";
1961
+ this.deleteAllBtn.setAttribute("aria-label", "Tout supprimer");
1962
+ this.deleteAllBtn.appendChild(parseSvg(ICON_TRASH));
1963
+ const deleteAllLabel = document.createElement("span");
1964
+ setText(deleteAllLabel, " Tout supprimer");
1965
+ this.deleteAllBtn.appendChild(deleteAllLabel);
1966
+ this.deleteAllBtn.addEventListener("click", () => this.confirmDeleteAll());
1967
+ const headerRight = el("div", { class: "sp-panel-header-right" });
1968
+ headerRight.appendChild(this.deleteAllBtn);
1969
+ headerRight.appendChild(closeBtn);
1970
+ header.appendChild(title);
1971
+ header.appendChild(headerRight);
1972
+ const filters = el("div", { class: "sp-filters" });
1973
+ const searchWrap = el("div", { class: "sp-search-wrap" });
1974
+ const searchIcon = parseSvg(ICON_SEARCH);
1975
+ searchIcon.setAttribute("class", "sp-search-icon");
1976
+ this.searchInput = document.createElement("input");
1977
+ this.searchInput.type = "text";
1978
+ this.searchInput.className = "sp-search";
1979
+ this.searchInput.placeholder = "Rechercher...";
1980
+ this.searchInput.setAttribute("aria-label", "Rechercher dans les feedbacks");
1981
+ this.searchInput.addEventListener("input", () => {
1982
+ if (this.searchTimeout) clearTimeout(this.searchTimeout);
1983
+ this.searchTimeout = setTimeout(() => this.loadFeedbacks(), 200);
1984
+ });
1985
+ searchWrap.appendChild(searchIcon);
1986
+ searchWrap.appendChild(this.searchInput);
1987
+ const chips = el("div", { class: "sp-chips" });
1988
+ const chipOptions = [
1989
+ { value: "all", label: "Tous" },
1990
+ { value: "question", label: "Question" },
1991
+ { value: "changement", label: "Changement" },
1992
+ { value: "bug", label: "Bug" },
1993
+ { value: "autre", label: "Autre" }
1994
+ ];
1995
+ for (const option of chipOptions) {
1996
+ const chip = document.createElement("button");
1997
+ chip.className = `sp-chip ${option.value === "all" ? "sp-chip--active" : ""}`;
1998
+ if (option.value !== "all") {
1999
+ chip.style.borderColor = getTypeColor(option.value, this.colors);
2000
+ }
2001
+ setText(chip, option.label);
2002
+ chip.dataset.filter = option.value;
2003
+ chip.addEventListener("click", () => this.toggleFilter(option.value, chips));
2004
+ chips.appendChild(chip);
2005
+ }
2006
+ filters.appendChild(searchWrap);
2007
+ filters.appendChild(chips);
2008
+ this.listContainer = el("div", { class: "sp-list" });
2009
+ this.root.appendChild(header);
2010
+ this.root.appendChild(filters);
2011
+ this.root.appendChild(this.listContainer);
2012
+ shadowRoot.appendChild(this.root);
2013
+ this.bus.on("panel:toggle", (open) => {
2014
+ open ? this.open() : this.close();
2015
+ });
2016
+ shadowRoot.addEventListener("keydown", (e) => {
2017
+ if (e.key === "Escape" && this.isOpen) this.close();
2018
+ });
2019
+ this.onMarkerClick = ((e) => {
2020
+ this.scrollToFeedback(e.detail.feedbackId);
2021
+ });
2022
+ document.addEventListener("sp-marker-click", this.onMarkerClick);
2023
+ }
2024
+ colors;
2025
+ bus;
2026
+ apiClient;
2027
+ projectName;
2028
+ markers;
2029
+ root;
2030
+ listContainer;
2031
+ searchInput;
2032
+ deleteAllBtn;
2033
+ activeFilters = /* @__PURE__ */ new Set(["all"]);
2034
+ feedbacks = [];
2035
+ isOpen = false;
2036
+ searchTimeout = null;
2037
+ onMarkerClick;
2038
+ async open() {
2039
+ if (this.isOpen) return;
2040
+ this.isOpen = true;
2041
+ this.root.classList.add("sp-panel--open");
2042
+ this.bus.emit("open");
2043
+ await this.loadFeedbacks();
2044
+ }
2045
+ close() {
2046
+ if (!this.isOpen) return;
2047
+ this.isOpen = false;
2048
+ this.root.classList.remove("sp-panel--open");
2049
+ this.bus.emit("close");
2050
+ }
2051
+ showLoading() {
2052
+ this.listContainer.replaceChildren();
2053
+ const loading = el("div", { class: "sp-loading" });
2054
+ const spinner = el("div", { class: "sp-spinner" });
2055
+ loading.appendChild(spinner);
2056
+ this.listContainer.appendChild(loading);
2057
+ }
2058
+ showError() {
2059
+ this.listContainer.replaceChildren();
2060
+ const empty = el("div", { class: "sp-empty" });
2061
+ const text = el("div", { class: "sp-empty-text" });
2062
+ setText(text, "Erreur de chargement");
2063
+ const retryBtn = document.createElement("button");
2064
+ retryBtn.className = "sp-btn-ghost";
2065
+ retryBtn.style.marginTop = "8px";
2066
+ setText(retryBtn, "R\xE9essayer");
2067
+ retryBtn.addEventListener("click", () => this.loadFeedbacks());
2068
+ empty.appendChild(text);
2069
+ empty.appendChild(retryBtn);
2070
+ this.listContainer.appendChild(empty);
2071
+ }
2072
+ async loadFeedbacks() {
2073
+ const search = this.searchInput.value.trim() || void 0;
2074
+ const typeFilter = this.activeFilters.has("all") ? void 0 : Array.from(this.activeFilters)[0];
2075
+ const options = { limit: 50 };
2076
+ if (typeFilter) options.type = typeFilter;
2077
+ if (search) options.search = search;
2078
+ const hasContent = this.feedbacks.length > 0;
2079
+ if (!hasContent) this.showLoading();
2080
+ try {
2081
+ const { feedbacks } = await this.apiClient.getFeedbacks(this.projectName, options);
2082
+ this.feedbacks = feedbacks;
2083
+ this.renderList();
2084
+ this.markers.render(feedbacks);
2085
+ } catch (error) {
2086
+ if (!hasContent) this.showError();
2087
+ this.bus.emit("feedback:error", error instanceof Error ? error : new Error(String(error)));
2088
+ }
2089
+ }
2090
+ renderList() {
2091
+ this.listContainer.replaceChildren();
2092
+ if (this.feedbacks.length === 0) {
2093
+ const empty = el("div", { class: "sp-empty" });
2094
+ const emptyText = el("div", { class: "sp-empty-text" });
2095
+ setText(emptyText, "Aucun feedback pour le moment");
2096
+ empty.appendChild(emptyText);
2097
+ this.listContainer.appendChild(empty);
2098
+ return;
2099
+ }
2100
+ this.feedbacks.forEach((feedback, index2) => {
2101
+ const card = this.createCard(feedback, index2 + 1);
2102
+ card.style.setProperty("--sp-card-i", String(index2));
2103
+ this.listContainer.appendChild(card);
2104
+ });
2105
+ }
2106
+ createCard(feedback, number) {
2107
+ const isResolved = feedback.status === "resolved";
2108
+ const typeColor = getTypeColor(feedback.type, this.colors);
2109
+ const card = el("div", {
2110
+ class: `sp-card ${isResolved ? "sp-card--resolved" : ""}`
2111
+ });
2112
+ card.dataset.feedbackId = feedback.id;
2113
+ const bar = el("div", { class: "sp-card-bar" });
2114
+ bar.style.background = isResolved ? "#9ca3af" : typeColor;
2115
+ const body = el("div", { class: "sp-card-body" });
2116
+ const header = el("div", { class: "sp-card-header" });
2117
+ const num = el("span", { class: "sp-card-number" });
2118
+ setText(num, `#${number}`);
2119
+ const badge = el("span", { class: "sp-badge" });
2120
+ const typeBg = getTypeBgColor(feedback.type, this.colors);
2121
+ badge.style.background = typeBg;
2122
+ badge.style.color = typeColor;
2123
+ setText(badge, TYPE_LABELS[feedback.type] ?? feedback.type);
2124
+ const date = el("span", { class: "sp-card-date" });
2125
+ setText(date, formatRelativeDate(feedback.createdAt));
2126
+ header.appendChild(num);
2127
+ header.appendChild(badge);
2128
+ header.appendChild(date);
2129
+ const message = el("div", { class: "sp-card-message" });
2130
+ setText(message, feedback.message);
2131
+ const expandBtn = document.createElement("button");
2132
+ expandBtn.className = "sp-card-expand";
2133
+ setText(expandBtn, "Voir plus");
2134
+ expandBtn.style.display = "none";
2135
+ expandBtn.setAttribute("aria-expanded", "false");
2136
+ expandBtn.addEventListener("click", (e) => {
2137
+ e.stopPropagation();
2138
+ const isExpanded = message.classList.toggle("sp-card-message--expanded");
2139
+ setText(expandBtn, isExpanded ? "Voir moins" : "Voir plus");
2140
+ expandBtn.setAttribute("aria-expanded", String(isExpanded));
2141
+ });
2142
+ requestAnimationFrame(() => {
2143
+ if (message.scrollHeight > message.clientHeight) {
2144
+ expandBtn.style.display = "block";
2145
+ }
2146
+ });
2147
+ const footer = el("div", { class: "sp-card-footer" });
2148
+ const resolveBtn = document.createElement("button");
2149
+ resolveBtn.className = "sp-btn-resolve";
2150
+ if (isResolved) {
2151
+ resolveBtn.appendChild(parseSvg(ICON_UNDO));
2152
+ const span = document.createElement("span");
2153
+ setText(span, " Rouvrir");
2154
+ resolveBtn.appendChild(span);
2155
+ } else {
2156
+ resolveBtn.appendChild(parseSvg(ICON_CHECK));
2157
+ const span = document.createElement("span");
2158
+ setText(span, " R\xE9soudre");
2159
+ resolveBtn.appendChild(span);
2160
+ }
2161
+ resolveBtn.addEventListener("click", async (e) => {
2162
+ e.stopPropagation();
2163
+ await this.toggleResolve(feedback, resolveBtn);
2164
+ });
2165
+ const deleteBtn = document.createElement("button");
2166
+ deleteBtn.className = "sp-btn-delete";
2167
+ deleteBtn.appendChild(parseSvg(ICON_TRASH));
2168
+ const deleteLabel = document.createElement("span");
2169
+ setText(deleteLabel, " Supprimer");
2170
+ deleteBtn.appendChild(deleteLabel);
2171
+ deleteBtn.addEventListener("click", async (e) => {
2172
+ e.stopPropagation();
2173
+ await this.deleteFeedback(feedback, deleteBtn);
2174
+ });
2175
+ footer.appendChild(resolveBtn);
2176
+ footer.appendChild(deleteBtn);
2177
+ body.appendChild(header);
2178
+ body.appendChild(message);
2179
+ body.appendChild(expandBtn);
2180
+ body.appendChild(footer);
2181
+ card.appendChild(bar);
2182
+ card.appendChild(body);
2183
+ card.addEventListener("mouseenter", () => {
2184
+ this.markers.highlight(feedback.id);
2185
+ });
2186
+ card.addEventListener("click", () => {
2187
+ if (feedback.annotations.length > 0) {
2188
+ const ann = feedback.annotations[0];
2189
+ window.scrollTo({ left: ann.scrollX, top: ann.scrollY, behavior: "smooth" });
2190
+ this.markers.pinHighlight(feedback);
2191
+ }
2192
+ });
2193
+ return card;
2194
+ }
2195
+ async deleteFeedback(feedback, btn) {
2196
+ btn.disabled = true;
2197
+ try {
2198
+ await this.apiClient.deleteFeedback(feedback.id);
2199
+ this.bus.emit("feedback:deleted", feedback.id);
2200
+ await this.loadFeedbacks();
2201
+ } catch (error) {
2202
+ btn.disabled = false;
2203
+ this.bus.emit("feedback:error", error instanceof Error ? error : new Error(String(error)));
2204
+ }
2205
+ }
2206
+ async confirmDeleteAll() {
2207
+ const confirmed = await this.showConfirmDialog(
2208
+ "Tout supprimer",
2209
+ "Supprimer tous les feedbacks de ce projet ? Cette action est irr\xE9versible."
2210
+ );
2211
+ if (!confirmed) return;
2212
+ this.deleteAllBtn.disabled = true;
2213
+ try {
2214
+ await this.apiClient.deleteAllFeedbacks(this.projectName);
2215
+ this.bus.emit("feedback:all-deleted");
2216
+ await this.loadFeedbacks();
2217
+ } catch (error) {
2218
+ this.bus.emit("feedback:error", error instanceof Error ? error : new Error(String(error)));
2219
+ } finally {
2220
+ this.deleteAllBtn.disabled = false;
2221
+ }
2222
+ }
2223
+ showConfirmDialog(title, message) {
2224
+ return new Promise((resolve) => {
2225
+ const backdrop = el("div", { class: "sp-confirm-backdrop" });
2226
+ const titleId = `sp-confirm-title-${Date.now()}`;
2227
+ const messageId = `sp-confirm-msg-${Date.now()}`;
2228
+ const dialog = el("div", { class: "sp-confirm-dialog" });
2229
+ dialog.setAttribute("role", "alertdialog");
2230
+ dialog.setAttribute("aria-modal", "true");
2231
+ dialog.setAttribute("aria-labelledby", titleId);
2232
+ dialog.setAttribute("aria-describedby", messageId);
2233
+ const titleEl = el("div", { class: "sp-confirm-title" });
2234
+ titleEl.id = titleId;
2235
+ setText(titleEl, title);
2236
+ const messageEl = el("div", { class: "sp-confirm-message" });
2237
+ messageEl.id = messageId;
2238
+ setText(messageEl, message);
2239
+ const btnRow = el("div", { class: "sp-confirm-actions" });
2240
+ const cancelBtn = document.createElement("button");
2241
+ cancelBtn.type = "button";
2242
+ cancelBtn.className = "sp-btn-ghost";
2243
+ setText(cancelBtn, "Annuler");
2244
+ const confirmBtn = document.createElement("button");
2245
+ confirmBtn.type = "button";
2246
+ confirmBtn.className = "sp-btn-danger";
2247
+ setText(confirmBtn, "Supprimer");
2248
+ let closed = false;
2249
+ const close = (result) => {
2250
+ if (closed) return;
2251
+ closed = true;
2252
+ backdrop.removeEventListener("keydown", onKeydown);
2253
+ backdrop.style.opacity = "0";
2254
+ dialog.style.transform = "translateY(8px) scale(0.97)";
2255
+ setTimeout(() => {
2256
+ backdrop.remove();
2257
+ resolve(result);
2258
+ }, 200);
2259
+ };
2260
+ const onKeydown = (e) => {
2261
+ const ke = e;
2262
+ if (ke.key === "Escape") {
2263
+ close(false);
2264
+ return;
2265
+ }
2266
+ if (ke.key === "Tab") {
2267
+ ke.preventDefault();
2268
+ const active = backdrop.getRootNode().activeElement;
2269
+ if (active === cancelBtn) {
2270
+ confirmBtn.focus();
2271
+ } else {
2272
+ cancelBtn.focus();
2273
+ }
2274
+ }
2275
+ };
2276
+ backdrop.addEventListener("keydown", onKeydown);
2277
+ cancelBtn.addEventListener("click", () => close(false));
2278
+ confirmBtn.addEventListener("click", () => close(true));
2279
+ backdrop.addEventListener("click", (e) => {
2280
+ if (e.target === backdrop) close(false);
2281
+ });
2282
+ btnRow.appendChild(cancelBtn);
2283
+ btnRow.appendChild(confirmBtn);
2284
+ dialog.appendChild(titleEl);
2285
+ dialog.appendChild(messageEl);
2286
+ dialog.appendChild(btnRow);
2287
+ backdrop.appendChild(dialog);
2288
+ this.root.getRootNode() instanceof ShadowRoot ? this.root.getRootNode().appendChild(backdrop) : this.root.appendChild(backdrop);
2289
+ requestAnimationFrame(() => {
2290
+ backdrop.style.opacity = "1";
2291
+ dialog.style.transform = "translateY(0) scale(1)";
2292
+ cancelBtn.focus();
2293
+ });
2294
+ });
2295
+ }
2296
+ async toggleResolve(feedback, btn) {
2297
+ btn.disabled = true;
2298
+ try {
2299
+ const newResolved = feedback.status !== "resolved";
2300
+ await this.apiClient.resolveFeedback(feedback.id, newResolved);
2301
+ await this.loadFeedbacks();
2302
+ } catch (error) {
2303
+ btn.disabled = false;
2304
+ this.bus.emit("feedback:error", error instanceof Error ? error : new Error(String(error)));
2305
+ }
2306
+ }
2307
+ toggleFilter(value, container) {
2308
+ this.activeFilters.clear();
2309
+ this.activeFilters.add(value);
2310
+ const chips = container.querySelectorAll(".sp-chip");
2311
+ for (const chip of chips) {
2312
+ const isActive = this.activeFilters.has(chip.dataset.filter ?? "");
2313
+ chip.classList.toggle("sp-chip--active", isActive);
2314
+ }
2315
+ this.loadFeedbacks();
2316
+ }
2317
+ scrollToFeedback(feedbackId) {
2318
+ const escapedId = CSS.escape(feedbackId);
2319
+ const card = this.listContainer.querySelector(`[data-feedback-id="${escapedId}"]`);
2320
+ if (card) {
2321
+ card.scrollIntoView({ behavior: "smooth", block: "center" });
2322
+ card.classList.add("sp-anim-flash");
2323
+ card.addEventListener(
2324
+ "animationend",
2325
+ () => {
2326
+ card.classList.remove("sp-anim-flash");
2327
+ },
2328
+ { once: true }
2329
+ );
2330
+ }
2331
+ }
2332
+ /** Refresh the panel after a new feedback is submitted */
2333
+ async refresh() {
2334
+ if (this.isOpen) {
2335
+ await this.loadFeedbacks();
2336
+ }
2337
+ }
2338
+ destroy() {
2339
+ if (this.searchTimeout) clearTimeout(this.searchTimeout);
2340
+ document.removeEventListener("sp-marker-click", this.onMarkerClick);
2341
+ this.root.remove();
2342
+ }
2343
+ };
2344
+
2345
+ // src/styles/animations.ts
2346
+ var SPRING_LINEAR = `linear(0, 0.006, 0.025, 0.06, 0.11, 0.17, 0.25, 0.34, 0.45, 0.56, 0.67, 0.78, 0.88, 0.95, 1.01, 1.04, 1.05, 1.04, 1.02, 1, 0.99, 1)`;
2347
+ var EASE_OUT_EXPO = `cubic-bezier(0.16, 1, 0.3, 1)`;
2348
+ var SPRING_OVERSHOOT = `cubic-bezier(0.34, 1.56, 0.64, 1)`;
2349
+ var EASE_OUT_QUART = `cubic-bezier(0.25, 1, 0.5, 1)`;
2350
+ var ANIMATION_CSS = `
2351
+ /* ---- Keyframes ---- */
2352
+
2353
+ @keyframes sp-fab-in {
2354
+ from {
2355
+ transform: scale(0) rotate(-180deg);
2356
+ opacity: 0;
2357
+ }
2358
+ to {
2359
+ transform: scale(1) rotate(0deg);
2360
+ opacity: 1;
2361
+ }
2362
+ }
2363
+
2364
+ @keyframes sp-fab-glow {
2365
+ 0%, 100% { box-shadow: 0 4px 20px var(--sp-accent-glow), 0 2px 8px rgba(0, 0, 0, 0.08); }
2366
+ 50% { box-shadow: 0 4px 28px var(--sp-accent-glow), 0 2px 12px rgba(0, 0, 0, 0.1); }
2367
+ }
2368
+
2369
+ @keyframes sp-marker-in {
2370
+ 0% {
2371
+ transform: scale(0);
2372
+ opacity: 0;
2373
+ }
2374
+ 60% {
2375
+ transform: scale(1.2);
2376
+ opacity: 1;
2377
+ }
2378
+ 100% {
2379
+ transform: scale(1);
2380
+ }
2381
+ }
2382
+
2383
+ @keyframes sp-pulse-ring {
2384
+ 0% {
2385
+ box-shadow: 0 0 0 0 var(--sp-accent-glow);
2386
+ }
2387
+ 70% {
2388
+ box-shadow: 0 0 0 8px transparent;
2389
+ }
2390
+ 100% {
2391
+ box-shadow: 0 0 0 0 transparent;
2392
+ }
2393
+ }
2394
+
2395
+ @keyframes sp-flash-bg {
2396
+ 0% { background-color: var(--sp-accent-light); }
2397
+ 100% { background-color: transparent; }
2398
+ }
2399
+
2400
+ @keyframes sp-slide-up {
2401
+ from {
2402
+ transform: translateY(8px);
2403
+ opacity: 0;
2404
+ }
2405
+ to {
2406
+ transform: translateY(0);
2407
+ opacity: 1;
2408
+ }
2409
+ }
2410
+
2411
+ @keyframes sp-fade-in {
2412
+ from { opacity: 0; }
2413
+ to { opacity: 1; }
2414
+ }
2415
+
2416
+ @keyframes sp-shimmer {
2417
+ 0% { background-position: -200% 0; }
2418
+ 100% { background-position: 200% 0; }
2419
+ }
2420
+
2421
+ /* ---- Animation classes ---- */
2422
+
2423
+ .sp-anim-fab-in {
2424
+ animation: sp-fab-in 0.5s ${SPRING_LINEAR} both;
2425
+ }
2426
+
2427
+ .sp-anim-marker-in {
2428
+ animation: sp-marker-in 0.35s ${SPRING_OVERSHOOT} both;
2429
+ }
2430
+
2431
+ .sp-anim-pulse {
2432
+ animation: sp-pulse-ring 0.7s ease-out;
2433
+ }
2434
+
2435
+ .sp-anim-flash {
2436
+ animation: sp-flash-bg 0.5s ${EASE_OUT_QUART};
2437
+ }
2438
+
2439
+ .sp-anim-slide-up {
2440
+ animation: sp-slide-up 0.3s ${EASE_OUT_EXPO} both;
2441
+ }
2442
+
2443
+ .sp-anim-fade-in {
2444
+ animation: sp-fade-in 0.2s ease-out both;
2445
+ }
2446
+
2447
+ /* ---- Transition utilities ---- */
2448
+
2449
+ .sp-panel {
2450
+ transform: translateX(110%);
2451
+ transition: transform 0.4s ${EASE_OUT_EXPO};
2452
+ }
2453
+
2454
+ .sp-panel.sp-panel--open {
2455
+ transform: translateX(0);
2456
+ }
2457
+
2458
+ .sp-radial-item {
2459
+ opacity: 0;
2460
+ pointer-events: none;
2461
+ transform: translate(0, 0) scale(0.8);
2462
+ transition:
2463
+ transform 0.35s ${SPRING_OVERSHOOT},
2464
+ opacity 0.2s ease,
2465
+ background 0.2s ease,
2466
+ border-color 0.2s ease,
2467
+ box-shadow 0.2s ease;
2468
+ }
2469
+
2470
+ .sp-radial-item.sp-radial-item--open {
2471
+ opacity: 1;
2472
+ pointer-events: auto;
2473
+ }
2474
+
2475
+ /* Stagger delay via CSS custom property --sp-i */
2476
+ .sp-radial-item {
2477
+ transition-delay: calc(var(--sp-i, 0) * 50ms);
2478
+ }
2479
+
2480
+ /* ---- Card stagger animation ---- */
2481
+
2482
+ @keyframes sp-card-in {
2483
+ from {
2484
+ transform: translateY(12px);
2485
+ opacity: 0;
2486
+ }
2487
+ to {
2488
+ transform: translateY(0);
2489
+ opacity: 1;
2490
+ }
2491
+ }
2492
+
2493
+ .sp-card {
2494
+ animation: sp-card-in 0.35s ${EASE_OUT_EXPO} both;
2495
+ animation-delay: calc(var(--sp-card-i, 0) * 40ms);
2496
+ }
2497
+
2498
+ /* ---- Loading spinner ---- */
2499
+
2500
+ @keyframes sp-spin {
2501
+ to { transform: rotate(360deg); }
2502
+ }
2503
+
2504
+ .sp-spinner {
2505
+ width: 20px;
2506
+ height: 20px;
2507
+ border: 2px solid var(--sp-border);
2508
+ border-top-color: var(--sp-accent);
2509
+ border-radius: 50%;
2510
+ animation: sp-spin 0.6s linear infinite;
2511
+ }
2512
+
2513
+ /* ---- Badge bounce ---- */
2514
+
2515
+ @keyframes sp-badge-in {
2516
+ 0% { transform: scale(0); }
2517
+ 60% { transform: scale(1.3); }
2518
+ 100% { transform: scale(1); }
2519
+ }
2520
+
2521
+ .sp-fab-badge {
2522
+ animation: sp-badge-in 0.4s ${SPRING_OVERSHOOT} both;
2523
+ }
2524
+
2525
+ /* ---- Reduced motion ---- */
2526
+
2527
+ @media (prefers-reduced-motion: reduce) {
2528
+ *, *::before, *::after {
2529
+ animation-duration: 0.01ms !important;
2530
+ animation-iteration-count: 1 !important;
2531
+ transition-duration: 0.01ms !important;
2532
+ }
2533
+ }
2534
+
2535
+ `;
2536
+
2537
+ // src/styles/base.ts
2538
+ function buildStyles(colors) {
2539
+ return `
2540
+ :host {
2541
+ all: initial;
2542
+ position: fixed;
2543
+ z-index: 2147483647;
2544
+ font-family: var(--sp-font);
2545
+ font-size: 14px;
2546
+ line-height: 1.5;
2547
+ color: var(--sp-text);
2548
+ -webkit-font-smoothing: antialiased;
2549
+ -moz-osx-font-smoothing: grayscale;
2550
+ ${cssVariables(colors)}
2551
+ }
2552
+
2553
+ *, *::before, *::after {
2554
+ box-sizing: border-box;
2555
+ margin: 0;
2556
+ padding: 0;
2557
+ }
2558
+
2559
+ /* ============================
2560
+ Focus visible (accessibility)
2561
+ ============================ */
2562
+
2563
+ :focus-visible {
2564
+ outline: 2px solid var(--sp-accent);
2565
+ outline-offset: 2px;
2566
+ }
2567
+
2568
+ /* ============================
2569
+ FAB (Floating Action Button)
2570
+ ============================ */
2571
+
2572
+ .sp-fab {
2573
+ position: fixed;
2574
+ width: 52px;
2575
+ height: 52px;
2576
+ border-radius: var(--sp-radius-full);
2577
+ background: var(--sp-accent-gradient);
2578
+ color: #fff;
2579
+ border: none;
2580
+ cursor: pointer;
2581
+ display: flex;
2582
+ align-items: center;
2583
+ justify-content: center;
2584
+ box-shadow:
2585
+ 0 4px 20px var(--sp-accent-glow),
2586
+ 0 2px 8px rgba(0, 0, 0, 0.08);
2587
+ transition:
2588
+ transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),
2589
+ box-shadow 0.3s ease;
2590
+ outline: none;
2591
+ }
2592
+
2593
+ .sp-fab:focus-visible {
2594
+ outline: 2px solid #fff;
2595
+ outline-offset: 3px;
2596
+ }
2597
+
2598
+ .sp-fab:hover {
2599
+ transform: translateY(-2px) scale(1.05);
2600
+ box-shadow:
2601
+ 0 8px 28px var(--sp-accent-glow),
2602
+ 0 4px 12px rgba(0, 0, 0, 0.1);
2603
+ }
2604
+
2605
+ .sp-fab:active {
2606
+ transform: translateY(0) scale(0.95);
2607
+ transition-duration: 0.1s;
2608
+ }
2609
+
2610
+ .sp-fab--bottom-right {
2611
+ bottom: 24px;
2612
+ right: 24px;
2613
+ }
2614
+
2615
+ .sp-fab--bottom-left {
2616
+ bottom: 24px;
2617
+ left: 24px;
2618
+ }
2619
+
2620
+ .sp-fab svg {
2621
+ width: 22px;
2622
+ height: 22px;
2623
+ fill: currentColor;
2624
+ transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
2625
+ }
2626
+
2627
+ /* ---- FAB Badge ---- */
2628
+
2629
+ .sp-fab-badge {
2630
+ position: absolute;
2631
+ top: -4px;
2632
+ right: -4px;
2633
+ min-width: 20px;
2634
+ height: 20px;
2635
+ padding: 0 6px;
2636
+ border-radius: var(--sp-radius-full);
2637
+ background: #ef4444;
2638
+ color: #fff;
2639
+ font-size: 11px;
2640
+ font-weight: 700;
2641
+ display: flex;
2642
+ align-items: center;
2643
+ justify-content: center;
2644
+ border: 2px solid #fff;
2645
+ pointer-events: none;
2646
+ font-family: var(--sp-font);
2647
+ line-height: 1;
2648
+ }
2649
+
2650
+ /* ============================
2651
+ Radial Menu
2652
+ ============================ */
2653
+
2654
+ .sp-radial {
2655
+ position: fixed;
2656
+ pointer-events: none;
2657
+ width: 52px;
2658
+ height: 52px;
2659
+ }
2660
+
2661
+ .sp-radial--bottom-right {
2662
+ bottom: 24px;
2663
+ right: 24px;
2664
+ }
2665
+
2666
+ .sp-radial--bottom-left {
2667
+ bottom: 24px;
2668
+ left: 24px;
2669
+ }
2670
+
2671
+ .sp-radial-item {
2672
+ position: absolute;
2673
+ left: 4px;
2674
+ bottom: 4px;
2675
+ width: 44px;
2676
+ height: 44px;
2677
+ border-radius: var(--sp-radius-full);
2678
+ background: var(--sp-glass-bg-heavy);
2679
+ backdrop-filter: blur(var(--sp-blur));
2680
+ -webkit-backdrop-filter: blur(var(--sp-blur));
2681
+ color: var(--sp-text);
2682
+ border: 1px solid var(--sp-glass-border);
2683
+ cursor: pointer;
2684
+ display: flex;
2685
+ align-items: center;
2686
+ justify-content: center;
2687
+ box-shadow: var(--sp-shadow-md);
2688
+ font-size: 12px;
2689
+ font-weight: 600;
2690
+ }
2691
+
2692
+ .sp-radial-item:hover,
2693
+ .sp-radial-item:focus-visible {
2694
+ background: rgba(255, 255, 255, 0.95);
2695
+ border-color: var(--sp-accent);
2696
+ color: var(--sp-accent);
2697
+ box-shadow:
2698
+ var(--sp-shadow-md),
2699
+ 0 0 0 3px var(--sp-accent-light);
2700
+ outline: none;
2701
+ }
2702
+
2703
+ .sp-radial-item svg {
2704
+ width: 18px;
2705
+ height: 18px;
2706
+ flex-shrink: 0;
2707
+ stroke: currentColor;
2708
+ fill: none;
2709
+ }
2710
+
2711
+ .sp-radial-label {
2712
+ white-space: nowrap;
2713
+ font-size: 12px;
2714
+ font-weight: 500;
2715
+ color: var(--sp-text);
2716
+ pointer-events: none;
2717
+ opacity: 0;
2718
+ padding: 4px 12px;
2719
+ border-radius: var(--sp-radius);
2720
+ background: var(--sp-glass-bg-heavy);
2721
+ backdrop-filter: blur(12px);
2722
+ -webkit-backdrop-filter: blur(12px);
2723
+ border: 1px solid var(--sp-glass-border);
2724
+ box-shadow: var(--sp-shadow-sm);
2725
+ transform: translateX(4px);
2726
+ transition: opacity 0.2s ease, transform 0.2s ease;
2727
+ }
2728
+
2729
+ .sp-radial-item:hover .sp-radial-label,
2730
+ .sp-radial-item:focus-visible .sp-radial-label {
2731
+ opacity: 1;
2732
+ transform: translateX(0);
2733
+ }
2734
+
2735
+ /* ============================
2736
+ Panel (Side drawer)
2737
+ ============================ */
2738
+
2739
+ .sp-panel {
2740
+ position: fixed;
2741
+ top: 0;
2742
+ right: 0;
2743
+ width: 400px;
2744
+ max-width: 100vw;
2745
+ height: 100vh;
2746
+ background: var(--sp-glass-bg);
2747
+ backdrop-filter: blur(var(--sp-blur-heavy));
2748
+ -webkit-backdrop-filter: blur(var(--sp-blur-heavy));
2749
+ border-left: 1px solid var(--sp-glass-border);
2750
+ box-shadow: var(--sp-shadow-xl);
2751
+ display: flex;
2752
+ flex-direction: column;
2753
+ overflow: hidden;
2754
+ }
2755
+
2756
+ @media (max-width: 480px) {
2757
+ .sp-panel {
2758
+ width: 100vw;
2759
+ border-left: none;
2760
+ }
2761
+ }
2762
+
2763
+ .sp-panel-header {
2764
+ display: flex;
2765
+ align-items: center;
2766
+ justify-content: space-between;
2767
+ padding: 20px 24px;
2768
+ border-bottom: 1px solid var(--sp-border);
2769
+ background: var(--sp-glass-bg-heavy);
2770
+ backdrop-filter: blur(var(--sp-blur));
2771
+ -webkit-backdrop-filter: blur(var(--sp-blur));
2772
+ }
2773
+
2774
+ .sp-panel-title {
2775
+ font-size: 17px;
2776
+ font-weight: 700;
2777
+ color: var(--sp-text);
2778
+ letter-spacing: -0.02em;
2779
+ }
2780
+
2781
+ .sp-panel-close {
2782
+ width: 32px;
2783
+ height: 32px;
2784
+ border-radius: var(--sp-radius);
2785
+ border: none;
2786
+ background: transparent;
2787
+ cursor: pointer;
2788
+ display: flex;
2789
+ align-items: center;
2790
+ justify-content: center;
2791
+ color: var(--sp-text-tertiary);
2792
+ transition: all 0.2s ease;
2793
+ }
2794
+
2795
+ .sp-panel-close:hover {
2796
+ background: var(--sp-bg-hover);
2797
+ color: var(--sp-text);
2798
+ }
2799
+
2800
+ .sp-panel-close svg {
2801
+ width: 16px;
2802
+ height: 16px;
2803
+ }
2804
+
2805
+ /* ============================
2806
+ Filters & Search
2807
+ ============================ */
2808
+
2809
+ .sp-filters {
2810
+ padding: 16px 24px;
2811
+ border-bottom: 1px solid var(--sp-border);
2812
+ background: var(--sp-glass-bg-heavy);
2813
+ backdrop-filter: blur(var(--sp-blur));
2814
+ -webkit-backdrop-filter: blur(var(--sp-blur));
2815
+ position: sticky;
2816
+ top: 0;
2817
+ z-index: 1;
2818
+ }
2819
+
2820
+ .sp-search-wrap {
2821
+ position: relative;
2822
+ margin-bottom: 12px;
2823
+ }
2824
+
2825
+ .sp-search {
2826
+ width: 100%;
2827
+ height: 40px;
2828
+ padding: 0 12px 0 38px;
2829
+ border-radius: var(--sp-radius);
2830
+ border: 1px solid var(--sp-border);
2831
+ background: var(--sp-glass-bg-heavy);
2832
+ color: var(--sp-text);
2833
+ font-family: var(--sp-font);
2834
+ font-size: 13px;
2835
+ outline: none;
2836
+ transition: all 0.2s ease;
2837
+ }
2838
+
2839
+ .sp-search::placeholder {
2840
+ color: var(--sp-text-tertiary);
2841
+ }
2842
+
2843
+ .sp-search:focus {
2844
+ border-color: var(--sp-accent);
2845
+ box-shadow: 0 0 0 3px var(--sp-accent-light);
2846
+ background: #fff;
2847
+ }
2848
+
2849
+ .sp-search-icon {
2850
+ position: absolute;
2851
+ left: 12px;
2852
+ top: 50%;
2853
+ transform: translateY(-50%);
2854
+ color: var(--sp-text-tertiary);
2855
+ width: 16px;
2856
+ height: 16px;
2857
+ transition: color 0.2s ease;
2858
+ }
2859
+
2860
+ .sp-search:focus ~ .sp-search-icon,
2861
+ .sp-search-wrap:focus-within .sp-search-icon {
2862
+ color: var(--sp-accent);
2863
+ }
2864
+
2865
+ .sp-chips {
2866
+ display: flex;
2867
+ gap: 6px;
2868
+ flex-wrap: wrap;
2869
+ }
2870
+
2871
+ .sp-chip {
2872
+ padding: 5px 14px;
2873
+ border-radius: var(--sp-radius-full);
2874
+ border: 1px solid var(--sp-border);
2875
+ background: var(--sp-glass-bg-heavy);
2876
+ color: var(--sp-text-secondary);
2877
+ font-family: var(--sp-font);
2878
+ font-size: 12px;
2879
+ font-weight: 500;
2880
+ cursor: pointer;
2881
+ transition: all 0.2s ease;
2882
+ white-space: nowrap;
2883
+ letter-spacing: 0.01em;
2884
+ }
2885
+
2886
+ .sp-chip:hover {
2887
+ border-color: var(--sp-accent);
2888
+ color: var(--sp-accent);
2889
+ background: var(--sp-accent-light);
2890
+ }
2891
+
2892
+ .sp-chip--active {
2893
+ background: var(--sp-accent-gradient);
2894
+ border-color: transparent;
2895
+ color: #fff;
2896
+ box-shadow: 0 2px 8px var(--sp-accent-glow);
2897
+ }
2898
+
2899
+ .sp-chip--active:hover {
2900
+ background: var(--sp-accent-gradient);
2901
+ border-color: transparent;
2902
+ color: #fff;
2903
+ }
2904
+
2905
+ /* ============================
2906
+ Feedback Cards
2907
+ ============================ */
2908
+
2909
+ .sp-list {
2910
+ flex: 1;
2911
+ overflow-y: auto;
2912
+ padding: 8px 12px;
2913
+ }
2914
+
2915
+ .sp-list::-webkit-scrollbar {
2916
+ width: 6px;
2917
+ }
2918
+
2919
+ .sp-list::-webkit-scrollbar-track {
2920
+ background: transparent;
2921
+ }
2922
+
2923
+ .sp-list::-webkit-scrollbar-thumb {
2924
+ background: var(--sp-border);
2925
+ border-radius: var(--sp-radius-full);
2926
+ }
2927
+
2928
+ .sp-list::-webkit-scrollbar-thumb:hover {
2929
+ background: var(--sp-text-tertiary);
2930
+ }
2931
+
2932
+ .sp-card {
2933
+ display: flex;
2934
+ padding: 14px 16px;
2935
+ margin-bottom: 6px;
2936
+ cursor: pointer;
2937
+ border-radius: var(--sp-radius);
2938
+ background: var(--sp-glass-bg-heavy);
2939
+ border: 1px solid var(--sp-glass-border);
2940
+ transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
2941
+ }
2942
+
2943
+ .sp-card:hover {
2944
+ background: #fff;
2945
+ border-color: var(--sp-border);
2946
+ box-shadow: var(--sp-shadow-md);
2947
+ transform: translateY(-2px);
2948
+ }
2949
+
2950
+ .sp-card:active {
2951
+ transform: translateY(0) scale(0.99);
2952
+ transition-duration: 0.1s;
2953
+ }
2954
+
2955
+ .sp-card-bar {
2956
+ width: 3px;
2957
+ border-radius: var(--sp-radius-full);
2958
+ margin-right: 14px;
2959
+ flex-shrink: 0;
2960
+ }
2961
+
2962
+ .sp-card-body {
2963
+ flex: 1;
2964
+ min-width: 0;
2965
+ }
2966
+
2967
+ .sp-card-header {
2968
+ display: flex;
2969
+ align-items: center;
2970
+ gap: 8px;
2971
+ margin-bottom: 6px;
2972
+ }
2973
+
2974
+ .sp-card-number {
2975
+ font-size: 12px;
2976
+ font-weight: 700;
2977
+ color: var(--sp-text-tertiary);
2978
+ font-variant-numeric: tabular-nums;
2979
+ }
2980
+
2981
+ .sp-badge {
2982
+ padding: 2px 10px;
2983
+ border-radius: var(--sp-radius-full);
2984
+ font-size: 11px;
2985
+ font-weight: 600;
2986
+ letter-spacing: 0.02em;
2987
+ }
2988
+
2989
+ .sp-card-date {
2990
+ font-size: 11px;
2991
+ color: var(--sp-text-tertiary);
2992
+ margin-left: auto;
2993
+ }
2994
+
2995
+ .sp-card-message {
2996
+ font-size: 13px;
2997
+ line-height: 1.5;
2998
+ color: var(--sp-text);
2999
+ display: -webkit-box;
3000
+ -webkit-line-clamp: 3;
3001
+ -webkit-box-orient: vertical;
3002
+ overflow: hidden;
3003
+ }
3004
+
3005
+ .sp-card-message--expanded {
3006
+ -webkit-line-clamp: unset;
3007
+ }
3008
+
3009
+ .sp-card-expand {
3010
+ font-size: 12px;
3011
+ font-weight: 500;
3012
+ color: var(--sp-accent);
3013
+ cursor: pointer;
3014
+ background: none;
3015
+ border: none;
3016
+ padding: 4px 0;
3017
+ font-family: var(--sp-font);
3018
+ transition: opacity 0.15s ease;
3019
+ }
3020
+
3021
+ .sp-card-expand:hover {
3022
+ opacity: 0.8;
3023
+ }
3024
+
3025
+ .sp-card-footer {
3026
+ display: flex;
3027
+ align-items: center;
3028
+ justify-content: flex-end;
3029
+ gap: 6px;
3030
+ margin-top: 10px;
3031
+ }
3032
+
3033
+ .sp-btn-resolve,
3034
+ .sp-btn-delete {
3035
+ padding: 5px 14px;
3036
+ border-radius: var(--sp-radius-full);
3037
+ border: 1px solid var(--sp-border);
3038
+ background: transparent;
3039
+ color: var(--sp-text-secondary);
3040
+ font-family: var(--sp-font);
3041
+ font-size: 12px;
3042
+ font-weight: 500;
3043
+ cursor: pointer;
3044
+ display: flex;
3045
+ align-items: center;
3046
+ gap: 4px;
3047
+ transition: all 0.2s ease;
3048
+ }
3049
+
3050
+ .sp-btn-resolve svg,
3051
+ .sp-btn-delete svg {
3052
+ width: 14px;
3053
+ height: 14px;
3054
+ }
3055
+
3056
+ .sp-btn-resolve:hover {
3057
+ border-color: #22c55e;
3058
+ color: #22c55e;
3059
+ background: rgba(34, 197, 94, 0.06);
3060
+ }
3061
+
3062
+ .sp-btn-delete:hover {
3063
+ border-color: #ef4444;
3064
+ color: #ef4444;
3065
+ background: rgba(239, 68, 68, 0.06);
3066
+ }
3067
+
3068
+ .sp-btn-resolve:disabled,
3069
+ .sp-btn-delete:disabled {
3070
+ opacity: 0.5;
3071
+ cursor: not-allowed;
3072
+ pointer-events: none;
3073
+ }
3074
+
3075
+ /* ---- Delete All (header) ---- */
3076
+
3077
+ .sp-panel-header-right {
3078
+ display: flex;
3079
+ align-items: center;
3080
+ gap: 8px;
3081
+ }
3082
+
3083
+ .sp-btn-delete-all {
3084
+ padding: 5px 12px;
3085
+ border-radius: var(--sp-radius-full);
3086
+ border: 1px solid var(--sp-border);
3087
+ background: transparent;
3088
+ color: var(--sp-text-tertiary);
3089
+ font-family: var(--sp-font);
3090
+ font-size: 11px;
3091
+ font-weight: 500;
3092
+ cursor: pointer;
3093
+ display: flex;
3094
+ align-items: center;
3095
+ gap: 4px;
3096
+ transition: all 0.2s ease;
3097
+ }
3098
+
3099
+ .sp-btn-delete-all svg {
3100
+ width: 13px;
3101
+ height: 13px;
3102
+ }
3103
+
3104
+ .sp-btn-delete-all:hover {
3105
+ border-color: #ef4444;
3106
+ color: #ef4444;
3107
+ background: rgba(239, 68, 68, 0.06);
3108
+ }
3109
+
3110
+ .sp-btn-delete-all:disabled {
3111
+ opacity: 0.5;
3112
+ cursor: not-allowed;
3113
+ pointer-events: none;
3114
+ }
3115
+
3116
+ /* ---- Confirm Dialog ---- */
3117
+
3118
+ .sp-confirm-backdrop {
3119
+ position: fixed;
3120
+ inset: 0;
3121
+ background: var(--sp-backdrop, rgba(15, 23, 42, 0.2));
3122
+ backdrop-filter: blur(var(--sp-blur));
3123
+ -webkit-backdrop-filter: blur(var(--sp-blur));
3124
+ display: flex;
3125
+ align-items: center;
3126
+ justify-content: center;
3127
+ z-index: 2147483647;
3128
+ opacity: 0;
3129
+ transition: opacity 0.2s ease;
3130
+ }
3131
+
3132
+ .sp-confirm-dialog {
3133
+ width: 340px;
3134
+ padding: 28px;
3135
+ border-radius: 20px;
3136
+ background: var(--sp-glass-bg-heavy);
3137
+ backdrop-filter: blur(var(--sp-blur-heavy));
3138
+ -webkit-backdrop-filter: blur(var(--sp-blur-heavy));
3139
+ border: 1px solid var(--sp-glass-border);
3140
+ box-shadow: var(--sp-shadow-xl);
3141
+ font-family: var(--sp-font);
3142
+ transform: translateY(8px) scale(0.97);
3143
+ transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
3144
+ }
3145
+
3146
+ .sp-confirm-title {
3147
+ font-size: 17px;
3148
+ font-weight: 700;
3149
+ color: var(--sp-text);
3150
+ letter-spacing: -0.02em;
3151
+ margin-bottom: 8px;
3152
+ }
3153
+
3154
+ .sp-confirm-message {
3155
+ font-size: 14px;
3156
+ color: var(--sp-text-secondary);
3157
+ line-height: 1.5;
3158
+ margin-bottom: 20px;
3159
+ }
3160
+
3161
+ .sp-confirm-actions {
3162
+ display: flex;
3163
+ gap: 8px;
3164
+ justify-content: flex-end;
3165
+ }
3166
+
3167
+ .sp-btn-danger {
3168
+ height: 40px;
3169
+ padding: 0 22px;
3170
+ border-radius: var(--sp-radius);
3171
+ border: none;
3172
+ background: #ef4444;
3173
+ color: #fff;
3174
+ font-family: var(--sp-font);
3175
+ font-size: 14px;
3176
+ font-weight: 600;
3177
+ cursor: pointer;
3178
+ transition: all 0.2s ease;
3179
+ box-shadow: 0 2px 8px rgba(239, 68, 68, 0.25);
3180
+ }
3181
+
3182
+ .sp-btn-danger:hover {
3183
+ background: #dc2626;
3184
+ box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
3185
+ transform: translateY(-1px);
3186
+ }
3187
+
3188
+ .sp-btn-danger:active {
3189
+ transform: translateY(0) scale(0.98);
3190
+ transition-duration: 0.1s;
3191
+ }
3192
+
3193
+ .sp-card--resolved {
3194
+ opacity: 0.5;
3195
+ }
3196
+
3197
+ .sp-card--resolved .sp-card-message {
3198
+ text-decoration: line-through;
3199
+ text-decoration-color: var(--sp-text-tertiary);
3200
+ }
3201
+
3202
+ /* ============================
3203
+ Loading State
3204
+ ============================ */
3205
+
3206
+ .sp-loading {
3207
+ display: flex;
3208
+ align-items: center;
3209
+ justify-content: center;
3210
+ padding: 48px 24px;
3211
+ }
3212
+
3213
+ /* ============================
3214
+ Identity Form
3215
+ ============================ */
3216
+
3217
+ .sp-identity-title {
3218
+ font-size: 17px;
3219
+ font-weight: 700;
3220
+ color: var(--sp-text);
3221
+ letter-spacing: -0.02em;
3222
+ }
3223
+
3224
+ .sp-input {
3225
+ width: 100%;
3226
+ height: 42px;
3227
+ padding: 0 14px;
3228
+ border-radius: var(--sp-radius);
3229
+ border: 1px solid var(--sp-border);
3230
+ background: var(--sp-glass-bg-heavy);
3231
+ color: var(--sp-text);
3232
+ font-family: var(--sp-font);
3233
+ font-size: 14px;
3234
+ outline: none;
3235
+ transition: all 0.2s ease;
3236
+ }
3237
+
3238
+ .sp-input::placeholder {
3239
+ color: var(--sp-text-tertiary);
3240
+ }
3241
+
3242
+ .sp-input:focus {
3243
+ border-color: var(--sp-accent);
3244
+ box-shadow: 0 0 0 3px var(--sp-accent-light);
3245
+ background: #fff;
3246
+ }
3247
+
3248
+ .sp-input-label {
3249
+ font-size: 13px;
3250
+ font-weight: 500;
3251
+ color: var(--sp-text-secondary);
3252
+ margin-bottom: 6px;
3253
+ display: block;
3254
+ }
3255
+
3256
+ /* ============================
3257
+ Buttons
3258
+ ============================ */
3259
+
3260
+ .sp-btn-primary {
3261
+ height: 40px;
3262
+ padding: 0 22px;
3263
+ border-radius: var(--sp-radius);
3264
+ border: none;
3265
+ background: var(--sp-accent-gradient);
3266
+ color: #fff;
3267
+ font-family: var(--sp-font);
3268
+ font-size: 14px;
3269
+ font-weight: 600;
3270
+ cursor: pointer;
3271
+ transition: all 0.2s ease;
3272
+ box-shadow: 0 2px 8px var(--sp-accent-glow);
3273
+ }
3274
+
3275
+ .sp-btn-primary:hover {
3276
+ box-shadow: 0 4px 16px var(--sp-accent-glow);
3277
+ transform: translateY(-1px);
3278
+ }
3279
+
3280
+ .sp-btn-primary:active {
3281
+ transform: translateY(0) scale(0.98);
3282
+ transition-duration: 0.1s;
3283
+ }
3284
+
3285
+ .sp-btn-primary:disabled {
3286
+ opacity: 0.4;
3287
+ cursor: not-allowed;
3288
+ transform: none;
3289
+ box-shadow: none;
3290
+ }
3291
+
3292
+ .sp-btn-ghost {
3293
+ height: 40px;
3294
+ padding: 0 22px;
3295
+ border-radius: var(--sp-radius);
3296
+ border: 1px solid var(--sp-border);
3297
+ background: var(--sp-glass-bg-heavy);
3298
+ color: var(--sp-text-secondary);
3299
+ font-family: var(--sp-font);
3300
+ font-size: 14px;
3301
+ font-weight: 500;
3302
+ cursor: pointer;
3303
+ transition: all 0.2s ease;
3304
+ }
3305
+
3306
+ .sp-btn-ghost:hover {
3307
+ border-color: var(--sp-accent);
3308
+ color: var(--sp-accent);
3309
+ background: var(--sp-accent-light);
3310
+ }
3311
+
3312
+ /* ============================
3313
+ Empty State
3314
+ ============================ */
3315
+
3316
+ .sp-empty {
3317
+ display: flex;
3318
+ flex-direction: column;
3319
+ align-items: center;
3320
+ justify-content: center;
3321
+ padding: 56px 24px;
3322
+ color: var(--sp-text-tertiary);
3323
+ text-align: center;
3324
+ gap: 8px;
3325
+ animation: sp-fade-in 0.3s ease-out both;
3326
+ }
3327
+
3328
+ .sp-empty-text {
3329
+ font-size: 14px;
3330
+ font-weight: 500;
3331
+ }
3332
+
3333
+ ${ANIMATION_CSS}
3334
+ `;
3335
+ }
3336
+
3337
+ // src/tooltip.ts
3338
+ var SHOW_DELAY = 120;
3339
+ var HIDE_DELAY = 80;
3340
+ var Tooltip = class {
3341
+ constructor(colors) {
3342
+ this.colors = colors;
3343
+ this.root = el("div", {
3344
+ style: `
3345
+ position: fixed;
3346
+ z-index: 2147483647;
3347
+ max-width: 280px;
3348
+ padding: 12px 14px;
3349
+ border-radius: 14px;
3350
+ background: rgba(255, 255, 255, 0.88);
3351
+ backdrop-filter: blur(24px);
3352
+ -webkit-backdrop-filter: blur(24px);
3353
+ border: 1px solid rgba(255, 255, 255, 0.35);
3354
+ box-shadow: 0 8px 32px rgba(0,0,0,0.1), 0 2px 8px rgba(0,0,0,0.04);
3355
+ font-family: "Inter", system-ui, -apple-system, sans-serif;
3356
+ pointer-events: auto;
3357
+ opacity: 0;
3358
+ transform: translateY(6px) scale(0.97);
3359
+ transition: opacity 0.2s cubic-bezier(0.16, 1, 0.3, 1), transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
3360
+ visibility: hidden;
3361
+ -webkit-font-smoothing: antialiased;
3362
+ `
3363
+ });
3364
+ this.arrow = el("div", {
3365
+ style: `
3366
+ position: absolute;
3367
+ width: 12px;
3368
+ height: 12px;
3369
+ background: rgba(255, 255, 255, 0.88);
3370
+ border: 1px solid rgba(255, 255, 255, 0.35);
3371
+ transform: rotate(45deg);
3372
+ pointer-events: none;
3373
+ `
3374
+ });
3375
+ this.root.appendChild(this.arrow);
3376
+ this.root.addEventListener("mouseenter", () => this.cancelHide());
3377
+ this.root.addEventListener("mouseleave", () => this.scheduleHide());
3378
+ document.body.appendChild(this.root);
3379
+ }
3380
+ colors;
3381
+ root;
3382
+ arrow;
3383
+ showTimer = null;
3384
+ hideTimer = null;
3385
+ currentFeedbackId = null;
3386
+ show(feedback, anchorRect) {
3387
+ if (this.currentFeedbackId === feedback.id) return;
3388
+ this.cancelHide();
3389
+ this.cancelShow();
3390
+ this.showTimer = setTimeout(() => {
3391
+ this.currentFeedbackId = feedback.id;
3392
+ this.render(feedback);
3393
+ this.position(anchorRect);
3394
+ this.root.style.visibility = "visible";
3395
+ this.root.style.opacity = "1";
3396
+ this.root.style.transform = "translateY(0) scale(1)";
3397
+ }, SHOW_DELAY);
3398
+ }
3399
+ scheduleHide() {
3400
+ this.cancelHide();
3401
+ this.hideTimer = setTimeout(() => this.hide(), HIDE_DELAY);
3402
+ }
3403
+ hide() {
3404
+ this.cancelShow();
3405
+ this.currentFeedbackId = null;
3406
+ this.root.style.opacity = "0";
3407
+ this.root.style.transform = "translateY(6px) scale(0.97)";
3408
+ setTimeout(() => {
3409
+ if (!this.currentFeedbackId) {
3410
+ this.root.style.visibility = "hidden";
3411
+ }
3412
+ }, 200);
3413
+ }
3414
+ cancelShow() {
3415
+ if (this.showTimer) {
3416
+ clearTimeout(this.showTimer);
3417
+ this.showTimer = null;
3418
+ }
3419
+ }
3420
+ cancelHide() {
3421
+ if (this.hideTimer) {
3422
+ clearTimeout(this.hideTimer);
3423
+ this.hideTimer = null;
3424
+ }
3425
+ }
3426
+ render(feedback) {
3427
+ const children = Array.from(this.root.children);
3428
+ for (const child of children) {
3429
+ if (child !== this.arrow) child.remove();
3430
+ }
3431
+ const typeColor = getTypeColor(feedback.type, this.colors);
3432
+ const typeBg = getTypeBgColor(feedback.type, this.colors);
3433
+ const typeLabel = feedback.type.charAt(0).toUpperCase() + feedback.type.slice(1);
3434
+ const header = el("div", { style: "display:flex;align-items:center;gap:8px;margin-bottom:8px;" });
3435
+ const badge = el("span", {
3436
+ style: `
3437
+ padding:3px 10px;border-radius:9999px;
3438
+ font-size:11px;font-weight:600;
3439
+ color:${typeColor};background:${typeBg};
3440
+ letter-spacing:0.02em;
3441
+ `
3442
+ });
3443
+ setText(badge, typeLabel);
3444
+ const date = el("span", { style: "font-size:11px;color:#64748b;margin-left:auto;" });
3445
+ setText(date, formatRelativeDate(feedback.createdAt));
3446
+ header.appendChild(badge);
3447
+ header.appendChild(date);
3448
+ const body = el("div", {
3449
+ style: "font-size:13px;line-height:1.55;color:#0f172a;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;"
3450
+ });
3451
+ setText(body, feedback.message);
3452
+ this.root.insertBefore(header, this.arrow);
3453
+ this.root.insertBefore(body, this.arrow);
3454
+ }
3455
+ position(anchorRect) {
3456
+ const tooltipRect = this.root.getBoundingClientRect();
3457
+ const gap = 10;
3458
+ let top = anchorRect.top - tooltipRect.height - gap;
3459
+ let left = anchorRect.left + anchorRect.width / 2 - tooltipRect.width / 2;
3460
+ let isAbove = true;
3461
+ if (top < 8) {
3462
+ top = anchorRect.bottom + gap;
3463
+ isAbove = false;
3464
+ }
3465
+ left = Math.max(8, Math.min(left, window.innerWidth - tooltipRect.width - 8));
3466
+ this.root.style.top = `${top}px`;
3467
+ this.root.style.left = `${left}px`;
3468
+ const arrowLeft = Math.max(16, Math.min(anchorRect.left + anchorRect.width / 2 - left - 6, tooltipRect.width - 22));
3469
+ if (isAbove) {
3470
+ this.arrow.style.cssText = `
3471
+ position:absolute;
3472
+ width:12px;height:12px;
3473
+ background:rgba(255, 255, 255, 0.88);
3474
+ border-right:1px solid rgba(255, 255, 255, 0.35);
3475
+ border-bottom:1px solid rgba(255, 255, 255, 0.35);
3476
+ transform:rotate(45deg);
3477
+ pointer-events:none;
3478
+ bottom:-6px;
3479
+ left:${arrowLeft}px;
3480
+ `;
3481
+ } else {
3482
+ this.arrow.style.cssText = `
3483
+ position:absolute;
3484
+ width:12px;height:12px;
3485
+ background:rgba(255, 255, 255, 0.88);
3486
+ border-left:1px solid rgba(255, 255, 255, 0.35);
3487
+ border-top:1px solid rgba(255, 255, 255, 0.35);
3488
+ transform:rotate(45deg);
3489
+ pointer-events:none;
3490
+ top:-6px;
3491
+ left:${arrowLeft}px;
3492
+ `;
3493
+ }
3494
+ }
3495
+ /** Check if a DOM node belongs to this tooltip (for MutationObserver filtering). */
3496
+ contains(node) {
3497
+ return this.root.contains(node);
3498
+ }
3499
+ destroy() {
3500
+ this.cancelShow();
3501
+ this.cancelHide();
3502
+ this.root.remove();
3503
+ }
3504
+ };
3505
+
3506
+ // src/launcher.ts
3507
+ function launch(config2) {
3508
+ if (!config2.forceShow) {
3509
+ try {
3510
+ const meta = import.meta;
3511
+ const proc = globalThis.process;
3512
+ const mode = meta.env?.MODE ?? proc?.env?.NODE_ENV;
3513
+ if (mode === "production") {
3514
+ return { destroy: () => {
3515
+ } };
3516
+ }
3517
+ } catch {
3518
+ }
3519
+ }
3520
+ if (window.innerWidth < 768) {
3521
+ return { destroy: () => {
3522
+ } };
3523
+ }
3524
+ const colors = buildThemeColors(config2.accentColor);
3525
+ const bus = new EventBus();
3526
+ const apiClient = new ApiClient(config2.endpoint);
3527
+ if (config2.onOpen) bus.on("open", config2.onOpen);
3528
+ if (config2.onClose) bus.on("close", config2.onClose);
3529
+ if (config2.onFeedbackSent) bus.on("feedback:sent", config2.onFeedbackSent);
3530
+ if (config2.onError) bus.on("feedback:error", config2.onError);
3531
+ if (config2.onAnnotationStart) bus.on("annotation:start", config2.onAnnotationStart);
3532
+ if (config2.onAnnotationEnd) bus.on("annotation:end", config2.onAnnotationEnd);
3533
+ const host = document.createElement("siteping-widget");
3534
+ host.style.cssText = "position:fixed;z-index:2147483647;";
3535
+ const shadowMode = config2.__testMode ? "open" : "closed";
3536
+ const shadow = host.attachShadow({ mode: shadowMode });
3537
+ const sheet = new CSSStyleSheet();
3538
+ sheet.replaceSync(buildStyles(colors));
3539
+ shadow.adoptedStyleSheets = [sheet];
3540
+ document.body.appendChild(host);
3541
+ const tooltip = new Tooltip(colors);
3542
+ const markers = new MarkerManager(colors, tooltip, bus);
3543
+ const fab = new Fab(shadow, config2, bus);
3544
+ const panel = new Panel(shadow, colors, bus, apiClient, config2.projectName, markers);
3545
+ const annotator = new Annotator(config2, colors, bus);
3546
+ const unsubAnnotation = bus.on("annotation:complete", async (data) => {
3547
+ const { annotation, type, message } = data;
3548
+ let identity = getIdentity();
3549
+ if (!identity) {
3550
+ identity = await promptIdentity(shadow);
3551
+ if (!identity) return;
3552
+ saveIdentity(identity);
3553
+ }
3554
+ const payload = {
3555
+ projectName: config2.projectName,
3556
+ type,
3557
+ message,
3558
+ url: window.location.href,
3559
+ viewport: `${window.innerWidth}x${window.innerHeight}`,
3560
+ userAgent: navigator.userAgent,
3561
+ authorName: identity.name,
3562
+ authorEmail: identity.email,
3563
+ annotations: [annotation],
3564
+ clientId: crypto.randomUUID()
3565
+ };
3566
+ try {
3567
+ const response = await apiClient.sendFeedback(payload);
3568
+ bus.emit("feedback:sent", response);
3569
+ markers.addFeedback(response, markers.count + 1);
3570
+ await panel.refresh();
3571
+ } catch (error) {
3572
+ bus.emit("feedback:error", error instanceof Error ? error : new Error(String(error)));
3573
+ }
3574
+ });
3575
+ apiClient.getFeedbacks(config2.projectName, { limit: 50 }).then(({ feedbacks }) => {
3576
+ markers.render(feedbacks);
3577
+ }).catch(() => {
3578
+ });
3579
+ flushRetryQueue(config2.endpoint);
3580
+ return {
3581
+ destroy: () => {
3582
+ unsubAnnotation();
3583
+ fab.destroy();
3584
+ panel.destroy();
3585
+ annotator.destroy();
3586
+ markers.destroy();
3587
+ tooltip.destroy();
3588
+ bus.removeAll();
3589
+ host.remove();
3590
+ }
3591
+ };
3592
+ }
3593
+ function promptIdentity(shadowRoot) {
3594
+ return new Promise((resolve) => {
3595
+ const backdrop = document.createElement("div");
3596
+ backdrop.style.cssText = `
3597
+ position:fixed;inset:0;
3598
+ background:rgba(15, 23, 42, 0.2);
3599
+ backdrop-filter:blur(8px);
3600
+ -webkit-backdrop-filter:blur(8px);
3601
+ display:flex;align-items:center;justify-content:center;
3602
+ z-index:2147483647;
3603
+ opacity:0;transition:opacity 0.25s ease;
3604
+ `;
3605
+ const modal = document.createElement("div");
3606
+ modal.style.cssText = `
3607
+ width:340px;padding:28px;border-radius:20px;
3608
+ background:rgba(255, 255, 255, 0.85);
3609
+ backdrop-filter:blur(32px);
3610
+ -webkit-backdrop-filter:blur(32px);
3611
+ border:1px solid rgba(255, 255, 255, 0.35);
3612
+ box-shadow:0 16px 48px rgba(0,0,0,0.12), 0 8px 16px rgba(0,0,0,0.06);
3613
+ font-family:"Inter",system-ui,-apple-system,sans-serif;
3614
+ transform:translateY(12px) scale(0.97);
3615
+ transition:transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
3616
+ -webkit-font-smoothing:antialiased;
3617
+ `;
3618
+ const title = document.createElement("div");
3619
+ title.className = "sp-identity-title";
3620
+ title.textContent = "Identifiez-vous";
3621
+ title.style.marginBottom = "20px";
3622
+ const nameLabel = document.createElement("label");
3623
+ nameLabel.className = "sp-input-label";
3624
+ nameLabel.textContent = "Nom";
3625
+ const nameInput = document.createElement("input");
3626
+ nameInput.className = "sp-input";
3627
+ nameInput.type = "text";
3628
+ nameInput.placeholder = "Votre nom";
3629
+ nameInput.style.marginBottom = "14px";
3630
+ const emailLabel = document.createElement("label");
3631
+ emailLabel.className = "sp-input-label";
3632
+ emailLabel.textContent = "Email";
3633
+ const emailInput = document.createElement("input");
3634
+ emailInput.className = "sp-input";
3635
+ emailInput.type = "email";
3636
+ emailInput.placeholder = "votre@email.com";
3637
+ const btnRow = document.createElement("div");
3638
+ btnRow.style.cssText = "display:flex;gap:8px;justify-content:flex-end;margin-top:20px;";
3639
+ const cancelBtn = document.createElement("button");
3640
+ cancelBtn.className = "sp-btn-ghost";
3641
+ cancelBtn.textContent = "Annuler";
3642
+ cancelBtn.addEventListener("click", () => {
3643
+ backdrop.style.opacity = "0";
3644
+ modal.style.transform = "translateY(12px) scale(0.97)";
3645
+ setTimeout(() => {
3646
+ backdrop.remove();
3647
+ resolve(null);
3648
+ }, 250);
3649
+ });
3650
+ const submitBtn = document.createElement("button");
3651
+ submitBtn.className = "sp-btn-primary";
3652
+ submitBtn.textContent = "Continuer";
3653
+ submitBtn.addEventListener("click", () => {
3654
+ const name = nameInput.value.trim();
3655
+ const email = emailInput.value.trim();
3656
+ if (!name || !email) return;
3657
+ backdrop.style.opacity = "0";
3658
+ modal.style.transform = "translateY(12px) scale(0.97)";
3659
+ setTimeout(() => {
3660
+ backdrop.remove();
3661
+ resolve({ name, email });
3662
+ }, 250);
3663
+ });
3664
+ btnRow.appendChild(cancelBtn);
3665
+ btnRow.appendChild(submitBtn);
3666
+ modal.appendChild(title);
3667
+ modal.appendChild(nameLabel);
3668
+ modal.appendChild(nameInput);
3669
+ modal.appendChild(emailLabel);
3670
+ modal.appendChild(emailInput);
3671
+ modal.appendChild(btnRow);
3672
+ backdrop.appendChild(modal);
3673
+ shadowRoot.appendChild(backdrop);
3674
+ requestAnimationFrame(() => {
3675
+ backdrop.style.opacity = "1";
3676
+ modal.style.transform = "translateY(0) scale(1)";
3677
+ nameInput.focus();
3678
+ });
3679
+ });
3680
+ }
3681
+
3682
+ // src/index.ts
3683
+ function initSiteping(config2) {
3684
+ return launch(config2);
3685
+ }
3686
+ export {
3687
+ initSiteping
3688
+ };
3689
+ //# sourceMappingURL=index.js.map