tangled
alpha
login
or
join now
margin.at
/
margin
85
fork
atom
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
85
fork
atom
overview
issues
3
pulls
1
pipelines
New extension using wxt.dev
scanash.com
1 month ago
5edfcb08
74e72aad
+6065
-7528
47 changed files
expand all
collapse all
unified
split
.github
workflows
release-extension.yml
.gitignore
extension
.prettierrc
background
service-worker.js
bun.lock
content
content.css
content.js
eslint.config.js
icons
site.webmanifest
manifest.chrome.json
manifest.firefox.json
manifest.json
package-lock.json
package.json
popup
popup.css
popup.html
popup.js
postcss.config.js
public
icons
icon-128.png
icon-16.png
icon-32.png
icon-48.png
icon-64.png
logo.svg
semble-logo.svg
sidepanel
sidepanel.css
sidepanel.html
sidepanel.js
src
assets
styles.css
components
CollectionIcon.tsx
popup
App.tsx
sidepanel
App.tsx
entrypoints
background.ts
content.ts
popup
index.html
main.tsx
sidepanel
index.html
main.tsx
utils
api.ts
messaging.ts
overlay-styles.ts
storage.ts
text-matcher.ts
types.ts
tailwind.config.js
tsconfig.json
wxt.config.ts
+27
-22
.github/workflows/release-extension.yml
···
15
15
- name: Checkout code
16
16
uses: actions/checkout@v4
17
17
18
18
-
- name: Update Manifest Version
18
18
+
- name: Setup Node.js
19
19
+
uses: actions/setup-node@v4
20
20
+
with:
21
21
+
node-version: '20'
22
22
+
cache: 'npm'
23
23
+
cache-dependency-path: extension/package-lock.json
24
24
+
25
25
+
- name: Install dependencies
26
26
+
run: |
27
27
+
cd extension
28
28
+
npm ci
29
29
+
30
30
+
- name: Update Version
19
31
run: |
20
32
VERSION=${GITHUB_REF_NAME#v}
21
21
-
echo "Updating manifests to version $VERSION"
22
22
-
33
33
+
echo "Updating to version $VERSION"
23
34
cd extension
24
24
-
for manifest in manifest.json manifest.chrome.json manifest.firefox.json; do
25
25
-
if [ -f "$manifest" ]; then
26
26
-
tmp=$(mktemp)
27
27
-
jq --arg v "$VERSION" '.version = $v' "$manifest" > "$tmp" && mv "$tmp" "$manifest"
28
28
-
echo "Updated $manifest"
29
29
-
fi
30
30
-
done
31
31
-
cd ..
35
35
+
tmp=$(mktemp)
36
36
+
jq --arg v "$VERSION" '.version = $v' package.json > "$tmp" && mv "$tmp" package.json
32
37
33
33
-
- name: Build Extension (Chrome)
38
38
+
- name: Build Chrome Extension
34
39
run: |
35
40
cd extension
36
36
-
cp manifest.chrome.json manifest.json
37
37
-
zip -r ../margin-extension-chrome.zip . -x "*.DS_Store" -x "*.git*" -x "manifest.*.json"
38
38
-
cd ..
41
41
+
npm run zip
42
42
+
cp .output/margin-extension-*.zip ../margin-extension-chrome.zip
39
43
40
40
-
- name: Build Extension (Firefox)
44
44
+
- name: Build Firefox Extension
41
45
run: |
42
46
cd extension
43
43
-
cp manifest.firefox.json manifest.json
44
44
-
zip -r ../margin-extension-firefox.xpi . -x "*.DS_Store" -x "*.git*" -x "manifest.*.json"
45
45
-
cd ..
47
47
+
npm run zip:firefox
48
48
+
cp .output/margin-extension-*-firefox.zip ../margin-extension-firefox.xpi
49
49
+
cp .output/margin-extension-*-sources.zip ../margin-extension-sources.zip
46
50
47
51
- name: Publish to Chrome Web Store
48
52
continue-on-error: true
···
83
87
AMO_JWT_ISSUER: ${{ secrets.AMO_JWT_ISSUER }}
84
88
AMO_JWT_SECRET: ${{ secrets.AMO_JWT_SECRET }}
85
89
run: |
86
86
-
cd extension
90
90
+
cd extension/.output/firefox-mv3
87
91
COMMIT_MSG=$(git log -1 --pretty=%s)
88
92
echo "{\"release_notes\": {\"en-US\": \"$COMMIT_MSG\"}, \"notes\": \"Thank you!\"}" > amo-metadata.json
89
89
-
npx web-ext sign --channel=listed --api-key=$AMO_JWT_ISSUER --api-secret=$AMO_JWT_SECRET --source-dir=. --artifacts-dir=../web-ext-artifacts --approval-timeout=300000 --amo-metadata=amo-metadata.json || echo "Web-ext sign timed out (expected), continuing..."
93
93
+
npx web-ext sign --channel=listed --api-key=$AMO_JWT_ISSUER --api-secret=$AMO_JWT_SECRET --source-dir=. --artifacts-dir=../../../web-ext-artifacts --approval-timeout=300000 --amo-metadata=amo-metadata.json || echo "Web-ext sign timed out (expected), continuing..."
90
94
rm amo-metadata.json
91
91
-
cd ..
95
95
+
cd ../../..
92
96
93
97
- name: Prepare signed Firefox XPI
94
98
run: |
···
106
110
files: |
107
111
margin-extension-chrome.zip
108
112
margin-extension-firefox.xpi
113
113
+
margin-extension-sources.zip
109
114
generate_release_notes: true
110
115
draft: false
111
116
prerelease: false
+4
.gitignore
···
8
8
margin-backend
9
9
backend/server
10
10
11
11
+
# WXT
12
12
+
extension/.output/
13
13
+
extension/.wxt/
14
14
+
11
15
# Environment
12
16
.env
13
17
.env.local
+7
extension/.prettierrc
···
1
1
+
{
2
2
+
"semi": true,
3
3
+
"singleQuote": true,
4
4
+
"tabWidth": 2,
5
5
+
"trailingComma": "es5",
6
6
+
"printWidth": 100
7
7
+
}
-764
extension/background/service-worker.js
···
1
1
-
let API_BASE = "https://margin.at";
2
2
-
let WEB_BASE = "https://margin.at";
3
3
-
4
4
-
const hasSidePanel =
5
5
-
typeof chrome !== "undefined" && typeof chrome.sidePanel !== "undefined";
6
6
-
const hasSidebarAction =
7
7
-
typeof browser !== "undefined" &&
8
8
-
typeof browser.sidebarAction !== "undefined";
9
9
-
const hasNotifications =
10
10
-
typeof chrome !== "undefined" && typeof chrome.notifications !== "undefined";
11
11
-
12
12
-
chrome.storage.local.get(["apiUrl"], (result) => {
13
13
-
if (result.apiUrl) {
14
14
-
updateBaseUrls(result.apiUrl);
15
15
-
}
16
16
-
});
17
17
-
18
18
-
function updateBaseUrls(url) {
19
19
-
let cleanUrl = url.replace(/\/$/, "");
20
20
-
21
21
-
if (cleanUrl.includes("ngrok") && cleanUrl.startsWith("http://")) {
22
22
-
cleanUrl = cleanUrl.replace("http://", "https://");
23
23
-
}
24
24
-
25
25
-
API_BASE = cleanUrl;
26
26
-
WEB_BASE = cleanUrl;
27
27
-
API_BASE = cleanUrl;
28
28
-
WEB_BASE = cleanUrl;
29
29
-
}
30
30
-
31
31
-
function showNotification(title, message) {
32
32
-
if (hasNotifications) {
33
33
-
chrome.notifications.create({
34
34
-
type: "basic",
35
35
-
iconUrl: "icons/android-chrome-192x192.png",
36
36
-
title: title,
37
37
-
message: message,
38
38
-
});
39
39
-
}
40
40
-
}
41
41
-
42
42
-
async function createHighlight(payload) {
43
43
-
if (!API_BASE) {
44
44
-
throw new Error("API URL not configured");
45
45
-
}
46
46
-
47
47
-
const cookie = await chrome.cookies.get({
48
48
-
url: API_BASE,
49
49
-
name: "margin_session",
50
50
-
});
51
51
-
52
52
-
if (!cookie) {
53
53
-
throw new Error("Not authenticated");
54
54
-
}
55
55
-
56
56
-
const res = await fetch(`${API_BASE}/api/highlights`, {
57
57
-
method: "POST",
58
58
-
headers: {
59
59
-
"Content-Type": "application/json",
60
60
-
"X-Session-Token": cookie.value,
61
61
-
},
62
62
-
credentials: "include",
63
63
-
body: JSON.stringify(payload),
64
64
-
});
65
65
-
66
66
-
if (!res.ok) {
67
67
-
const errText = await res.text();
68
68
-
throw new Error(`Failed to create highlight: ${res.status} ${errText}`);
69
69
-
}
70
70
-
71
71
-
return res.json();
72
72
-
}
73
73
-
74
74
-
function refreshTabAnnotations(tabId) {
75
75
-
if (!tabId) return;
76
76
-
chrome.tabs.sendMessage(tabId, { type: "REFRESH_ANNOTATIONS" }).catch(() => {
77
77
-
/* ignore missing content script */
78
78
-
});
79
79
-
}
80
80
-
81
81
-
async function openAnnotationUI(tabId, windowId) {
82
82
-
if (hasSidePanel) {
83
83
-
try {
84
84
-
let targetWindowId = windowId;
85
85
-
86
86
-
if (!targetWindowId) {
87
87
-
const tab = await chrome.tabs.get(tabId);
88
88
-
targetWindowId = tab.windowId;
89
89
-
}
90
90
-
91
91
-
await chrome.sidePanel.open({ windowId: targetWindowId });
92
92
-
return true;
93
93
-
} catch (err) {
94
94
-
console.error("Could not open Chrome side panel:", err);
95
95
-
}
96
96
-
}
97
97
-
98
98
-
if (hasSidebarAction) {
99
99
-
try {
100
100
-
await browser.sidebarAction.open();
101
101
-
return true;
102
102
-
} catch (err) {
103
103
-
console.warn("Could not open Firefox sidebar:", err);
104
104
-
}
105
105
-
}
106
106
-
107
107
-
return false;
108
108
-
}
109
109
-
110
110
-
chrome.runtime.onInstalled.addListener(async () => {
111
111
-
const stored = await chrome.storage.local.get(["apiUrl"]);
112
112
-
if (!stored.apiUrl) {
113
113
-
await chrome.storage.local.set({ apiUrl: "https://margin.at" });
114
114
-
updateBaseUrls("https://margin.at");
115
115
-
}
116
116
-
117
117
-
await ensureContextMenus();
118
118
-
119
119
-
if (hasSidebarAction) {
120
120
-
try {
121
121
-
await browser.sidebarAction.close();
122
122
-
} catch {
123
123
-
/* ignore */
124
124
-
}
125
125
-
}
126
126
-
});
127
127
-
128
128
-
chrome.runtime.onStartup.addListener(async () => {
129
129
-
await ensureContextMenus();
130
130
-
});
131
131
-
132
132
-
async function ensureContextMenus() {
133
133
-
await chrome.contextMenus.removeAll();
134
134
-
135
135
-
chrome.contextMenus.create({
136
136
-
id: "margin-annotate",
137
137
-
title: 'Annotate "%s"',
138
138
-
contexts: ["selection"],
139
139
-
});
140
140
-
141
141
-
chrome.contextMenus.create({
142
142
-
id: "margin-highlight",
143
143
-
title: 'Highlight "%s"',
144
144
-
contexts: ["selection"],
145
145
-
});
146
146
-
147
147
-
chrome.contextMenus.create({
148
148
-
id: "margin-bookmark",
149
149
-
title: "Bookmark this page",
150
150
-
contexts: ["page"],
151
151
-
});
152
152
-
153
153
-
chrome.contextMenus.create({
154
154
-
id: "margin-open-sidebar",
155
155
-
title: "Open Margin Sidebar",
156
156
-
contexts: ["page", "selection", "link"],
157
157
-
});
158
158
-
}
159
159
-
160
160
-
chrome.action.onClicked.addListener(async () => {
161
161
-
const stored = await chrome.storage.local.get(["apiUrl"]);
162
162
-
const webUrl = stored.apiUrl || WEB_BASE;
163
163
-
chrome.tabs.create({ url: webUrl });
164
164
-
});
165
165
-
166
166
-
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
167
167
-
if (info.menuItemId === "margin-open-sidebar") {
168
168
-
await openAnnotationUI(tab.id, tab.windowId);
169
169
-
return;
170
170
-
}
171
171
-
172
172
-
if (info.menuItemId === "margin-bookmark") {
173
173
-
const cookie = await chrome.cookies.get({
174
174
-
url: API_BASE,
175
175
-
name: "margin_session",
176
176
-
});
177
177
-
178
178
-
if (!cookie) {
179
179
-
showNotification("Margin", "Please sign in to bookmark pages");
180
180
-
return;
181
181
-
}
182
182
-
183
183
-
try {
184
184
-
const res = await fetch(`${API_BASE}/api/bookmarks`, {
185
185
-
method: "POST",
186
186
-
credentials: "include",
187
187
-
headers: {
188
188
-
"Content-Type": "application/json",
189
189
-
"X-Session-Token": cookie.value,
190
190
-
},
191
191
-
body: JSON.stringify({
192
192
-
url: tab.url,
193
193
-
title: tab.title,
194
194
-
}),
195
195
-
});
196
196
-
197
197
-
if (res.ok) {
198
198
-
showNotification("Margin", "Page bookmarked!");
199
199
-
}
200
200
-
} catch (err) {
201
201
-
console.error("Bookmark error:", err);
202
202
-
}
203
203
-
return;
204
204
-
}
205
205
-
206
206
-
if (info.menuItemId === "margin-annotate") {
207
207
-
let selector = null;
208
208
-
let canonicalUrl = null;
209
209
-
210
210
-
try {
211
211
-
const response = await chrome.tabs.sendMessage(tab.id, {
212
212
-
type: "GET_SELECTOR_FOR_ANNOTATE_INLINE",
213
213
-
selectionText: info.selectionText,
214
214
-
});
215
215
-
selector = response?.selector;
216
216
-
canonicalUrl = response?.canonicalUrl;
217
217
-
} catch {
218
218
-
/* ignore */
219
219
-
}
220
220
-
221
221
-
if (!selector && info.selectionText) {
222
222
-
selector = {
223
223
-
type: "TextQuoteSelector",
224
224
-
exact: info.selectionText,
225
225
-
};
226
226
-
}
227
227
-
228
228
-
const targetUrl = canonicalUrl || tab.url;
229
229
-
230
230
-
if (selector) {
231
231
-
try {
232
232
-
await chrome.tabs.sendMessage(tab.id, {
233
233
-
type: "SHOW_INLINE_ANNOTATE",
234
234
-
data: {
235
235
-
url: targetUrl,
236
236
-
title: tab.title,
237
237
-
selector: selector,
238
238
-
},
239
239
-
});
240
240
-
return;
241
241
-
} catch (e) {
242
242
-
console.debug("Inline annotate failed, falling back to new tab:", e);
243
243
-
}
244
244
-
}
245
245
-
246
246
-
if (WEB_BASE) {
247
247
-
let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(targetUrl)}`;
248
248
-
if (selector) {
249
249
-
composeUrl += `&selector=${encodeURIComponent(JSON.stringify(selector))}`;
250
250
-
}
251
251
-
chrome.tabs.create({ url: composeUrl });
252
252
-
}
253
253
-
return;
254
254
-
}
255
255
-
256
256
-
if (info.menuItemId === "margin-highlight") {
257
257
-
let selector = null;
258
258
-
let canonicalUrl = null;
259
259
-
260
260
-
try {
261
261
-
const response = await chrome.tabs.sendMessage(tab.id, {
262
262
-
type: "GET_SELECTOR_FOR_HIGHLIGHT",
263
263
-
selectionText: info.selectionText,
264
264
-
});
265
265
-
if (response?.selector) {
266
266
-
selector = response.selector;
267
267
-
canonicalUrl = response.canonicalUrl;
268
268
-
}
269
269
-
if (response && response.success) return;
270
270
-
} catch {
271
271
-
/* ignore */
272
272
-
}
273
273
-
274
274
-
if (!selector && info.selectionText) {
275
275
-
selector = {
276
276
-
type: "TextQuoteSelector",
277
277
-
exact: info.selectionText,
278
278
-
};
279
279
-
}
280
280
-
281
281
-
const targetUrl = canonicalUrl || tab.url;
282
282
-
283
283
-
if (selector) {
284
284
-
try {
285
285
-
await createHighlight({
286
286
-
url: targetUrl,
287
287
-
title: tab.title,
288
288
-
selector: selector,
289
289
-
});
290
290
-
showNotification("Margin", "Text highlighted!");
291
291
-
refreshTabAnnotations(tab.id);
292
292
-
} catch (err) {
293
293
-
console.error("Highlight API error:", err);
294
294
-
if (err?.message === "Not authenticated") {
295
295
-
showNotification("Margin", "Please sign in to create highlights");
296
296
-
return;
297
297
-
}
298
298
-
showNotification("Margin", "Error creating highlight");
299
299
-
}
300
300
-
} else {
301
301
-
showNotification("Margin", "No text selected");
302
302
-
}
303
303
-
}
304
304
-
});
305
305
-
306
306
-
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
307
307
-
handleMessage(request, sender, sendResponse);
308
308
-
return true;
309
309
-
});
310
310
-
311
311
-
async function handleMessage(request, sender, sendResponse) {
312
312
-
try {
313
313
-
if (request.type === "UPDATE_SETTINGS") {
314
314
-
const result = await chrome.storage.local.get(["apiUrl"]);
315
315
-
if (result.apiUrl) updateBaseUrls(result.apiUrl);
316
316
-
sendResponse({ success: true });
317
317
-
return;
318
318
-
}
319
319
-
320
320
-
switch (request.type) {
321
321
-
case "CHECK_SESSION": {
322
322
-
if (!API_BASE) {
323
323
-
sendResponse({ success: true, data: { authenticated: false } });
324
324
-
return;
325
325
-
}
326
326
-
327
327
-
const cookie = await chrome.cookies.get({
328
328
-
url: API_BASE,
329
329
-
name: "margin_session",
330
330
-
});
331
331
-
332
332
-
if (!cookie) {
333
333
-
sendResponse({ success: true, data: { authenticated: false } });
334
334
-
return;
335
335
-
}
336
336
-
337
337
-
try {
338
338
-
const response = await fetch(`${API_BASE}/auth/session`, {
339
339
-
credentials: "include",
340
340
-
});
341
341
-
342
342
-
if (!response.ok) {
343
343
-
sendResponse({ success: true, data: { authenticated: false } });
344
344
-
return;
345
345
-
}
346
346
-
347
347
-
const sessionData = await response.json();
348
348
-
sendResponse({
349
349
-
success: true,
350
350
-
data: {
351
351
-
authenticated: true,
352
352
-
did: sessionData.did,
353
353
-
handle: sessionData.handle,
354
354
-
},
355
355
-
});
356
356
-
} catch (err) {
357
357
-
console.error("Session check error:", err);
358
358
-
sendResponse({ success: true, data: { authenticated: false } });
359
359
-
}
360
360
-
break;
361
361
-
}
362
362
-
363
363
-
case "GET_ANNOTATIONS": {
364
364
-
const stored = await chrome.storage.local.get(["apiUrl"]);
365
365
-
const currentApiUrl = stored.apiUrl
366
366
-
? stored.apiUrl.replace(/\/$/, "")
367
367
-
: API_BASE;
368
368
-
369
369
-
const pageUrl = request.data.url;
370
370
-
const citedUrls = request.data.citedUrls || [];
371
371
-
const uniqueUrls = [...new Set([pageUrl, ...citedUrls])];
372
372
-
373
373
-
const fetchPromises = uniqueUrls.map((u) =>
374
374
-
fetch(
375
375
-
`${currentApiUrl}/api/targets?source=${encodeURIComponent(u)}`,
376
376
-
).then((r) => r.json().catch(() => ({}))),
377
377
-
);
378
378
-
379
379
-
const results = await Promise.all(fetchPromises);
380
380
-
let allItems = [];
381
381
-
const seenIds = new Set();
382
382
-
383
383
-
results.forEach((data) => {
384
384
-
const items = [
385
385
-
...(data.annotations || []),
386
386
-
...(data.highlights || []),
387
387
-
...(data.bookmarks || []),
388
388
-
];
389
389
-
items.forEach((item) => {
390
390
-
const id = item.uri || item.id;
391
391
-
if (id && !seenIds.has(id)) {
392
392
-
seenIds.add(id);
393
393
-
allItems.push(item);
394
394
-
}
395
395
-
});
396
396
-
});
397
397
-
398
398
-
sendResponse({ success: true, data: allItems });
399
399
-
400
400
-
if (sender.tab) {
401
401
-
const count = allItems.length;
402
402
-
chrome.action
403
403
-
.setBadgeText({
404
404
-
text: count > 0 ? count.toString() : "",
405
405
-
tabId: sender.tab.id,
406
406
-
})
407
407
-
.catch(() => {
408
408
-
/* ignore */
409
409
-
});
410
410
-
chrome.action
411
411
-
.setBadgeBackgroundColor({
412
412
-
color: "#6366f1",
413
413
-
tabId: sender.tab.id,
414
414
-
})
415
415
-
.catch(() => {
416
416
-
/* ignore */
417
417
-
});
418
418
-
}
419
419
-
break;
420
420
-
}
421
421
-
422
422
-
case "CREATE_ANNOTATION": {
423
423
-
const cookie = await chrome.cookies.get({
424
424
-
url: API_BASE,
425
425
-
name: "margin_session",
426
426
-
});
427
427
-
428
428
-
if (!cookie) {
429
429
-
sendResponse({ success: false, error: "Not authenticated" });
430
430
-
return;
431
431
-
}
432
432
-
433
433
-
const payload = {
434
434
-
url: request.data.url,
435
435
-
text: request.data.text,
436
436
-
title: request.data.title,
437
437
-
};
438
438
-
439
439
-
if (request.data.selector) {
440
440
-
payload.selector = request.data.selector;
441
441
-
}
442
442
-
443
443
-
const createRes = await fetch(`${API_BASE}/api/annotations`, {
444
444
-
method: "POST",
445
445
-
credentials: "include",
446
446
-
headers: {
447
447
-
"Content-Type": "application/json",
448
448
-
"X-Session-Token": cookie.value,
449
449
-
},
450
450
-
body: JSON.stringify(payload),
451
451
-
});
452
452
-
453
453
-
if (!createRes.ok) {
454
454
-
const errorText = await createRes.text();
455
455
-
throw new Error(
456
456
-
`Failed to create annotation: ${createRes.status} ${errorText}`,
457
457
-
);
458
458
-
}
459
459
-
460
460
-
const createData = await createRes.json();
461
461
-
sendResponse({ success: true, data: createData });
462
462
-
break;
463
463
-
}
464
464
-
465
465
-
case "OPEN_LOGIN":
466
466
-
if (!WEB_BASE) {
467
467
-
chrome.runtime.openOptionsPage();
468
468
-
return;
469
469
-
}
470
470
-
chrome.tabs.create({ url: `${WEB_BASE}/login` });
471
471
-
break;
472
472
-
473
473
-
case "OPEN_WEB":
474
474
-
if (!WEB_BASE) {
475
475
-
chrome.runtime.openOptionsPage();
476
476
-
return;
477
477
-
}
478
478
-
chrome.tabs.create({ url: `${WEB_BASE}` });
479
479
-
break;
480
480
-
481
481
-
case "OPEN_COMPOSE": {
482
482
-
if (!WEB_BASE) {
483
483
-
chrome.runtime.openOptionsPage();
484
484
-
return;
485
485
-
}
486
486
-
const { url, selector } = request.data;
487
487
-
let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(url)}`;
488
488
-
if (selector) {
489
489
-
composeUrl += `&selector=${encodeURIComponent(JSON.stringify(selector))}`;
490
490
-
}
491
491
-
chrome.tabs.create({ url: composeUrl });
492
492
-
break;
493
493
-
}
494
494
-
495
495
-
case "OPEN_APP_URL": {
496
496
-
if (!WEB_BASE) {
497
497
-
chrome.runtime.openOptionsPage();
498
498
-
return;
499
499
-
}
500
500
-
const path = request.data.path;
501
501
-
const safePath = path.startsWith("/") ? path : `/${path}`;
502
502
-
chrome.tabs.create({ url: `${WEB_BASE}${safePath}` });
503
503
-
break;
504
504
-
}
505
505
-
506
506
-
case "OPEN_SIDE_PANEL":
507
507
-
if (sender.tab && sender.tab.windowId) {
508
508
-
chrome.sidePanel
509
509
-
.open({ windowId: sender.tab.windowId })
510
510
-
.catch((err) => console.error("Failed to open side panel", err));
511
511
-
}
512
512
-
break;
513
513
-
514
514
-
case "CREATE_BOOKMARK": {
515
515
-
if (!API_BASE) {
516
516
-
sendResponse({ success: false, error: "API URL not configured" });
517
517
-
return;
518
518
-
}
519
519
-
520
520
-
const cookie = await chrome.cookies.get({
521
521
-
url: API_BASE,
522
522
-
name: "margin_session",
523
523
-
});
524
524
-
525
525
-
if (!cookie) {
526
526
-
sendResponse({ success: false, error: "Not authenticated" });
527
527
-
return;
528
528
-
}
529
529
-
530
530
-
const bookmarkRes = await fetch(`${API_BASE}/api/bookmarks`, {
531
531
-
method: "POST",
532
532
-
credentials: "include",
533
533
-
headers: {
534
534
-
"Content-Type": "application/json",
535
535
-
"X-Session-Token": cookie.value,
536
536
-
},
537
537
-
body: JSON.stringify({
538
538
-
url: request.data.url,
539
539
-
title: request.data.title,
540
540
-
}),
541
541
-
});
542
542
-
543
543
-
if (!bookmarkRes.ok) {
544
544
-
const errorText = await bookmarkRes.text();
545
545
-
throw new Error(
546
546
-
`Failed to create bookmark: ${bookmarkRes.status} ${errorText}`,
547
547
-
);
548
548
-
}
549
549
-
550
550
-
const bookmarkData = await bookmarkRes.json();
551
551
-
sendResponse({ success: true, data: bookmarkData });
552
552
-
break;
553
553
-
}
554
554
-
555
555
-
case "CREATE_HIGHLIGHT": {
556
556
-
try {
557
557
-
const highlightData = await createHighlight({
558
558
-
url: request.data.url,
559
559
-
title: request.data.title,
560
560
-
selector: request.data.selector,
561
561
-
color: request.data.color || "yellow",
562
562
-
});
563
563
-
sendResponse({ success: true, data: highlightData });
564
564
-
refreshTabAnnotations(sender.tab?.id);
565
565
-
} catch (error) {
566
566
-
sendResponse({ success: false, error: error.message });
567
567
-
}
568
568
-
break;
569
569
-
}
570
570
-
571
571
-
case "GET_USER_BOOKMARKS": {
572
572
-
if (!API_BASE) {
573
573
-
sendResponse({ success: false, error: "API URL not configured" });
574
574
-
return;
575
575
-
}
576
576
-
577
577
-
const did = request.data.did;
578
578
-
const res = await fetch(
579
579
-
`${API_BASE}/api/users/${encodeURIComponent(did)}/bookmarks`,
580
580
-
);
581
581
-
582
582
-
if (!res.ok) {
583
583
-
throw new Error(`Failed to fetch bookmarks: ${res.status}`);
584
584
-
}
585
585
-
586
586
-
const data = await res.json();
587
587
-
sendResponse({ success: true, data: data.items || [] });
588
588
-
break;
589
589
-
}
590
590
-
591
591
-
case "GET_USER_HIGHLIGHTS": {
592
592
-
if (!API_BASE) {
593
593
-
sendResponse({ success: false, error: "API URL not configured" });
594
594
-
return;
595
595
-
}
596
596
-
597
597
-
const did = request.data.did;
598
598
-
const res = await fetch(
599
599
-
`${API_BASE}/api/users/${encodeURIComponent(did)}/highlights`,
600
600
-
);
601
601
-
602
602
-
if (!res.ok) {
603
603
-
throw new Error(`Failed to fetch highlights: ${res.status}`);
604
604
-
}
605
605
-
606
606
-
const data = await res.json();
607
607
-
sendResponse({ success: true, data: data.items || [] });
608
608
-
break;
609
609
-
}
610
610
-
611
611
-
case "GET_USER_COLLECTIONS": {
612
612
-
if (!API_BASE) {
613
613
-
sendResponse({ success: false, error: "API URL not configured" });
614
614
-
return;
615
615
-
}
616
616
-
617
617
-
const did = request.data.did;
618
618
-
const res = await fetch(
619
619
-
`${API_BASE}/api/collections?author=${encodeURIComponent(did)}`,
620
620
-
);
621
621
-
622
622
-
if (!res.ok) {
623
623
-
throw new Error(`Failed to fetch collections: ${res.status}`);
624
624
-
}
625
625
-
626
626
-
const data = await res.json();
627
627
-
sendResponse({ success: true, data: data.items || [] });
628
628
-
break;
629
629
-
}
630
630
-
631
631
-
case "GET_CONTAINING_COLLECTIONS": {
632
632
-
if (!API_BASE) {
633
633
-
sendResponse({ success: false, error: "API URL not configured" });
634
634
-
return;
635
635
-
}
636
636
-
637
637
-
const uri = request.data.uri;
638
638
-
const res = await fetch(
639
639
-
`${API_BASE}/api/collections/containing?uri=${encodeURIComponent(uri)}`,
640
640
-
);
641
641
-
642
642
-
if (!res.ok) {
643
643
-
throw new Error(
644
644
-
`Failed to fetch containing collections: ${res.status}`,
645
645
-
);
646
646
-
}
647
647
-
648
648
-
const data = await res.json();
649
649
-
sendResponse({ success: true, data: data || [] });
650
650
-
break;
651
651
-
}
652
652
-
653
653
-
case "ADD_TO_COLLECTION": {
654
654
-
if (!API_BASE) {
655
655
-
sendResponse({ success: false, error: "API URL not configured" });
656
656
-
return;
657
657
-
}
658
658
-
659
659
-
const cookie = await chrome.cookies.get({
660
660
-
url: API_BASE,
661
661
-
name: "margin_session",
662
662
-
});
663
663
-
664
664
-
if (!cookie) {
665
665
-
sendResponse({ success: false, error: "Not authenticated" });
666
666
-
return;
667
667
-
}
668
668
-
669
669
-
const { collectionUri, annotationUri } = request.data;
670
670
-
const res = await fetch(
671
671
-
`${API_BASE}/api/collections/${encodeURIComponent(collectionUri)}/items`,
672
672
-
{
673
673
-
method: "POST",
674
674
-
credentials: "include",
675
675
-
headers: {
676
676
-
"Content-Type": "application/json",
677
677
-
"X-Session-Token": cookie.value,
678
678
-
},
679
679
-
body: JSON.stringify({
680
680
-
annotationUri: annotationUri,
681
681
-
}),
682
682
-
},
683
683
-
);
684
684
-
685
685
-
if (!res.ok) {
686
686
-
const errText = await res.text();
687
687
-
throw new Error(
688
688
-
`Failed to add to collection: ${res.status} ${errText}`,
689
689
-
);
690
690
-
}
691
691
-
692
692
-
const data = await res.json();
693
693
-
sendResponse({ success: true, data });
694
694
-
break;
695
695
-
}
696
696
-
697
697
-
case "GET_REPLIES": {
698
698
-
if (!API_BASE) {
699
699
-
sendResponse({ success: false, error: "API URL not configured" });
700
700
-
return;
701
701
-
}
702
702
-
703
703
-
const uri = request.data.uri;
704
704
-
const res = await fetch(
705
705
-
`${API_BASE}/api/replies?uri=${encodeURIComponent(uri)}`,
706
706
-
);
707
707
-
708
708
-
if (!res.ok) {
709
709
-
throw new Error(`Failed to fetch replies: ${res.status}`);
710
710
-
}
711
711
-
712
712
-
const data = await res.json();
713
713
-
sendResponse({ success: true, data: data.items || [] });
714
714
-
break;
715
715
-
}
716
716
-
717
717
-
case "CREATE_REPLY": {
718
718
-
if (!API_BASE) {
719
719
-
sendResponse({ success: false, error: "API URL not configured" });
720
720
-
return;
721
721
-
}
722
722
-
723
723
-
const cookie = await chrome.cookies.get({
724
724
-
url: API_BASE,
725
725
-
name: "margin_session",
726
726
-
});
727
727
-
728
728
-
if (!cookie) {
729
729
-
sendResponse({ success: false, error: "Not authenticated" });
730
730
-
return;
731
731
-
}
732
732
-
733
733
-
const { parentUri, parentCid, rootUri, rootCid, text } = request.data;
734
734
-
const res = await fetch(`${API_BASE}/api/annotations/reply`, {
735
735
-
method: "POST",
736
736
-
credentials: "include",
737
737
-
headers: {
738
738
-
"Content-Type": "application/json",
739
739
-
"X-Session-Token": cookie.value,
740
740
-
},
741
741
-
body: JSON.stringify({
742
742
-
parentUri,
743
743
-
parentCid,
744
744
-
rootUri,
745
745
-
rootCid,
746
746
-
text,
747
747
-
}),
748
748
-
});
749
749
-
750
750
-
if (!res.ok) {
751
751
-
const errText = await res.text();
752
752
-
throw new Error(`Failed to create reply: ${res.status} ${errText}`);
753
753
-
}
754
754
-
755
755
-
const data = await res.json();
756
756
-
sendResponse({ success: true, data });
757
757
-
break;
758
758
-
}
759
759
-
}
760
760
-
} catch (error) {
761
761
-
console.error("Service worker error:", error);
762
762
-
sendResponse({ success: false, error: error.message });
763
763
-
}
764
764
-
}
+1301
extension/bun.lock
···
1
1
+
{
2
2
+
"lockfileVersion": 1,
3
3
+
"workspaces": {
4
4
+
"": {
5
5
+
"name": "margin-extension",
6
6
+
"dependencies": {
7
7
+
"@webext-core/messaging": "^1.4.0",
8
8
+
"clsx": "^2.1.1",
9
9
+
"lucide-react": "^0.563.0",
10
10
+
"react": "^18.3.1",
11
11
+
"react-dom": "^18.3.1",
12
12
+
},
13
13
+
"devDependencies": {
14
14
+
"@eslint/js": "^9.39.2",
15
15
+
"@types/react": "^18.3.12",
16
16
+
"@types/react-dom": "^18.3.1",
17
17
+
"@wxt-dev/module-react": "^1.1.1",
18
18
+
"autoprefixer": "^10.4.20",
19
19
+
"eslint": "^9.39.2",
20
20
+
"eslint-config-prettier": "^10.1.8",
21
21
+
"postcss": "^8.4.49",
22
22
+
"prettier": "^3.8.1",
23
23
+
"tailwindcss": "^3.4.15",
24
24
+
"typescript": "^5.6.3",
25
25
+
"typescript-eslint": "^8.54.0",
26
26
+
"wxt": "^0.19.13",
27
27
+
},
28
28
+
},
29
29
+
},
30
30
+
"packages": {
31
31
+
"@1natsu/wait-element": ["@1natsu/wait-element@4.1.2", "", { "dependencies": { "defu": "^6.1.4", "many-keys-map": "^2.0.1" } }, "sha512-qWxSJD+Q5b8bKOvESFifvfZ92DuMsY+03SBNjTO34ipJLP6mZ9yK4bQz/vlh48aEQXoJfaZBqUwKL5BdI5iiWw=="],
32
32
+
33
33
+
"@aklinker1/rollup-plugin-visualizer": ["@aklinker1/rollup-plugin-visualizer@5.12.0", "", { "dependencies": { "open": "^8.4.0", "picomatch": "^2.3.1", "source-map": "^0.7.4", "yargs": "^17.5.1" }, "peerDependencies": { "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ=="],
34
34
+
35
35
+
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
36
36
+
37
37
+
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
38
38
+
39
39
+
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
40
40
+
41
41
+
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
42
42
+
43
43
+
"@babel/generator": ["@babel/generator@7.29.0", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ=="],
44
44
+
45
45
+
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
46
46
+
47
47
+
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
48
48
+
49
49
+
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
50
50
+
51
51
+
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
52
52
+
53
53
+
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
54
54
+
55
55
+
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
56
56
+
57
57
+
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
58
58
+
59
59
+
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
60
60
+
61
61
+
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
62
62
+
63
63
+
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
64
64
+
65
65
+
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
66
66
+
67
67
+
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
68
68
+
69
69
+
"@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="],
70
70
+
71
71
+
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
72
72
+
73
73
+
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
74
74
+
75
75
+
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
76
76
+
77
77
+
"@devicefarmer/adbkit": ["@devicefarmer/adbkit@3.3.8", "", { "dependencies": { "@devicefarmer/adbkit-logcat": "^2.1.2", "@devicefarmer/adbkit-monkey": "~1.2.1", "bluebird": "~3.7", "commander": "^9.1.0", "debug": "~4.3.1", "node-forge": "^1.3.1", "split": "~1.0.1" }, "bin": { "adbkit": "bin/adbkit" } }, "sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw=="],
78
78
+
79
79
+
"@devicefarmer/adbkit-logcat": ["@devicefarmer/adbkit-logcat@2.1.3", "", {}, "sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw=="],
80
80
+
81
81
+
"@devicefarmer/adbkit-monkey": ["@devicefarmer/adbkit-monkey@1.2.1", "", {}, "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg=="],
82
82
+
83
83
+
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
84
84
+
85
85
+
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
86
86
+
87
87
+
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
88
88
+
89
89
+
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
90
90
+
91
91
+
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
92
92
+
93
93
+
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
94
94
+
95
95
+
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
96
96
+
97
97
+
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
98
98
+
99
99
+
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
100
100
+
101
101
+
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
102
102
+
103
103
+
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
104
104
+
105
105
+
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
106
106
+
107
107
+
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
108
108
+
109
109
+
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
110
110
+
111
111
+
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
112
112
+
113
113
+
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
114
114
+
115
115
+
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
116
116
+
117
117
+
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
118
118
+
119
119
+
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
120
120
+
121
121
+
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
122
122
+
123
123
+
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
124
124
+
125
125
+
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
126
126
+
127
127
+
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
128
128
+
129
129
+
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
130
130
+
131
131
+
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
132
132
+
133
133
+
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
134
134
+
135
135
+
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
136
136
+
137
137
+
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
138
138
+
139
139
+
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
140
140
+
141
141
+
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
142
142
+
143
143
+
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
144
144
+
145
145
+
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
146
146
+
147
147
+
"@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
148
148
+
149
149
+
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
150
150
+
151
151
+
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
152
152
+
153
153
+
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
154
154
+
155
155
+
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
156
156
+
157
157
+
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
158
158
+
159
159
+
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
160
160
+
161
161
+
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
162
162
+
163
163
+
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
164
164
+
165
165
+
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
166
166
+
167
167
+
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
168
168
+
169
169
+
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
170
170
+
171
171
+
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
172
172
+
173
173
+
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
174
174
+
175
175
+
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
176
176
+
177
177
+
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
178
178
+
179
179
+
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
180
180
+
181
181
+
"@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="],
182
182
+
183
183
+
"@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="],
184
184
+
185
185
+
"@pnpm/npm-conf": ["@pnpm/npm-conf@3.0.2", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA=="],
186
186
+
187
187
+
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
188
188
+
189
189
+
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
190
190
+
191
191
+
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
192
192
+
193
193
+
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
194
194
+
195
195
+
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
196
196
+
197
197
+
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
198
198
+
199
199
+
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
200
200
+
201
201
+
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
202
202
+
203
203
+
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
204
204
+
205
205
+
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
206
206
+
207
207
+
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
208
208
+
209
209
+
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
210
210
+
211
211
+
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
212
212
+
213
213
+
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
214
214
+
215
215
+
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
216
216
+
217
217
+
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
218
218
+
219
219
+
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
220
220
+
221
221
+
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
222
222
+
223
223
+
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
224
224
+
225
225
+
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
226
226
+
227
227
+
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
228
228
+
229
229
+
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
230
230
+
231
231
+
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
232
232
+
233
233
+
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
234
234
+
235
235
+
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
236
236
+
237
237
+
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
238
238
+
239
239
+
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
240
240
+
241
241
+
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
242
242
+
243
243
+
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
244
244
+
245
245
+
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
246
246
+
247
247
+
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
248
248
+
249
249
+
"@types/chrome": ["@types/chrome@0.0.280", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-AotSmZrL9bcZDDmSI1D9dE7PGbhOur5L0cKxXd7IqbVizQWCY4gcvupPUVsQ4FfDj3V2tt/iOpomT9EY0s+w1g=="],
250
250
+
251
251
+
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
252
252
+
253
253
+
"@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="],
254
254
+
255
255
+
"@types/filewriter": ["@types/filewriter@0.0.33", "", {}, "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="],
256
256
+
257
257
+
"@types/har-format": ["@types/har-format@1.2.16", "", {}, "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A=="],
258
258
+
259
259
+
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
260
260
+
261
261
+
"@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="],
262
262
+
263
263
+
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
264
264
+
265
265
+
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
266
266
+
267
267
+
"@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="],
268
268
+
269
269
+
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
270
270
+
271
271
+
"@types/webextension-polyfill": ["@types/webextension-polyfill@0.12.4", "", {}, "sha512-wK8YdSI0pDiaehSLDIvtvonYmLwUUivg4Z6JCJO8rkyssMAG82cFJgwPK/V7NO61mJBLg/tXeoXQL8AFzpXZmQ=="],
272
272
+
273
273
+
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="],
274
274
+
275
275
+
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="],
276
276
+
277
277
+
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="],
278
278
+
279
279
+
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="],
280
280
+
281
281
+
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="],
282
282
+
283
283
+
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="],
284
284
+
285
285
+
"@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
286
286
+
287
287
+
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="],
288
288
+
289
289
+
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="],
290
290
+
291
291
+
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
292
292
+
293
293
+
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="],
294
294
+
295
295
+
"@webext-core/fake-browser": ["@webext-core/fake-browser@1.3.4", "", { "dependencies": { "lodash.merge": "^4.6.2" } }, "sha512-nZcVWr3JpwpS5E6hKpbAwAMBM/AXZShnfW0F76udW8oLd6Kv0nbW6vFS07md4Na/0ntQonk3hFnlQYGYBAlTrA=="],
296
296
+
297
297
+
"@webext-core/isolated-element": ["@webext-core/isolated-element@1.1.3", "", { "dependencies": { "is-potential-custom-element-name": "^1.0.1" } }, "sha512-rbtnReIGdiVQb2UhK3MiECU6JqsiIo2K/luWvOdOw57Ot770Iw4KLCEPXUQMITIH5V5er2jfVK8hSWXaEOQGNQ=="],
298
298
+
299
299
+
"@webext-core/match-patterns": ["@webext-core/match-patterns@1.0.3", "", {}, "sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A=="],
300
300
+
301
301
+
"@webext-core/messaging": ["@webext-core/messaging@1.4.0", "", { "dependencies": { "serialize-error": "^11.0.0", "webextension-polyfill": "^0.10.0" } }, "sha512-gzXQ13HfKR3Yrn9TnrvTC/5seA7uPFvaqxqNFBsFOOdSZa5LyXt58Rhym8BYXarkWUGp+fh8f6AYM3RYuNbS+A=="],
302
302
+
303
303
+
"@wxt-dev/browser": ["@wxt-dev/browser@0.1.32", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-jvfSppeLzlH4sOkIvMBJoA1pKoI+U5gTkjDwMKdkTWh0P/fj+KDyze3lzo3S6372viCm8tXUKNez+VKyVz2ZDw=="],
304
304
+
305
305
+
"@wxt-dev/module-react": ["@wxt-dev/module-react@1.1.5", "", { "dependencies": { "@vitejs/plugin-react": "^4.4.1 || ^5.0.0" }, "peerDependencies": { "wxt": ">=0.19.16" } }, "sha512-KgsUrsgH5rBT8MwiipnDEOHBXmLvTIdFICrI7KjngqSf9DpVRn92HsKmToxY0AYpkP19hHWta2oNYFTzmmm++g=="],
306
306
+
307
307
+
"@wxt-dev/storage": ["@wxt-dev/storage@1.2.6", "", { "dependencies": { "@wxt-dev/browser": "^0.1.4", "async-mutex": "^0.5.0", "dequal": "^2.0.3" } }, "sha512-f6AknnpJvhNHW4s0WqwSGCuZAj0fjP3EVNPBO5kB30pY+3Zt/nqZGqJN6FgBLCSkYjPJ8VL1hNX5LMVmvxQoDw=="],
308
308
+
309
309
+
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
310
310
+
311
311
+
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
312
312
+
313
313
+
"adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="],
314
314
+
315
315
+
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
316
316
+
317
317
+
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
318
318
+
319
319
+
"ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="],
320
320
+
321
321
+
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
322
322
+
323
323
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
324
324
+
325
325
+
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
326
326
+
327
327
+
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
328
328
+
329
329
+
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
330
330
+
331
331
+
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
332
332
+
333
333
+
"array-differ": ["array-differ@4.0.0", "", {}, "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw=="],
334
334
+
335
335
+
"array-union": ["array-union@3.0.1", "", {}, "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw=="],
336
336
+
337
337
+
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
338
338
+
339
339
+
"async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="],
340
340
+
341
341
+
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
342
342
+
343
343
+
"atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="],
344
344
+
345
345
+
"autoprefixer": ["autoprefixer@10.4.24", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001766", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw=="],
346
346
+
347
347
+
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
348
348
+
349
349
+
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
350
350
+
351
351
+
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
352
352
+
353
353
+
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
354
354
+
355
355
+
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
356
356
+
357
357
+
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
358
358
+
359
359
+
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
360
360
+
361
361
+
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
362
362
+
363
363
+
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
364
364
+
365
365
+
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
366
366
+
367
367
+
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
368
368
+
369
369
+
"c12": ["c12@3.3.3", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
370
370
+
371
371
+
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
372
372
+
373
373
+
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
374
374
+
375
375
+
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
376
376
+
377
377
+
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
378
378
+
379
379
+
"caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="],
380
380
+
381
381
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
382
382
+
383
383
+
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
384
384
+
385
385
+
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
386
386
+
387
387
+
"chrome-launcher": ["chrome-launcher@1.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q=="],
388
388
+
389
389
+
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
390
390
+
391
391
+
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
392
392
+
393
393
+
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
394
394
+
395
395
+
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
396
396
+
397
397
+
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
398
398
+
399
399
+
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
400
400
+
401
401
+
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
402
402
+
403
403
+
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
404
404
+
405
405
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
406
406
+
407
407
+
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
408
408
+
409
409
+
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
410
410
+
411
411
+
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
412
412
+
413
413
+
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
414
414
+
415
415
+
"concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="],
416
416
+
417
417
+
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
418
418
+
419
419
+
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
420
420
+
421
421
+
"configstore": ["configstore@7.1.0", "", { "dependencies": { "atomically": "^2.0.3", "dot-prop": "^9.0.0", "graceful-fs": "^4.2.11", "xdg-basedir": "^5.1.0" } }, "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg=="],
422
422
+
423
423
+
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
424
424
+
425
425
+
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
426
426
+
427
427
+
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
428
428
+
429
429
+
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
430
430
+
431
431
+
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
432
432
+
433
433
+
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
434
434
+
435
435
+
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
436
436
+
437
437
+
"cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="],
438
438
+
439
439
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
440
440
+
441
441
+
"debounce": ["debounce@1.2.1", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="],
442
442
+
443
443
+
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
444
444
+
445
445
+
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
446
446
+
447
447
+
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
448
448
+
449
449
+
"default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
450
450
+
451
451
+
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
452
452
+
453
453
+
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
454
454
+
455
455
+
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
456
456
+
457
457
+
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
458
458
+
459
459
+
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
460
460
+
461
461
+
"didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="],
462
462
+
463
463
+
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
464
464
+
465
465
+
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
466
466
+
467
467
+
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
468
468
+
469
469
+
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
470
470
+
471
471
+
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
472
472
+
473
473
+
"dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="],
474
474
+
475
475
+
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
476
476
+
477
477
+
"dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="],
478
478
+
479
479
+
"electron-to-chromium": ["electron-to-chromium@1.5.283", "", {}, "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w=="],
480
480
+
481
481
+
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
482
482
+
483
483
+
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
484
484
+
485
485
+
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
486
486
+
487
487
+
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
488
488
+
489
489
+
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
490
490
+
491
491
+
"es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="],
492
492
+
493
493
+
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
494
494
+
495
495
+
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
496
496
+
497
497
+
"escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="],
498
498
+
499
499
+
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
500
500
+
501
501
+
"eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
502
502
+
503
503
+
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
504
504
+
505
505
+
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
506
506
+
507
507
+
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
508
508
+
509
509
+
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
510
510
+
511
511
+
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
512
512
+
513
513
+
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
514
514
+
515
515
+
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
516
516
+
517
517
+
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
518
518
+
519
519
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
520
520
+
521
521
+
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
522
522
+
523
523
+
"execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
524
524
+
525
525
+
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
526
526
+
527
527
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
528
528
+
529
529
+
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
530
530
+
531
531
+
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
532
532
+
533
533
+
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
534
534
+
535
535
+
"fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="],
536
536
+
537
537
+
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
538
538
+
539
539
+
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
540
540
+
541
541
+
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
542
542
+
543
543
+
"filesize": ["filesize@10.1.6", "", {}, "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w=="],
544
544
+
545
545
+
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
546
546
+
547
547
+
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
548
548
+
549
549
+
"firefox-profile": ["firefox-profile@4.7.0", "", { "dependencies": { "adm-zip": "~0.5.x", "fs-extra": "^11.2.0", "ini": "^4.1.3", "minimist": "^1.2.8", "xml2js": "^0.6.2" }, "bin": { "firefox-profile": "lib/cli.js" } }, "sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ=="],
550
550
+
551
551
+
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
552
552
+
553
553
+
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
554
554
+
555
555
+
"form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="],
556
556
+
557
557
+
"formdata-node": ["formdata-node@6.0.3", "", {}, "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg=="],
558
558
+
559
559
+
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
560
560
+
561
561
+
"fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
562
562
+
563
563
+
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
564
564
+
565
565
+
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
566
566
+
567
567
+
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
568
568
+
569
569
+
"fx-runner": ["fx-runner@1.4.0", "", { "dependencies": { "commander": "2.9.0", "shell-quote": "1.7.3", "spawn-sync": "1.0.15", "when": "3.7.7", "which": "1.2.4", "winreg": "0.0.12" }, "bin": { "fx-runner": "bin/fx-runner" } }, "sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg=="],
570
570
+
571
571
+
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
572
572
+
573
573
+
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
574
574
+
575
575
+
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
576
576
+
577
577
+
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
578
578
+
579
579
+
"get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
580
580
+
581
581
+
"giget": ["giget@1.2.5", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.5.4", "pathe": "^2.0.3", "tar": "^6.2.1" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-r1ekGw/Bgpi3HLV3h1MRBIlSAdHoIMklpaQ3OQLFcRw9PwAj2rqigvIbg+dBUI51OxVI2jsEtDywDBjSiuf7Ug=="],
582
582
+
583
583
+
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
584
584
+
585
585
+
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
586
586
+
587
587
+
"global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="],
588
588
+
589
589
+
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
590
590
+
591
591
+
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
592
592
+
593
593
+
"graceful-readlink": ["graceful-readlink@1.0.1", "", {}, "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w=="],
594
594
+
595
595
+
"growly": ["growly@1.3.0", "", {}, "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw=="],
596
596
+
597
597
+
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
598
598
+
599
599
+
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
600
600
+
601
601
+
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
602
602
+
603
603
+
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
604
604
+
605
605
+
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
606
606
+
607
607
+
"human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
608
608
+
609
609
+
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
610
610
+
611
611
+
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
612
612
+
613
613
+
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
614
614
+
615
615
+
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
616
616
+
617
617
+
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
618
618
+
619
619
+
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
620
620
+
621
621
+
"ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="],
622
622
+
623
623
+
"is-absolute": ["is-absolute@0.1.7", "", { "dependencies": { "is-relative": "^0.1.0" } }, "sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA=="],
624
624
+
625
625
+
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
626
626
+
627
627
+
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
628
628
+
629
629
+
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
630
630
+
631
631
+
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
632
632
+
633
633
+
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
634
634
+
635
635
+
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
636
636
+
637
637
+
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
638
638
+
639
639
+
"is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="],
640
640
+
641
641
+
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
642
642
+
643
643
+
"is-installed-globally": ["is-installed-globally@1.0.0", "", { "dependencies": { "global-directory": "^4.0.1", "is-path-inside": "^4.0.0" } }, "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ=="],
644
644
+
645
645
+
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
646
646
+
647
647
+
"is-npm": ["is-npm@6.1.0", "", {}, "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA=="],
648
648
+
649
649
+
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
650
650
+
651
651
+
"is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="],
652
652
+
653
653
+
"is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="],
654
654
+
655
655
+
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
656
656
+
657
657
+
"is-primitive": ["is-primitive@3.0.1", "", {}, "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w=="],
658
658
+
659
659
+
"is-relative": ["is-relative@0.1.3", "", {}, "sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA=="],
660
660
+
661
661
+
"is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
662
662
+
663
663
+
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
664
664
+
665
665
+
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
666
666
+
667
667
+
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
668
668
+
669
669
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
670
670
+
671
671
+
"isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="],
672
672
+
673
673
+
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
674
674
+
675
675
+
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
676
676
+
677
677
+
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
678
678
+
679
679
+
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
680
680
+
681
681
+
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
682
682
+
683
683
+
"json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="],
684
684
+
685
685
+
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
686
686
+
687
687
+
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
688
688
+
689
689
+
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
690
690
+
691
691
+
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
692
692
+
693
693
+
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
694
694
+
695
695
+
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
696
696
+
697
697
+
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
698
698
+
699
699
+
"ky": ["ky@1.14.3", "", {}, "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw=="],
700
700
+
701
701
+
"latest-version": ["latest-version@9.0.0", "", { "dependencies": { "package-json": "^10.0.0" } }, "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA=="],
702
702
+
703
703
+
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
704
704
+
705
705
+
"lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
706
706
+
707
707
+
"lighthouse-logger": ["lighthouse-logger@2.0.2", "", { "dependencies": { "debug": "^4.4.1", "marky": "^1.2.2" } }, "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg=="],
708
708
+
709
709
+
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
710
710
+
711
711
+
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
712
712
+
713
713
+
"linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="],
714
714
+
715
715
+
"listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="],
716
716
+
717
717
+
"local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
718
718
+
719
719
+
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
720
720
+
721
721
+
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
722
722
+
723
723
+
"log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
724
724
+
725
725
+
"log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
726
726
+
727
727
+
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
728
728
+
729
729
+
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
730
730
+
731
731
+
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
732
732
+
733
733
+
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
734
734
+
735
735
+
"magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="],
736
736
+
737
737
+
"make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="],
738
738
+
739
739
+
"many-keys-map": ["many-keys-map@2.0.1", "", {}, "sha512-DHnZAD4phTbZ+qnJdjoNEVU1NecYoSdbOOoVmTDH46AuxDkEVh3MxTVpXq10GtcTC6mndN9dkv1rNfpjRcLnOw=="],
740
740
+
741
741
+
"marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="],
742
742
+
743
743
+
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
744
744
+
745
745
+
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
746
746
+
747
747
+
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
748
748
+
749
749
+
"mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
750
750
+
751
751
+
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
752
752
+
753
753
+
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
754
754
+
755
755
+
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
756
756
+
757
757
+
"minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
758
758
+
759
759
+
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
760
760
+
761
761
+
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
762
762
+
763
763
+
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
764
764
+
765
765
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
766
766
+
767
767
+
"multimatch": ["multimatch@6.0.0", "", { "dependencies": { "@types/minimatch": "^3.0.5", "array-differ": "^4.0.0", "array-union": "^3.0.1", "minimatch": "^3.0.4" } }, "sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ=="],
768
768
+
769
769
+
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
770
770
+
771
771
+
"nano-spawn": ["nano-spawn@0.2.1", "", {}, "sha512-/pULofvsF8mOVcl/nUeVXL/GYOEvc7eJWSIxa+K4OYUolvXa5zwSgevsn4eoHs1xvh/BO3vx/PZiD9+Ow2ZVuw=="],
772
772
+
773
773
+
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
774
774
+
775
775
+
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
776
776
+
777
777
+
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
778
778
+
779
779
+
"node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="],
780
780
+
781
781
+
"node-notifier": ["node-notifier@10.0.1", "", { "dependencies": { "growly": "^1.3.0", "is-wsl": "^2.2.0", "semver": "^7.3.5", "shellwords": "^0.1.1", "uuid": "^8.3.2", "which": "^2.0.2" } }, "sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ=="],
782
782
+
783
783
+
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
784
784
+
785
785
+
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
786
786
+
787
787
+
"npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
788
788
+
789
789
+
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
790
790
+
791
791
+
"nypm": ["nypm@0.3.12", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.2.3", "execa": "^8.0.1", "pathe": "^1.1.2", "pkg-types": "^1.2.0", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA=="],
792
792
+
793
793
+
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
794
794
+
795
795
+
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
796
796
+
797
797
+
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
798
798
+
799
799
+
"ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="],
800
800
+
801
801
+
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
802
802
+
803
803
+
"onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
804
804
+
805
805
+
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
806
806
+
807
807
+
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
808
808
+
809
809
+
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
810
810
+
811
811
+
"os-shim": ["os-shim@0.1.3", "", {}, "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A=="],
812
812
+
813
813
+
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
814
814
+
815
815
+
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
816
816
+
817
817
+
"package-json": ["package-json@10.0.1", "", { "dependencies": { "ky": "^1.2.0", "registry-auth-token": "^5.0.2", "registry-url": "^6.0.1", "semver": "^7.6.0" } }, "sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg=="],
818
818
+
819
819
+
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
820
820
+
821
821
+
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
822
822
+
823
823
+
"parse-json": ["parse-json@7.1.1", "", { "dependencies": { "@babel/code-frame": "^7.21.4", "error-ex": "^1.3.2", "json-parse-even-better-errors": "^3.0.0", "lines-and-columns": "^2.0.3", "type-fest": "^3.8.0" } }, "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw=="],
824
824
+
825
825
+
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
826
826
+
827
827
+
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
828
828
+
829
829
+
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
830
830
+
831
831
+
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
832
832
+
833
833
+
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
834
834
+
835
835
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
836
836
+
837
837
+
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
838
838
+
839
839
+
"pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="],
840
840
+
841
841
+
"pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="],
842
842
+
843
843
+
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
844
844
+
845
845
+
"pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="],
846
846
+
847
847
+
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
848
848
+
849
849
+
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
850
850
+
851
851
+
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
852
852
+
853
853
+
"postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="],
854
854
+
855
855
+
"postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="],
856
856
+
857
857
+
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
858
858
+
859
859
+
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
860
860
+
861
861
+
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
862
862
+
863
863
+
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
864
864
+
865
865
+
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
866
866
+
867
867
+
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
868
868
+
869
869
+
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
870
870
+
871
871
+
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
872
872
+
873
873
+
"promise-toolbox": ["promise-toolbox@0.21.0", "", { "dependencies": { "make-error": "^1.3.2" } }, "sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg=="],
874
874
+
875
875
+
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
876
876
+
877
877
+
"proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="],
878
878
+
879
879
+
"publish-browser-extension": ["publish-browser-extension@3.0.3", "", { "dependencies": { "cac": "^6.7.14", "consola": "^3.4.2", "dotenv": "^17.2.3", "form-data-encoder": "^4.1.0", "formdata-node": "^6.0.3", "listr2": "^8.3.3", "ofetch": "^1.4.1", "zod": "^3.25.76 || ^4.0.0" }, "bin": { "publish-extension": "bin/publish-extension.cjs" } }, "sha512-cBINZCkLo7YQaGoUvEHthZ0sDzgJQht28IS+SFMT2omSNhGsPiVNRkWir3qLiTrhGhW9Ci2KVHpA1QAMoBdL2g=="],
880
880
+
881
881
+
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
882
882
+
883
883
+
"pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="],
884
884
+
885
885
+
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
886
886
+
887
887
+
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
888
888
+
889
889
+
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
890
890
+
891
891
+
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
892
892
+
893
893
+
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
894
894
+
895
895
+
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
896
896
+
897
897
+
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
898
898
+
899
899
+
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
900
900
+
901
901
+
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
902
902
+
903
903
+
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
904
904
+
905
905
+
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
906
906
+
907
907
+
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
908
908
+
909
909
+
"registry-auth-token": ["registry-auth-token@5.1.1", "", { "dependencies": { "@pnpm/npm-conf": "^3.0.2" } }, "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q=="],
910
910
+
911
911
+
"registry-url": ["registry-url@6.0.1", "", { "dependencies": { "rc": "1.2.8" } }, "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q=="],
912
912
+
913
913
+
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
914
914
+
915
915
+
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
916
916
+
917
917
+
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
918
918
+
919
919
+
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
920
920
+
921
921
+
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
922
922
+
923
923
+
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
924
924
+
925
925
+
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
926
926
+
927
927
+
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
928
928
+
929
929
+
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
930
930
+
931
931
+
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
932
932
+
933
933
+
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
934
934
+
935
935
+
"sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
936
936
+
937
937
+
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
938
938
+
939
939
+
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
940
940
+
941
941
+
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
942
942
+
943
943
+
"serialize-error": ["serialize-error@11.0.3", "", { "dependencies": { "type-fest": "^2.12.2" } }, "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g=="],
944
944
+
945
945
+
"set-value": ["set-value@4.1.0", "", { "dependencies": { "is-plain-object": "^2.0.4", "is-primitive": "^3.0.1" } }, "sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw=="],
946
946
+
947
947
+
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
948
948
+
949
949
+
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
950
950
+
951
951
+
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
952
952
+
953
953
+
"shell-quote": ["shell-quote@1.7.3", "", {}, "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="],
954
954
+
955
955
+
"shellwords": ["shellwords@0.1.1", "", {}, "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="],
956
956
+
957
957
+
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
958
958
+
959
959
+
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
960
960
+
961
961
+
"slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
962
962
+
963
963
+
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
964
964
+
965
965
+
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
966
966
+
967
967
+
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
968
968
+
969
969
+
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
970
970
+
971
971
+
"spawn-sync": ["spawn-sync@1.0.15", "", { "dependencies": { "concat-stream": "^1.4.7", "os-shim": "^0.1.2" } }, "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw=="],
972
972
+
973
973
+
"split": ["split@1.0.1", "", { "dependencies": { "through": "2" } }, "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg=="],
974
974
+
975
975
+
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
976
976
+
977
977
+
"stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
978
978
+
979
979
+
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
980
980
+
981
981
+
"string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
982
982
+
983
983
+
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
984
984
+
985
985
+
"strip-bom": ["strip-bom@5.0.0", "", {}, "sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A=="],
986
986
+
987
987
+
"strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
988
988
+
989
989
+
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
990
990
+
991
991
+
"strip-literal": ["strip-literal@2.1.1", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q=="],
992
992
+
993
993
+
"stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="],
994
994
+
995
995
+
"stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
996
996
+
997
997
+
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
998
998
+
999
999
+
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
1000
1000
+
1001
1001
+
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
1002
1002
+
1003
1003
+
"tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="],
1004
1004
+
1005
1005
+
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
1006
1006
+
1007
1007
+
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
1008
1008
+
1009
1009
+
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
1010
1010
+
1011
1011
+
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
1012
1012
+
1013
1013
+
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
1014
1014
+
1015
1015
+
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
1016
1016
+
1017
1017
+
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
1018
1018
+
1019
1019
+
"tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
1020
1020
+
1021
1021
+
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
1022
1022
+
1023
1023
+
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
1024
1024
+
1025
1025
+
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
1026
1026
+
1027
1027
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1028
1028
+
1029
1029
+
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
1030
1030
+
1031
1031
+
"type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
1032
1032
+
1033
1033
+
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
1034
1034
+
1035
1035
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
1036
1036
+
1037
1037
+
"typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="],
1038
1038
+
1039
1039
+
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
1040
1040
+
1041
1041
+
"uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
1042
1042
+
1043
1043
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
1044
1044
+
1045
1045
+
"unimport": ["unimport@3.14.6", "", { "dependencies": { "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "fast-glob": "^3.3.3", "local-pkg": "^1.0.0", "magic-string": "^0.30.17", "mlly": "^1.7.4", "pathe": "^2.0.1", "picomatch": "^4.0.2", "pkg-types": "^1.3.0", "scule": "^1.3.0", "strip-literal": "^2.1.1", "unplugin": "^1.16.1" } }, "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g=="],
1046
1046
+
1047
1047
+
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
1048
1048
+
1049
1049
+
"unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="],
1050
1050
+
1051
1051
+
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
1052
1052
+
1053
1053
+
"update-notifier": ["update-notifier@7.3.1", "", { "dependencies": { "boxen": "^8.0.1", "chalk": "^5.3.0", "configstore": "^7.0.0", "is-in-ci": "^1.0.0", "is-installed-globally": "^1.0.0", "is-npm": "^6.0.0", "latest-version": "^9.0.0", "pupa": "^3.1.0", "semver": "^7.6.3", "xdg-basedir": "^5.1.0" } }, "sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA=="],
1054
1054
+
1055
1055
+
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
1056
1056
+
1057
1057
+
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
1058
1058
+
1059
1059
+
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
1060
1060
+
1061
1061
+
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
1062
1062
+
1063
1063
+
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
1064
1064
+
1065
1065
+
"watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="],
1066
1066
+
1067
1067
+
"web-ext-run": ["web-ext-run@0.2.4", "", { "dependencies": { "@babel/runtime": "7.28.2", "@devicefarmer/adbkit": "3.3.8", "chrome-launcher": "1.2.0", "debounce": "1.2.1", "es6-error": "4.1.1", "firefox-profile": "4.7.0", "fx-runner": "1.4.0", "multimatch": "6.0.0", "node-notifier": "10.0.1", "parse-json": "7.1.1", "pino": "9.7.0", "promise-toolbox": "0.21.0", "set-value": "4.1.0", "source-map-support": "0.5.21", "strip-bom": "5.0.0", "strip-json-comments": "5.0.2", "tmp": "0.2.5", "update-notifier": "7.3.1", "watchpack": "2.4.4", "zip-dir": "2.0.0" } }, "sha512-rQicL7OwuqWdQWI33JkSXKcp7cuv1mJG8u3jRQwx/8aDsmhbTHs9ZRmNYOL+LX0wX8edIEQX8jj4bB60GoXtKA=="],
1068
1068
+
1069
1069
+
"webextension-polyfill": ["webextension-polyfill@0.12.0", "", {}, "sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q=="],
1070
1070
+
1071
1071
+
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
1072
1072
+
1073
1073
+
"when": ["when@3.7.7", "", {}, "sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw=="],
1074
1074
+
1075
1075
+
"when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
1076
1076
+
1077
1077
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
1078
1078
+
1079
1079
+
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
1080
1080
+
1081
1081
+
"winreg": ["winreg@0.0.12", "", {}, "sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ=="],
1082
1082
+
1083
1083
+
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
1084
1084
+
1085
1085
+
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
1086
1086
+
1087
1087
+
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
1088
1088
+
1089
1089
+
"wxt": ["wxt@0.19.29", "", { "dependencies": { "@1natsu/wait-element": "^4.1.2", "@aklinker1/rollup-plugin-visualizer": "5.12.0", "@types/chrome": "^0.0.280", "@types/webextension-polyfill": "^0.12.1", "@webext-core/fake-browser": "^1.3.1", "@webext-core/isolated-element": "^1.1.2", "@webext-core/match-patterns": "^1.0.3", "@wxt-dev/storage": "^1.0.0", "async-mutex": "^0.5.0", "c12": "^3.0.2", "cac": "^6.7.14", "chokidar": "^4.0.3", "ci-info": "^4.1.0", "consola": "^3.2.3", "defu": "^6.1.4", "dotenv": "^16.4.5", "dotenv-expand": "^12.0.1", "esbuild": "^0.25.0", "fast-glob": "^3.3.2", "filesize": "^10.1.6", "fs-extra": "^11.2.0", "get-port-please": "^3.1.2", "giget": "^1.2.3", "hookable": "^5.5.3", "import-meta-resolve": "^4.1.0", "is-wsl": "^3.1.0", "jiti": "^2.4.2", "json5": "^2.2.3", "jszip": "^3.10.1", "linkedom": "^0.18.5", "magicast": "^0.3.5", "minimatch": "^10.0.1", "nano-spawn": "^0.2.0", "normalize-path": "^3.0.0", "nypm": "^0.3.12", "ohash": "^1.1.4", "open": "^10.1.0", "ora": "^8.1.1", "perfect-debounce": "^1.0.0", "picocolors": "^1.1.1", "prompts": "^2.4.2", "publish-browser-extension": "^2.3.0 || ^3.0.0", "scule": "^1.3.0", "unimport": "^3.13.1", "vite": "^5.0.0 || ^6.0.0", "vite-node": "^2.1.4 || ^3.0.0", "web-ext-run": "^0.2.1", "webextension-polyfill": "^0.12.0" }, "bin": { "wxt": "bin/wxt.mjs", "wxt-publish-extension": "bin/wxt-publish-extension.cjs" } }, "sha512-n6DRR34OAFczJfZOwJeY5dn+j+w2BTquW2nAX32vk3FMLWUhzpv5svMvSUTyNiFq3P0o3U7YxfxHdmKJnXZHBA=="],
1090
1090
+
1091
1091
+
"xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="],
1092
1092
+
1093
1093
+
"xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
1094
1094
+
1095
1095
+
"xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
1096
1096
+
1097
1097
+
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
1098
1098
+
1099
1099
+
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
1100
1100
+
1101
1101
+
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
1102
1102
+
1103
1103
+
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
1104
1104
+
1105
1105
+
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
1106
1106
+
1107
1107
+
"zip-dir": ["zip-dir@2.0.0", "", { "dependencies": { "async": "^3.2.0", "jszip": "^3.2.2" } }, "sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg=="],
1108
1108
+
1109
1109
+
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
1110
1110
+
1111
1111
+
"@aklinker1/rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
1112
1112
+
1113
1113
+
"@babel/core/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
1114
1114
+
1115
1115
+
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1116
1116
+
1117
1117
+
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1118
1118
+
1119
1119
+
"@devicefarmer/adbkit/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
1120
1120
+
1121
1121
+
"@devicefarmer/adbkit/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
1122
1122
+
1123
1123
+
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
1124
1124
+
1125
1125
+
"@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="],
1126
1126
+
1127
1127
+
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
1128
1128
+
1129
1129
+
"@rollup/pluginutils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
1130
1130
+
1131
1131
+
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
1132
1132
+
1133
1133
+
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
1134
1134
+
1135
1135
+
"@webext-core/messaging/webextension-polyfill": ["webextension-polyfill@0.10.0", "", {}, "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g=="],
1136
1136
+
1137
1137
+
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
1138
1138
+
1139
1139
+
"boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
1140
1140
+
1141
1141
+
"boxen/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
1142
1142
+
1143
1143
+
"c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
1144
1144
+
1145
1145
+
"c12/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
1146
1146
+
1147
1147
+
"c12/giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
1148
1148
+
1149
1149
+
"c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
1150
1150
+
1151
1151
+
"c12/ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
1152
1152
+
1153
1153
+
"c12/perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
1154
1154
+
1155
1155
+
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
1156
1156
+
1157
1157
+
"chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
1158
1158
+
1159
1159
+
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
1160
1160
+
1161
1161
+
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
1162
1162
+
1163
1163
+
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
1164
1164
+
1165
1165
+
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
1166
1166
+
1167
1167
+
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
1168
1168
+
1169
1169
+
"dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
1170
1170
+
1171
1171
+
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
1172
1172
+
1173
1173
+
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1174
1174
+
1175
1175
+
"fx-runner/commander": ["commander@2.9.0", "", { "dependencies": { "graceful-readlink": ">= 1.0.0" } }, "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A=="],
1176
1176
+
1177
1177
+
"fx-runner/which": ["which@1.2.4", "", { "dependencies": { "is-absolute": "^0.1.7", "isexe": "^1.1.1" }, "bin": { "which": "./bin/which" } }, "sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA=="],
1178
1178
+
1179
1179
+
"giget/nypm": ["nypm@0.5.4", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "tinyexec": "^0.3.2", "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-X0SNNrZiGU8/e/zAB7sCTtdxWTMSIO73q+xuKgglm2Yvzwlo8UoC5FNySQFCvl84uPaeADkqHUZUkWy4aH4xOA=="],
1180
1180
+
1181
1181
+
"global-directory/ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="],
1182
1182
+
1183
1183
+
"is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
1184
1184
+
1185
1185
+
"log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
1186
1186
+
1187
1187
+
"log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
1188
1188
+
1189
1189
+
"log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
1190
1190
+
1191
1191
+
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
1192
1192
+
1193
1193
+
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
1194
1194
+
1195
1195
+
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
1196
1196
+
1197
1197
+
"node-notifier/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
1198
1198
+
1199
1199
+
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
1200
1200
+
1201
1201
+
"nypm/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
1202
1202
+
1203
1203
+
"nypm/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
1204
1204
+
1205
1205
+
"ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
1206
1206
+
1207
1207
+
"parse-json/lines-and-columns": ["lines-and-columns@2.0.4", "", {}, "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A=="],
1208
1208
+
1209
1209
+
"parse-json/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
1210
1210
+
1211
1211
+
"publish-browser-extension/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
1212
1212
+
1213
1213
+
"rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
1214
1214
+
1215
1215
+
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
1216
1216
+
1217
1217
+
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
1218
1218
+
1219
1219
+
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
1220
1220
+
1221
1221
+
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
1222
1222
+
1223
1223
+
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
1224
1224
+
1225
1225
+
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
1226
1226
+
1227
1227
+
"tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
1228
1228
+
1229
1229
+
"unimport/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
1230
1230
+
1231
1231
+
"unimport/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
1232
1232
+
1233
1233
+
"unimport/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
1234
1234
+
1235
1235
+
"update-notifier/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
1236
1236
+
1237
1237
+
"vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
1238
1238
+
1239
1239
+
"web-ext-run/strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="],
1240
1240
+
1241
1241
+
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
1242
1242
+
1243
1243
+
"wxt/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
1244
1244
+
1245
1245
+
"wxt/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
1246
1246
+
1247
1247
+
"wxt/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
1248
1248
+
1249
1249
+
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
1250
1250
+
1251
1251
+
"@aklinker1/rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
1252
1252
+
1253
1253
+
"@aklinker1/rollup-plugin-visualizer/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
1254
1254
+
1255
1255
+
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
1256
1256
+
1257
1257
+
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
1258
1258
+
1259
1259
+
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
1260
1260
+
1261
1261
+
"c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
1262
1262
+
1263
1263
+
"c12/giget/nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="],
1264
1264
+
1265
1265
+
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
1266
1266
+
1267
1267
+
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1268
1268
+
1269
1269
+
"fx-runner/which/isexe": ["isexe@1.1.2", "", {}, "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw=="],
1270
1270
+
1271
1271
+
"giget/nypm/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
1272
1272
+
1273
1273
+
"log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
1274
1274
+
1275
1275
+
"log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
1276
1276
+
1277
1277
+
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
1278
1278
+
1279
1279
+
"nypm/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
1280
1280
+
1281
1281
+
"nypm/pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
1282
1282
+
1283
1283
+
"unimport/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
1284
1284
+
1285
1285
+
"wxt/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
1286
1286
+
1287
1287
+
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
1288
1288
+
1289
1289
+
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
1290
1290
+
1291
1291
+
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1292
1292
+
1293
1293
+
"c12/giget/nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="],
1294
1294
+
1295
1295
+
"c12/giget/nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
1296
1296
+
1297
1297
+
"giget/nypm/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
1298
1298
+
1299
1299
+
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
1300
1300
+
}
1301
1301
+
}
-21
extension/content/content.css
···
1
1
-
::highlight(margin-highlight-preview) {
2
2
-
background-color: rgba(149, 122, 134, 0.3);
3
3
-
color: inherit;
4
4
-
}
5
5
-
6
6
-
::highlight(margin-scroll-highlight) {
7
7
-
background-color: rgba(149, 122, 134, 0.5);
8
8
-
color: inherit;
9
9
-
}
10
10
-
11
11
-
::highlight(margin-page-highlights) {
12
12
-
background-color: rgba(149, 122, 134, 0.25);
13
13
-
color: inherit;
14
14
-
}
15
15
-
16
16
-
.margin-notification {
17
17
-
position: fixed;
18
18
-
bottom: 24px;
19
19
-
right: 24px;
20
20
-
z-index: 999999;
21
21
-
}
-1263
extension/content/content.js
···
1
1
-
(() => {
2
2
-
let sidebarHost = null;
3
3
-
let sidebarShadow = null;
4
4
-
let popoverEl = null;
5
5
-
6
6
-
let activeItems = [];
7
7
-
let currentSelection = null;
8
8
-
9
9
-
const OVERLAY_STYLES = `
10
10
-
:host {
11
11
-
all: initial;
12
12
-
--bg-primary: #0a0a0d;
13
13
-
--bg-secondary: #121216;
14
14
-
--bg-tertiary: #1a1a1f;
15
15
-
--bg-card: #0f0f13;
16
16
-
--bg-elevated: #18181d;
17
17
-
--bg-hover: #1e1e24;
18
18
-
19
19
-
--text-primary: #eaeaee;
20
20
-
--text-secondary: #b7b6c5;
21
21
-
--text-tertiary: #6e6d7a;
22
22
-
--border: rgba(183, 182, 197, 0.12);
23
23
-
24
24
-
--accent: #957a86;
25
25
-
--accent-hover: #a98d98;
26
26
-
--accent-subtle: rgba(149, 122, 134, 0.15);
27
27
-
}
28
28
-
29
29
-
:host(.light) {
30
30
-
--bg-primary: #f8f8fa;
31
31
-
--bg-secondary: #ffffff;
32
32
-
--bg-tertiary: #f0f0f4;
33
33
-
--bg-card: #ffffff;
34
34
-
--bg-elevated: #ffffff;
35
35
-
--bg-hover: #eeeef2;
36
36
-
37
37
-
--text-primary: #18171c;
38
38
-
--text-secondary: #5c495a;
39
39
-
--text-tertiary: #8a8494;
40
40
-
--border: rgba(92, 73, 90, 0.12);
41
41
-
42
42
-
--accent: #7a5f6d;
43
43
-
--accent-hover: #664e5b;
44
44
-
--accent-subtle: rgba(149, 122, 134, 0.12);
45
45
-
}
46
46
-
47
47
-
.margin-overlay {
48
48
-
position: absolute;
49
49
-
top: 0;
50
50
-
left: 0;
51
51
-
width: 100%;
52
52
-
height: 100%;
53
53
-
pointer-events: none;
54
54
-
}
55
55
-
56
56
-
.margin-popover {
57
57
-
position: absolute;
58
58
-
width: 300px;
59
59
-
background: var(--bg-card);
60
60
-
border: 1px solid var(--border);
61
61
-
border-radius: 12px;
62
62
-
padding: 0;
63
63
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
64
64
-
display: flex;
65
65
-
flex-direction: column;
66
66
-
pointer-events: auto;
67
67
-
z-index: 2147483647;
68
68
-
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
69
69
-
color: var(--text-primary);
70
70
-
opacity: 0;
71
71
-
transform: translateY(-4px);
72
72
-
animation: popover-in 0.15s forwards;
73
73
-
max-height: 400px;
74
74
-
overflow: hidden;
75
75
-
}
76
76
-
@keyframes popover-in { to { opacity: 1; transform: translateY(0); } }
77
77
-
78
78
-
.popover-header {
79
79
-
padding: 10px 14px;
80
80
-
border-bottom: 1px solid var(--border);
81
81
-
display: flex;
82
82
-
justify-content: space-between;
83
83
-
align-items: center;
84
84
-
background: var(--bg-primary);
85
85
-
border-radius: 12px 12px 0 0;
86
86
-
font-weight: 500;
87
87
-
font-size: 11px;
88
88
-
color: var(--text-tertiary);
89
89
-
text-transform: uppercase;
90
90
-
letter-spacing: 0.5px;
91
91
-
}
92
92
-
.popover-close {
93
93
-
background: none;
94
94
-
border: none;
95
95
-
color: var(--text-tertiary);
96
96
-
cursor: pointer;
97
97
-
padding: 2px;
98
98
-
font-size: 16px;
99
99
-
line-height: 1;
100
100
-
opacity: 0.6;
101
101
-
transition: opacity 0.15s;
102
102
-
}
103
103
-
.popover-close:hover { opacity: 1; }
104
104
-
105
105
-
.popover-scroll-area {
106
106
-
overflow-y: auto;
107
107
-
max-height: 340px;
108
108
-
}
109
109
-
110
110
-
.comment-item {
111
111
-
padding: 12px 14px;
112
112
-
border-bottom: 1px solid var(--border);
113
113
-
}
114
114
-
.comment-item:last-child {
115
115
-
border-bottom: none;
116
116
-
}
117
117
-
118
118
-
.comment-header {
119
119
-
display: flex;
120
120
-
align-items: center;
121
121
-
gap: 8px;
122
122
-
margin-bottom: 6px;
123
123
-
}
124
124
-
.comment-avatar {
125
125
-
width: 22px;
126
126
-
height: 22px;
127
127
-
border-radius: 50%;
128
128
-
background: var(--accent);
129
129
-
display: flex;
130
130
-
align-items: center;
131
131
-
justify-content: center;
132
132
-
font-size: 9px;
133
133
-
font-weight: 600;
134
134
-
color: white;
135
135
-
}
136
136
-
.comment-handle {
137
137
-
font-size: 12px;
138
138
-
font-weight: 600;
139
139
-
color: var(--text-primary);
140
140
-
}
141
141
-
.comment-time {
142
142
-
font-size: 11px;
143
143
-
color: var(--text-tertiary);
144
144
-
margin-left: auto;
145
145
-
}
146
146
-
147
147
-
.comment-text {
148
148
-
font-size: 13px;
149
149
-
line-height: 1.5;
150
150
-
color: var(--text-primary);
151
151
-
margin-bottom: 8px;
152
152
-
}
153
153
-
154
154
-
.highlight-only-badge {
155
155
-
display: inline-flex;
156
156
-
align-items: center;
157
157
-
gap: 4px;
158
158
-
font-size: 11px;
159
159
-
color: var(--text-tertiary);
160
160
-
font-style: italic;
161
161
-
}
162
162
-
163
163
-
.comment-actions {
164
164
-
display: flex;
165
165
-
gap: 8px;
166
166
-
margin-top: 8px;
167
167
-
}
168
168
-
.highlight-only-badge {
169
169
-
font-size: 11px;
170
170
-
color: var(--text-tertiary);
171
171
-
font-style: italic;
172
172
-
opacity: 0.7;
173
173
-
}
174
174
-
.comment-action-btn {
175
175
-
background: none;
176
176
-
border: none;
177
177
-
padding: 4px 8px;
178
178
-
color: var(--text-tertiary);
179
179
-
font-size: 11px;
180
180
-
cursor: pointer;
181
181
-
border-radius: 4px;
182
182
-
transition: all 0.15s;
183
183
-
}
184
184
-
.comment-action-btn:hover {
185
185
-
background: var(--bg-hover);
186
186
-
color: var(--text-secondary);
187
187
-
}
188
188
-
189
189
-
.margin-selection-popup {
190
190
-
position: fixed;
191
191
-
display: flex;
192
192
-
gap: 4px;
193
193
-
padding: 6px;
194
194
-
background: var(--bg-card);
195
195
-
border: 1px solid var(--border);
196
196
-
border-radius: 8px;
197
197
-
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
198
198
-
z-index: 2147483647;
199
199
-
pointer-events: auto;
200
200
-
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
201
201
-
animation: popover-in 0.15s forwards;
202
202
-
}
203
203
-
.selection-btn {
204
204
-
display: flex;
205
205
-
align-items: center;
206
206
-
gap: 6px;
207
207
-
padding: 6px 12px;
208
208
-
background: transparent;
209
209
-
border: none;
210
210
-
border-radius: 6px;
211
211
-
color: var(--text-primary);
212
212
-
font-size: 12px;
213
213
-
font-weight: 500;
214
214
-
cursor: pointer;
215
215
-
transition: background 0.15s;
216
216
-
}
217
217
-
.selection-btn:hover {
218
218
-
background: var(--bg-hover);
219
219
-
}
220
220
-
.selection-btn svg {
221
221
-
width: 14px;
222
222
-
height: 14px;
223
223
-
}
224
224
-
.inline-compose-modal {
225
225
-
position: fixed;
226
226
-
width: 340px;
227
227
-
max-width: calc(100vw - 40px);
228
228
-
background: var(--bg-card);
229
229
-
border: 1px solid var(--border);
230
230
-
border-radius: 12px;
231
231
-
padding: 16px;
232
232
-
box-sizing: border-box;
233
233
-
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
234
234
-
z-index: 2147483647;
235
235
-
pointer-events: auto;
236
236
-
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
237
237
-
color: var(--text-primary);
238
238
-
animation: popover-in 0.15s forwards;
239
239
-
overflow: hidden;
240
240
-
}
241
241
-
.inline-compose-modal * {
242
242
-
box-sizing: border-box;
243
243
-
}
244
244
-
.inline-compose-quote {
245
245
-
padding: 8px 12px;
246
246
-
background: var(--accent-subtle);
247
247
-
border-left: 2px solid var(--accent);
248
248
-
border-radius: 4px;
249
249
-
font-size: 12px;
250
250
-
color: var(--text-secondary);
251
251
-
font-style: italic;
252
252
-
margin-bottom: 12px;
253
253
-
max-height: 60px;
254
254
-
overflow: hidden;
255
255
-
word-break: break-word;
256
256
-
}
257
257
-
.inline-compose-textarea {
258
258
-
width: 100%;
259
259
-
min-height: 80px;
260
260
-
padding: 10px 12px;
261
261
-
background: var(--bg-elevated);
262
262
-
border: 1px solid var(--border);
263
263
-
border-radius: 8px;
264
264
-
color: var(--text-primary);
265
265
-
font-family: inherit;
266
266
-
font-size: 13px;
267
267
-
resize: vertical;
268
268
-
margin-bottom: 12px;
269
269
-
box-sizing: border-box;
270
270
-
}
271
271
-
.inline-compose-textarea:focus {
272
272
-
outline: none;
273
273
-
border-color: var(--accent);
274
274
-
}
275
275
-
.inline-compose-actions {
276
276
-
display: flex;
277
277
-
justify-content: flex-end;
278
278
-
gap: 8px;
279
279
-
}
280
280
-
.btn-cancel {
281
281
-
padding: 8px 16px;
282
282
-
background: transparent;
283
283
-
border: 1px solid var(--border);
284
284
-
border-radius: 6px;
285
285
-
color: var(--text-secondary);
286
286
-
font-size: 13px;
287
287
-
cursor: pointer;
288
288
-
}
289
289
-
.btn-cancel:hover {
290
290
-
background: var(--bg-hover);
291
291
-
color: var(--text-primary);
292
292
-
}
293
293
-
.btn-submit {
294
294
-
padding: 8px 16px;
295
295
-
background: var(--accent);
296
296
-
border: none;
297
297
-
border-radius: 6px;
298
298
-
color: white;
299
299
-
font-size: 13px;
300
300
-
font-weight: 500;
301
301
-
cursor: pointer;
302
302
-
}
303
303
-
.btn-submit:hover {
304
304
-
background: var(--accent-hover);
305
305
-
}
306
306
-
.btn-submit:disabled {
307
307
-
opacity: 0.5;
308
308
-
cursor: not-allowed;
309
309
-
}
310
310
-
.reply-section {
311
311
-
border-top: 1px solid var(--border);
312
312
-
padding: 10px 14px;
313
313
-
background: var(--bg-primary);
314
314
-
border-radius: 0 0 12px 12px;
315
315
-
}
316
316
-
.reply-textarea {
317
317
-
width: 100%;
318
318
-
min-height: 50px;
319
319
-
padding: 8px 10px;
320
320
-
background: var(--bg-elevated);
321
321
-
border: 1px solid var(--border);
322
322
-
border-radius: 6px;
323
323
-
color: var(--text-primary);
324
324
-
font-family: inherit;
325
325
-
font-size: 12px;
326
326
-
resize: none;
327
327
-
margin-bottom: 8px;
328
328
-
}
329
329
-
.reply-textarea:focus {
330
330
-
outline: none;
331
331
-
border-color: var(--accent);
332
332
-
}
333
333
-
.reply-submit {
334
334
-
padding: 6px 12px;
335
335
-
background: var(--accent);
336
336
-
border: none;
337
337
-
border-radius: 4px;
338
338
-
color: white;
339
339
-
font-size: 11px;
340
340
-
font-weight: 500;
341
341
-
cursor: pointer;
342
342
-
float: right;
343
343
-
}
344
344
-
.reply-submit:disabled {
345
345
-
opacity: 0.5;
346
346
-
}
347
347
-
.reply-item {
348
348
-
padding: 8px 0;
349
349
-
border-top: 1px solid var(--border);
350
350
-
}
351
351
-
.reply-item:first-child {
352
352
-
border-top: none;
353
353
-
}
354
354
-
.reply-author {
355
355
-
font-size: 11px;
356
356
-
font-weight: 600;
357
357
-
color: var(--text-secondary);
358
358
-
margin-bottom: 4px;
359
359
-
}
360
360
-
.reply-text {
361
361
-
font-size: 12px;
362
362
-
color: var(--text-primary);
363
363
-
line-height: 1.4;
364
364
-
}
365
365
-
`;
366
366
-
367
367
-
class DOMTextMatcher {
368
368
-
constructor() {
369
369
-
this.textNodes = [];
370
370
-
this.corpus = "";
371
371
-
this.indices = [];
372
372
-
this.buildMap();
373
373
-
}
374
374
-
375
375
-
buildMap() {
376
376
-
const walker = document.createTreeWalker(
377
377
-
document.body,
378
378
-
NodeFilter.SHOW_TEXT,
379
379
-
{
380
380
-
acceptNode: (node) => {
381
381
-
if (!node.parentNode) return NodeFilter.FILTER_REJECT;
382
382
-
const tag = node.parentNode.tagName;
383
383
-
if (
384
384
-
["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "INPUT"].includes(tag)
385
385
-
)
386
386
-
return NodeFilter.FILTER_REJECT;
387
387
-
if (node.textContent.trim().length === 0)
388
388
-
return NodeFilter.FILTER_SKIP;
389
389
-
390
390
-
if (node.parentNode.offsetParent === null)
391
391
-
return NodeFilter.FILTER_REJECT;
392
392
-
393
393
-
return NodeFilter.FILTER_ACCEPT;
394
394
-
},
395
395
-
},
396
396
-
);
397
397
-
398
398
-
let currentNode;
399
399
-
let index = 0;
400
400
-
while ((currentNode = walker.nextNode())) {
401
401
-
const text = currentNode.textContent;
402
402
-
this.textNodes.push(currentNode);
403
403
-
this.corpus += text;
404
404
-
this.indices.push({
405
405
-
start: index,
406
406
-
node: currentNode,
407
407
-
length: text.length,
408
408
-
});
409
409
-
index += text.length;
410
410
-
}
411
411
-
}
412
412
-
413
413
-
findRange(searchText) {
414
414
-
if (!searchText) return null;
415
415
-
416
416
-
let matchIndex = this.corpus.indexOf(searchText);
417
417
-
418
418
-
if (matchIndex === -1) {
419
419
-
const normalizedSearch = searchText.replace(/\s+/g, " ").trim();
420
420
-
matchIndex = this.corpus.indexOf(normalizedSearch);
421
421
-
422
422
-
if (matchIndex === -1) {
423
423
-
const fuzzyMatch = this.fuzzyFindInCorpus(searchText);
424
424
-
if (fuzzyMatch) {
425
425
-
const start = this.mapIndexToPoint(fuzzyMatch.start);
426
426
-
const end = this.mapIndexToPoint(fuzzyMatch.end);
427
427
-
if (start && end) {
428
428
-
const range = document.createRange();
429
429
-
range.setStart(start.node, start.offset);
430
430
-
range.setEnd(end.node, end.offset);
431
431
-
return range;
432
432
-
}
433
433
-
}
434
434
-
return null;
435
435
-
}
436
436
-
}
437
437
-
438
438
-
const start = this.mapIndexToPoint(matchIndex);
439
439
-
const end = this.mapIndexToPoint(matchIndex + searchText.length);
440
440
-
441
441
-
if (start && end) {
442
442
-
const range = document.createRange();
443
443
-
range.setStart(start.node, start.offset);
444
444
-
range.setEnd(end.node, end.offset);
445
445
-
return range;
446
446
-
}
447
447
-
return null;
448
448
-
}
449
449
-
450
450
-
fuzzyFindInCorpus(searchText) {
451
451
-
const searchWords = searchText
452
452
-
.trim()
453
453
-
.split(/\s+/)
454
454
-
.filter((w) => w.length > 0);
455
455
-
if (searchWords.length === 0) return null;
456
456
-
457
457
-
const corpusLower = this.corpus.toLowerCase();
458
458
-
459
459
-
const firstWord = searchWords[0].toLowerCase();
460
460
-
let searchStart = 0;
461
461
-
462
462
-
while (searchStart < corpusLower.length) {
463
463
-
const wordStart = corpusLower.indexOf(firstWord, searchStart);
464
464
-
if (wordStart === -1) break;
465
465
-
466
466
-
let corpusPos = wordStart;
467
467
-
let matched = true;
468
468
-
let lastMatchEnd = wordStart;
469
469
-
470
470
-
for (const word of searchWords) {
471
471
-
const wordLower = word.toLowerCase();
472
472
-
while (
473
473
-
corpusPos < corpusLower.length &&
474
474
-
/\s/.test(this.corpus[corpusPos])
475
475
-
) {
476
476
-
corpusPos++;
477
477
-
}
478
478
-
const corpusSlice = corpusLower.slice(
479
479
-
corpusPos,
480
480
-
corpusPos + wordLower.length,
481
481
-
);
482
482
-
if (corpusSlice !== wordLower) {
483
483
-
matched = false;
484
484
-
break;
485
485
-
}
486
486
-
487
487
-
corpusPos += wordLower.length;
488
488
-
lastMatchEnd = corpusPos;
489
489
-
}
490
490
-
491
491
-
if (matched) {
492
492
-
return { start: wordStart, end: lastMatchEnd };
493
493
-
}
494
494
-
495
495
-
searchStart = wordStart + 1;
496
496
-
}
497
497
-
498
498
-
return null;
499
499
-
}
500
500
-
501
501
-
mapIndexToPoint(corpusIndex) {
502
502
-
for (const info of this.indices) {
503
503
-
if (
504
504
-
corpusIndex >= info.start &&
505
505
-
corpusIndex < info.start + info.length
506
506
-
) {
507
507
-
return { node: info.node, offset: corpusIndex - info.start };
508
508
-
}
509
509
-
}
510
510
-
if (this.indices.length > 0) {
511
511
-
const last = this.indices[this.indices.length - 1];
512
512
-
if (corpusIndex === last.start + last.length) {
513
513
-
return { node: last.node, offset: last.length };
514
514
-
}
515
515
-
}
516
516
-
return null;
517
517
-
}
518
518
-
}
519
519
-
520
520
-
function applyTheme(theme) {
521
521
-
if (!sidebarHost) return;
522
522
-
sidebarHost.classList.remove("light", "dark");
523
523
-
if (theme === "system" || !theme) {
524
524
-
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
525
525
-
sidebarHost.classList.add("light");
526
526
-
}
527
527
-
} else {
528
528
-
sidebarHost.classList.add(theme);
529
529
-
}
530
530
-
}
531
531
-
532
532
-
window
533
533
-
.matchMedia("(prefers-color-scheme: light)")
534
534
-
.addEventListener("change", (e) => {
535
535
-
chrome.storage.local.get(["theme"], (result) => {
536
536
-
if (!result.theme || result.theme === "system") {
537
537
-
if (e.matches) {
538
538
-
sidebarHost?.classList.add("light");
539
539
-
} else {
540
540
-
sidebarHost?.classList.remove("light");
541
541
-
}
542
542
-
}
543
543
-
});
544
544
-
});
545
545
-
546
546
-
function initOverlay() {
547
547
-
sidebarHost = document.createElement("div");
548
548
-
sidebarHost.id = "margin-overlay-host";
549
549
-
sidebarHost.style.cssText = `
550
550
-
position: absolute; top: 0; left: 0; width: 100%;
551
551
-
height: 0;
552
552
-
overflow: visible;
553
553
-
pointer-events: none; z-index: 2147483647;
554
554
-
`;
555
555
-
document.body?.appendChild(sidebarHost) ||
556
556
-
document.documentElement.appendChild(sidebarHost);
557
557
-
558
558
-
sidebarShadow = sidebarHost.attachShadow({ mode: "open" });
559
559
-
const styleEl = document.createElement("style");
560
560
-
styleEl.textContent = OVERLAY_STYLES;
561
561
-
sidebarShadow.appendChild(styleEl);
562
562
-
563
563
-
const container = document.createElement("div");
564
564
-
container.className = "margin-overlay";
565
565
-
container.id = "margin-overlay-container";
566
566
-
sidebarShadow.appendChild(container);
567
567
-
568
568
-
if (typeof chrome !== "undefined" && chrome.storage) {
569
569
-
chrome.storage.local.get(["showOverlay", "theme"], (result) => {
570
570
-
applyTheme(result.theme);
571
571
-
if (result.showOverlay === false) {
572
572
-
sidebarHost.style.display = "none";
573
573
-
} else {
574
574
-
fetchAnnotations();
575
575
-
}
576
576
-
});
577
577
-
} else {
578
578
-
fetchAnnotations();
579
579
-
}
580
580
-
581
581
-
document.addEventListener("mousemove", handleMouseMove);
582
582
-
document.addEventListener("click", handleDocumentClick, true);
583
583
-
584
584
-
chrome.storage.onChanged.addListener((changes, area) => {
585
585
-
if (area === "local") {
586
586
-
if (changes.theme) {
587
587
-
applyTheme(changes.theme.newValue);
588
588
-
}
589
589
-
if (changes.showOverlay) {
590
590
-
if (changes.showOverlay.newValue === false) {
591
591
-
sidebarHost.style.display = "none";
592
592
-
activeItems = [];
593
593
-
if (typeof CSS !== "undefined" && CSS.highlights) {
594
594
-
CSS.highlights.clear();
595
595
-
}
596
596
-
} else {
597
597
-
sidebarHost.style.display = "";
598
598
-
fetchAnnotations();
599
599
-
}
600
600
-
}
601
601
-
}
602
602
-
});
603
603
-
}
604
604
-
605
605
-
function showInlineComposeModal() {
606
606
-
if (!sidebarShadow || !currentSelection) return;
607
607
-
608
608
-
const container = sidebarShadow.getElementById("margin-overlay-container");
609
609
-
if (!container) return;
610
610
-
611
611
-
const existingModal = container.querySelector(".inline-compose-modal");
612
612
-
if (existingModal) existingModal.remove();
613
613
-
614
614
-
const modal = document.createElement("div");
615
615
-
modal.className = "inline-compose-modal";
616
616
-
617
617
-
modal.style.left = `${Math.max(20, (window.innerWidth - 340) / 2)}px`;
618
618
-
modal.style.top = `${Math.min(200, window.innerHeight / 4)}px`;
619
619
-
620
620
-
const truncatedQuote =
621
621
-
currentSelection.text.length > 100
622
622
-
? currentSelection.text.substring(0, 100) + "..."
623
623
-
: currentSelection.text;
624
624
-
625
625
-
modal.innerHTML = `
626
626
-
<div class="inline-compose-quote">"${truncatedQuote}"</div>
627
627
-
<textarea class="inline-compose-textarea" placeholder="Add your annotation..." autofocus></textarea>
628
628
-
<div class="inline-compose-actions">
629
629
-
<button class="btn-cancel">Cancel</button>
630
630
-
<button class="btn-submit">Post Annotation</button>
631
631
-
</div>
632
632
-
`;
633
633
-
634
634
-
const textarea = modal.querySelector("textarea");
635
635
-
const submitBtn = modal.querySelector(".btn-submit");
636
636
-
const cancelBtn = modal.querySelector(".btn-cancel");
637
637
-
638
638
-
cancelBtn.addEventListener("click", () => {
639
639
-
modal.remove();
640
640
-
});
641
641
-
642
642
-
submitBtn.addEventListener("click", async () => {
643
643
-
const text = textarea.value.trim();
644
644
-
if (!text) return;
645
645
-
646
646
-
submitBtn.disabled = true;
647
647
-
submitBtn.textContent = "Posting...";
648
648
-
649
649
-
chrome.runtime.sendMessage(
650
650
-
{
651
651
-
type: "CREATE_ANNOTATION",
652
652
-
data: {
653
653
-
url: currentSelection.url || window.location.href,
654
654
-
title: currentSelection.title || document.title,
655
655
-
text: text,
656
656
-
selector: currentSelection.selector,
657
657
-
},
658
658
-
},
659
659
-
(res) => {
660
660
-
if (res && res.success) {
661
661
-
modal.remove();
662
662
-
fetchAnnotations();
663
663
-
} else {
664
664
-
submitBtn.disabled = false;
665
665
-
submitBtn.textContent = "Post Annotation";
666
666
-
alert(
667
667
-
"Failed to create annotation: " + (res?.error || "Unknown error"),
668
668
-
);
669
669
-
}
670
670
-
},
671
671
-
);
672
672
-
});
673
673
-
674
674
-
container.appendChild(modal);
675
675
-
textarea.focus();
676
676
-
677
677
-
const handleEscape = (e) => {
678
678
-
if (e.key === "Escape") {
679
679
-
modal.remove();
680
680
-
document.removeEventListener("keydown", handleEscape);
681
681
-
}
682
682
-
};
683
683
-
document.addEventListener("keydown", handleEscape);
684
684
-
}
685
685
-
686
686
-
let hoverIndicator = null;
687
687
-
688
688
-
function handleMouseMove(e) {
689
689
-
const x = e.clientX;
690
690
-
const y = e.clientY;
691
691
-
692
692
-
if (sidebarHost && sidebarHost.style.display === "none") return;
693
693
-
694
694
-
let foundItems = [];
695
695
-
let firstRange = null;
696
696
-
for (const { range, item } of activeItems) {
697
697
-
const rects = range.getClientRects();
698
698
-
for (const rect of rects) {
699
699
-
if (
700
700
-
x >= rect.left &&
701
701
-
x <= rect.right &&
702
702
-
y >= rect.top &&
703
703
-
y <= rect.bottom
704
704
-
) {
705
705
-
let container = range.commonAncestorContainer;
706
706
-
if (container.nodeType === Node.TEXT_NODE) {
707
707
-
container = container.parentNode;
708
708
-
}
709
709
-
710
710
-
if (
711
711
-
container &&
712
712
-
(e.target.contains(container) || container.contains(e.target))
713
713
-
) {
714
714
-
if (!firstRange) firstRange = range;
715
715
-
if (!foundItems.some((f) => f.item === item)) {
716
716
-
foundItems.push({ range, item, rect });
717
717
-
}
718
718
-
}
719
719
-
break;
720
720
-
}
721
721
-
}
722
722
-
}
723
723
-
724
724
-
if (foundItems.length > 0) {
725
725
-
document.body.style.cursor = "pointer";
726
726
-
727
727
-
if (!hoverIndicator && sidebarShadow) {
728
728
-
const container = sidebarShadow.getElementById(
729
729
-
"margin-overlay-container",
730
730
-
);
731
731
-
if (container) {
732
732
-
hoverIndicator = document.createElement("div");
733
733
-
hoverIndicator.className = "margin-hover-indicator";
734
734
-
hoverIndicator.style.cssText = `
735
735
-
position: fixed;
736
736
-
display: flex;
737
737
-
align-items: center;
738
738
-
pointer-events: none;
739
739
-
z-index: 2147483647;
740
740
-
opacity: 0;
741
741
-
transition: opacity 0.15s, transform 0.15s;
742
742
-
transform: scale(0.8);
743
743
-
`;
744
744
-
container.appendChild(hoverIndicator);
745
745
-
}
746
746
-
}
747
747
-
748
748
-
if (hoverIndicator) {
749
749
-
const authorsMap = new Map();
750
750
-
foundItems.forEach(({ item }) => {
751
751
-
const author = item.author || item.creator || {};
752
752
-
const id = author.did || author.handle || "unknown";
753
753
-
if (!authorsMap.has(id)) {
754
754
-
authorsMap.set(id, author);
755
755
-
}
756
756
-
});
757
757
-
const uniqueAuthors = Array.from(authorsMap.values());
758
758
-
759
759
-
const maxShow = 3;
760
760
-
const displayAuthors = uniqueAuthors.slice(0, maxShow);
761
761
-
const overflow = uniqueAuthors.length - maxShow;
762
762
-
763
763
-
let html = displayAuthors
764
764
-
.map((author, i) => {
765
765
-
const avatar = author.avatar;
766
766
-
const handle = author.handle || "U";
767
767
-
const marginLeft = i === 0 ? "0" : "-8px";
768
768
-
769
769
-
if (avatar) {
770
770
-
return `<img src="${avatar}" style="width: 24px; height: 24px; border-radius: 50%; object-fit: cover; border: 2px solid #09090b; margin-left: ${marginLeft};">`;
771
771
-
} else {
772
772
-
return `<div style="width: 24px; height: 24px; border-radius: 50%; background: #6366f1; color: white; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; font-family: -apple-system, sans-serif; border: 2px solid #09090b; margin-left: ${marginLeft};">${handle[0]?.toUpperCase() || "U"}</div>`;
773
773
-
}
774
774
-
})
775
775
-
.join("");
776
776
-
777
777
-
if (overflow > 0) {
778
778
-
html += `<div style="width: 24px; height: 24px; border-radius: 50%; background: #27272a; color: #a1a1aa; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; font-family: -apple-system, sans-serif; border: 2px solid #09090b; margin-left: -8px;">+${overflow}</div>`;
779
779
-
}
780
780
-
781
781
-
hoverIndicator.innerHTML = html;
782
782
-
783
783
-
const firstRect = firstRange.getClientRects()[0];
784
784
-
const totalWidth =
785
785
-
Math.min(uniqueAuthors.length, maxShow + (overflow > 0 ? 1 : 0)) *
786
786
-
18 +
787
787
-
8;
788
788
-
const leftPos = firstRect.left - totalWidth;
789
789
-
const topPos = firstRect.top + firstRect.height / 2 - 12;
790
790
-
791
791
-
hoverIndicator.style.left = `${leftPos}px`;
792
792
-
hoverIndicator.style.top = `${topPos}px`;
793
793
-
hoverIndicator.style.opacity = "1";
794
794
-
hoverIndicator.style.transform = "scale(1)";
795
795
-
}
796
796
-
} else {
797
797
-
document.body.style.cursor = "";
798
798
-
if (hoverIndicator) {
799
799
-
hoverIndicator.style.opacity = "0";
800
800
-
hoverIndicator.style.transform = "scale(0.8)";
801
801
-
}
802
802
-
}
803
803
-
}
804
804
-
805
805
-
function handleDocumentClick(e) {
806
806
-
const x = e.clientX;
807
807
-
const y = e.clientY;
808
808
-
809
809
-
if (sidebarHost && sidebarHost.style.display === "none") return;
810
810
-
811
811
-
if (popoverEl && sidebarShadow) {
812
812
-
const rect = popoverEl.getBoundingClientRect();
813
813
-
if (
814
814
-
x >= rect.left &&
815
815
-
x <= rect.right &&
816
816
-
y >= rect.top &&
817
817
-
y <= rect.bottom
818
818
-
) {
819
819
-
return;
820
820
-
}
821
821
-
}
822
822
-
823
823
-
let clickedItems = [];
824
824
-
for (const { range, item } of activeItems) {
825
825
-
const rects = range.getClientRects();
826
826
-
for (const rect of rects) {
827
827
-
if (
828
828
-
x >= rect.left &&
829
829
-
x <= rect.right &&
830
830
-
y >= rect.top &&
831
831
-
y <= rect.bottom
832
832
-
) {
833
833
-
let container = range.commonAncestorContainer;
834
834
-
if (container.nodeType === Node.TEXT_NODE) {
835
835
-
container = container.parentNode;
836
836
-
}
837
837
-
838
838
-
if (
839
839
-
container &&
840
840
-
(e.target.contains(container) || container.contains(e.target))
841
841
-
) {
842
842
-
if (!clickedItems.includes(item)) {
843
843
-
clickedItems.push(item);
844
844
-
}
845
845
-
}
846
846
-
break;
847
847
-
}
848
848
-
}
849
849
-
}
850
850
-
851
851
-
if (clickedItems.length > 0) {
852
852
-
e.preventDefault();
853
853
-
e.stopPropagation();
854
854
-
855
855
-
if (popoverEl) {
856
856
-
const currentIds = popoverEl.dataset.itemIds;
857
857
-
const newIds = clickedItems
858
858
-
.map((i) => i.uri || i.id)
859
859
-
.sort()
860
860
-
.join(",");
861
861
-
862
862
-
if (currentIds === newIds) {
863
863
-
popoverEl.remove();
864
864
-
popoverEl = null;
865
865
-
return;
866
866
-
}
867
867
-
}
868
868
-
869
869
-
const firstItem = clickedItems[0];
870
870
-
const match = activeItems.find((x) => x.item === firstItem);
871
871
-
if (match) {
872
872
-
const rects = match.range.getClientRects();
873
873
-
if (rects.length > 0) {
874
874
-
const rect = rects[0];
875
875
-
const top = rect.top + window.scrollY;
876
876
-
const left = rect.left + window.scrollX;
877
877
-
showPopover(clickedItems, top, left);
878
878
-
}
879
879
-
}
880
880
-
} else {
881
881
-
if (popoverEl) {
882
882
-
popoverEl.remove();
883
883
-
popoverEl = null;
884
884
-
}
885
885
-
}
886
886
-
}
887
887
-
888
888
-
function renderBadges(annotations) {
889
889
-
if (!sidebarShadow) return;
890
890
-
891
891
-
const itemsToRender = annotations || [];
892
892
-
activeItems = [];
893
893
-
const rangesByColor = {};
894
894
-
895
895
-
const matcher = new DOMTextMatcher();
896
896
-
897
897
-
itemsToRender.forEach((item) => {
898
898
-
const selector = item.target?.selector || item.selector;
899
899
-
if (!selector?.exact) return;
900
900
-
901
901
-
const range = matcher.findRange(selector.exact);
902
902
-
if (range) {
903
903
-
activeItems.push({ range, item });
904
904
-
905
905
-
const color = item.color || "#6366f1";
906
906
-
if (!rangesByColor[color]) rangesByColor[color] = [];
907
907
-
rangesByColor[color].push(range);
908
908
-
}
909
909
-
});
910
910
-
911
911
-
if (typeof CSS !== "undefined" && CSS.highlights) {
912
912
-
CSS.highlights.clear();
913
913
-
for (const [color, ranges] of Object.entries(rangesByColor)) {
914
914
-
const highlight = new Highlight(...ranges);
915
915
-
const safeColor = color.replace(/[^a-zA-Z0-9]/g, "");
916
916
-
const name = `margin-hl-${safeColor}`;
917
917
-
CSS.highlights.set(name, highlight);
918
918
-
injectHighlightStyle(name, color);
919
919
-
}
920
920
-
}
921
921
-
}
922
922
-
923
923
-
const injectedStyles = new Set();
924
924
-
function injectHighlightStyle(name, color) {
925
925
-
if (injectedStyles.has(name)) return;
926
926
-
const style = document.createElement("style");
927
927
-
style.textContent = `
928
928
-
::highlight(${name}) {
929
929
-
text-decoration: underline;
930
930
-
text-decoration-color: ${color};
931
931
-
text-decoration-thickness: 2px;
932
932
-
text-underline-offset: 2px;
933
933
-
cursor: pointer;
934
934
-
}
935
935
-
`;
936
936
-
document.head.appendChild(style);
937
937
-
injectedStyles.add(name);
938
938
-
}
939
939
-
940
940
-
function showPopover(items, top, left) {
941
941
-
if (popoverEl) popoverEl.remove();
942
942
-
const container = sidebarShadow.getElementById("margin-overlay-container");
943
943
-
popoverEl = document.createElement("div");
944
944
-
popoverEl.className = "margin-popover";
945
945
-
946
946
-
const ids = items
947
947
-
.map((i) => i.uri || i.id)
948
948
-
.sort()
949
949
-
.join(",");
950
950
-
popoverEl.dataset.itemIds = ids;
951
951
-
952
952
-
const popWidth = 300;
953
953
-
const screenWidth = window.innerWidth;
954
954
-
let finalLeft = left;
955
955
-
if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20;
956
956
-
957
957
-
popoverEl.style.top = `${top + 20}px`;
958
958
-
popoverEl.style.left = `${finalLeft}px`;
959
959
-
960
960
-
const count = items.length;
961
961
-
const title = count === 1 ? "1 Comment" : `${count} Comments`;
962
962
-
963
963
-
let contentHtml = items
964
964
-
.map((item) => {
965
965
-
const author = item.author || item.creator || {};
966
966
-
const handle = author.handle || "User";
967
967
-
const avatar = author.avatar;
968
968
-
const text = item.body?.value || item.text || "";
969
969
-
const id = item.id || item.uri;
970
970
-
const isHighlight = item.type === "Highlight";
971
971
-
972
972
-
let avatarHtml = `<div class="comment-avatar">${handle[0]?.toUpperCase() || "U"}</div>`;
973
973
-
if (avatar) {
974
974
-
avatarHtml = `<img src="${avatar}" class="comment-avatar" style="object-fit: cover;">`;
975
975
-
}
976
976
-
977
977
-
let bodyHtml = "";
978
978
-
if (isHighlight && !text) {
979
979
-
bodyHtml = `<div class="highlight-only-badge">Highlighted</div>`;
980
980
-
} else {
981
981
-
bodyHtml = `<div class="comment-text">${text}</div>`;
982
982
-
}
983
983
-
984
984
-
return `
985
985
-
<div class="comment-item">
986
986
-
<div class="comment-header">
987
987
-
${avatarHtml}
988
988
-
<span class="comment-handle">@${handle}</span>
989
989
-
</div>
990
990
-
${bodyHtml}
991
991
-
<div class="comment-actions">
992
992
-
${!isHighlight ? `<button class="comment-action-btn btn-reply" data-id="${id}">Reply</button>` : ""}
993
993
-
<button class="comment-action-btn btn-share" data-id="${id}" data-text="${text}">Share</button>
994
994
-
</div>
995
995
-
</div>
996
996
-
`;
997
997
-
})
998
998
-
.join("");
999
999
-
1000
1000
-
popoverEl.innerHTML = `
1001
1001
-
<div class="popover-header">
1002
1002
-
<span>${title}</span>
1003
1003
-
<button class="popover-close">✕</button>
1004
1004
-
</div>
1005
1005
-
<div class="popover-scroll-area">
1006
1006
-
${contentHtml}
1007
1007
-
</div>
1008
1008
-
`;
1009
1009
-
1010
1010
-
popoverEl.querySelector(".popover-close").addEventListener("click", (e) => {
1011
1011
-
e.stopPropagation();
1012
1012
-
popoverEl.remove();
1013
1013
-
popoverEl = null;
1014
1014
-
});
1015
1015
-
1016
1016
-
const replyBtns = popoverEl.querySelectorAll(".btn-reply");
1017
1017
-
replyBtns.forEach((btn) => {
1018
1018
-
btn.addEventListener("click", (e) => {
1019
1019
-
e.stopPropagation();
1020
1020
-
const id = btn.getAttribute("data-id");
1021
1021
-
if (id) {
1022
1022
-
chrome.runtime.sendMessage({
1023
1023
-
type: "OPEN_APP_URL",
1024
1024
-
data: { path: `/annotation/${encodeURIComponent(id)}` },
1025
1025
-
});
1026
1026
-
}
1027
1027
-
});
1028
1028
-
});
1029
1029
-
1030
1030
-
const shareBtns = popoverEl.querySelectorAll(".btn-share");
1031
1031
-
shareBtns.forEach((btn) => {
1032
1032
-
btn.addEventListener("click", async () => {
1033
1033
-
const id = btn.getAttribute("data-id");
1034
1034
-
const text = btn.getAttribute("data-text");
1035
1035
-
const u = `https://margin.at/annotation/${encodeURIComponent(id)}`;
1036
1036
-
const shareText = text ? `${text}\n${u}` : u;
1037
1037
-
1038
1038
-
try {
1039
1039
-
await navigator.clipboard.writeText(shareText);
1040
1040
-
const originalText = btn.innerText;
1041
1041
-
btn.innerText = "Copied!";
1042
1042
-
setTimeout(() => (btn.innerText = originalText), 2000);
1043
1043
-
} catch (e) {
1044
1044
-
console.error("Failed to copy", e);
1045
1045
-
}
1046
1046
-
});
1047
1047
-
});
1048
1048
-
1049
1049
-
container.appendChild(popoverEl);
1050
1050
-
1051
1051
-
setTimeout(() => {
1052
1052
-
document.addEventListener("click", closePopoverOutside);
1053
1053
-
}, 0);
1054
1054
-
}
1055
1055
-
1056
1056
-
function closePopoverOutside() {
1057
1057
-
if (popoverEl) {
1058
1058
-
popoverEl.remove();
1059
1059
-
popoverEl = null;
1060
1060
-
document.removeEventListener("click", closePopoverOutside);
1061
1061
-
}
1062
1062
-
}
1063
1063
-
1064
1064
-
function fetchAnnotations(retryCount = 0) {
1065
1065
-
if (typeof chrome !== "undefined" && chrome.runtime) {
1066
1066
-
const citedUrls = Array.from(document.querySelectorAll("[cite]"))
1067
1067
-
.map((el) => el.getAttribute("cite"))
1068
1068
-
.filter((url) => url && url.startsWith("http"));
1069
1069
-
const uniqueCitedUrls = [...new Set(citedUrls)];
1070
1070
-
1071
1071
-
chrome.runtime.sendMessage(
1072
1072
-
{
1073
1073
-
type: "GET_ANNOTATIONS",
1074
1074
-
data: {
1075
1075
-
url: window.location.href,
1076
1076
-
citedUrls: uniqueCitedUrls,
1077
1077
-
},
1078
1078
-
},
1079
1079
-
(res) => {
1080
1080
-
if (res && res.success && res.data && res.data.length > 0) {
1081
1081
-
renderBadges(res.data);
1082
1082
-
} else if (retryCount < 3) {
1083
1083
-
setTimeout(
1084
1084
-
() => fetchAnnotations(retryCount + 1),
1085
1085
-
1000 * (retryCount + 1),
1086
1086
-
);
1087
1087
-
}
1088
1088
-
},
1089
1089
-
);
1090
1090
-
}
1091
1091
-
}
1092
1092
-
1093
1093
-
function findCanonicalUrl(range) {
1094
1094
-
if (!range) return null;
1095
1095
-
let node = range.commonAncestorContainer;
1096
1096
-
if (node.nodeType === Node.TEXT_NODE) {
1097
1097
-
node = node.parentNode;
1098
1098
-
}
1099
1099
-
1100
1100
-
while (node && node !== document.body) {
1101
1101
-
if (
1102
1102
-
(node.tagName === "BLOCKQUOTE" || node.tagName === "Q") &&
1103
1103
-
node.hasAttribute("cite")
1104
1104
-
) {
1105
1105
-
if (node.contains(range.commonAncestorContainer)) {
1106
1106
-
return node.getAttribute("cite");
1107
1107
-
}
1108
1108
-
}
1109
1109
-
node = node.parentNode;
1110
1110
-
}
1111
1111
-
return null;
1112
1112
-
}
1113
1113
-
1114
1114
-
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
1115
1115
-
if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") {
1116
1116
-
const sel = window.getSelection();
1117
1117
-
if (!sel || !sel.toString()) {
1118
1118
-
sendResponse({ selector: null });
1119
1119
-
return true;
1120
1120
-
}
1121
1121
-
const exact = sel.toString().trim();
1122
1122
-
const canonicalUrl = findCanonicalUrl(sel.getRangeAt(0));
1123
1123
-
1124
1124
-
sendResponse({
1125
1125
-
selector: { type: "TextQuoteSelector", exact },
1126
1126
-
canonicalUrl,
1127
1127
-
});
1128
1128
-
return true;
1129
1129
-
}
1130
1130
-
1131
1131
-
if (request.type === "SHOW_INLINE_ANNOTATE") {
1132
1132
-
currentSelection = {
1133
1133
-
text: request.data.selector?.exact || "",
1134
1134
-
selector: request.data.selector,
1135
1135
-
url: request.data.url,
1136
1136
-
title: request.data.title,
1137
1137
-
};
1138
1138
-
showInlineComposeModal();
1139
1139
-
sendResponse({ success: true });
1140
1140
-
return true;
1141
1141
-
}
1142
1142
-
1143
1143
-
if (request.type === "GET_SELECTOR_FOR_HIGHLIGHT") {
1144
1144
-
const sel = window.getSelection();
1145
1145
-
if (!sel || !sel.toString().trim()) {
1146
1146
-
sendResponse({ success: false, selector: null });
1147
1147
-
return true;
1148
1148
-
}
1149
1149
-
const exact = sel.toString().trim();
1150
1150
-
const canonicalUrl = findCanonicalUrl(sel.getRangeAt(0));
1151
1151
-
1152
1152
-
sendResponse({
1153
1153
-
success: false,
1154
1154
-
selector: { type: "TextQuoteSelector", exact },
1155
1155
-
canonicalUrl,
1156
1156
-
});
1157
1157
-
return true;
1158
1158
-
}
1159
1159
-
1160
1160
-
if (request.type === "REFRESH_ANNOTATIONS") {
1161
1161
-
fetchAnnotations();
1162
1162
-
sendResponse({ success: true });
1163
1163
-
return true;
1164
1164
-
}
1165
1165
-
1166
1166
-
if (request.type === "UPDATE_OVERLAY_VISIBILITY") {
1167
1167
-
if (sidebarHost) {
1168
1168
-
sidebarHost.style.display = request.show ? "block" : "none";
1169
1169
-
}
1170
1170
-
if (request.show) {
1171
1171
-
fetchAnnotations();
1172
1172
-
} else {
1173
1173
-
activeItems = [];
1174
1174
-
if (typeof CSS !== "undefined" && CSS.highlights) {
1175
1175
-
CSS.highlights.clear();
1176
1176
-
}
1177
1177
-
}
1178
1178
-
sendResponse({ success: true });
1179
1179
-
return true;
1180
1180
-
}
1181
1181
-
1182
1182
-
if (request.type === "SCROLL_TO_TEXT") {
1183
1183
-
const selector = request.selector;
1184
1184
-
if (selector?.exact) {
1185
1185
-
const matcher = new DOMTextMatcher();
1186
1186
-
const range = matcher.findRange(selector.exact);
1187
1187
-
if (range) {
1188
1188
-
const rect = range.getBoundingClientRect();
1189
1189
-
window.scrollTo({
1190
1190
-
top: window.scrollY + rect.top - window.innerHeight / 3,
1191
1191
-
behavior: "smooth",
1192
1192
-
});
1193
1193
-
const highlight = new Highlight(range);
1194
1194
-
CSS.highlights.set("margin-scroll-flash", highlight);
1195
1195
-
injectHighlightStyle("margin-scroll-flash", "#8b5cf6");
1196
1196
-
setTimeout(() => CSS.highlights.delete("margin-scroll-flash"), 2000);
1197
1197
-
}
1198
1198
-
}
1199
1199
-
}
1200
1200
-
return true;
1201
1201
-
});
1202
1202
-
1203
1203
-
if (document.readyState === "loading") {
1204
1204
-
document.addEventListener("DOMContentLoaded", initOverlay);
1205
1205
-
} else {
1206
1206
-
initOverlay();
1207
1207
-
}
1208
1208
-
1209
1209
-
window.addEventListener("load", () => {
1210
1210
-
if (typeof chrome !== "undefined" && chrome.storage) {
1211
1211
-
chrome.storage.local.get(["showOverlay"], (result) => {
1212
1212
-
if (result.showOverlay !== false) {
1213
1213
-
setTimeout(() => fetchAnnotations(), 500);
1214
1214
-
}
1215
1215
-
});
1216
1216
-
} else {
1217
1217
-
setTimeout(() => fetchAnnotations(), 500);
1218
1218
-
}
1219
1219
-
});
1220
1220
-
1221
1221
-
let lastUrl = window.location.href;
1222
1222
-
1223
1223
-
function checkUrlChange() {
1224
1224
-
if (window.location.href !== lastUrl) {
1225
1225
-
lastUrl = window.location.href;
1226
1226
-
onUrlChange();
1227
1227
-
}
1228
1228
-
}
1229
1229
-
1230
1230
-
function onUrlChange() {
1231
1231
-
if (typeof CSS !== "undefined" && CSS.highlights) {
1232
1232
-
CSS.highlights.clear();
1233
1233
-
}
1234
1234
-
activeItems = [];
1235
1235
-
1236
1236
-
if (typeof chrome !== "undefined" && chrome.storage) {
1237
1237
-
chrome.storage.local.get(["showOverlay"], (result) => {
1238
1238
-
if (result.showOverlay !== false) {
1239
1239
-
fetchAnnotations();
1240
1240
-
}
1241
1241
-
});
1242
1242
-
} else {
1243
1243
-
fetchAnnotations();
1244
1244
-
}
1245
1245
-
}
1246
1246
-
1247
1247
-
window.addEventListener("popstate", onUrlChange);
1248
1248
-
1249
1249
-
const originalPushState = history.pushState;
1250
1250
-
const originalReplaceState = history.replaceState;
1251
1251
-
1252
1252
-
history.pushState = function (...args) {
1253
1253
-
originalPushState.apply(this, args);
1254
1254
-
checkUrlChange();
1255
1255
-
};
1256
1256
-
1257
1257
-
history.replaceState = function (...args) {
1258
1258
-
originalReplaceState.apply(this, args);
1259
1259
-
checkUrlChange();
1260
1260
-
};
1261
1261
-
1262
1262
-
setInterval(checkUrlChange, 1000);
1263
1263
-
})();
+14
-21
extension/eslint.config.js
···
1
1
-
import js from "@eslint/js";
2
2
-
import globals from "globals";
1
1
+
import js from '@eslint/js';
2
2
+
import tseslint from 'typescript-eslint';
3
3
+
import eslintConfigPrettier from 'eslint-config-prettier';
3
4
4
4
-
export default [
5
5
-
{ ignores: ["dist"] },
5
5
+
export default tseslint.config(
6
6
+
js.configs.recommended,
7
7
+
...tseslint.configs.recommended,
8
8
+
eslintConfigPrettier,
9
9
+
{
10
10
+
ignores: ['.output/', '.wxt/', 'node_modules/', '*.config.js'],
11
11
+
},
6
12
{
7
7
-
files: ["**/*.js"],
8
8
-
languageOptions: {
9
9
-
ecmaVersion: 2020,
10
10
-
globals: {
11
11
-
...globals.browser,
12
12
-
...globals.webextensions,
13
13
-
},
14
14
-
parserOptions: {
15
15
-
ecmaVersion: "latest",
16
16
-
sourceType: "module",
17
17
-
},
18
18
-
},
19
13
rules: {
20
20
-
...js.configs.recommended.rules,
21
21
-
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
22
22
-
"no-undef": "warn",
14
14
+
'@typescript-eslint/no-explicit-any': 'off',
15
15
+
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
23
16
},
24
24
-
},
25
25
-
];
17
17
+
}
18
18
+
);
extension/icons/favicon-16x16.png
extension/public/icons/icon-16.png
extension/icons/favicon-32x32.png
extension/public/icons/icon-32.png
extension/icons/icon-128x128.png
extension/public/icons/icon-128.png
extension/icons/icon-48x48.png
extension/public/icons/icon-48.png
extension/icons/icon-64x64.png
extension/public/icons/icon-64.png
extension/icons/logo.svg
extension/public/icons/logo.svg
-19
extension/icons/site.webmanifest
···
1
1
-
{
2
2
-
"name": "Margin",
3
3
-
"short_name": "Margin",
4
4
-
"icons": [
5
5
-
{
6
6
-
"src": "/android-chrome-192x192.png",
7
7
-
"sizes": "192x192",
8
8
-
"type": "image/png"
9
9
-
},
10
10
-
{
11
11
-
"src": "/android-chrome-512x512.png",
12
12
-
"sizes": "512x512",
13
13
-
"type": "image/png"
14
14
-
}
15
15
-
],
16
16
-
"theme_color": "#ffffff",
17
17
-
"background_color": "#ffffff",
18
18
-
"display": "standalone"
19
19
-
}
-56
extension/manifest.chrome.json
···
1
1
-
{
2
2
-
"manifest_version": 3,
3
3
-
"name": "Margin",
4
4
-
"description": "Write in the margins of the web. Annotate any URL with AT Protocol.",
5
5
-
"version": "0.0.11",
6
6
-
"icons": {
7
7
-
"16": "icons/favicon-16x16.png",
8
8
-
"32": "icons/favicon-32x32.png",
9
9
-
"48": "icons/icon-48x48.png",
10
10
-
"128": "icons/icon-128x128.png"
11
11
-
},
12
12
-
"action": {
13
13
-
"default_popup": "popup/popup.html",
14
14
-
"default_icon": {
15
15
-
"16": "icons/favicon-16x16.png",
16
16
-
"32": "icons/favicon-32x32.png",
17
17
-
"48": "icons/icon-48x48.png"
18
18
-
},
19
19
-
"default_title": "Margin - View annotations"
20
20
-
},
21
21
-
"background": {
22
22
-
"service_worker": "background/service-worker.js",
23
23
-
"type": "module"
24
24
-
},
25
25
-
"content_scripts": [
26
26
-
{
27
27
-
"matches": ["<all_urls>"],
28
28
-
"js": ["content/content.js"],
29
29
-
"css": ["content/content.css"],
30
30
-
"run_at": "document_idle"
31
31
-
}
32
32
-
],
33
33
-
"permissions": [
34
34
-
"storage",
35
35
-
"activeTab",
36
36
-
"tabs",
37
37
-
"cookies",
38
38
-
"contextMenus",
39
39
-
"sidePanel"
40
40
-
],
41
41
-
"side_panel": {
42
42
-
"default_path": "sidepanel/sidepanel.html"
43
43
-
},
44
44
-
"optional_permissions": ["notifications"],
45
45
-
"host_permissions": ["<all_urls>"],
46
46
-
"options_ui": {
47
47
-
"page": "popup/popup.html",
48
48
-
"open_in_tab": true
49
49
-
},
50
50
-
"browser_specific_settings": {
51
51
-
"gecko": {
52
52
-
"id": "hello@margin.at",
53
53
-
"strict_min_version": "109.0"
54
54
-
}
55
55
-
}
56
56
-
}
-59
extension/manifest.firefox.json
···
1
1
-
{
2
2
-
"manifest_version": 3,
3
3
-
"name": "Margin",
4
4
-
"description": "Write in the margins of the web. Annotate any URL with AT Protocol.",
5
5
-
"version": "0.0.11",
6
6
-
"icons": {
7
7
-
"16": "icons/favicon-16x16.png",
8
8
-
"32": "icons/favicon-32x32.png",
9
9
-
"48": "icons/icon-48x48.png",
10
10
-
"64": "icons/icon-64x64.png",
11
11
-
"128": "icons/icon-128x128.png"
12
12
-
},
13
13
-
"action": {
14
14
-
"default_popup": "popup/popup.html",
15
15
-
"default_icon": {
16
16
-
"16": "icons/favicon-16x16.png",
17
17
-
"32": "icons/favicon-32x32.png",
18
18
-
"48": "icons/icon-48x48.png"
19
19
-
},
20
20
-
"default_title": "Margin - View annotations"
21
21
-
},
22
22
-
"sidebar_action": {
23
23
-
"default_panel": "sidepanel/sidepanel.html",
24
24
-
"default_icon": {
25
25
-
"16": "icons/favicon-16x16.png",
26
26
-
"32": "icons/favicon-32x32.png"
27
27
-
},
28
28
-
"default_title": "Margin Sidebar",
29
29
-
"open_at_install": false
30
30
-
},
31
31
-
"background": {
32
32
-
"scripts": ["background/service-worker.js"],
33
33
-
"type": "module"
34
34
-
},
35
35
-
"content_scripts": [
36
36
-
{
37
37
-
"matches": ["<all_urls>"],
38
38
-
"js": ["content/content.js"],
39
39
-
"css": ["content/content.css"],
40
40
-
"run_at": "document_idle"
41
41
-
}
42
42
-
],
43
43
-
"permissions": ["storage", "activeTab", "tabs", "cookies", "contextMenus"],
44
44
-
"optional_permissions": ["notifications"],
45
45
-
"host_permissions": ["<all_urls>"],
46
46
-
"options_ui": {
47
47
-
"page": "popup/popup.html",
48
48
-
"open_in_tab": true
49
49
-
},
50
50
-
"browser_specific_settings": {
51
51
-
"gecko": {
52
52
-
"id": "hello@margin.at",
53
53
-
"strict_min_version": "140.0",
54
54
-
"data_collection_permissions": {
55
55
-
"required": ["none"]
56
56
-
}
57
57
-
}
58
58
-
}
59
59
-
}
-52
extension/manifest.json
···
1
1
-
{
2
2
-
"manifest_version": 3,
3
3
-
"name": "Margin",
4
4
-
"description": "Write in the margins of the web. Annotate any URL with AT Protocol.",
5
5
-
"version": "0.0.11",
6
6
-
"icons": {
7
7
-
"16": "icons/favicon-16x16.png",
8
8
-
"32": "icons/favicon-32x32.png",
9
9
-
"48": "icons/icon-48x48.png",
10
10
-
"128": "icons/icon-128x128.png"
11
11
-
},
12
12
-
"action": {
13
13
-
"default_popup": "popup/popup.html",
14
14
-
"default_icon": {
15
15
-
"16": "icons/favicon-16x16.png",
16
16
-
"32": "icons/favicon-32x32.png",
17
17
-
"48": "icons/icon-48x48.png"
18
18
-
},
19
19
-
"default_title": "Margin - View annotations"
20
20
-
},
21
21
-
"background": {
22
22
-
"service_worker": "background/service-worker.js",
23
23
-
"type": "module"
24
24
-
},
25
25
-
"content_scripts": [
26
26
-
{
27
27
-
"matches": ["<all_urls>"],
28
28
-
"js": ["content/content.js"],
29
29
-
"css": ["content/content.css"],
30
30
-
"run_at": "document_idle"
31
31
-
}
32
32
-
],
33
33
-
"permissions": [
34
34
-
"storage",
35
35
-
"activeTab",
36
36
-
"tabs",
37
37
-
"cookies",
38
38
-
"contextMenus",
39
39
-
"sidePanel"
40
40
-
],
41
41
-
"side_panel": {
42
42
-
"default_path": "sidepanel/sidepanel.html"
43
43
-
},
44
44
-
"optional_permissions": ["notifications"],
45
45
-
"host_permissions": ["<all_urls>"],
46
46
-
"browser_specific_settings": {
47
47
-
"gecko": {
48
48
-
"id": "hello@margin.at",
49
49
-
"strict_min_version": "109.0"
50
50
-
}
51
51
-
}
52
52
-
}
-1091
extension/package-lock.json
···
1
1
-
{
2
2
-
"name": "margin-extension",
3
3
-
"version": "0.1.0",
4
4
-
"lockfileVersion": 3,
5
5
-
"requires": true,
6
6
-
"packages": {
7
7
-
"": {
8
8
-
"name": "margin-extension",
9
9
-
"version": "0.1.0",
10
10
-
"devDependencies": {
11
11
-
"@eslint/js": "^9.39.2",
12
12
-
"eslint": "^9.39.2",
13
13
-
"globals": "^17.0.0"
14
14
-
}
15
15
-
},
16
16
-
"node_modules/@eslint-community/eslint-utils": {
17
17
-
"version": "4.9.1",
18
18
-
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
19
19
-
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
20
20
-
"dev": true,
21
21
-
"license": "MIT",
22
22
-
"dependencies": {
23
23
-
"eslint-visitor-keys": "^3.4.3"
24
24
-
},
25
25
-
"engines": {
26
26
-
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
27
27
-
},
28
28
-
"funding": {
29
29
-
"url": "https://opencollective.com/eslint"
30
30
-
},
31
31
-
"peerDependencies": {
32
32
-
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
33
33
-
}
34
34
-
},
35
35
-
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
36
36
-
"version": "3.4.3",
37
37
-
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
38
38
-
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
39
39
-
"dev": true,
40
40
-
"license": "Apache-2.0",
41
41
-
"engines": {
42
42
-
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
43
43
-
},
44
44
-
"funding": {
45
45
-
"url": "https://opencollective.com/eslint"
46
46
-
}
47
47
-
},
48
48
-
"node_modules/@eslint-community/regexpp": {
49
49
-
"version": "4.12.2",
50
50
-
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
51
51
-
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
52
52
-
"dev": true,
53
53
-
"license": "MIT",
54
54
-
"engines": {
55
55
-
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
56
56
-
}
57
57
-
},
58
58
-
"node_modules/@eslint/config-array": {
59
59
-
"version": "0.21.1",
60
60
-
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
61
61
-
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
62
62
-
"dev": true,
63
63
-
"license": "Apache-2.0",
64
64
-
"dependencies": {
65
65
-
"@eslint/object-schema": "^2.1.7",
66
66
-
"debug": "^4.3.1",
67
67
-
"minimatch": "^3.1.2"
68
68
-
},
69
69
-
"engines": {
70
70
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
71
71
-
}
72
72
-
},
73
73
-
"node_modules/@eslint/config-helpers": {
74
74
-
"version": "0.4.2",
75
75
-
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
76
76
-
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
77
77
-
"dev": true,
78
78
-
"license": "Apache-2.0",
79
79
-
"dependencies": {
80
80
-
"@eslint/core": "^0.17.0"
81
81
-
},
82
82
-
"engines": {
83
83
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
84
84
-
}
85
85
-
},
86
86
-
"node_modules/@eslint/core": {
87
87
-
"version": "0.17.0",
88
88
-
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
89
89
-
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
90
90
-
"dev": true,
91
91
-
"license": "Apache-2.0",
92
92
-
"dependencies": {
93
93
-
"@types/json-schema": "^7.0.15"
94
94
-
},
95
95
-
"engines": {
96
96
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
97
97
-
}
98
98
-
},
99
99
-
"node_modules/@eslint/eslintrc": {
100
100
-
"version": "3.3.3",
101
101
-
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
102
102
-
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
103
103
-
"dev": true,
104
104
-
"license": "MIT",
105
105
-
"dependencies": {
106
106
-
"ajv": "^6.12.4",
107
107
-
"debug": "^4.3.2",
108
108
-
"espree": "^10.0.1",
109
109
-
"globals": "^14.0.0",
110
110
-
"ignore": "^5.2.0",
111
111
-
"import-fresh": "^3.2.1",
112
112
-
"js-yaml": "^4.1.1",
113
113
-
"minimatch": "^3.1.2",
114
114
-
"strip-json-comments": "^3.1.1"
115
115
-
},
116
116
-
"engines": {
117
117
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
118
118
-
},
119
119
-
"funding": {
120
120
-
"url": "https://opencollective.com/eslint"
121
121
-
}
122
122
-
},
123
123
-
"node_modules/@eslint/eslintrc/node_modules/globals": {
124
124
-
"version": "14.0.0",
125
125
-
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
126
126
-
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
127
127
-
"dev": true,
128
128
-
"license": "MIT",
129
129
-
"engines": {
130
130
-
"node": ">=18"
131
131
-
},
132
132
-
"funding": {
133
133
-
"url": "https://github.com/sponsors/sindresorhus"
134
134
-
}
135
135
-
},
136
136
-
"node_modules/@eslint/js": {
137
137
-
"version": "9.39.2",
138
138
-
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
139
139
-
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
140
140
-
"dev": true,
141
141
-
"license": "MIT",
142
142
-
"engines": {
143
143
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
144
144
-
},
145
145
-
"funding": {
146
146
-
"url": "https://eslint.org/donate"
147
147
-
}
148
148
-
},
149
149
-
"node_modules/@eslint/object-schema": {
150
150
-
"version": "2.1.7",
151
151
-
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
152
152
-
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
153
153
-
"dev": true,
154
154
-
"license": "Apache-2.0",
155
155
-
"engines": {
156
156
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
157
157
-
}
158
158
-
},
159
159
-
"node_modules/@eslint/plugin-kit": {
160
160
-
"version": "0.4.1",
161
161
-
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
162
162
-
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
163
163
-
"dev": true,
164
164
-
"license": "Apache-2.0",
165
165
-
"dependencies": {
166
166
-
"@eslint/core": "^0.17.0",
167
167
-
"levn": "^0.4.1"
168
168
-
},
169
169
-
"engines": {
170
170
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
171
171
-
}
172
172
-
},
173
173
-
"node_modules/@humanfs/core": {
174
174
-
"version": "0.19.1",
175
175
-
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
176
176
-
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
177
177
-
"dev": true,
178
178
-
"license": "Apache-2.0",
179
179
-
"engines": {
180
180
-
"node": ">=18.18.0"
181
181
-
}
182
182
-
},
183
183
-
"node_modules/@humanfs/node": {
184
184
-
"version": "0.16.7",
185
185
-
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
186
186
-
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
187
187
-
"dev": true,
188
188
-
"license": "Apache-2.0",
189
189
-
"dependencies": {
190
190
-
"@humanfs/core": "^0.19.1",
191
191
-
"@humanwhocodes/retry": "^0.4.0"
192
192
-
},
193
193
-
"engines": {
194
194
-
"node": ">=18.18.0"
195
195
-
}
196
196
-
},
197
197
-
"node_modules/@humanwhocodes/module-importer": {
198
198
-
"version": "1.0.1",
199
199
-
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
200
200
-
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
201
201
-
"dev": true,
202
202
-
"license": "Apache-2.0",
203
203
-
"engines": {
204
204
-
"node": ">=12.22"
205
205
-
},
206
206
-
"funding": {
207
207
-
"type": "github",
208
208
-
"url": "https://github.com/sponsors/nzakas"
209
209
-
}
210
210
-
},
211
211
-
"node_modules/@humanwhocodes/retry": {
212
212
-
"version": "0.4.3",
213
213
-
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
214
214
-
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
215
215
-
"dev": true,
216
216
-
"license": "Apache-2.0",
217
217
-
"engines": {
218
218
-
"node": ">=18.18"
219
219
-
},
220
220
-
"funding": {
221
221
-
"type": "github",
222
222
-
"url": "https://github.com/sponsors/nzakas"
223
223
-
}
224
224
-
},
225
225
-
"node_modules/@types/estree": {
226
226
-
"version": "1.0.8",
227
227
-
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
228
228
-
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
229
229
-
"dev": true,
230
230
-
"license": "MIT"
231
231
-
},
232
232
-
"node_modules/@types/json-schema": {
233
233
-
"version": "7.0.15",
234
234
-
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
235
235
-
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
236
236
-
"dev": true,
237
237
-
"license": "MIT"
238
238
-
},
239
239
-
"node_modules/acorn": {
240
240
-
"version": "8.15.0",
241
241
-
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
242
242
-
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
243
243
-
"dev": true,
244
244
-
"license": "MIT",
245
245
-
"peer": true,
246
246
-
"bin": {
247
247
-
"acorn": "bin/acorn"
248
248
-
},
249
249
-
"engines": {
250
250
-
"node": ">=0.4.0"
251
251
-
}
252
252
-
},
253
253
-
"node_modules/acorn-jsx": {
254
254
-
"version": "5.3.2",
255
255
-
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
256
256
-
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
257
257
-
"dev": true,
258
258
-
"license": "MIT",
259
259
-
"peerDependencies": {
260
260
-
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
261
261
-
}
262
262
-
},
263
263
-
"node_modules/ajv": {
264
264
-
"version": "6.12.6",
265
265
-
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
266
266
-
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
267
267
-
"dev": true,
268
268
-
"license": "MIT",
269
269
-
"dependencies": {
270
270
-
"fast-deep-equal": "^3.1.1",
271
271
-
"fast-json-stable-stringify": "^2.0.0",
272
272
-
"json-schema-traverse": "^0.4.1",
273
273
-
"uri-js": "^4.2.2"
274
274
-
},
275
275
-
"funding": {
276
276
-
"type": "github",
277
277
-
"url": "https://github.com/sponsors/epoberezkin"
278
278
-
}
279
279
-
},
280
280
-
"node_modules/ansi-styles": {
281
281
-
"version": "4.3.0",
282
282
-
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
283
283
-
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
284
284
-
"dev": true,
285
285
-
"license": "MIT",
286
286
-
"dependencies": {
287
287
-
"color-convert": "^2.0.1"
288
288
-
},
289
289
-
"engines": {
290
290
-
"node": ">=8"
291
291
-
},
292
292
-
"funding": {
293
293
-
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
294
294
-
}
295
295
-
},
296
296
-
"node_modules/argparse": {
297
297
-
"version": "2.0.1",
298
298
-
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
299
299
-
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
300
300
-
"dev": true,
301
301
-
"license": "Python-2.0"
302
302
-
},
303
303
-
"node_modules/balanced-match": {
304
304
-
"version": "1.0.2",
305
305
-
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
306
306
-
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
307
307
-
"dev": true,
308
308
-
"license": "MIT"
309
309
-
},
310
310
-
"node_modules/brace-expansion": {
311
311
-
"version": "1.1.12",
312
312
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
313
313
-
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
314
314
-
"dev": true,
315
315
-
"license": "MIT",
316
316
-
"dependencies": {
317
317
-
"balanced-match": "^1.0.0",
318
318
-
"concat-map": "0.0.1"
319
319
-
}
320
320
-
},
321
321
-
"node_modules/callsites": {
322
322
-
"version": "3.1.0",
323
323
-
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
324
324
-
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
325
325
-
"dev": true,
326
326
-
"license": "MIT",
327
327
-
"engines": {
328
328
-
"node": ">=6"
329
329
-
}
330
330
-
},
331
331
-
"node_modules/chalk": {
332
332
-
"version": "4.1.2",
333
333
-
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
334
334
-
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
335
335
-
"dev": true,
336
336
-
"license": "MIT",
337
337
-
"dependencies": {
338
338
-
"ansi-styles": "^4.1.0",
339
339
-
"supports-color": "^7.1.0"
340
340
-
},
341
341
-
"engines": {
342
342
-
"node": ">=10"
343
343
-
},
344
344
-
"funding": {
345
345
-
"url": "https://github.com/chalk/chalk?sponsor=1"
346
346
-
}
347
347
-
},
348
348
-
"node_modules/color-convert": {
349
349
-
"version": "2.0.1",
350
350
-
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
351
351
-
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
352
352
-
"dev": true,
353
353
-
"license": "MIT",
354
354
-
"dependencies": {
355
355
-
"color-name": "~1.1.4"
356
356
-
},
357
357
-
"engines": {
358
358
-
"node": ">=7.0.0"
359
359
-
}
360
360
-
},
361
361
-
"node_modules/color-name": {
362
362
-
"version": "1.1.4",
363
363
-
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
364
364
-
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
365
365
-
"dev": true,
366
366
-
"license": "MIT"
367
367
-
},
368
368
-
"node_modules/concat-map": {
369
369
-
"version": "0.0.1",
370
370
-
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
371
371
-
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
372
372
-
"dev": true,
373
373
-
"license": "MIT"
374
374
-
},
375
375
-
"node_modules/cross-spawn": {
376
376
-
"version": "7.0.6",
377
377
-
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
378
378
-
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
379
379
-
"dev": true,
380
380
-
"license": "MIT",
381
381
-
"dependencies": {
382
382
-
"path-key": "^3.1.0",
383
383
-
"shebang-command": "^2.0.0",
384
384
-
"which": "^2.0.1"
385
385
-
},
386
386
-
"engines": {
387
387
-
"node": ">= 8"
388
388
-
}
389
389
-
},
390
390
-
"node_modules/debug": {
391
391
-
"version": "4.4.3",
392
392
-
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
393
393
-
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
394
394
-
"dev": true,
395
395
-
"license": "MIT",
396
396
-
"dependencies": {
397
397
-
"ms": "^2.1.3"
398
398
-
},
399
399
-
"engines": {
400
400
-
"node": ">=6.0"
401
401
-
},
402
402
-
"peerDependenciesMeta": {
403
403
-
"supports-color": {
404
404
-
"optional": true
405
405
-
}
406
406
-
}
407
407
-
},
408
408
-
"node_modules/deep-is": {
409
409
-
"version": "0.1.4",
410
410
-
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
411
411
-
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
412
412
-
"dev": true,
413
413
-
"license": "MIT"
414
414
-
},
415
415
-
"node_modules/escape-string-regexp": {
416
416
-
"version": "4.0.0",
417
417
-
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
418
418
-
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
419
419
-
"dev": true,
420
420
-
"license": "MIT",
421
421
-
"engines": {
422
422
-
"node": ">=10"
423
423
-
},
424
424
-
"funding": {
425
425
-
"url": "https://github.com/sponsors/sindresorhus"
426
426
-
}
427
427
-
},
428
428
-
"node_modules/eslint": {
429
429
-
"version": "9.39.2",
430
430
-
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
431
431
-
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
432
432
-
"dev": true,
433
433
-
"license": "MIT",
434
434
-
"peer": true,
435
435
-
"dependencies": {
436
436
-
"@eslint-community/eslint-utils": "^4.8.0",
437
437
-
"@eslint-community/regexpp": "^4.12.1",
438
438
-
"@eslint/config-array": "^0.21.1",
439
439
-
"@eslint/config-helpers": "^0.4.2",
440
440
-
"@eslint/core": "^0.17.0",
441
441
-
"@eslint/eslintrc": "^3.3.1",
442
442
-
"@eslint/js": "9.39.2",
443
443
-
"@eslint/plugin-kit": "^0.4.1",
444
444
-
"@humanfs/node": "^0.16.6",
445
445
-
"@humanwhocodes/module-importer": "^1.0.1",
446
446
-
"@humanwhocodes/retry": "^0.4.2",
447
447
-
"@types/estree": "^1.0.6",
448
448
-
"ajv": "^6.12.4",
449
449
-
"chalk": "^4.0.0",
450
450
-
"cross-spawn": "^7.0.6",
451
451
-
"debug": "^4.3.2",
452
452
-
"escape-string-regexp": "^4.0.0",
453
453
-
"eslint-scope": "^8.4.0",
454
454
-
"eslint-visitor-keys": "^4.2.1",
455
455
-
"espree": "^10.4.0",
456
456
-
"esquery": "^1.5.0",
457
457
-
"esutils": "^2.0.2",
458
458
-
"fast-deep-equal": "^3.1.3",
459
459
-
"file-entry-cache": "^8.0.0",
460
460
-
"find-up": "^5.0.0",
461
461
-
"glob-parent": "^6.0.2",
462
462
-
"ignore": "^5.2.0",
463
463
-
"imurmurhash": "^0.1.4",
464
464
-
"is-glob": "^4.0.0",
465
465
-
"json-stable-stringify-without-jsonify": "^1.0.1",
466
466
-
"lodash.merge": "^4.6.2",
467
467
-
"minimatch": "^3.1.2",
468
468
-
"natural-compare": "^1.4.0",
469
469
-
"optionator": "^0.9.3"
470
470
-
},
471
471
-
"bin": {
472
472
-
"eslint": "bin/eslint.js"
473
473
-
},
474
474
-
"engines": {
475
475
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
476
476
-
},
477
477
-
"funding": {
478
478
-
"url": "https://eslint.org/donate"
479
479
-
},
480
480
-
"peerDependencies": {
481
481
-
"jiti": "*"
482
482
-
},
483
483
-
"peerDependenciesMeta": {
484
484
-
"jiti": {
485
485
-
"optional": true
486
486
-
}
487
487
-
}
488
488
-
},
489
489
-
"node_modules/eslint-scope": {
490
490
-
"version": "8.4.0",
491
491
-
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
492
492
-
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
493
493
-
"dev": true,
494
494
-
"license": "BSD-2-Clause",
495
495
-
"dependencies": {
496
496
-
"esrecurse": "^4.3.0",
497
497
-
"estraverse": "^5.2.0"
498
498
-
},
499
499
-
"engines": {
500
500
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
501
501
-
},
502
502
-
"funding": {
503
503
-
"url": "https://opencollective.com/eslint"
504
504
-
}
505
505
-
},
506
506
-
"node_modules/eslint-visitor-keys": {
507
507
-
"version": "4.2.1",
508
508
-
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
509
509
-
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
510
510
-
"dev": true,
511
511
-
"license": "Apache-2.0",
512
512
-
"engines": {
513
513
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
514
514
-
},
515
515
-
"funding": {
516
516
-
"url": "https://opencollective.com/eslint"
517
517
-
}
518
518
-
},
519
519
-
"node_modules/espree": {
520
520
-
"version": "10.4.0",
521
521
-
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
522
522
-
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
523
523
-
"dev": true,
524
524
-
"license": "BSD-2-Clause",
525
525
-
"dependencies": {
526
526
-
"acorn": "^8.15.0",
527
527
-
"acorn-jsx": "^5.3.2",
528
528
-
"eslint-visitor-keys": "^4.2.1"
529
529
-
},
530
530
-
"engines": {
531
531
-
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
532
532
-
},
533
533
-
"funding": {
534
534
-
"url": "https://opencollective.com/eslint"
535
535
-
}
536
536
-
},
537
537
-
"node_modules/esquery": {
538
538
-
"version": "1.7.0",
539
539
-
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
540
540
-
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
541
541
-
"dev": true,
542
542
-
"license": "BSD-3-Clause",
543
543
-
"dependencies": {
544
544
-
"estraverse": "^5.1.0"
545
545
-
},
546
546
-
"engines": {
547
547
-
"node": ">=0.10"
548
548
-
}
549
549
-
},
550
550
-
"node_modules/esrecurse": {
551
551
-
"version": "4.3.0",
552
552
-
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
553
553
-
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
554
554
-
"dev": true,
555
555
-
"license": "BSD-2-Clause",
556
556
-
"dependencies": {
557
557
-
"estraverse": "^5.2.0"
558
558
-
},
559
559
-
"engines": {
560
560
-
"node": ">=4.0"
561
561
-
}
562
562
-
},
563
563
-
"node_modules/estraverse": {
564
564
-
"version": "5.3.0",
565
565
-
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
566
566
-
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
567
567
-
"dev": true,
568
568
-
"license": "BSD-2-Clause",
569
569
-
"engines": {
570
570
-
"node": ">=4.0"
571
571
-
}
572
572
-
},
573
573
-
"node_modules/esutils": {
574
574
-
"version": "2.0.3",
575
575
-
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
576
576
-
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
577
577
-
"dev": true,
578
578
-
"license": "BSD-2-Clause",
579
579
-
"engines": {
580
580
-
"node": ">=0.10.0"
581
581
-
}
582
582
-
},
583
583
-
"node_modules/fast-deep-equal": {
584
584
-
"version": "3.1.3",
585
585
-
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
586
586
-
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
587
587
-
"dev": true,
588
588
-
"license": "MIT"
589
589
-
},
590
590
-
"node_modules/fast-json-stable-stringify": {
591
591
-
"version": "2.1.0",
592
592
-
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
593
593
-
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
594
594
-
"dev": true,
595
595
-
"license": "MIT"
596
596
-
},
597
597
-
"node_modules/fast-levenshtein": {
598
598
-
"version": "2.0.6",
599
599
-
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
600
600
-
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
601
601
-
"dev": true,
602
602
-
"license": "MIT"
603
603
-
},
604
604
-
"node_modules/file-entry-cache": {
605
605
-
"version": "8.0.0",
606
606
-
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
607
607
-
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
608
608
-
"dev": true,
609
609
-
"license": "MIT",
610
610
-
"dependencies": {
611
611
-
"flat-cache": "^4.0.0"
612
612
-
},
613
613
-
"engines": {
614
614
-
"node": ">=16.0.0"
615
615
-
}
616
616
-
},
617
617
-
"node_modules/find-up": {
618
618
-
"version": "5.0.0",
619
619
-
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
620
620
-
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
621
621
-
"dev": true,
622
622
-
"license": "MIT",
623
623
-
"dependencies": {
624
624
-
"locate-path": "^6.0.0",
625
625
-
"path-exists": "^4.0.0"
626
626
-
},
627
627
-
"engines": {
628
628
-
"node": ">=10"
629
629
-
},
630
630
-
"funding": {
631
631
-
"url": "https://github.com/sponsors/sindresorhus"
632
632
-
}
633
633
-
},
634
634
-
"node_modules/flat-cache": {
635
635
-
"version": "4.0.1",
636
636
-
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
637
637
-
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
638
638
-
"dev": true,
639
639
-
"license": "MIT",
640
640
-
"dependencies": {
641
641
-
"flatted": "^3.2.9",
642
642
-
"keyv": "^4.5.4"
643
643
-
},
644
644
-
"engines": {
645
645
-
"node": ">=16"
646
646
-
}
647
647
-
},
648
648
-
"node_modules/flatted": {
649
649
-
"version": "3.3.3",
650
650
-
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
651
651
-
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
652
652
-
"dev": true,
653
653
-
"license": "ISC"
654
654
-
},
655
655
-
"node_modules/glob-parent": {
656
656
-
"version": "6.0.2",
657
657
-
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
658
658
-
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
659
659
-
"dev": true,
660
660
-
"license": "ISC",
661
661
-
"dependencies": {
662
662
-
"is-glob": "^4.0.3"
663
663
-
},
664
664
-
"engines": {
665
665
-
"node": ">=10.13.0"
666
666
-
}
667
667
-
},
668
668
-
"node_modules/globals": {
669
669
-
"version": "17.0.0",
670
670
-
"resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz",
671
671
-
"integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==",
672
672
-
"dev": true,
673
673
-
"license": "MIT",
674
674
-
"engines": {
675
675
-
"node": ">=18"
676
676
-
},
677
677
-
"funding": {
678
678
-
"url": "https://github.com/sponsors/sindresorhus"
679
679
-
}
680
680
-
},
681
681
-
"node_modules/has-flag": {
682
682
-
"version": "4.0.0",
683
683
-
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
684
684
-
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
685
685
-
"dev": true,
686
686
-
"license": "MIT",
687
687
-
"engines": {
688
688
-
"node": ">=8"
689
689
-
}
690
690
-
},
691
691
-
"node_modules/ignore": {
692
692
-
"version": "5.3.2",
693
693
-
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
694
694
-
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
695
695
-
"dev": true,
696
696
-
"license": "MIT",
697
697
-
"engines": {
698
698
-
"node": ">= 4"
699
699
-
}
700
700
-
},
701
701
-
"node_modules/import-fresh": {
702
702
-
"version": "3.3.1",
703
703
-
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
704
704
-
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
705
705
-
"dev": true,
706
706
-
"license": "MIT",
707
707
-
"dependencies": {
708
708
-
"parent-module": "^1.0.0",
709
709
-
"resolve-from": "^4.0.0"
710
710
-
},
711
711
-
"engines": {
712
712
-
"node": ">=6"
713
713
-
},
714
714
-
"funding": {
715
715
-
"url": "https://github.com/sponsors/sindresorhus"
716
716
-
}
717
717
-
},
718
718
-
"node_modules/imurmurhash": {
719
719
-
"version": "0.1.4",
720
720
-
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
721
721
-
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
722
722
-
"dev": true,
723
723
-
"license": "MIT",
724
724
-
"engines": {
725
725
-
"node": ">=0.8.19"
726
726
-
}
727
727
-
},
728
728
-
"node_modules/is-extglob": {
729
729
-
"version": "2.1.1",
730
730
-
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
731
731
-
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
732
732
-
"dev": true,
733
733
-
"license": "MIT",
734
734
-
"engines": {
735
735
-
"node": ">=0.10.0"
736
736
-
}
737
737
-
},
738
738
-
"node_modules/is-glob": {
739
739
-
"version": "4.0.3",
740
740
-
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
741
741
-
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
742
742
-
"dev": true,
743
743
-
"license": "MIT",
744
744
-
"dependencies": {
745
745
-
"is-extglob": "^2.1.1"
746
746
-
},
747
747
-
"engines": {
748
748
-
"node": ">=0.10.0"
749
749
-
}
750
750
-
},
751
751
-
"node_modules/isexe": {
752
752
-
"version": "2.0.0",
753
753
-
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
754
754
-
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
755
755
-
"dev": true,
756
756
-
"license": "ISC"
757
757
-
},
758
758
-
"node_modules/js-yaml": {
759
759
-
"version": "4.1.1",
760
760
-
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
761
761
-
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
762
762
-
"dev": true,
763
763
-
"license": "MIT",
764
764
-
"dependencies": {
765
765
-
"argparse": "^2.0.1"
766
766
-
},
767
767
-
"bin": {
768
768
-
"js-yaml": "bin/js-yaml.js"
769
769
-
}
770
770
-
},
771
771
-
"node_modules/json-buffer": {
772
772
-
"version": "3.0.1",
773
773
-
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
774
774
-
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
775
775
-
"dev": true,
776
776
-
"license": "MIT"
777
777
-
},
778
778
-
"node_modules/json-schema-traverse": {
779
779
-
"version": "0.4.1",
780
780
-
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
781
781
-
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
782
782
-
"dev": true,
783
783
-
"license": "MIT"
784
784
-
},
785
785
-
"node_modules/json-stable-stringify-without-jsonify": {
786
786
-
"version": "1.0.1",
787
787
-
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
788
788
-
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
789
789
-
"dev": true,
790
790
-
"license": "MIT"
791
791
-
},
792
792
-
"node_modules/keyv": {
793
793
-
"version": "4.5.4",
794
794
-
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
795
795
-
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
796
796
-
"dev": true,
797
797
-
"license": "MIT",
798
798
-
"dependencies": {
799
799
-
"json-buffer": "3.0.1"
800
800
-
}
801
801
-
},
802
802
-
"node_modules/levn": {
803
803
-
"version": "0.4.1",
804
804
-
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
805
805
-
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
806
806
-
"dev": true,
807
807
-
"license": "MIT",
808
808
-
"dependencies": {
809
809
-
"prelude-ls": "^1.2.1",
810
810
-
"type-check": "~0.4.0"
811
811
-
},
812
812
-
"engines": {
813
813
-
"node": ">= 0.8.0"
814
814
-
}
815
815
-
},
816
816
-
"node_modules/locate-path": {
817
817
-
"version": "6.0.0",
818
818
-
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
819
819
-
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
820
820
-
"dev": true,
821
821
-
"license": "MIT",
822
822
-
"dependencies": {
823
823
-
"p-locate": "^5.0.0"
824
824
-
},
825
825
-
"engines": {
826
826
-
"node": ">=10"
827
827
-
},
828
828
-
"funding": {
829
829
-
"url": "https://github.com/sponsors/sindresorhus"
830
830
-
}
831
831
-
},
832
832
-
"node_modules/lodash.merge": {
833
833
-
"version": "4.6.2",
834
834
-
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
835
835
-
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
836
836
-
"dev": true,
837
837
-
"license": "MIT"
838
838
-
},
839
839
-
"node_modules/minimatch": {
840
840
-
"version": "3.1.2",
841
841
-
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
842
842
-
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
843
843
-
"dev": true,
844
844
-
"license": "ISC",
845
845
-
"dependencies": {
846
846
-
"brace-expansion": "^1.1.7"
847
847
-
},
848
848
-
"engines": {
849
849
-
"node": "*"
850
850
-
}
851
851
-
},
852
852
-
"node_modules/ms": {
853
853
-
"version": "2.1.3",
854
854
-
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
855
855
-
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
856
856
-
"dev": true,
857
857
-
"license": "MIT"
858
858
-
},
859
859
-
"node_modules/natural-compare": {
860
860
-
"version": "1.4.0",
861
861
-
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
862
862
-
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
863
863
-
"dev": true,
864
864
-
"license": "MIT"
865
865
-
},
866
866
-
"node_modules/optionator": {
867
867
-
"version": "0.9.4",
868
868
-
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
869
869
-
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
870
870
-
"dev": true,
871
871
-
"license": "MIT",
872
872
-
"dependencies": {
873
873
-
"deep-is": "^0.1.3",
874
874
-
"fast-levenshtein": "^2.0.6",
875
875
-
"levn": "^0.4.1",
876
876
-
"prelude-ls": "^1.2.1",
877
877
-
"type-check": "^0.4.0",
878
878
-
"word-wrap": "^1.2.5"
879
879
-
},
880
880
-
"engines": {
881
881
-
"node": ">= 0.8.0"
882
882
-
}
883
883
-
},
884
884
-
"node_modules/p-limit": {
885
885
-
"version": "3.1.0",
886
886
-
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
887
887
-
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
888
888
-
"dev": true,
889
889
-
"license": "MIT",
890
890
-
"dependencies": {
891
891
-
"yocto-queue": "^0.1.0"
892
892
-
},
893
893
-
"engines": {
894
894
-
"node": ">=10"
895
895
-
},
896
896
-
"funding": {
897
897
-
"url": "https://github.com/sponsors/sindresorhus"
898
898
-
}
899
899
-
},
900
900
-
"node_modules/p-locate": {
901
901
-
"version": "5.0.0",
902
902
-
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
903
903
-
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
904
904
-
"dev": true,
905
905
-
"license": "MIT",
906
906
-
"dependencies": {
907
907
-
"p-limit": "^3.0.2"
908
908
-
},
909
909
-
"engines": {
910
910
-
"node": ">=10"
911
911
-
},
912
912
-
"funding": {
913
913
-
"url": "https://github.com/sponsors/sindresorhus"
914
914
-
}
915
915
-
},
916
916
-
"node_modules/parent-module": {
917
917
-
"version": "1.0.1",
918
918
-
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
919
919
-
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
920
920
-
"dev": true,
921
921
-
"license": "MIT",
922
922
-
"dependencies": {
923
923
-
"callsites": "^3.0.0"
924
924
-
},
925
925
-
"engines": {
926
926
-
"node": ">=6"
927
927
-
}
928
928
-
},
929
929
-
"node_modules/path-exists": {
930
930
-
"version": "4.0.0",
931
931
-
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
932
932
-
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
933
933
-
"dev": true,
934
934
-
"license": "MIT",
935
935
-
"engines": {
936
936
-
"node": ">=8"
937
937
-
}
938
938
-
},
939
939
-
"node_modules/path-key": {
940
940
-
"version": "3.1.1",
941
941
-
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
942
942
-
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
943
943
-
"dev": true,
944
944
-
"license": "MIT",
945
945
-
"engines": {
946
946
-
"node": ">=8"
947
947
-
}
948
948
-
},
949
949
-
"node_modules/prelude-ls": {
950
950
-
"version": "1.2.1",
951
951
-
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
952
952
-
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
953
953
-
"dev": true,
954
954
-
"license": "MIT",
955
955
-
"engines": {
956
956
-
"node": ">= 0.8.0"
957
957
-
}
958
958
-
},
959
959
-
"node_modules/punycode": {
960
960
-
"version": "2.3.1",
961
961
-
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
962
962
-
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
963
963
-
"dev": true,
964
964
-
"license": "MIT",
965
965
-
"engines": {
966
966
-
"node": ">=6"
967
967
-
}
968
968
-
},
969
969
-
"node_modules/resolve-from": {
970
970
-
"version": "4.0.0",
971
971
-
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
972
972
-
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
973
973
-
"dev": true,
974
974
-
"license": "MIT",
975
975
-
"engines": {
976
976
-
"node": ">=4"
977
977
-
}
978
978
-
},
979
979
-
"node_modules/shebang-command": {
980
980
-
"version": "2.0.0",
981
981
-
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
982
982
-
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
983
983
-
"dev": true,
984
984
-
"license": "MIT",
985
985
-
"dependencies": {
986
986
-
"shebang-regex": "^3.0.0"
987
987
-
},
988
988
-
"engines": {
989
989
-
"node": ">=8"
990
990
-
}
991
991
-
},
992
992
-
"node_modules/shebang-regex": {
993
993
-
"version": "3.0.0",
994
994
-
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
995
995
-
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
996
996
-
"dev": true,
997
997
-
"license": "MIT",
998
998
-
"engines": {
999
999
-
"node": ">=8"
1000
1000
-
}
1001
1001
-
},
1002
1002
-
"node_modules/strip-json-comments": {
1003
1003
-
"version": "3.1.1",
1004
1004
-
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
1005
1005
-
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
1006
1006
-
"dev": true,
1007
1007
-
"license": "MIT",
1008
1008
-
"engines": {
1009
1009
-
"node": ">=8"
1010
1010
-
},
1011
1011
-
"funding": {
1012
1012
-
"url": "https://github.com/sponsors/sindresorhus"
1013
1013
-
}
1014
1014
-
},
1015
1015
-
"node_modules/supports-color": {
1016
1016
-
"version": "7.2.0",
1017
1017
-
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
1018
1018
-
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
1019
1019
-
"dev": true,
1020
1020
-
"license": "MIT",
1021
1021
-
"dependencies": {
1022
1022
-
"has-flag": "^4.0.0"
1023
1023
-
},
1024
1024
-
"engines": {
1025
1025
-
"node": ">=8"
1026
1026
-
}
1027
1027
-
},
1028
1028
-
"node_modules/type-check": {
1029
1029
-
"version": "0.4.0",
1030
1030
-
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
1031
1031
-
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
1032
1032
-
"dev": true,
1033
1033
-
"license": "MIT",
1034
1034
-
"dependencies": {
1035
1035
-
"prelude-ls": "^1.2.1"
1036
1036
-
},
1037
1037
-
"engines": {
1038
1038
-
"node": ">= 0.8.0"
1039
1039
-
}
1040
1040
-
},
1041
1041
-
"node_modules/uri-js": {
1042
1042
-
"version": "4.4.1",
1043
1043
-
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
1044
1044
-
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
1045
1045
-
"dev": true,
1046
1046
-
"license": "BSD-2-Clause",
1047
1047
-
"dependencies": {
1048
1048
-
"punycode": "^2.1.0"
1049
1049
-
}
1050
1050
-
},
1051
1051
-
"node_modules/which": {
1052
1052
-
"version": "2.0.2",
1053
1053
-
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
1054
1054
-
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
1055
1055
-
"dev": true,
1056
1056
-
"license": "ISC",
1057
1057
-
"dependencies": {
1058
1058
-
"isexe": "^2.0.0"
1059
1059
-
},
1060
1060
-
"bin": {
1061
1061
-
"node-which": "bin/node-which"
1062
1062
-
},
1063
1063
-
"engines": {
1064
1064
-
"node": ">= 8"
1065
1065
-
}
1066
1066
-
},
1067
1067
-
"node_modules/word-wrap": {
1068
1068
-
"version": "1.2.5",
1069
1069
-
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
1070
1070
-
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
1071
1071
-
"dev": true,
1072
1072
-
"license": "MIT",
1073
1073
-
"engines": {
1074
1074
-
"node": ">=0.10.0"
1075
1075
-
}
1076
1076
-
},
1077
1077
-
"node_modules/yocto-queue": {
1078
1078
-
"version": "0.1.0",
1079
1079
-
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
1080
1080
-
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
1081
1081
-
"dev": true,
1082
1082
-
"license": "MIT",
1083
1083
-
"engines": {
1084
1084
-
"node": ">=10"
1085
1085
-
},
1086
1086
-
"funding": {
1087
1087
-
"url": "https://github.com/sponsors/sindresorhus"
1088
1088
-
}
1089
1089
-
}
1090
1090
-
}
1091
1091
-
}
+30
-3
extension/package.json
···
1
1
{
2
2
"name": "margin-extension",
3
3
-
"version": "0.1.0",
3
3
+
"description": "Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.",
4
4
"private": true,
5
5
+
"version": "1.0.0",
5
6
"type": "module",
6
7
"scripts": {
7
7
-
"lint": "eslint ."
8
8
+
"dev": "wxt",
9
9
+
"dev:firefox": "wxt -b firefox",
10
10
+
"build": "wxt build",
11
11
+
"build:firefox": "wxt build -b firefox",
12
12
+
"zip": "wxt zip",
13
13
+
"zip:firefox": "wxt zip -b firefox",
14
14
+
"lint": "eslint src",
15
15
+
"lint:fix": "eslint src --fix",
16
16
+
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
17
17
+
"postinstall": "wxt prepare"
18
18
+
},
19
19
+
"dependencies": {
20
20
+
"@webext-core/messaging": "^1.4.0",
21
21
+
"clsx": "^2.1.1",
22
22
+
"lucide-react": "^0.563.0",
23
23
+
"react": "^18.3.1",
24
24
+
"react-dom": "^18.3.1"
8
25
},
9
26
"devDependencies": {
10
27
"@eslint/js": "^9.39.2",
28
28
+
"@types/react": "^18.3.12",
29
29
+
"@types/react-dom": "^18.3.1",
30
30
+
"@wxt-dev/module-react": "^1.1.1",
31
31
+
"autoprefixer": "^10.4.20",
11
32
"eslint": "^9.39.2",
12
12
-
"globals": "^17.0.0"
33
33
+
"eslint-config-prettier": "^10.1.8",
34
34
+
"postcss": "^8.4.49",
35
35
+
"prettier": "^3.8.1",
36
36
+
"tailwindcss": "^3.4.15",
37
37
+
"typescript": "^5.6.3",
38
38
+
"typescript-eslint": "^8.54.0",
39
39
+
"wxt": "^0.19.13"
13
40
}
14
41
}
-817
extension/popup/popup.css
···
1
1
-
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap");
2
2
-
3
3
-
:root {
4
4
-
--bg-primary: #0a0a0d;
5
5
-
--bg-secondary: #121216;
6
6
-
--bg-tertiary: #1a1a1f;
7
7
-
--bg-card: #0f0f13;
8
8
-
--bg-elevated: #18181d;
9
9
-
--bg-hover: #1e1e24;
10
10
-
11
11
-
--text-primary: #eaeaee;
12
12
-
--text-secondary: #b7b6c5;
13
13
-
--text-tertiary: #6e6d7a;
14
14
-
15
15
-
--border: rgba(183, 182, 197, 0.12);
16
16
-
--border-hover: rgba(183, 182, 197, 0.2);
17
17
-
18
18
-
--accent: #957a86;
19
19
-
--accent-hover: #a98d98;
20
20
-
--accent-subtle: rgba(149, 122, 134, 0.15);
21
21
-
--accent-text: #c4a8b2;
22
22
-
23
23
-
--success: #7fb069;
24
24
-
--error: #d97766;
25
25
-
--warning: #e8a54b;
26
26
-
27
27
-
--radius-sm: 6px;
28
28
-
--radius-md: 8px;
29
29
-
--radius-lg: 12px;
30
30
-
--radius-full: 9999px;
31
31
-
32
32
-
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
33
33
-
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
34
34
-
}
35
35
-
36
36
-
@media (prefers-color-scheme: light) {
37
37
-
:root {
38
38
-
--bg-primary: #f8f8fa;
39
39
-
--bg-secondary: #ffffff;
40
40
-
--bg-tertiary: #f0f0f4;
41
41
-
--bg-card: #ffffff;
42
42
-
--bg-elevated: #ffffff;
43
43
-
--bg-hover: #eeeef2;
44
44
-
45
45
-
--text-primary: #18171c;
46
46
-
--text-secondary: #5c495a;
47
47
-
--text-tertiary: #8a8494;
48
48
-
49
49
-
--border: rgba(92, 73, 90, 0.12);
50
50
-
--border-hover: rgba(92, 73, 90, 0.22);
51
51
-
52
52
-
--accent: #7a5f6d;
53
53
-
--accent-hover: #664e5b;
54
54
-
--accent-subtle: rgba(149, 122, 134, 0.12);
55
55
-
--accent-text: #5c495a;
56
56
-
57
57
-
--shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
58
58
-
--shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
59
59
-
}
60
60
-
}
61
61
-
62
62
-
body.light {
63
63
-
--bg-primary: #f8f8fa;
64
64
-
--bg-secondary: #ffffff;
65
65
-
--bg-tertiary: #f0f0f4;
66
66
-
--bg-card: #ffffff;
67
67
-
--bg-elevated: #ffffff;
68
68
-
--bg-hover: #eeeef2;
69
69
-
70
70
-
--text-primary: #18171c;
71
71
-
--text-secondary: #5c495a;
72
72
-
--text-tertiary: #8a8494;
73
73
-
74
74
-
--border: rgba(92, 73, 90, 0.12);
75
75
-
--border-hover: rgba(92, 73, 90, 0.22);
76
76
-
77
77
-
--accent: #7a5f6d;
78
78
-
--accent-hover: #664e5b;
79
79
-
--accent-subtle: rgba(149, 122, 134, 0.12);
80
80
-
--accent-text: #5c495a;
81
81
-
82
82
-
--shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
83
83
-
--shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
84
84
-
}
85
85
-
86
86
-
body.dark {
87
87
-
--bg-primary: #0a0a0d;
88
88
-
--bg-secondary: #121216;
89
89
-
--bg-tertiary: #1a1a1f;
90
90
-
--bg-card: #0f0f13;
91
91
-
--bg-elevated: #18181d;
92
92
-
--bg-hover: #1e1e24;
93
93
-
94
94
-
--text-primary: #eaeaee;
95
95
-
--text-secondary: #b7b6c5;
96
96
-
--text-tertiary: #6e6d7a;
97
97
-
98
98
-
--border: rgba(183, 182, 197, 0.12);
99
99
-
--border-hover: rgba(183, 182, 197, 0.2);
100
100
-
101
101
-
--accent: #957a86;
102
102
-
--accent-hover: #a98d98;
103
103
-
--accent-subtle: rgba(149, 122, 134, 0.15);
104
104
-
--accent-text: #c4a8b2;
105
105
-
106
106
-
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
107
107
-
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
108
108
-
}
109
109
-
110
110
-
* {
111
111
-
box-sizing: border-box;
112
112
-
margin: 0;
113
113
-
padding: 0;
114
114
-
}
115
115
-
116
116
-
body {
117
117
-
width: 380px;
118
118
-
height: 520px;
119
119
-
font-family:
120
120
-
"IBM Plex Sans",
121
121
-
-apple-system,
122
122
-
BlinkMacSystemFont,
123
123
-
sans-serif;
124
124
-
color: var(--text-primary);
125
125
-
background-color: var(--bg-primary);
126
126
-
overflow: hidden;
127
127
-
-webkit-font-smoothing: antialiased;
128
128
-
}
129
129
-
130
130
-
.popup {
131
131
-
display: flex;
132
132
-
flex-direction: column;
133
133
-
height: 100%;
134
134
-
}
135
135
-
136
136
-
.popup-header {
137
137
-
padding: 14px 16px;
138
138
-
border-bottom: 1px solid var(--border);
139
139
-
display: flex;
140
140
-
justify-content: space-between;
141
141
-
align-items: center;
142
142
-
background: var(--bg-primary);
143
143
-
}
144
144
-
145
145
-
.popup-brand {
146
146
-
display: flex;
147
147
-
align-items: center;
148
148
-
gap: 10px;
149
149
-
}
150
150
-
151
151
-
.popup-logo {
152
152
-
color: var(--accent);
153
153
-
}
154
154
-
155
155
-
.popup-title {
156
156
-
font-weight: 600;
157
157
-
font-size: 15px;
158
158
-
color: var(--text-primary);
159
159
-
letter-spacing: -0.02em;
160
160
-
}
161
161
-
162
162
-
.user-info {
163
163
-
display: flex;
164
164
-
align-items: center;
165
165
-
gap: 8px;
166
166
-
}
167
167
-
168
168
-
.user-handle {
169
169
-
font-size: 12px;
170
170
-
color: var(--text-secondary);
171
171
-
background: var(--bg-tertiary);
172
172
-
padding: 4px 10px;
173
173
-
border-radius: var(--radius-full);
174
174
-
}
175
175
-
176
176
-
.tabs {
177
177
-
display: flex;
178
178
-
border-bottom: 1px solid var(--border);
179
179
-
background: var(--bg-primary);
180
180
-
padding: 4px 8px;
181
181
-
gap: 4px;
182
182
-
}
183
183
-
184
184
-
.tab-btn {
185
185
-
flex: 1;
186
186
-
padding: 10px 8px;
187
187
-
background: transparent;
188
188
-
border: none;
189
189
-
font-size: 12px;
190
190
-
font-weight: 500;
191
191
-
color: var(--text-tertiary);
192
192
-
cursor: pointer;
193
193
-
border-radius: var(--radius-sm);
194
194
-
transition: all 0.15s;
195
195
-
}
196
196
-
197
197
-
.tab-btn:hover {
198
198
-
color: var(--text-secondary);
199
199
-
background: var(--bg-hover);
200
200
-
}
201
201
-
202
202
-
.tab-btn.active {
203
203
-
color: var(--text-primary);
204
204
-
background: var(--bg-tertiary);
205
205
-
}
206
206
-
207
207
-
.tab-content {
208
208
-
display: none;
209
209
-
flex: 1;
210
210
-
flex-direction: column;
211
211
-
overflow-y: auto;
212
212
-
}
213
213
-
214
214
-
.tab-content.active {
215
215
-
display: flex;
216
216
-
}
217
217
-
218
218
-
.popup-content {
219
219
-
flex: 1;
220
220
-
overflow-y: auto;
221
221
-
display: flex;
222
222
-
flex-direction: column;
223
223
-
background: var(--bg-primary);
224
224
-
}
225
225
-
226
226
-
.loading {
227
227
-
display: none;
228
228
-
flex-direction: column;
229
229
-
align-items: center;
230
230
-
justify-content: center;
231
231
-
height: 100%;
232
232
-
color: var(--text-tertiary);
233
233
-
gap: 12px;
234
234
-
}
235
235
-
236
236
-
.spinner {
237
237
-
width: 20px;
238
238
-
height: 20px;
239
239
-
border: 2px solid var(--border);
240
240
-
border-top-color: var(--accent);
241
241
-
border-radius: 50%;
242
242
-
animation: spin 1s linear infinite;
243
243
-
}
244
244
-
245
245
-
@keyframes spin {
246
246
-
to {
247
247
-
transform: rotate(360deg);
248
248
-
}
249
249
-
}
250
250
-
251
251
-
.login-prompt {
252
252
-
display: flex;
253
253
-
flex-direction: column;
254
254
-
align-items: center;
255
255
-
justify-content: center;
256
256
-
height: 100%;
257
257
-
padding: 32px;
258
258
-
text-align: center;
259
259
-
gap: 20px;
260
260
-
}
261
261
-
262
262
-
.login-at-logo {
263
263
-
font-size: 3.5rem;
264
264
-
font-weight: 700;
265
265
-
color: var(--accent);
266
266
-
line-height: 1;
267
267
-
}
268
268
-
269
269
-
.login-title {
270
270
-
font-size: 1rem;
271
271
-
font-weight: 600;
272
272
-
color: var(--text-primary);
273
273
-
}
274
274
-
275
275
-
.login-text {
276
276
-
font-size: 13px;
277
277
-
color: var(--text-secondary);
278
278
-
line-height: 1.5;
279
279
-
}
280
280
-
281
281
-
.quick-actions {
282
282
-
padding: 12px 16px;
283
283
-
border-bottom: 1px solid var(--border);
284
284
-
background: var(--bg-primary);
285
285
-
}
286
286
-
287
287
-
.create-form {
288
288
-
padding: 16px;
289
289
-
border-bottom: 1px solid var(--border);
290
290
-
background: var(--bg-primary);
291
291
-
}
292
292
-
293
293
-
.form-header {
294
294
-
display: flex;
295
295
-
justify-content: space-between;
296
296
-
align-items: center;
297
297
-
margin-bottom: 10px;
298
298
-
}
299
299
-
300
300
-
.form-title {
301
301
-
font-size: 12px;
302
302
-
font-weight: 600;
303
303
-
color: var(--text-primary);
304
304
-
letter-spacing: -0.01em;
305
305
-
}
306
306
-
307
307
-
.current-url {
308
308
-
font-size: 11px;
309
309
-
color: var(--text-tertiary);
310
310
-
max-width: 150px;
311
311
-
overflow: hidden;
312
312
-
text-overflow: ellipsis;
313
313
-
white-space: nowrap;
314
314
-
}
315
315
-
316
316
-
.annotation-input {
317
317
-
width: 100%;
318
318
-
padding: 12px;
319
319
-
border: 1px solid var(--border);
320
320
-
border-radius: var(--radius-md);
321
321
-
font-family: inherit;
322
322
-
font-size: 13px;
323
323
-
resize: none;
324
324
-
margin-bottom: 10px;
325
325
-
background: var(--bg-elevated);
326
326
-
color: var(--text-primary);
327
327
-
transition: border-color 0.15s;
328
328
-
}
329
329
-
330
330
-
.annotation-input::placeholder {
331
331
-
color: var(--text-tertiary);
332
332
-
}
333
333
-
334
334
-
.annotation-input:focus {
335
335
-
outline: none;
336
336
-
border-color: var(--accent);
337
337
-
}
338
338
-
339
339
-
.form-actions {
340
340
-
display: flex;
341
341
-
justify-content: flex-end;
342
342
-
}
343
343
-
344
344
-
.quote-preview {
345
345
-
margin-bottom: 12px;
346
346
-
padding: 10px 12px;
347
347
-
background: var(--accent-subtle);
348
348
-
border-left: 2px solid var(--accent);
349
349
-
border-radius: var(--radius-sm);
350
350
-
}
351
351
-
352
352
-
.quote-preview-header {
353
353
-
display: flex;
354
354
-
justify-content: space-between;
355
355
-
align-items: center;
356
356
-
margin-bottom: 6px;
357
357
-
font-size: 10px;
358
358
-
font-weight: 600;
359
359
-
text-transform: uppercase;
360
360
-
letter-spacing: 0.5px;
361
361
-
color: var(--accent-text);
362
362
-
}
363
363
-
364
364
-
.quote-preview-clear {
365
365
-
background: none;
366
366
-
border: none;
367
367
-
color: var(--text-tertiary);
368
368
-
font-size: 14px;
369
369
-
cursor: pointer;
370
370
-
padding: 0 4px;
371
371
-
line-height: 1;
372
372
-
}
373
373
-
374
374
-
.quote-preview-clear:hover {
375
375
-
color: var(--text-primary);
376
376
-
}
377
377
-
378
378
-
.quote-preview-text {
379
379
-
font-size: 12px;
380
380
-
font-style: italic;
381
381
-
color: var(--text-secondary);
382
382
-
line-height: 1.4;
383
383
-
max-height: 60px;
384
384
-
overflow: hidden;
385
385
-
}
386
386
-
387
387
-
.annotations-section {
388
388
-
flex: 1;
389
389
-
}
390
390
-
391
391
-
.section-header {
392
392
-
display: flex;
393
393
-
justify-content: space-between;
394
394
-
align-items: center;
395
395
-
padding: 14px 16px;
396
396
-
background: var(--bg-primary);
397
397
-
}
398
398
-
399
399
-
.section-title {
400
400
-
font-size: 11px;
401
401
-
font-weight: 600;
402
402
-
text-transform: uppercase;
403
403
-
color: var(--text-tertiary);
404
404
-
letter-spacing: 0.5px;
405
405
-
}
406
406
-
407
407
-
.annotation-count {
408
408
-
font-size: 11px;
409
409
-
background: var(--bg-tertiary);
410
410
-
padding: 3px 8px;
411
411
-
border-radius: var(--radius-full);
412
412
-
color: var(--text-secondary);
413
413
-
}
414
414
-
415
415
-
.annotations {
416
416
-
display: flex;
417
417
-
flex-direction: column;
418
418
-
gap: 1px;
419
419
-
background: var(--border);
420
420
-
}
421
421
-
422
422
-
.annotation-item {
423
423
-
padding: 14px 16px;
424
424
-
background: var(--bg-primary);
425
425
-
transition: background 0.15s;
426
426
-
}
427
427
-
428
428
-
.annotation-item:hover {
429
429
-
background: var(--bg-hover);
430
430
-
}
431
431
-
432
432
-
.annotation-item-header {
433
433
-
display: flex;
434
434
-
align-items: center;
435
435
-
margin-bottom: 8px;
436
436
-
gap: 10px;
437
437
-
}
438
438
-
439
439
-
.annotation-item-avatar {
440
440
-
width: 26px;
441
441
-
height: 26px;
442
442
-
border-radius: 50%;
443
443
-
background: var(--accent);
444
444
-
color: var(--bg-primary);
445
445
-
display: flex;
446
446
-
align-items: center;
447
447
-
justify-content: center;
448
448
-
font-size: 10px;
449
449
-
font-weight: 600;
450
450
-
}
451
451
-
452
452
-
.annotation-item-meta {
453
453
-
flex: 1;
454
454
-
}
455
455
-
456
456
-
.annotation-item-author {
457
457
-
font-size: 12px;
458
458
-
font-weight: 600;
459
459
-
color: var(--text-primary);
460
460
-
}
461
461
-
462
462
-
.annotation-item-time {
463
463
-
font-size: 11px;
464
464
-
color: var(--text-tertiary);
465
465
-
}
466
466
-
467
467
-
.annotation-type-badge {
468
468
-
font-size: 10px;
469
469
-
padding: 3px 8px;
470
470
-
border-radius: var(--radius-full);
471
471
-
font-weight: 500;
472
472
-
}
473
473
-
474
474
-
.annotation-type-badge.highlight {
475
475
-
background: var(--accent-subtle);
476
476
-
color: var(--accent-text);
477
477
-
}
478
478
-
479
479
-
.annotation-item-quote {
480
480
-
padding: 8px 12px;
481
481
-
border-left: 2px solid var(--accent);
482
482
-
margin-bottom: 8px;
483
483
-
font-size: 12px;
484
484
-
color: var(--text-secondary);
485
485
-
font-style: italic;
486
486
-
background: var(--accent-subtle);
487
487
-
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
488
488
-
}
489
489
-
490
490
-
.annotation-item-text {
491
491
-
font-size: 13px;
492
492
-
line-height: 1.5;
493
493
-
color: var(--text-primary);
494
494
-
}
495
495
-
496
496
-
.bookmark-item {
497
497
-
padding: 14px 16px;
498
498
-
background: var(--bg-primary);
499
499
-
text-decoration: none;
500
500
-
color: inherit;
501
501
-
display: block;
502
502
-
transition: background 0.15s;
503
503
-
}
504
504
-
505
505
-
.bookmark-item:hover {
506
506
-
background: var(--bg-hover);
507
507
-
}
508
508
-
509
509
-
.bookmark-title {
510
510
-
font-size: 13px;
511
511
-
font-weight: 500;
512
512
-
margin-bottom: 4px;
513
513
-
white-space: nowrap;
514
514
-
overflow: hidden;
515
515
-
text-overflow: ellipsis;
516
516
-
color: var(--text-primary);
517
517
-
}
518
518
-
519
519
-
.bookmark-url {
520
520
-
font-size: 11px;
521
521
-
color: var(--text-tertiary);
522
522
-
white-space: nowrap;
523
523
-
overflow: hidden;
524
524
-
text-overflow: ellipsis;
525
525
-
}
526
526
-
527
527
-
.empty-state {
528
528
-
display: flex;
529
529
-
flex-direction: column;
530
530
-
align-items: center;
531
531
-
justify-content: center;
532
532
-
padding: 40px 16px;
533
533
-
text-align: center;
534
534
-
color: var(--text-tertiary);
535
535
-
}
536
536
-
537
537
-
.empty-icon {
538
538
-
margin-bottom: 12px;
539
539
-
color: var(--text-tertiary);
540
540
-
opacity: 0.4;
541
541
-
}
542
542
-
543
543
-
.empty-text {
544
544
-
font-size: 13px;
545
545
-
color: var(--text-secondary);
546
546
-
}
547
547
-
548
548
-
.btn {
549
549
-
padding: 10px 18px;
550
550
-
border-radius: var(--radius-md);
551
551
-
border: none;
552
552
-
font-weight: 600;
553
553
-
cursor: pointer;
554
554
-
font-size: 13px;
555
555
-
transition: all 0.15s;
556
556
-
display: inline-flex;
557
557
-
align-items: center;
558
558
-
justify-content: center;
559
559
-
gap: 8px;
560
560
-
}
561
561
-
562
562
-
.btn-small {
563
563
-
padding: 8px 14px;
564
564
-
font-size: 12px;
565
565
-
}
566
566
-
567
567
-
.btn-primary {
568
568
-
background: var(--accent);
569
569
-
color: white;
570
570
-
}
571
571
-
572
572
-
.btn-primary:hover {
573
573
-
background: var(--accent-hover);
574
574
-
}
575
575
-
576
576
-
.btn-secondary {
577
577
-
background: var(--bg-tertiary);
578
578
-
border: 1px solid var(--border);
579
579
-
color: var(--text-primary);
580
580
-
width: 100%;
581
581
-
}
582
582
-
583
583
-
.btn-secondary:hover {
584
584
-
background: var(--bg-hover);
585
585
-
border-color: var(--border-hover);
586
586
-
}
587
587
-
588
588
-
.btn-icon {
589
589
-
background: none;
590
590
-
border: none;
591
591
-
color: var(--text-tertiary);
592
592
-
cursor: pointer;
593
593
-
padding: 6px;
594
594
-
border-radius: var(--radius-sm);
595
595
-
}
596
596
-
597
597
-
.btn-icon:hover {
598
598
-
color: var(--text-primary);
599
599
-
background: var(--bg-hover);
600
600
-
}
601
601
-
602
602
-
.popup-link {
603
603
-
font-size: 12px;
604
604
-
color: var(--text-tertiary);
605
605
-
text-decoration: none;
606
606
-
}
607
607
-
608
608
-
.popup-link:hover {
609
609
-
color: var(--accent-text);
610
610
-
}
611
611
-
612
612
-
.popup-footer {
613
613
-
padding: 12px 16px;
614
614
-
border-top: 1px solid var(--border);
615
615
-
background: var(--bg-primary);
616
616
-
}
617
617
-
618
618
-
.settings-view {
619
619
-
position: absolute;
620
620
-
top: 0;
621
621
-
left: 0;
622
622
-
width: 100%;
623
623
-
height: 100%;
624
624
-
background: var(--bg-primary);
625
625
-
z-index: 20;
626
626
-
display: flex;
627
627
-
flex-direction: column;
628
628
-
padding: 16px;
629
629
-
}
630
630
-
631
631
-
.settings-header {
632
632
-
display: flex;
633
633
-
justify-content: space-between;
634
634
-
align-items: center;
635
635
-
margin-bottom: 24px;
636
636
-
color: var(--text-primary);
637
637
-
}
638
638
-
639
639
-
.setting-item {
640
640
-
margin-bottom: 20px;
641
641
-
}
642
642
-
643
643
-
.setting-label {
644
644
-
font-size: 13px;
645
645
-
font-weight: 500;
646
646
-
color: var(--text-primary);
647
647
-
margin-bottom: 6px;
648
648
-
}
649
649
-
650
650
-
.setting-help {
651
651
-
font-size: 11px;
652
652
-
color: var(--text-tertiary);
653
653
-
margin-top: 4px;
654
654
-
}
655
655
-
656
656
-
::-webkit-scrollbar {
657
657
-
width: 8px;
658
658
-
}
659
659
-
660
660
-
::-webkit-scrollbar-track {
661
661
-
background: transparent;
662
662
-
}
663
663
-
664
664
-
::-webkit-scrollbar-thumb {
665
665
-
background: var(--bg-hover);
666
666
-
border-radius: var(--radius-full);
667
667
-
}
668
668
-
669
669
-
::-webkit-scrollbar-thumb:hover {
670
670
-
background: var(--text-tertiary);
671
671
-
}
672
672
-
673
673
-
.collection-selector {
674
674
-
position: absolute;
675
675
-
top: 0;
676
676
-
left: 0;
677
677
-
width: 100%;
678
678
-
height: 100%;
679
679
-
background: var(--bg-primary);
680
680
-
z-index: 30;
681
681
-
display: flex;
682
682
-
flex-direction: column;
683
683
-
padding: 16px;
684
684
-
}
685
685
-
686
686
-
.collection-list {
687
687
-
display: flex;
688
688
-
flex-direction: column;
689
689
-
gap: 8px;
690
690
-
overflow-y: auto;
691
691
-
flex: 1;
692
692
-
}
693
693
-
694
694
-
.collection-select-btn {
695
695
-
display: flex;
696
696
-
align-items: center;
697
697
-
gap: 12px;
698
698
-
padding: 12px;
699
699
-
background: var(--bg-primary);
700
700
-
border: 1px solid var(--border);
701
701
-
border-radius: var(--radius-md);
702
702
-
color: var(--text-primary);
703
703
-
font-size: 14px;
704
704
-
cursor: pointer;
705
705
-
text-align: left;
706
706
-
transition: all 0.15s;
707
707
-
}
708
708
-
709
709
-
.collection-select-btn:hover {
710
710
-
border-color: var(--accent);
711
711
-
background: var(--bg-hover);
712
712
-
}
713
713
-
714
714
-
.collection-select-btn:disabled {
715
715
-
opacity: 0.6;
716
716
-
cursor: not-allowed;
717
717
-
}
718
718
-
719
719
-
.annotation-item-actions {
720
720
-
display: flex;
721
721
-
align-items: center;
722
722
-
gap: 8px;
723
723
-
margin-left: auto;
724
724
-
}
725
725
-
726
726
-
.toggle-switch {
727
727
-
position: relative;
728
728
-
display: inline-block;
729
729
-
width: 40px;
730
730
-
height: 22px;
731
731
-
flex-shrink: 0;
732
732
-
}
733
733
-
734
734
-
.toggle-switch input {
735
735
-
opacity: 0;
736
736
-
width: 0;
737
737
-
height: 0;
738
738
-
}
739
739
-
740
740
-
.toggle-slider {
741
741
-
position: absolute;
742
742
-
cursor: pointer;
743
743
-
top: 0;
744
744
-
left: 0;
745
745
-
right: 0;
746
746
-
bottom: 0;
747
747
-
background-color: var(--bg-tertiary);
748
748
-
transition: 0.2s;
749
749
-
border-radius: 22px;
750
750
-
}
751
751
-
752
752
-
.toggle-slider:before {
753
753
-
position: absolute;
754
754
-
content: "";
755
755
-
height: 16px;
756
756
-
width: 16px;
757
757
-
left: 3px;
758
758
-
bottom: 3px;
759
759
-
background-color: var(--text-tertiary);
760
760
-
transition: 0.2s;
761
761
-
border-radius: 50%;
762
762
-
}
763
763
-
764
764
-
.toggle-switch input:checked + .toggle-slider {
765
765
-
background-color: var(--accent);
766
766
-
}
767
767
-
768
768
-
.toggle-switch input:checked + .toggle-slider:before {
769
769
-
transform: translateX(18px);
770
770
-
background-color: white;
771
771
-
}
772
772
-
773
773
-
.settings-input {
774
774
-
width: 100%;
775
775
-
padding: 10px 12px;
776
776
-
background: var(--bg-elevated);
777
777
-
border: 1px solid var(--border);
778
778
-
border-radius: var(--radius-md);
779
779
-
color: var(--text-primary);
780
780
-
font-size: 13px;
781
781
-
}
782
782
-
783
783
-
.settings-input:focus {
784
784
-
outline: none;
785
785
-
border-color: var(--accent);
786
786
-
}
787
787
-
788
788
-
.theme-toggle-group {
789
789
-
display: flex;
790
790
-
background: var(--bg-tertiary);
791
791
-
padding: 3px;
792
792
-
border-radius: var(--radius-md);
793
793
-
gap: 2px;
794
794
-
margin-top: 8px;
795
795
-
}
796
796
-
797
797
-
.theme-btn {
798
798
-
flex: 1;
799
799
-
padding: 6px;
800
800
-
border: none;
801
801
-
background: transparent;
802
802
-
color: var(--text-tertiary);
803
803
-
font-size: 12px;
804
804
-
font-weight: 500;
805
805
-
border-radius: var(--radius-sm);
806
806
-
cursor: pointer;
807
807
-
transition: all 0.15s ease;
808
808
-
}
809
809
-
810
810
-
.theme-btn:hover {
811
811
-
color: var(--text-secondary);
812
812
-
}
813
813
-
814
814
-
.theme-btn.active {
815
815
-
background: var(--bg-primary);
816
816
-
color: var(--text-primary);
817
817
-
}
-308
extension/popup/popup.html
···
1
1
-
<!doctype html>
2
2
-
<html lang="en">
3
3
-
<head>
4
4
-
<meta charset="UTF-8" />
5
5
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
-
<title>Margin</title>
7
7
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
-
<link
10
10
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
11
11
-
rel="stylesheet"
12
12
-
/>
13
13
-
<link rel="stylesheet" href="popup.css" />
14
14
-
</head>
15
15
-
16
16
-
<body>
17
17
-
<div class="popup">
18
18
-
<header class="popup-header">
19
19
-
<div class="popup-brand">
20
20
-
<span class="popup-logo">
21
21
-
<img src="../icons/logo.svg" alt="Margin" width="20" height="20" />
22
22
-
</span>
23
23
-
<span class="popup-title">Margin</span>
24
24
-
</div>
25
25
-
<div id="user-info" class="user-info" style="display: none">
26
26
-
<span id="user-handle" class="user-handle"></span>
27
27
-
</div>
28
28
-
</header>
29
29
-
30
30
-
<div class="popup-content">
31
31
-
<div id="loading" class="loading">
32
32
-
<div class="spinner"></div>
33
33
-
<span>Loading...</span>
34
34
-
</div>
35
35
-
36
36
-
<div id="login-prompt" class="login-prompt" style="display: none">
37
37
-
<span class="login-at-logo">@</span>
38
38
-
<h2 class="login-title">Sign in with AT Protocol</h2>
39
39
-
<p class="login-text">
40
40
-
Connect your Bluesky account to annotate, highlight, and bookmark
41
41
-
the web.
42
42
-
</p>
43
43
-
<button id="sign-in" class="btn btn-primary">Continue</button>
44
44
-
</div>
45
45
-
46
46
-
<div id="main-content" style="display: none">
47
47
-
<div class="tabs">
48
48
-
<button class="tab-btn active" data-tab="page">Page</button>
49
49
-
<button class="tab-btn" data-tab="bookmarks">My Bookmarks</button>
50
50
-
<button class="tab-btn" data-tab="highlights">My Highlights</button>
51
51
-
</div>
52
52
-
53
53
-
<div id="tab-page" class="tab-content active">
54
54
-
<div class="quick-actions">
55
55
-
<button
56
56
-
id="bookmark-page"
57
57
-
class="btn btn-secondary btn-small"
58
58
-
title="Bookmark this page"
59
59
-
>
60
60
-
<svg
61
61
-
width="14"
62
62
-
height="14"
63
63
-
viewBox="0 0 24 24"
64
64
-
fill="none"
65
65
-
stroke="currentColor"
66
66
-
stroke-width="2"
67
67
-
stroke-linecap="round"
68
68
-
stroke-linejoin="round"
69
69
-
>
70
70
-
<path
71
71
-
d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"
72
72
-
></path>
73
73
-
</svg>
74
74
-
Bookmark Page
75
75
-
</button>
76
76
-
</div>
77
77
-
78
78
-
<div id="create-form" class="create-form">
79
79
-
<div class="form-header">
80
80
-
<span class="form-title">New Annotation</span>
81
81
-
<span id="current-url" class="current-url"></span>
82
82
-
</div>
83
83
-
<textarea
84
84
-
id="annotation-text"
85
85
-
class="annotation-input"
86
86
-
placeholder="Write your annotation..."
87
87
-
rows="3"
88
88
-
></textarea>
89
89
-
<div class="form-actions">
90
90
-
<button
91
91
-
id="submit-annotation"
92
92
-
class="btn btn-primary btn-small"
93
93
-
>
94
94
-
Post
95
95
-
</button>
96
96
-
</div>
97
97
-
</div>
98
98
-
99
99
-
<div class="annotations-section">
100
100
-
<div class="section-header">
101
101
-
<span class="section-title">Annotations on this page</span>
102
102
-
<span id="annotation-count" class="annotation-count">0</span>
103
103
-
</div>
104
104
-
<div id="annotations" class="annotations"></div>
105
105
-
<div id="empty" class="empty-state" style="display: none">
106
106
-
<span class="empty-icon">
107
107
-
<svg
108
108
-
width="32"
109
109
-
height="32"
110
110
-
viewBox="0 0 24 24"
111
111
-
fill="none"
112
112
-
stroke="currentColor"
113
113
-
stroke-width="2"
114
114
-
stroke-linecap="round"
115
115
-
stroke-linejoin="round"
116
116
-
>
117
117
-
<path d="M22 12h-6l-2 3h-4l-2-3H2"></path>
118
118
-
<path
119
119
-
d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"
120
120
-
></path>
121
121
-
</svg>
122
122
-
</span>
123
123
-
<p class="empty-text">No annotations yet</p>
124
124
-
</div>
125
125
-
</div>
126
126
-
</div>
127
127
-
128
128
-
<div id="tab-bookmarks" class="tab-content">
129
129
-
<div class="section-header">
130
130
-
<span class="section-title">My Saved Pages</span>
131
131
-
</div>
132
132
-
<div id="bookmarks-list" class="annotations"></div>
133
133
-
<div id="bookmarks-empty" class="empty-state" style="display: none">
134
134
-
<span class="empty-icon">
135
135
-
<svg
136
136
-
width="32"
137
137
-
height="32"
138
138
-
viewBox="0 0 24 24"
139
139
-
fill="none"
140
140
-
stroke="currentColor"
141
141
-
stroke-width="2"
142
142
-
stroke-linecap="round"
143
143
-
stroke-linejoin="round"
144
144
-
>
145
145
-
<path
146
146
-
d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"
147
147
-
></path>
148
148
-
</svg>
149
149
-
</span>
150
150
-
<p class="empty-text">You haven't bookmarked any pages yet</p>
151
151
-
</div>
152
152
-
</div>
153
153
-
154
154
-
<div id="tab-highlights" class="tab-content">
155
155
-
<div class="section-header">
156
156
-
<span class="section-title">My Highlights</span>
157
157
-
</div>
158
158
-
<div id="highlights-list" class="annotations"></div>
159
159
-
<div
160
160
-
id="highlights-empty"
161
161
-
class="empty-state"
162
162
-
style="display: none"
163
163
-
>
164
164
-
<span class="empty-icon">
165
165
-
<svg
166
166
-
width="32"
167
167
-
height="32"
168
168
-
viewBox="0 0 24 24"
169
169
-
fill="none"
170
170
-
stroke="currentColor"
171
171
-
stroke-width="2"
172
172
-
stroke-linecap="round"
173
173
-
stroke-linejoin="round"
174
174
-
>
175
175
-
<path
176
176
-
d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"
177
177
-
></path>
178
178
-
</svg>
179
179
-
</span>
180
180
-
<p class="empty-text">You haven't highlighted anything yet</p>
181
181
-
</div>
182
182
-
</div>
183
183
-
</div>
184
184
-
</div>
185
185
-
186
186
-
<footer class="popup-footer">
187
187
-
<div
188
188
-
style="
189
189
-
display: flex;
190
190
-
justify-content: space-between;
191
191
-
align-items: center;
192
192
-
"
193
193
-
>
194
194
-
<a href="#" id="open-web" class="popup-link">Open Margin</a>
195
195
-
<button id="toggle-settings" class="btn-icon" title="Settings">
196
196
-
<svg
197
197
-
width="16"
198
198
-
height="16"
199
199
-
viewBox="0 0 24 24"
200
200
-
fill="none"
201
201
-
stroke="currentColor"
202
202
-
stroke-width="2"
203
203
-
stroke-linecap="round"
204
204
-
stroke-linejoin="round"
205
205
-
>
206
206
-
<circle cx="12" cy="12" r="3"></circle>
207
207
-
<path
208
208
-
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
209
209
-
></path>
210
210
-
</svg>
211
211
-
</button>
212
212
-
</div>
213
213
-
</footer>
214
214
-
215
215
-
<div id="settings-view" class="settings-view" style="display: none">
216
216
-
<div class="settings-header">
217
217
-
<h3 class="settings-title">Extension Settings</h3>
218
218
-
<button id="close-settings" class="btn-icon">×</button>
219
219
-
</div>
220
220
-
<div class="setting-item">
221
221
-
<div
222
222
-
style="
223
223
-
display: flex;
224
224
-
justify-content: space-between;
225
225
-
align-items: center;
226
226
-
"
227
227
-
>
228
228
-
<div>
229
229
-
<label>Show page overlays</label>
230
230
-
<p class="setting-help" style="margin-top: 2px">
231
231
-
Highlights, badges, and tooltips on pages
232
232
-
</p>
233
233
-
</div>
234
234
-
<label class="toggle-switch">
235
235
-
<input type="checkbox" id="overlay-toggle" checked />
236
236
-
<span class="toggle-slider"></span>
237
237
-
</label>
238
238
-
</div>
239
239
-
</div>
240
240
-
<div class="setting-item">
241
241
-
<label for="api-url">API URL (for self-hosting)</label>
242
242
-
<input
243
243
-
type="url"
244
244
-
id="api-url"
245
245
-
placeholder="http://localhost:8080"
246
246
-
class="settings-input"
247
247
-
/>
248
248
-
<p class="setting-help">Enter your backend URL</p>
249
249
-
</div>
250
250
-
<div class="setting-item">
251
251
-
<label for="theme-select">Theme</label>
252
252
-
<div class="theme-toggle-group">
253
253
-
<button class="theme-btn active" data-theme="system">Auto</button>
254
254
-
<button class="theme-btn" data-theme="light">Light</button>
255
255
-
<button class="theme-btn" data-theme="dark">Dark</button>
256
256
-
</div>
257
257
-
</div>
258
258
-
<button id="save-settings" class="btn btn-primary" style="width: 100%">
259
259
-
Save
260
260
-
</button>
261
261
-
</div>
262
262
-
263
263
-
<div
264
264
-
id="collection-selector"
265
265
-
class="collection-selector"
266
266
-
style="display: none"
267
267
-
>
268
268
-
<div
269
269
-
class="selector-header"
270
270
-
style="
271
271
-
display: flex;
272
272
-
justify-content: space-between;
273
273
-
align-items: center;
274
274
-
margin-bottom: 12px;
275
275
-
padding-bottom: 8px;
276
276
-
border-bottom: 1px solid var(--border);
277
277
-
"
278
278
-
>
279
279
-
<h3 class="selector-title" style="margin: 0; font-size: 14px">
280
280
-
Add to Collection
281
281
-
</h3>
282
282
-
<button id="close-collection-selector" class="btn-icon">×</button>
283
283
-
</div>
284
284
-
<div
285
285
-
id="collection-loading"
286
286
-
class="spinner"
287
287
-
style="display: none; margin: 20px auto"
288
288
-
></div>
289
289
-
<div
290
290
-
id="collection-list"
291
291
-
class="collection-list"
292
292
-
style="
293
293
-
display: flex;
294
294
-
flex-direction: column;
295
295
-
gap: 4px;
296
296
-
max-height: 200px;
297
297
-
overflow-y: auto;
298
298
-
"
299
299
-
></div>
300
300
-
<div id="collections-empty" class="empty-state" style="display: none">
301
301
-
<p class="empty-text">No collections found</p>
302
302
-
</div>
303
303
-
</div>
304
304
-
</div>
305
305
-
306
306
-
<script src="popup.js"></script>
307
307
-
</body>
308
308
-
</html>
-835
extension/popup/popup.js
···
1
1
-
const browserAPI = typeof browser !== "undefined" ? browser : chrome;
2
2
-
3
3
-
document.addEventListener("DOMContentLoaded", async () => {
4
4
-
const views = {
5
5
-
loading: document.getElementById("loading"),
6
6
-
login: document.getElementById("login-prompt"),
7
7
-
main: document.getElementById("main-content"),
8
8
-
settings: document.getElementById("settings-view"),
9
9
-
collectionSelector: document.getElementById("collection-selector"),
10
10
-
};
11
11
-
12
12
-
const els = {
13
13
-
userHandle: document.getElementById("user-handle"),
14
14
-
userInfo: document.getElementById("user-info"),
15
15
-
signInBtn: document.getElementById("sign-in"),
16
16
-
openWebBtn: document.getElementById("open-web"),
17
17
-
submitBtn: document.getElementById("submit-annotation"),
18
18
-
textInput: document.getElementById("annotation-text"),
19
19
-
currentUrl: document.getElementById("current-url"),
20
20
-
annotationsList: document.getElementById("annotations"),
21
21
-
annotationCount: document.getElementById("annotation-count"),
22
22
-
emptyState: document.getElementById("empty"),
23
23
-
toggleSettings: document.getElementById("toggle-settings"),
24
24
-
closeSettings: document.getElementById("close-settings"),
25
25
-
saveSettings: document.getElementById("save-settings"),
26
26
-
apiUrlInput: document.getElementById("api-url"),
27
27
-
bookmarkBtn: document.getElementById("bookmark-page"),
28
28
-
29
29
-
tabs: document.querySelectorAll(".tab-btn"),
30
30
-
tabContents: document.querySelectorAll(".tab-content"),
31
31
-
bookmarksList: document.getElementById("bookmarks-list"),
32
32
-
bookmarksEmpty: document.getElementById("bookmarks-empty"),
33
33
-
highlightsList: document.getElementById("highlights-list"),
34
34
-
highlightsEmpty: document.getElementById("highlights-empty"),
35
35
-
36
36
-
closeCollectionSelector: document.getElementById(
37
37
-
"close-collection-selector",
38
38
-
),
39
39
-
collectionList: document.getElementById("collection-list"),
40
40
-
collectionLoading: document.getElementById("collection-loading"),
41
41
-
collectionsEmpty: document.getElementById("collections-empty"),
42
42
-
overlayToggle: document.getElementById("overlay-toggle"),
43
43
-
themeBtns: document.querySelectorAll(".theme-btn"),
44
44
-
};
45
45
-
46
46
-
let currentTab = null;
47
47
-
let apiUrl = "https://margin.at";
48
48
-
let currentUserDid = null;
49
49
-
let pendingSelector = null;
50
50
-
// let _activeAnnotationUriForCollection = null;
51
51
-
52
52
-
const storage = await browserAPI.storage.local.get([
53
53
-
"apiUrl",
54
54
-
"showOverlay",
55
55
-
"theme",
56
56
-
]);
57
57
-
if (storage.apiUrl) {
58
58
-
apiUrl = storage.apiUrl;
59
59
-
}
60
60
-
els.apiUrlInput.value = apiUrl;
61
61
-
62
62
-
if (els.overlayToggle) {
63
63
-
els.overlayToggle.checked = storage.showOverlay !== false;
64
64
-
}
65
65
-
66
66
-
const currentTheme = storage.theme || "system";
67
67
-
applyTheme(currentTheme);
68
68
-
updateThemeUI(currentTheme);
69
69
-
70
70
-
try {
71
71
-
const [tab] = await browserAPI.tabs.query({
72
72
-
active: true,
73
73
-
currentWindow: true,
74
74
-
});
75
75
-
currentTab = tab;
76
76
-
77
77
-
if (els.currentUrl) {
78
78
-
els.currentUrl.textContent = new URL(tab.url).hostname;
79
79
-
}
80
80
-
81
81
-
let pendingData = null;
82
82
-
if (browserAPI.storage.session) {
83
83
-
try {
84
84
-
const sessionData = await browserAPI.storage.session.get([
85
85
-
"pendingAnnotation",
86
86
-
]);
87
87
-
if (sessionData.pendingAnnotation) {
88
88
-
pendingData = sessionData.pendingAnnotation;
89
89
-
await browserAPI.storage.session.remove(["pendingAnnotation"]);
90
90
-
}
91
91
-
} catch {
92
92
-
/* ignore */
93
93
-
}
94
94
-
}
95
95
-
96
96
-
if (!pendingData) {
97
97
-
const localData = await browserAPI.storage.local.get([
98
98
-
"pendingAnnotation",
99
99
-
"pendingAnnotationExpiry",
100
100
-
]);
101
101
-
if (
102
102
-
localData.pendingAnnotation &&
103
103
-
localData.pendingAnnotationExpiry > Date.now()
104
104
-
) {
105
105
-
pendingData = localData.pendingAnnotation;
106
106
-
}
107
107
-
await browserAPI.storage.local.remove([
108
108
-
"pendingAnnotation",
109
109
-
"pendingAnnotationExpiry",
110
110
-
]);
111
111
-
}
112
112
-
113
113
-
if (pendingData?.selector) {
114
114
-
pendingSelector = pendingData.selector;
115
115
-
showQuotePreview(pendingSelector);
116
116
-
}
117
117
-
118
118
-
checkSession();
119
119
-
} catch (err) {
120
120
-
console.error("Init error:", err);
121
121
-
showView("login");
122
122
-
}
123
123
-
124
124
-
els.signInBtn?.addEventListener("click", () => {
125
125
-
browserAPI.runtime.sendMessage({ type: "OPEN_LOGIN" });
126
126
-
});
127
127
-
128
128
-
els.openWebBtn?.addEventListener("click", () => {
129
129
-
browserAPI.runtime.sendMessage({ type: "OPEN_WEB" });
130
130
-
});
131
131
-
132
132
-
els.tabs.forEach((btn) => {
133
133
-
btn.addEventListener("click", () => {
134
134
-
els.tabs.forEach((t) => t.classList.remove("active"));
135
135
-
els.tabContents.forEach((c) => c.classList.remove("active"));
136
136
-
137
137
-
const tabId = btn.getAttribute("data-tab");
138
138
-
btn.classList.add("active");
139
139
-
document.getElementById(`tab-${tabId}`).classList.add("active");
140
140
-
141
141
-
if (tabId === "bookmarks") loadBookmarks();
142
142
-
if (tabId === "highlights") loadHighlights();
143
143
-
});
144
144
-
});
145
145
-
146
146
-
els.submitBtn?.addEventListener("click", async () => {
147
147
-
const text = els.textInput.value.trim();
148
148
-
if (!text) return;
149
149
-
150
150
-
els.submitBtn.disabled = true;
151
151
-
els.submitBtn.textContent = "Posting...";
152
152
-
153
153
-
try {
154
154
-
const annotationData = {
155
155
-
url: currentTab.url,
156
156
-
text: text,
157
157
-
title: currentTab.title,
158
158
-
};
159
159
-
160
160
-
if (pendingSelector) {
161
161
-
annotationData.selector = pendingSelector;
162
162
-
}
163
163
-
164
164
-
const res = await sendMessage({
165
165
-
type: "CREATE_ANNOTATION",
166
166
-
data: annotationData,
167
167
-
});
168
168
-
169
169
-
if (res.success) {
170
170
-
els.textInput.value = "";
171
171
-
pendingSelector = null;
172
172
-
hideQuotePreview();
173
173
-
loadAnnotations();
174
174
-
} else {
175
175
-
alert("Failed to post annotation");
176
176
-
}
177
177
-
} catch (err) {
178
178
-
console.error("Post error:", err);
179
179
-
alert("Error posting annotation");
180
180
-
} finally {
181
181
-
els.submitBtn.disabled = false;
182
182
-
els.submitBtn.textContent = "Post";
183
183
-
}
184
184
-
});
185
185
-
186
186
-
els.bookmarkBtn?.addEventListener("click", async () => {
187
187
-
els.bookmarkBtn.disabled = true;
188
188
-
els.bookmarkBtn.textContent = "Saving...";
189
189
-
190
190
-
try {
191
191
-
const res = await sendMessage({
192
192
-
type: "CREATE_BOOKMARK",
193
193
-
data: {
194
194
-
url: currentTab.url,
195
195
-
title: currentTab.title,
196
196
-
},
197
197
-
});
198
198
-
199
199
-
if (res.success) {
200
200
-
els.bookmarkBtn.textContent = "✓ Bookmarked";
201
201
-
setTimeout(() => {
202
202
-
els.bookmarkBtn.textContent = "Bookmark Page";
203
203
-
els.bookmarkBtn.disabled = false;
204
204
-
}, 2000);
205
205
-
} else {
206
206
-
alert("Failed to bookmark page");
207
207
-
els.bookmarkBtn.textContent = "Bookmark Page";
208
208
-
els.bookmarkBtn.disabled = false;
209
209
-
}
210
210
-
} catch (err) {
211
211
-
console.error("Bookmark error:", err);
212
212
-
alert("Error bookmarking page");
213
213
-
els.bookmarkBtn.textContent = "Bookmark Page";
214
214
-
els.bookmarkBtn.disabled = false;
215
215
-
}
216
216
-
});
217
217
-
218
218
-
els.toggleSettings?.addEventListener("click", () => {
219
219
-
views.settings.style.display = "flex";
220
220
-
});
221
221
-
222
222
-
els.closeSettings?.addEventListener("click", () => {
223
223
-
views.settings.style.display = "none";
224
224
-
});
225
225
-
226
226
-
els.saveSettings?.addEventListener("click", async () => {
227
227
-
const newUrl = els.apiUrlInput.value.replace(/\/$/, "");
228
228
-
const showOverlay = els.overlayToggle?.checked ?? true;
229
229
-
230
230
-
await browserAPI.storage.local.set({ apiUrl: newUrl, showOverlay });
231
231
-
if (newUrl) {
232
232
-
apiUrl = newUrl;
233
233
-
}
234
234
-
await sendMessage({ type: "UPDATE_SETTINGS" });
235
235
-
236
236
-
const tabs = await browserAPI.tabs.query({});
237
237
-
for (const tab of tabs) {
238
238
-
if (tab.id) {
239
239
-
try {
240
240
-
await browserAPI.tabs.sendMessage(tab.id, {
241
241
-
type: "UPDATE_OVERLAY_VISIBILITY",
242
242
-
show: showOverlay,
243
243
-
});
244
244
-
} catch {
245
245
-
/* ignore */
246
246
-
}
247
247
-
}
248
248
-
}
249
249
-
250
250
-
views.settings.style.display = "none";
251
251
-
checkSession();
252
252
-
});
253
253
-
254
254
-
els.themeBtns.forEach((btn) => {
255
255
-
btn.addEventListener("click", async () => {
256
256
-
const theme = btn.getAttribute("data-theme");
257
257
-
await browserAPI.storage.local.set({ theme });
258
258
-
applyTheme(theme);
259
259
-
updateThemeUI(theme);
260
260
-
});
261
261
-
});
262
262
-
263
263
-
els.closeCollectionSelector?.addEventListener("click", () => {
264
264
-
views.collectionSelector.style.display = "none";
265
265
-
});
266
266
-
267
267
-
async function openCollectionSelector(annotationUri) {
268
268
-
if (!currentUserDid) {
269
269
-
console.error("No currentUserDid, returning early");
270
270
-
return;
271
271
-
}
272
272
-
views.collectionSelector.style.display = "flex";
273
273
-
els.collectionList.innerHTML = "";
274
274
-
els.collectionLoading.style.display = "block";
275
275
-
els.collectionsEmpty.style.display = "none";
276
276
-
277
277
-
try {
278
278
-
const [collectionsRes, containingRes] = await Promise.all([
279
279
-
sendMessage({
280
280
-
type: "GET_USER_COLLECTIONS",
281
281
-
data: { did: currentUserDid },
282
282
-
}),
283
283
-
sendMessage({
284
284
-
type: "GET_CONTAINING_COLLECTIONS",
285
285
-
data: { uri: annotationUri },
286
286
-
}),
287
287
-
]);
288
288
-
289
289
-
if (collectionsRes.success) {
290
290
-
const containingUris = containingRes.success
291
291
-
? new Set(containingRes.data)
292
292
-
: new Set();
293
293
-
renderCollectionList(
294
294
-
collectionsRes.data,
295
295
-
annotationUri,
296
296
-
containingUris,
297
297
-
);
298
298
-
}
299
299
-
} catch (err) {
300
300
-
console.error("Load collections error:", err);
301
301
-
els.collectionList.innerHTML =
302
302
-
'<p class="error">Failed to load collections</p>';
303
303
-
} finally {
304
304
-
els.collectionLoading.style.display = "none";
305
305
-
}
306
306
-
}
307
307
-
308
308
-
function renderCollectionList(
309
309
-
items,
310
310
-
annotationUri,
311
311
-
containingUris = new Set(),
312
312
-
) {
313
313
-
els.collectionList.innerHTML = "";
314
314
-
els.collectionList.dataset.annotationUri = annotationUri;
315
315
-
316
316
-
if (!items || items.length === 0) {
317
317
-
els.collectionsEmpty.style.display = "block";
318
318
-
return;
319
319
-
}
320
320
-
321
321
-
items.forEach((col) => {
322
322
-
const btn = document.createElement("button");
323
323
-
btn.className = "collection-select-btn";
324
324
-
const isAdded = containingUris.has(col.uri);
325
325
-
326
326
-
const iconSpan = document.createElement("span");
327
327
-
iconSpan.textContent = isAdded ? "✓" : "📁";
328
328
-
btn.appendChild(iconSpan);
329
329
-
330
330
-
const nameSpan = document.createElement("span");
331
331
-
nameSpan.textContent = col.name;
332
332
-
btn.appendChild(nameSpan);
333
333
-
334
334
-
if (isAdded) {
335
335
-
btn.classList.add("added");
336
336
-
btn.disabled = true;
337
337
-
}
338
338
-
339
339
-
btn.addEventListener("click", async () => {
340
340
-
if (btn.disabled) return;
341
341
-
const annUri = els.collectionList.dataset.annotationUri;
342
342
-
await handleAddToCollection(col.uri, btn, annUri);
343
343
-
});
344
344
-
345
345
-
els.collectionList.appendChild(btn);
346
346
-
});
347
347
-
}
348
348
-
349
349
-
async function handleAddToCollection(
350
350
-
collectionUri,
351
351
-
btnElement,
352
352
-
annotationUri,
353
353
-
) {
354
354
-
if (!annotationUri) {
355
355
-
console.error("No annotationUri provided!");
356
356
-
alert("Error: No item selected to add");
357
357
-
return;
358
358
-
}
359
359
-
360
360
-
const originalText = btnElement.textContent;
361
361
-
btnElement.disabled = true;
362
362
-
btnElement.textContent = "Adding...";
363
363
-
364
364
-
try {
365
365
-
const res = await sendMessage({
366
366
-
type: "ADD_TO_COLLECTION",
367
367
-
data: {
368
368
-
collectionUri: collectionUri,
369
369
-
annotationUri: annotationUri,
370
370
-
},
371
371
-
});
372
372
-
373
373
-
if (res && res.success) {
374
374
-
btnElement.textContent = "✓ Added";
375
375
-
btnElement.textContent = "✓ Added";
376
376
-
setTimeout(() => {
377
377
-
btnElement.textContent = originalText;
378
378
-
btnElement.disabled = false;
379
379
-
}, 2000);
380
380
-
} else {
381
381
-
alert(
382
382
-
"Failed to add to collection: " + (res?.error || "Unknown error"),
383
383
-
);
384
384
-
btnElement.textContent = originalText;
385
385
-
btnElement.disabled = false;
386
386
-
}
387
387
-
} catch (err) {
388
388
-
console.error("Add to collection error:", err);
389
389
-
alert("Error adding to collection: " + err.message);
390
390
-
btnElement.textContent = originalText;
391
391
-
btnElement.disabled = false;
392
392
-
}
393
393
-
}
394
394
-
395
395
-
async function checkSession() {
396
396
-
showView("loading");
397
397
-
try {
398
398
-
const res = await sendMessage({ type: "CHECK_SESSION" });
399
399
-
400
400
-
if (res.success && res.data?.authenticated) {
401
401
-
if (els.userHandle) {
402
402
-
const handle = res.data.handle || res.data.email || "User";
403
403
-
els.userHandle.textContent = "@" + handle;
404
404
-
}
405
405
-
els.userInfo.style.display = "flex";
406
406
-
currentUserDid = res.data.did;
407
407
-
showView("main");
408
408
-
loadAnnotations();
409
409
-
} else {
410
410
-
els.userInfo.style.display = "none";
411
411
-
showView("login");
412
412
-
}
413
413
-
} catch (err) {
414
414
-
console.error("Session check error:", err);
415
415
-
els.userInfo.style.display = "none";
416
416
-
showView("login");
417
417
-
}
418
418
-
}
419
419
-
420
420
-
async function loadAnnotations() {
421
421
-
try {
422
422
-
const res = await sendMessage({
423
423
-
type: "GET_ANNOTATIONS",
424
424
-
data: { url: currentTab.url },
425
425
-
});
426
426
-
427
427
-
if (res.success) {
428
428
-
if (currentUserDid) {
429
429
-
const isBookmarked = res.data.some(
430
430
-
(item) =>
431
431
-
item.type === "Bookmark" && item.creator.did === currentUserDid,
432
432
-
);
433
433
-
if (els.bookmarkBtn) {
434
434
-
if (isBookmarked) {
435
435
-
els.bookmarkBtn.textContent = "✓ Bookmarked";
436
436
-
els.bookmarkBtn.disabled = true;
437
437
-
} else {
438
438
-
els.bookmarkBtn.textContent = "Bookmark Page";
439
439
-
els.bookmarkBtn.disabled = false;
440
440
-
}
441
441
-
}
442
442
-
}
443
443
-
444
444
-
const listItems = res.data.filter((item) => item.type !== "Bookmark");
445
445
-
renderAnnotations(listItems);
446
446
-
}
447
447
-
} catch (err) {
448
448
-
console.error("Load annotations error:", err);
449
449
-
}
450
450
-
}
451
451
-
452
452
-
async function loadBookmarks() {
453
453
-
if (!currentUserDid) return;
454
454
-
els.bookmarksList.innerHTML =
455
455
-
'<div class="spinner" style="margin: 20px auto;"></div>';
456
456
-
els.bookmarksEmpty.style.display = "none";
457
457
-
458
458
-
try {
459
459
-
const res = await sendMessage({
460
460
-
type: "GET_USER_BOOKMARKS",
461
461
-
data: { did: currentUserDid },
462
462
-
});
463
463
-
464
464
-
if (res.success) {
465
465
-
renderBookmarks(res.data);
466
466
-
}
467
467
-
} catch (err) {
468
468
-
console.error("Load bookmarks error:", err);
469
469
-
els.bookmarksList.innerHTML =
470
470
-
'<p class="error">Failed to load bookmarks</p>';
471
471
-
}
472
472
-
}
473
473
-
474
474
-
async function loadHighlights() {
475
475
-
if (!currentUserDid) return;
476
476
-
els.highlightsList.innerHTML =
477
477
-
'<div class="spinner" style="margin: 20px auto;"></div>';
478
478
-
els.highlightsEmpty.style.display = "none";
479
479
-
480
480
-
try {
481
481
-
const res = await sendMessage({
482
482
-
type: "GET_USER_HIGHLIGHTS",
483
483
-
data: { did: currentUserDid },
484
484
-
});
485
485
-
486
486
-
if (res.success) {
487
487
-
renderHighlights(res.data);
488
488
-
}
489
489
-
} catch (err) {
490
490
-
console.error("Load highlights error:", err);
491
491
-
els.highlightsList.innerHTML =
492
492
-
'<p class="error">Failed to load highlights</p>';
493
493
-
}
494
494
-
}
495
495
-
496
496
-
function renderAnnotations(items) {
497
497
-
els.annotationsList.innerHTML = "";
498
498
-
els.annotationCount.textContent = items?.length || 0;
499
499
-
500
500
-
if (!items || items.length === 0) {
501
501
-
els.emptyState.style.display = "block";
502
502
-
return;
503
503
-
}
504
504
-
505
505
-
els.emptyState.style.display = "none";
506
506
-
items.forEach((item) => {
507
507
-
const el = document.createElement("div");
508
508
-
el.className = "annotation-item";
509
509
-
510
510
-
const author = item.creator || item.author || {};
511
511
-
const authorName = author.handle || author.displayName || "Unknown";
512
512
-
const authorInitial = authorName[0]?.toUpperCase() || "?";
513
513
-
const createdAt = item.created || item.createdAt;
514
514
-
const text = item.body?.value || item.text || "";
515
515
-
const selector = item.target?.selector;
516
516
-
517
517
-
const isHighlight = item.type === "Highlight";
518
518
-
const quote = selector?.exact || "";
519
519
-
520
520
-
const header = document.createElement("div");
521
521
-
header.className = "annotation-item-header";
522
522
-
523
523
-
const avatar = document.createElement("div");
524
524
-
avatar.className = "annotation-item-avatar";
525
525
-
if (author.avatar) {
526
526
-
const img = document.createElement("img");
527
527
-
img.src = author.avatar;
528
528
-
img.alt = authorName;
529
529
-
img.style.width = "100%";
530
530
-
img.style.height = "100%";
531
531
-
img.style.borderRadius = "50%";
532
532
-
img.style.objectFit = "cover";
533
533
-
avatar.appendChild(img);
534
534
-
avatar.style.background = "none";
535
535
-
} else {
536
536
-
avatar.textContent = authorInitial;
537
537
-
}
538
538
-
header.appendChild(avatar);
539
539
-
540
540
-
const meta = document.createElement("div");
541
541
-
meta.className = "annotation-item-meta";
542
542
-
543
543
-
const authorEl = document.createElement("div");
544
544
-
authorEl.className = "annotation-item-author";
545
545
-
authorEl.textContent = "@" + authorName;
546
546
-
meta.appendChild(authorEl);
547
547
-
548
548
-
const timeEl = document.createElement("div");
549
549
-
timeEl.className = "annotation-item-time";
550
550
-
timeEl.textContent = formatDate(createdAt);
551
551
-
meta.appendChild(timeEl);
552
552
-
553
553
-
header.appendChild(meta);
554
554
-
555
555
-
if (isHighlight) {
556
556
-
const badge = document.createElement("span");
557
557
-
badge.className = "annotation-type-badge highlight";
558
558
-
badge.textContent = "Highlight";
559
559
-
header.appendChild(badge);
560
560
-
}
561
561
-
562
562
-
el.appendChild(header);
563
563
-
564
564
-
const actions = document.createElement("div");
565
565
-
actions.className = "annotation-item-actions";
566
566
-
567
567
-
if (currentUserDid) {
568
568
-
const folderBtn = document.createElement("button");
569
569
-
folderBtn.className = "btn-icon";
570
570
-
folderBtn.innerHTML =
571
571
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
572
572
-
folderBtn.title = "Add to Collection";
573
573
-
folderBtn.addEventListener("click", (e) => {
574
574
-
e.stopPropagation();
575
575
-
const uri = item.id || item.uri;
576
576
-
openCollectionSelector(uri);
577
577
-
});
578
578
-
actions.appendChild(folderBtn);
579
579
-
}
580
580
-
581
581
-
header.appendChild(actions);
582
582
-
583
583
-
if (quote) {
584
584
-
const quoteEl = document.createElement("div");
585
585
-
quoteEl.className = "annotation-item-quote";
586
586
-
quoteEl.textContent = '"' + quote + '"';
587
587
-
el.appendChild(quoteEl);
588
588
-
}
589
589
-
590
590
-
if (text) {
591
591
-
const textEl = document.createElement("div");
592
592
-
textEl.className = "annotation-item-text";
593
593
-
textEl.textContent = text;
594
594
-
el.appendChild(textEl);
595
595
-
}
596
596
-
597
597
-
els.annotationsList.appendChild(el);
598
598
-
});
599
599
-
}
600
600
-
601
601
-
function renderBookmarks(items) {
602
602
-
els.bookmarksList.innerHTML = "";
603
603
-
604
604
-
if (!items || items.length === 0) {
605
605
-
els.bookmarksEmpty.style.display = "flex";
606
606
-
return;
607
607
-
}
608
608
-
609
609
-
els.bookmarksEmpty.style.display = "none";
610
610
-
items.forEach((item) => {
611
611
-
const el = document.createElement("a");
612
612
-
el.className = "bookmark-item";
613
613
-
el.href = item.source;
614
614
-
el.target = "_blank";
615
615
-
616
616
-
const row = document.createElement("div");
617
617
-
row.style.display = "flex";
618
618
-
row.style.justifyContent = "space-between";
619
619
-
row.style.alignItems = "center";
620
620
-
621
621
-
const content = document.createElement("div");
622
622
-
content.style.flex = "1";
623
623
-
content.style.overflow = "hidden";
624
624
-
625
625
-
const titleEl = document.createElement("div");
626
626
-
titleEl.className = "bookmark-title";
627
627
-
titleEl.textContent = item.title || item.source;
628
628
-
content.appendChild(titleEl);
629
629
-
630
630
-
const urlEl = document.createElement("div");
631
631
-
urlEl.className = "bookmark-url";
632
632
-
urlEl.textContent = new URL(item.source).hostname;
633
633
-
content.appendChild(urlEl);
634
634
-
635
635
-
row.appendChild(content);
636
636
-
637
637
-
if (currentUserDid) {
638
638
-
const folderBtn = document.createElement("button");
639
639
-
folderBtn.className = "btn-icon";
640
640
-
folderBtn.innerHTML =
641
641
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
642
642
-
folderBtn.title = "Add to Collection";
643
643
-
folderBtn.addEventListener("click", (e) => {
644
644
-
e.preventDefault();
645
645
-
e.stopPropagation();
646
646
-
const uri = item.id || item.uri;
647
647
-
openCollectionSelector(uri);
648
648
-
});
649
649
-
row.appendChild(folderBtn);
650
650
-
}
651
651
-
652
652
-
el.appendChild(row);
653
653
-
els.bookmarksList.appendChild(el);
654
654
-
});
655
655
-
}
656
656
-
657
657
-
function renderHighlights(items) {
658
658
-
els.highlightsList.innerHTML = "";
659
659
-
660
660
-
if (!items || items.length === 0) {
661
661
-
els.highlightsEmpty.style.display = "flex";
662
662
-
return;
663
663
-
}
664
664
-
665
665
-
els.highlightsEmpty.style.display = "none";
666
666
-
items.forEach((item) => {
667
667
-
const el = document.createElement("div");
668
668
-
el.className = "annotation-item";
669
669
-
670
670
-
const target = item.target || {};
671
671
-
const selector = target.selector || {};
672
672
-
const quote = selector.exact || "";
673
673
-
const url = target.source || "";
674
674
-
675
675
-
const header = document.createElement("div");
676
676
-
header.className = "annotation-item-header";
677
677
-
678
678
-
const meta = document.createElement("div");
679
679
-
meta.className = "annotation-item-meta";
680
680
-
681
681
-
const authorEl = document.createElement("div");
682
682
-
authorEl.className = "annotation-item-author";
683
683
-
authorEl.textContent = new URL(url).hostname;
684
684
-
meta.appendChild(authorEl);
685
685
-
686
686
-
const timeEl = document.createElement("div");
687
687
-
timeEl.className = "annotation-item-time";
688
688
-
timeEl.textContent = formatDate(item.created);
689
689
-
meta.appendChild(timeEl);
690
690
-
691
691
-
header.appendChild(meta);
692
692
-
693
693
-
const actions = document.createElement("div");
694
694
-
actions.className = "annotation-item-actions";
695
695
-
696
696
-
const folderBtn = document.createElement("button");
697
697
-
folderBtn.className = "btn-icon";
698
698
-
folderBtn.innerHTML =
699
699
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
700
700
-
folderBtn.title = "Add to Collection";
701
701
-
folderBtn.addEventListener("click", (e) => {
702
702
-
e.stopPropagation();
703
703
-
const uri = item.id || item.uri;
704
704
-
openCollectionSelector(uri);
705
705
-
});
706
706
-
actions.appendChild(folderBtn);
707
707
-
708
708
-
header.appendChild(actions);
709
709
-
el.appendChild(header);
710
710
-
711
711
-
if (quote) {
712
712
-
const quoteEl = document.createElement("div");
713
713
-
quoteEl.className = "annotation-item-quote";
714
714
-
quoteEl.style.marginLeft = "0";
715
715
-
quoteEl.style.borderColor = item.color || "#fcd34d";
716
716
-
quoteEl.textContent = '"' + quote + '"';
717
717
-
el.appendChild(quoteEl);
718
718
-
}
719
719
-
720
720
-
el.style.cursor = "pointer";
721
721
-
el.addEventListener("click", () => {
722
722
-
const textFragment = createTextFragment(url, selector);
723
723
-
browserAPI.tabs.create({ url: textFragment });
724
724
-
});
725
725
-
726
726
-
els.highlightsList.appendChild(el);
727
727
-
});
728
728
-
}
729
729
-
730
730
-
function createTextFragment(url, selector) {
731
731
-
if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact)
732
732
-
return url;
733
733
-
734
734
-
let fragment = ":~:text=";
735
735
-
if (selector.prefix) fragment += encodeURIComponent(selector.prefix) + "-,";
736
736
-
fragment += encodeURIComponent(selector.exact);
737
737
-
if (selector.suffix) fragment += ",-" + encodeURIComponent(selector.suffix);
738
738
-
739
739
-
return url + "#" + fragment;
740
740
-
}
741
741
-
742
742
-
function showQuotePreview(selector) {
743
743
-
if (!selector?.exact) return;
744
744
-
745
745
-
let preview = document.getElementById("quote-preview");
746
746
-
if (!preview) {
747
747
-
preview = document.createElement("div");
748
748
-
preview.id = "quote-preview";
749
749
-
preview.className = "quote-preview";
750
750
-
const form = document.getElementById("create-form");
751
751
-
if (form) {
752
752
-
form.insertBefore(preview, els.textInput);
753
753
-
}
754
754
-
}
755
755
-
756
756
-
const header = document.createElement("div");
757
757
-
header.className = "quote-preview-header";
758
758
-
759
759
-
const label = document.createElement("span");
760
760
-
label.textContent = "Annotating:";
761
761
-
header.appendChild(label);
762
762
-
763
763
-
const clearBtn = document.createElement("button");
764
764
-
clearBtn.className = "quote-preview-clear";
765
765
-
clearBtn.title = "Clear";
766
766
-
clearBtn.textContent = "×";
767
767
-
clearBtn.addEventListener("click", () => {
768
768
-
pendingSelector = null;
769
769
-
hideQuotePreview();
770
770
-
});
771
771
-
header.appendChild(clearBtn);
772
772
-
773
773
-
preview.appendChild(header);
774
774
-
775
775
-
const text = document.createElement("div");
776
776
-
text.className = "quote-preview-text";
777
777
-
text.textContent = '"' + selector.exact + '"';
778
778
-
preview.appendChild(text);
779
779
-
780
780
-
els.textInput?.focus();
781
781
-
}
782
782
-
783
783
-
function hideQuotePreview() {
784
784
-
const preview = document.getElementById("quote-preview");
785
785
-
if (preview) preview.remove();
786
786
-
}
787
787
-
788
788
-
function formatDate(dateString) {
789
789
-
if (!dateString) return "";
790
790
-
try {
791
791
-
return new Date(dateString).toLocaleDateString();
792
792
-
} catch {
793
793
-
return dateString;
794
794
-
}
795
795
-
}
796
796
-
797
797
-
function showView(viewName) {
798
798
-
Object.keys(views).forEach((key) => {
799
799
-
if (views[key]) views[key].style.display = "none";
800
800
-
});
801
801
-
if (views[viewName]) {
802
802
-
views[viewName].style.display =
803
803
-
viewName === "loading" || viewName === "settings" ? "flex" : "block";
804
804
-
}
805
805
-
}
806
806
-
807
807
-
function sendMessage(message) {
808
808
-
return new Promise((resolve, reject) => {
809
809
-
browserAPI.runtime.sendMessage(message, (response) => {
810
810
-
if (browserAPI.runtime.lastError) {
811
811
-
reject(browserAPI.runtime.lastError);
812
812
-
} else {
813
813
-
resolve(response);
814
814
-
}
815
815
-
});
816
816
-
});
817
817
-
}
818
818
-
});
819
819
-
820
820
-
function applyTheme(theme) {
821
821
-
document.body.classList.remove("light", "dark");
822
822
-
if (theme === "system") return;
823
823
-
document.body.classList.add(theme);
824
824
-
}
825
825
-
826
826
-
function updateThemeUI(theme) {
827
827
-
const btns = document.querySelectorAll(".theme-btn");
828
828
-
btns.forEach((btn) => {
829
829
-
if (btn.getAttribute("data-theme") === theme) {
830
830
-
btn.classList.add("active");
831
831
-
} else {
832
832
-
btn.classList.remove("active");
833
833
-
}
834
834
-
});
835
835
-
}
+6
extension/postcss.config.js
···
1
1
+
export default {
2
2
+
plugins: {
3
3
+
tailwindcss: {},
4
4
+
autoprefixer: {},
5
5
+
},
6
6
+
};
+1
extension/public/icons/semble-logo.svg
···
1
1
+
<svg width="24" height="24" viewBox="0 0 32 43" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M31.0164 33.1306C31.0164 38.581 25.7882 42.9994 15.8607 42.9994C5.93311 42.9994 0 37.5236 0 32.0732C0 26.6228 5.93311 23.2617 15.8607 23.2617C25.7882 23.2617 31.0164 27.6802 31.0164 33.1306Z" fill="#ff6400"></path><path d="M25.7295 19.3862C25.7295 22.5007 20.7964 22.2058 15.1558 22.2058C9.51511 22.2058 4.93445 22.1482 4.93445 19.0337C4.93445 15.9192 9.71537 12.6895 15.356 12.6895C20.9967 12.6895 25.7295 16.2717 25.7295 19.3862Z" fill="#ff6400"></path><path d="M25.0246 10.9256C25.0246 14.0401 20.7964 11.9829 15.1557 11.9829C9.51506 11.9829 6.34424 13.6876 6.34424 10.5731C6.34424 7.45857 9.51506 5.63867 15.1557 5.63867C20.7964 5.63867 25.0246 7.81103 25.0246 10.9256Z" fill="#ff6400"></path><path d="M20.4426 3.5755C20.4426 5.8323 18.2088 4.22951 15.2288 4.22951C12.2489 4.22951 10.5737 5.8323 10.5737 3.5755C10.5737 1.31871 12.2489 0 15.2288 0C18.2088 0 20.4426 1.31871 20.4426 3.5755Z" fill="#ff6400"></path></svg>
-877
extension/sidepanel/sidepanel.css
···
1
1
-
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap");
2
2
-
3
3
-
:root {
4
4
-
--bg-primary: #0a0a0d;
5
5
-
--bg-secondary: #121216;
6
6
-
--bg-tertiary: #1a1a1f;
7
7
-
--bg-card: #0f0f13;
8
8
-
--bg-elevated: #18181d;
9
9
-
--bg-hover: #1e1e24;
10
10
-
11
11
-
--text-primary: #eaeaee;
12
12
-
--text-secondary: #b7b6c5;
13
13
-
--text-tertiary: #6e6d7a;
14
14
-
15
15
-
--border: rgba(183, 182, 197, 0.12);
16
16
-
--border-hover: rgba(183, 182, 197, 0.2);
17
17
-
18
18
-
--accent: #957a86;
19
19
-
--accent-hover: #a98d98;
20
20
-
--accent-subtle: rgba(149, 122, 134, 0.15);
21
21
-
--accent-text: #c4a8b2;
22
22
-
23
23
-
--success: #7fb069;
24
24
-
--error: #d97766;
25
25
-
--warning: #e8a54b;
26
26
-
27
27
-
--radius-sm: 6px;
28
28
-
--radius-md: 8px;
29
29
-
--radius-lg: 12px;
30
30
-
--radius-full: 9999px;
31
31
-
32
32
-
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
33
33
-
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
34
34
-
}
35
35
-
36
36
-
@media (prefers-color-scheme: light) {
37
37
-
:root {
38
38
-
--bg-primary: #f8f8fa;
39
39
-
--bg-secondary: #ffffff;
40
40
-
--bg-tertiary: #f0f0f4;
41
41
-
--bg-card: #ffffff;
42
42
-
--bg-elevated: #ffffff;
43
43
-
--bg-hover: #eeeef2;
44
44
-
45
45
-
--text-primary: #18171c;
46
46
-
--text-secondary: #5c495a;
47
47
-
--text-tertiary: #8a8494;
48
48
-
49
49
-
--border: rgba(92, 73, 90, 0.12);
50
50
-
--border-hover: rgba(92, 73, 90, 0.22);
51
51
-
52
52
-
--accent: #7a5f6d;
53
53
-
--accent-hover: #664e5b;
54
54
-
--accent-subtle: rgba(149, 122, 134, 0.12);
55
55
-
--accent-text: #5c495a;
56
56
-
57
57
-
--shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
58
58
-
--shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
59
59
-
}
60
60
-
}
61
61
-
62
62
-
body.light {
63
63
-
--bg-primary: #f8f8fa;
64
64
-
--bg-secondary: #ffffff;
65
65
-
--bg-tertiary: #f0f0f4;
66
66
-
--bg-card: #ffffff;
67
67
-
--bg-elevated: #ffffff;
68
68
-
--bg-hover: #eeeef2;
69
69
-
70
70
-
--text-primary: #18171c;
71
71
-
--text-secondary: #5c495a;
72
72
-
--text-tertiary: #8a8494;
73
73
-
74
74
-
--border: rgba(92, 73, 90, 0.12);
75
75
-
--border-hover: rgba(92, 73, 90, 0.22);
76
76
-
77
77
-
--accent: #7a5f6d;
78
78
-
--accent-hover: #664e5b;
79
79
-
--accent-subtle: rgba(149, 122, 134, 0.12);
80
80
-
--accent-text: #5c495a;
81
81
-
82
82
-
--shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
83
83
-
--shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
84
84
-
}
85
85
-
86
86
-
body.dark {
87
87
-
--bg-primary: #0a0a0d;
88
88
-
--bg-secondary: #121216;
89
89
-
--bg-tertiary: #1a1a1f;
90
90
-
--bg-card: #0f0f13;
91
91
-
--bg-elevated: #18181d;
92
92
-
--bg-hover: #1e1e24;
93
93
-
94
94
-
--text-primary: #eaeaee;
95
95
-
--text-secondary: #b7b6c5;
96
96
-
--text-tertiary: #6e6d7a;
97
97
-
98
98
-
--border: rgba(183, 182, 197, 0.12);
99
99
-
--border-hover: rgba(183, 182, 197, 0.2);
100
100
-
101
101
-
--accent: #957a86;
102
102
-
--accent-hover: #a98d98;
103
103
-
--accent-subtle: rgba(149, 122, 134, 0.15);
104
104
-
--accent-text: #c4a8b2;
105
105
-
106
106
-
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
107
107
-
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
108
108
-
}
109
109
-
110
110
-
* {
111
111
-
margin: 0;
112
112
-
padding: 0;
113
113
-
box-sizing: border-box;
114
114
-
}
115
115
-
116
116
-
body {
117
117
-
font-family:
118
118
-
"IBM Plex Sans",
119
119
-
-apple-system,
120
120
-
BlinkMacSystemFont,
121
121
-
sans-serif;
122
122
-
background: var(--bg-primary);
123
123
-
color: var(--text-primary);
124
124
-
min-height: 100vh;
125
125
-
-webkit-font-smoothing: antialiased;
126
126
-
}
127
127
-
128
128
-
.sidebar {
129
129
-
display: flex;
130
130
-
flex-direction: column;
131
131
-
height: 100vh;
132
132
-
background: var(--bg-primary);
133
133
-
}
134
134
-
135
135
-
.sidebar-header {
136
136
-
display: flex;
137
137
-
align-items: center;
138
138
-
justify-content: space-between;
139
139
-
padding: 14px 16px;
140
140
-
border-bottom: 1px solid var(--border);
141
141
-
background: var(--bg-primary);
142
142
-
}
143
143
-
144
144
-
.sidebar-brand {
145
145
-
display: flex;
146
146
-
align-items: center;
147
147
-
gap: 10px;
148
148
-
}
149
149
-
150
150
-
.sidebar-logo {
151
151
-
color: var(--accent);
152
152
-
}
153
153
-
154
154
-
.sidebar-title {
155
155
-
font-weight: 600;
156
156
-
font-size: 15px;
157
157
-
color: var(--text-primary);
158
158
-
letter-spacing: -0.02em;
159
159
-
}
160
160
-
161
161
-
.user-info {
162
162
-
display: flex;
163
163
-
align-items: center;
164
164
-
gap: 8px;
165
165
-
}
166
166
-
167
167
-
.user-handle {
168
168
-
font-size: 12px;
169
169
-
color: var(--text-secondary);
170
170
-
background: var(--bg-tertiary);
171
171
-
padding: 4px 10px;
172
172
-
border-radius: var(--radius-full);
173
173
-
}
174
174
-
175
175
-
.current-page-info {
176
176
-
display: flex;
177
177
-
align-items: center;
178
178
-
gap: 8px;
179
179
-
padding: 10px 16px;
180
180
-
background: var(--bg-primary);
181
181
-
border-bottom: 1px solid var(--border);
182
182
-
}
183
183
-
184
184
-
.page-url {
185
185
-
font-size: 12px;
186
186
-
color: var(--text-tertiary);
187
187
-
white-space: nowrap;
188
188
-
overflow: hidden;
189
189
-
text-overflow: ellipsis;
190
190
-
}
191
191
-
192
192
-
.sidebar-content {
193
193
-
flex: 1;
194
194
-
overflow-y: auto;
195
195
-
display: flex;
196
196
-
flex-direction: column;
197
197
-
}
198
198
-
199
199
-
.loading {
200
200
-
display: flex;
201
201
-
flex-direction: column;
202
202
-
align-items: center;
203
203
-
justify-content: center;
204
204
-
height: 100%;
205
205
-
color: var(--text-tertiary);
206
206
-
gap: 12px;
207
207
-
}
208
208
-
209
209
-
.spinner {
210
210
-
width: 20px;
211
211
-
height: 20px;
212
212
-
border: 2px solid var(--border);
213
213
-
border-top-color: var(--accent);
214
214
-
border-radius: 50%;
215
215
-
animation: spin 1s linear infinite;
216
216
-
}
217
217
-
218
218
-
@keyframes spin {
219
219
-
to {
220
220
-
transform: rotate(360deg);
221
221
-
}
222
222
-
}
223
223
-
224
224
-
.login-prompt {
225
225
-
display: flex;
226
226
-
flex-direction: column;
227
227
-
align-items: center;
228
228
-
justify-content: center;
229
229
-
height: 100%;
230
230
-
padding: 32px;
231
231
-
text-align: center;
232
232
-
gap: 20px;
233
233
-
}
234
234
-
235
235
-
.login-at-logo {
236
236
-
font-size: 3.5rem;
237
237
-
font-weight: 700;
238
238
-
color: var(--accent);
239
239
-
line-height: 1;
240
240
-
}
241
241
-
242
242
-
.login-title {
243
243
-
font-size: 1rem;
244
244
-
font-weight: 600;
245
245
-
color: var(--text-primary);
246
246
-
}
247
247
-
248
248
-
.login-text {
249
249
-
font-size: 13px;
250
250
-
color: var(--text-secondary);
251
251
-
line-height: 1.5;
252
252
-
}
253
253
-
254
254
-
.tabs {
255
255
-
display: flex;
256
256
-
border-bottom: 1px solid var(--border);
257
257
-
background: var(--bg-primary);
258
258
-
padding: 4px 8px;
259
259
-
gap: 4px;
260
260
-
margin: 0;
261
261
-
}
262
262
-
263
263
-
.tab-btn {
264
264
-
flex: 1;
265
265
-
padding: 10px 8px;
266
266
-
background: transparent;
267
267
-
border: none;
268
268
-
font-size: 12px;
269
269
-
font-weight: 500;
270
270
-
color: var(--text-tertiary);
271
271
-
cursor: pointer;
272
272
-
border-radius: var(--radius-sm);
273
273
-
transition: all 0.15s;
274
274
-
}
275
275
-
276
276
-
.tab-btn:hover {
277
277
-
color: var(--text-secondary);
278
278
-
background: var(--bg-hover);
279
279
-
}
280
280
-
281
281
-
.tab-btn.active {
282
282
-
color: var(--text-primary);
283
283
-
background: var(--bg-tertiary);
284
284
-
}
285
285
-
286
286
-
.tab-content {
287
287
-
display: none;
288
288
-
flex: 1;
289
289
-
flex-direction: column;
290
290
-
overflow-y: auto;
291
291
-
}
292
292
-
293
293
-
.tab-content.active {
294
294
-
display: flex;
295
295
-
}
296
296
-
297
297
-
.quick-actions {
298
298
-
display: flex;
299
299
-
gap: 8px;
300
300
-
padding: 12px 16px;
301
301
-
border-bottom: 1px solid var(--border);
302
302
-
background: var(--bg-primary);
303
303
-
}
304
304
-
305
305
-
.btn {
306
306
-
padding: 10px 18px;
307
307
-
border-radius: var(--radius-md);
308
308
-
border: none;
309
309
-
font-weight: 600;
310
310
-
cursor: pointer;
311
311
-
font-size: 13px;
312
312
-
transition: all 0.15s;
313
313
-
display: inline-flex;
314
314
-
align-items: center;
315
315
-
justify-content: center;
316
316
-
gap: 8px;
317
317
-
}
318
318
-
319
319
-
.btn-small {
320
320
-
padding: 8px 14px;
321
321
-
font-size: 12px;
322
322
-
}
323
323
-
324
324
-
.btn-primary {
325
325
-
background: var(--accent);
326
326
-
color: white;
327
327
-
}
328
328
-
329
329
-
.btn-primary:hover {
330
330
-
background: var(--accent-hover);
331
331
-
}
332
332
-
333
333
-
.btn-primary:disabled {
334
334
-
opacity: 0.5;
335
335
-
cursor: not-allowed;
336
336
-
}
337
337
-
338
338
-
.btn-secondary {
339
339
-
background: var(--bg-tertiary);
340
340
-
border: 1px solid var(--border);
341
341
-
color: var(--text-primary);
342
342
-
flex: 1;
343
343
-
}
344
344
-
345
345
-
.btn-secondary:hover {
346
346
-
background: var(--bg-hover);
347
347
-
border-color: var(--border-hover);
348
348
-
}
349
349
-
350
350
-
.btn-icon-text {
351
351
-
flex: 1;
352
352
-
}
353
353
-
354
354
-
.btn-icon {
355
355
-
background: none;
356
356
-
border: none;
357
357
-
color: var(--text-tertiary);
358
358
-
cursor: pointer;
359
359
-
padding: 6px;
360
360
-
border-radius: var(--radius-sm);
361
361
-
}
362
362
-
363
363
-
.btn-icon:hover {
364
364
-
color: var(--text-primary);
365
365
-
background: var(--bg-hover);
366
366
-
}
367
367
-
368
368
-
.create-form {
369
369
-
padding: 16px;
370
370
-
border-bottom: 1px solid var(--border);
371
371
-
background: var(--bg-primary);
372
372
-
}
373
373
-
374
374
-
.form-header {
375
375
-
display: flex;
376
376
-
justify-content: space-between;
377
377
-
align-items: center;
378
378
-
margin-bottom: 10px;
379
379
-
}
380
380
-
381
381
-
.form-title {
382
382
-
font-size: 12px;
383
383
-
font-weight: 600;
384
384
-
color: var(--text-primary);
385
385
-
letter-spacing: -0.01em;
386
386
-
}
387
387
-
388
388
-
.annotation-input {
389
389
-
width: 100%;
390
390
-
padding: 12px;
391
391
-
border: 1px solid var(--border);
392
392
-
border-radius: var(--radius-md);
393
393
-
font-family: inherit;
394
394
-
font-size: 13px;
395
395
-
resize: none;
396
396
-
margin-bottom: 10px;
397
397
-
background: var(--bg-elevated);
398
398
-
color: var(--text-primary);
399
399
-
transition: border-color 0.15s;
400
400
-
}
401
401
-
402
402
-
.annotation-input::placeholder {
403
403
-
color: var(--text-tertiary);
404
404
-
}
405
405
-
406
406
-
.annotation-input:focus {
407
407
-
outline: none;
408
408
-
border-color: var(--accent);
409
409
-
}
410
410
-
411
411
-
.form-actions {
412
412
-
display: flex;
413
413
-
justify-content: flex-end;
414
414
-
}
415
415
-
416
416
-
.quote-preview {
417
417
-
margin-bottom: 12px;
418
418
-
padding: 12px;
419
419
-
background: var(--accent-subtle);
420
420
-
border-left: 2px solid var(--accent);
421
421
-
border-radius: var(--radius-sm);
422
422
-
}
423
423
-
424
424
-
.quote-preview-header {
425
425
-
display: flex;
426
426
-
justify-content: space-between;
427
427
-
align-items: center;
428
428
-
margin-bottom: 8px;
429
429
-
font-size: 10px;
430
430
-
font-weight: 600;
431
431
-
text-transform: uppercase;
432
432
-
letter-spacing: 0.5px;
433
433
-
color: var(--accent-text);
434
434
-
}
435
435
-
436
436
-
.quote-preview-clear {
437
437
-
background: none;
438
438
-
border: none;
439
439
-
color: var(--text-tertiary);
440
440
-
font-size: 16px;
441
441
-
cursor: pointer;
442
442
-
padding: 0 4px;
443
443
-
line-height: 1;
444
444
-
}
445
445
-
446
446
-
.quote-preview-clear:hover {
447
447
-
color: var(--text-primary);
448
448
-
}
449
449
-
450
450
-
.quote-preview-text {
451
451
-
font-size: 12px;
452
452
-
font-style: italic;
453
453
-
color: var(--text-secondary);
454
454
-
line-height: 1.5;
455
455
-
}
456
456
-
457
457
-
.annotations-section {
458
458
-
flex: 1;
459
459
-
}
460
460
-
461
461
-
.section-header {
462
462
-
display: flex;
463
463
-
justify-content: space-between;
464
464
-
align-items: center;
465
465
-
padding: 14px 16px;
466
466
-
background: var(--bg-primary);
467
467
-
border-bottom: 1px solid var(--border);
468
468
-
}
469
469
-
470
470
-
.section-title {
471
471
-
font-size: 11px;
472
472
-
font-weight: 600;
473
473
-
text-transform: uppercase;
474
474
-
color: var(--text-tertiary);
475
475
-
letter-spacing: 0.5px;
476
476
-
}
477
477
-
478
478
-
.annotation-count {
479
479
-
font-size: 11px;
480
480
-
background: var(--bg-tertiary);
481
481
-
padding: 3px 8px;
482
482
-
border-radius: var(--radius-full);
483
483
-
color: var(--text-secondary);
484
484
-
}
485
485
-
486
486
-
.annotations-list {
487
487
-
display: flex;
488
488
-
flex-direction: column;
489
489
-
gap: 1px;
490
490
-
background: var(--border);
491
491
-
}
492
492
-
493
493
-
.annotation-item {
494
494
-
padding: 14px 16px;
495
495
-
background: var(--bg-primary);
496
496
-
transition: background 0.15s;
497
497
-
}
498
498
-
499
499
-
.annotation-item:hover {
500
500
-
background: var(--bg-hover);
501
501
-
}
502
502
-
503
503
-
.annotation-item-header {
504
504
-
display: flex;
505
505
-
align-items: center;
506
506
-
margin-bottom: 8px;
507
507
-
gap: 10px;
508
508
-
}
509
509
-
510
510
-
.annotation-item-avatar {
511
511
-
width: 26px;
512
512
-
height: 26px;
513
513
-
border-radius: 50%;
514
514
-
background: var(--accent);
515
515
-
color: var(--bg-primary);
516
516
-
display: flex;
517
517
-
align-items: center;
518
518
-
justify-content: center;
519
519
-
font-size: 10px;
520
520
-
font-weight: 600;
521
521
-
}
522
522
-
523
523
-
.annotation-item-meta {
524
524
-
flex: 1;
525
525
-
}
526
526
-
527
527
-
.annotation-item-author {
528
528
-
font-size: 12px;
529
529
-
font-weight: 600;
530
530
-
color: var(--text-primary);
531
531
-
}
532
532
-
533
533
-
.annotation-item-time {
534
534
-
font-size: 11px;
535
535
-
color: var(--text-tertiary);
536
536
-
}
537
537
-
538
538
-
.annotation-type-badge {
539
539
-
font-size: 10px;
540
540
-
padding: 3px 8px;
541
541
-
border-radius: var(--radius-full);
542
542
-
font-weight: 500;
543
543
-
}
544
544
-
545
545
-
.annotation-type-badge.highlight {
546
546
-
background: var(--accent-subtle);
547
547
-
color: var(--accent-text);
548
548
-
}
549
549
-
550
550
-
.annotation-item-quote {
551
551
-
padding: 8px 12px;
552
552
-
border-left: 2px solid var(--accent);
553
553
-
margin-bottom: 8px;
554
554
-
font-size: 12px;
555
555
-
color: var(--text-secondary);
556
556
-
font-style: italic;
557
557
-
background: var(--accent-subtle);
558
558
-
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
559
559
-
}
560
560
-
561
561
-
.annotation-item-text {
562
562
-
font-size: 13px;
563
563
-
line-height: 1.5;
564
564
-
color: var(--text-primary);
565
565
-
}
566
566
-
567
567
-
.bookmarks-list {
568
568
-
display: flex;
569
569
-
flex-direction: column;
570
570
-
gap: 1px;
571
571
-
background: var(--border);
572
572
-
}
573
573
-
574
574
-
.bookmark-item {
575
575
-
padding: 14px 16px;
576
576
-
background: var(--bg-primary);
577
577
-
text-decoration: none;
578
578
-
color: inherit;
579
579
-
display: block;
580
580
-
transition: background 0.15s;
581
581
-
}
582
582
-
583
583
-
.bookmark-item:hover {
584
584
-
background: var(--bg-hover);
585
585
-
}
586
586
-
587
587
-
.bookmark-title {
588
588
-
font-size: 13px;
589
589
-
font-weight: 500;
590
590
-
margin-bottom: 4px;
591
591
-
white-space: nowrap;
592
592
-
overflow: hidden;
593
593
-
text-overflow: ellipsis;
594
594
-
color: var(--text-primary);
595
595
-
}
596
596
-
597
597
-
.bookmark-url {
598
598
-
font-size: 11px;
599
599
-
color: var(--text-tertiary);
600
600
-
white-space: nowrap;
601
601
-
overflow: hidden;
602
602
-
text-overflow: ellipsis;
603
603
-
}
604
604
-
605
605
-
.empty-state {
606
606
-
display: flex;
607
607
-
flex-direction: column;
608
608
-
align-items: center;
609
609
-
justify-content: center;
610
610
-
padding: 40px 16px;
611
611
-
text-align: center;
612
612
-
color: var(--text-tertiary);
613
613
-
}
614
614
-
615
615
-
.empty-icon {
616
616
-
margin-bottom: 12px;
617
617
-
color: var(--text-tertiary);
618
618
-
opacity: 0.4;
619
619
-
}
620
620
-
621
621
-
.empty-text {
622
622
-
font-size: 13px;
623
623
-
color: var(--text-secondary);
624
624
-
margin-bottom: 4px;
625
625
-
}
626
626
-
627
627
-
.empty-hint {
628
628
-
font-size: 12px;
629
629
-
color: var(--text-tertiary);
630
630
-
}
631
631
-
632
632
-
.sidebar-footer {
633
633
-
display: flex;
634
634
-
align-items: center;
635
635
-
justify-content: space-between;
636
636
-
padding: 12px 16px;
637
637
-
border-top: 1px solid var(--border);
638
638
-
background: var(--bg-primary);
639
639
-
}
640
640
-
641
641
-
.sidebar-link {
642
642
-
font-size: 12px;
643
643
-
color: var(--text-tertiary);
644
644
-
text-decoration: none;
645
645
-
}
646
646
-
647
647
-
.sidebar-link:hover {
648
648
-
color: var(--accent-text);
649
649
-
}
650
650
-
651
651
-
.settings-view {
652
652
-
position: absolute;
653
653
-
top: 0;
654
654
-
left: 0;
655
655
-
width: 100%;
656
656
-
height: 100%;
657
657
-
background: var(--bg-primary);
658
658
-
z-index: 20;
659
659
-
display: flex;
660
660
-
flex-direction: column;
661
661
-
padding: 16px;
662
662
-
}
663
663
-
664
664
-
.settings-header {
665
665
-
display: flex;
666
666
-
justify-content: space-between;
667
667
-
align-items: center;
668
668
-
margin-bottom: 24px;
669
669
-
color: var(--text-primary);
670
670
-
}
671
671
-
672
672
-
.settings-title {
673
673
-
font-size: 18px;
674
674
-
font-weight: 600;
675
675
-
}
676
676
-
677
677
-
.setting-item {
678
678
-
margin-bottom: 20px;
679
679
-
}
680
680
-
681
681
-
.setting-item label {
682
682
-
font-size: 13px;
683
683
-
font-weight: 500;
684
684
-
color: var(--text-primary);
685
685
-
margin-bottom: 6px;
686
686
-
display: block;
687
687
-
}
688
688
-
689
689
-
.settings-input {
690
690
-
width: 100%;
691
691
-
padding: 12px;
692
692
-
border: 1px solid var(--border);
693
693
-
border-radius: var(--radius-md);
694
694
-
font-family: inherit;
695
695
-
font-size: 13px;
696
696
-
background: var(--bg-elevated);
697
697
-
color: var(--text-primary);
698
698
-
transition: border-color 0.15s;
699
699
-
}
700
700
-
701
701
-
.settings-input:focus {
702
702
-
outline: none;
703
703
-
border-color: var(--accent);
704
704
-
}
705
705
-
706
706
-
.setting-help {
707
707
-
font-size: 11px;
708
708
-
color: var(--text-tertiary);
709
709
-
margin-top: 4px;
710
710
-
}
711
711
-
712
712
-
.scroll-to-btn {
713
713
-
display: inline-flex;
714
714
-
align-items: center;
715
715
-
gap: 4px;
716
716
-
padding: 6px 10px;
717
717
-
font-size: 11px;
718
718
-
color: var(--accent-text);
719
719
-
background: var(--accent-subtle);
720
720
-
border: none;
721
721
-
border-radius: var(--radius-sm);
722
722
-
cursor: pointer;
723
723
-
margin-top: 8px;
724
724
-
transition: all 0.15s;
725
725
-
}
726
726
-
727
727
-
.scroll-to-btn:hover {
728
728
-
background: rgba(149, 122, 134, 0.25);
729
729
-
}
730
730
-
731
731
-
::-webkit-scrollbar {
732
732
-
width: 8px;
733
733
-
}
734
734
-
735
735
-
::-webkit-scrollbar-track {
736
736
-
background: transparent;
737
737
-
}
738
738
-
739
739
-
::-webkit-scrollbar-thumb {
740
740
-
background: var(--bg-hover);
741
741
-
border-radius: var(--radius-full);
742
742
-
}
743
743
-
744
744
-
::-webkit-scrollbar-thumb:hover {
745
745
-
background: var(--text-tertiary);
746
746
-
}
747
747
-
748
748
-
.collection-selector {
749
749
-
position: absolute;
750
750
-
top: 0;
751
751
-
left: 0;
752
752
-
width: 100%;
753
753
-
height: 100%;
754
754
-
background: var(--bg-primary);
755
755
-
z-index: 30;
756
756
-
display: flex;
757
757
-
flex-direction: column;
758
758
-
padding: 16px;
759
759
-
}
760
760
-
761
761
-
.collection-list {
762
762
-
display: flex;
763
763
-
flex-direction: column;
764
764
-
gap: 8px;
765
765
-
overflow-y: auto;
766
766
-
flex: 1;
767
767
-
}
768
768
-
769
769
-
.collection-select-btn {
770
770
-
display: flex;
771
771
-
align-items: center;
772
772
-
gap: 12px;
773
773
-
padding: 12px;
774
774
-
background: var(--bg-primary);
775
775
-
border: 1px solid var(--border);
776
776
-
border-radius: var(--radius-md);
777
777
-
color: var(--text-primary);
778
778
-
font-size: 14px;
779
779
-
cursor: pointer;
780
780
-
text-align: left;
781
781
-
transition: all 0.15s;
782
782
-
}
783
783
-
784
784
-
.collection-select-btn:hover {
785
785
-
border-color: var(--accent);
786
786
-
background: var(--bg-hover);
787
787
-
}
788
788
-
789
789
-
.collection-select-btn:disabled {
790
790
-
opacity: 0.6;
791
791
-
cursor: not-allowed;
792
792
-
}
793
793
-
794
794
-
.annotation-item-actions {
795
795
-
display: flex;
796
796
-
align-items: center;
797
797
-
gap: 8px;
798
798
-
margin-left: auto;
799
799
-
}
800
800
-
801
801
-
.toggle-switch {
802
802
-
position: relative;
803
803
-
display: inline-block;
804
804
-
width: 40px;
805
805
-
height: 22px;
806
806
-
flex-shrink: 0;
807
807
-
}
808
808
-
809
809
-
.toggle-switch input {
810
810
-
opacity: 0;
811
811
-
width: 0;
812
812
-
height: 0;
813
813
-
}
814
814
-
815
815
-
.toggle-slider {
816
816
-
position: absolute;
817
817
-
cursor: pointer;
818
818
-
top: 0;
819
819
-
left: 0;
820
820
-
right: 0;
821
821
-
bottom: 0;
822
822
-
background-color: var(--bg-tertiary);
823
823
-
transition: 0.2s;
824
824
-
border-radius: 22px;
825
825
-
}
826
826
-
827
827
-
.toggle-slider:before {
828
828
-
position: absolute;
829
829
-
content: "";
830
830
-
height: 16px;
831
831
-
width: 16px;
832
832
-
left: 3px;
833
833
-
bottom: 3px;
834
834
-
background-color: var(--text-tertiary);
835
835
-
transition: 0.2s;
836
836
-
border-radius: 50%;
837
837
-
}
838
838
-
839
839
-
.toggle-switch input:checked + .toggle-slider {
840
840
-
background-color: var(--accent);
841
841
-
}
842
842
-
843
843
-
.toggle-switch input:checked + .toggle-slider:before {
844
844
-
transform: translateX(18px);
845
845
-
background-color: white;
846
846
-
}
847
847
-
848
848
-
.theme-toggle-group {
849
849
-
display: flex;
850
850
-
background: var(--bg-tertiary);
851
851
-
padding: 3px;
852
852
-
border-radius: var(--radius-md);
853
853
-
gap: 2px;
854
854
-
margin-top: 8px;
855
855
-
}
856
856
-
857
857
-
.theme-btn {
858
858
-
flex: 1;
859
859
-
padding: 6px;
860
860
-
border: none;
861
861
-
background: transparent;
862
862
-
color: var(--text-tertiary);
863
863
-
font-size: 12px;
864
864
-
font-weight: 500;
865
865
-
border-radius: var(--radius-sm);
866
866
-
cursor: pointer;
867
867
-
transition: all 0.15s ease;
868
868
-
}
869
869
-
870
870
-
.theme-btn:hover {
871
871
-
color: var(--text-secondary);
872
872
-
}
873
873
-
874
874
-
.theme-btn.active {
875
875
-
background: var(--bg-primary);
876
876
-
color: var(--text-primary);
877
877
-
}
-347
extension/sidepanel/sidepanel.html
···
1
1
-
<!doctype html>
2
2
-
<html lang="en">
3
3
-
<head>
4
4
-
<meta charset="UTF-8" />
5
5
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
-
<title>Margin</title>
7
7
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
8
8
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
-
<link
10
10
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
11
11
-
rel="stylesheet"
12
12
-
/>
13
13
-
<link rel="stylesheet" href="sidepanel.css" />
14
14
-
</head>
15
15
-
16
16
-
<body>
17
17
-
<div class="sidebar">
18
18
-
<header class="sidebar-header">
19
19
-
<div class="sidebar-brand">
20
20
-
<span class="sidebar-logo">
21
21
-
<img src="../icons/logo.svg" alt="Margin" width="20" height="20" />
22
22
-
</span>
23
23
-
<span class="sidebar-title">Margin</span>
24
24
-
</div>
25
25
-
<div id="user-info" class="user-info" style="display: none">
26
26
-
<span id="user-handle" class="user-handle"></span>
27
27
-
</div>
28
28
-
</header>
29
29
-
30
30
-
<div id="current-page-info" class="current-page-info">
31
31
-
<div class="page-favicon"></div>
32
32
-
<span id="current-page-url" class="page-url">Loading...</span>
33
33
-
</div>
34
34
-
35
35
-
<div class="sidebar-content">
36
36
-
<div id="loading" class="loading">
37
37
-
<div class="spinner"></div>
38
38
-
<span>Loading...</span>
39
39
-
</div>
40
40
-
41
41
-
<div id="login-prompt" class="login-prompt" style="display: none">
42
42
-
<span class="login-at-logo">@</span>
43
43
-
<h2 class="login-title">Sign in with AT Protocol</h2>
44
44
-
<p class="login-text">
45
45
-
Connect your Bluesky account to annotate, highlight, and bookmark
46
46
-
the web.
47
47
-
</p>
48
48
-
<button id="sign-in" class="btn btn-primary">Sign In</button>
49
49
-
</div>
50
50
-
51
51
-
<div id="main-content" style="display: none">
52
52
-
<div class="tabs">
53
53
-
<button class="tab-btn active" data-tab="page">This Page</button>
54
54
-
<button class="tab-btn" data-tab="highlights">Highlights</button>
55
55
-
<button class="tab-btn" data-tab="bookmarks">Bookmarks</button>
56
56
-
</div>
57
57
-
58
58
-
<div id="tab-page" class="tab-content active">
59
59
-
<div class="quick-actions">
60
60
-
<button
61
61
-
id="bookmark-page"
62
62
-
class="btn btn-secondary btn-icon-text"
63
63
-
title="Bookmark this page"
64
64
-
>
65
65
-
<svg
66
66
-
width="14"
67
67
-
height="14"
68
68
-
viewBox="0 0 24 24"
69
69
-
fill="none"
70
70
-
stroke="currentColor"
71
71
-
stroke-width="2"
72
72
-
stroke-linecap="round"
73
73
-
stroke-linejoin="round"
74
74
-
>
75
75
-
<path
76
76
-
d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"
77
77
-
></path>
78
78
-
</svg>
79
79
-
Bookmark
80
80
-
</button>
81
81
-
<button
82
82
-
id="refresh-annotations"
83
83
-
class="btn btn-secondary btn-icon-text"
84
84
-
title="Refresh"
85
85
-
>
86
86
-
<svg
87
87
-
width="14"
88
88
-
height="14"
89
89
-
viewBox="0 0 24 24"
90
90
-
fill="none"
91
91
-
stroke="currentColor"
92
92
-
stroke-width="2"
93
93
-
stroke-linecap="round"
94
94
-
stroke-linejoin="round"
95
95
-
>
96
96
-
<path
97
97
-
d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"
98
98
-
></path>
99
99
-
<path d="M3 3v5h5"></path>
100
100
-
<path
101
101
-
d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"
102
102
-
></path>
103
103
-
<path d="M16 21h5v-5"></path>
104
104
-
</svg>
105
105
-
Refresh
106
106
-
</button>
107
107
-
</div>
108
108
-
109
109
-
<div class="create-form">
110
110
-
<div class="form-header">
111
111
-
<span class="form-title">New Annotation</span>
112
112
-
</div>
113
113
-
<textarea
114
114
-
id="annotation-text"
115
115
-
class="annotation-input"
116
116
-
placeholder="Write your annotation..."
117
117
-
rows="3"
118
118
-
></textarea>
119
119
-
<div class="form-actions">
120
120
-
<button
121
121
-
id="submit-annotation"
122
122
-
class="btn btn-primary btn-small"
123
123
-
>
124
124
-
Post
125
125
-
</button>
126
126
-
</div>
127
127
-
</div>
128
128
-
129
129
-
<div class="annotations-section">
130
130
-
<div class="section-header">
131
131
-
<span class="section-title">Annotations on this page</span>
132
132
-
<span id="annotation-count" class="annotation-count">0</span>
133
133
-
</div>
134
134
-
<div id="annotations" class="annotations-list"></div>
135
135
-
<div id="empty" class="empty-state" style="display: none">
136
136
-
<span class="empty-icon">
137
137
-
<svg
138
138
-
width="32"
139
139
-
height="32"
140
140
-
viewBox="0 0 24 24"
141
141
-
fill="none"
142
142
-
stroke="currentColor"
143
143
-
stroke-width="2"
144
144
-
stroke-linecap="round"
145
145
-
stroke-linejoin="round"
146
146
-
>
147
147
-
<path d="M22 12h-6l-2 3h-4l-2-3H2"></path>
148
148
-
<path
149
149
-
d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"
150
150
-
></path>
151
151
-
</svg>
152
152
-
</span>
153
153
-
<p class="empty-text">No annotations yet</p>
154
154
-
<p class="empty-hint">
155
155
-
Select text and right-click to annotate or highlight
156
156
-
</p>
157
157
-
</div>
158
158
-
</div>
159
159
-
</div>
160
160
-
161
161
-
<div id="tab-highlights" class="tab-content">
162
162
-
<div class="section-header">
163
163
-
<span class="section-title">My Highlights</span>
164
164
-
</div>
165
165
-
<div id="highlights-list" class="annotations-list"></div>
166
166
-
<div
167
167
-
id="highlights-empty"
168
168
-
class="empty-state"
169
169
-
style="display: none"
170
170
-
>
171
171
-
<span class="empty-icon">
172
172
-
<svg
173
173
-
width="32"
174
174
-
height="32"
175
175
-
viewBox="0 0 24 24"
176
176
-
fill="none"
177
177
-
stroke="currentColor"
178
178
-
stroke-width="2"
179
179
-
stroke-linecap="round"
180
180
-
stroke-linejoin="round"
181
181
-
>
182
182
-
<path d="m9 11-6 6v3h9l3-3"></path>
183
183
-
<path
184
184
-
d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"
185
185
-
></path>
186
186
-
</svg>
187
187
-
</span>
188
188
-
<p class="empty-text">No highlights yet</p>
189
189
-
<p class="empty-hint">
190
190
-
Select text on any page and right-click → Highlight
191
191
-
</p>
192
192
-
</div>
193
193
-
</div>
194
194
-
195
195
-
<div id="tab-bookmarks" class="tab-content">
196
196
-
<div class="section-header">
197
197
-
<span class="section-title">My Bookmarks</span>
198
198
-
</div>
199
199
-
<div id="bookmarks-list" class="bookmarks-list"></div>
200
200
-
<div id="bookmarks-empty" class="empty-state" style="display: none">
201
201
-
<span class="empty-icon">
202
202
-
<svg
203
203
-
width="32"
204
204
-
height="32"
205
205
-
viewBox="0 0 24 24"
206
206
-
fill="none"
207
207
-
stroke="currentColor"
208
208
-
stroke-width="2"
209
209
-
stroke-linecap="round"
210
210
-
stroke-linejoin="round"
211
211
-
>
212
212
-
<path
213
213
-
d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"
214
214
-
></path>
215
215
-
</svg>
216
216
-
</span>
217
217
-
<p class="empty-text">No bookmarks yet</p>
218
218
-
<p class="empty-hint">
219
219
-
Right-click on any page → Bookmark this page
220
220
-
</p>
221
221
-
</div>
222
222
-
</div>
223
223
-
</div>
224
224
-
</div>
225
225
-
226
226
-
<footer class="sidebar-footer">
227
227
-
<a href="#" id="open-web" class="sidebar-link">Open Margin Web</a>
228
228
-
<button id="toggle-settings" class="btn-icon" title="Settings">
229
229
-
<svg
230
230
-
width="16"
231
231
-
height="16"
232
232
-
viewBox="0 0 24 24"
233
233
-
fill="none"
234
234
-
stroke="currentColor"
235
235
-
stroke-width="2"
236
236
-
stroke-linecap="round"
237
237
-
stroke-linejoin="round"
238
238
-
>
239
239
-
<circle cx="12" cy="12" r="3"></circle>
240
240
-
<path
241
241
-
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
242
242
-
></path>
243
243
-
</svg>
244
244
-
</button>
245
245
-
</footer>
246
246
-
247
247
-
<div id="settings-view" class="settings-view" style="display: none">
248
248
-
<div class="settings-header">
249
249
-
<h3 class="settings-title">Settings</h3>
250
250
-
<button id="close-settings" class="btn-icon">×</button>
251
251
-
</div>
252
252
-
<div class="setting-item">
253
253
-
<div
254
254
-
style="
255
255
-
display: flex;
256
256
-
justify-content: space-between;
257
257
-
align-items: center;
258
258
-
"
259
259
-
>
260
260
-
<div>
261
261
-
<label>Show page overlays</label>
262
262
-
<p class="setting-help" style="margin-top: 2px">
263
263
-
Display highlights, badges, and tooltips on pages
264
264
-
</p>
265
265
-
</div>
266
266
-
<label class="toggle-switch">
267
267
-
<input type="checkbox" id="overlay-toggle" checked />
268
268
-
<span class="toggle-slider"></span>
269
269
-
</label>
270
270
-
</div>
271
271
-
</div>
272
272
-
<div class="setting-item">
273
273
-
<label for="api-url">API URL</label>
274
274
-
<input
275
275
-
type="url"
276
276
-
id="api-url"
277
277
-
placeholder="http://localhost:8080"
278
278
-
class="settings-input"
279
279
-
/>
280
280
-
<p class="setting-help">Enter your Margin backend URL</p>
281
281
-
</div>
282
282
-
<div class="setting-item">
283
283
-
<label for="theme-select">Theme</label>
284
284
-
<div class="theme-toggle-group">
285
285
-
<button class="theme-btn active" data-theme="system">Auto</button>
286
286
-
<button class="theme-btn" data-theme="light">Light</button>
287
287
-
<button class="theme-btn" data-theme="dark">Dark</button>
288
288
-
</div>
289
289
-
</div>
290
290
-
<button id="save-settings" class="btn btn-primary" style="width: 100%">
291
291
-
Save
292
292
-
</button>
293
293
-
<button
294
294
-
id="sign-out"
295
295
-
class="btn btn-secondary"
296
296
-
style="width: 100%; margin-top: 8px"
297
297
-
>
298
298
-
Sign Out
299
299
-
</button>
300
300
-
</div>
301
301
-
302
302
-
<div
303
303
-
id="collection-selector"
304
304
-
class="collection-selector"
305
305
-
style="display: none"
306
306
-
>
307
307
-
<div
308
308
-
class="selector-header"
309
309
-
style="
310
310
-
display: flex;
311
311
-
justify-content: space-between;
312
312
-
align-items: center;
313
313
-
margin-bottom: 12px;
314
314
-
padding-bottom: 8px;
315
315
-
border-bottom: 1px solid var(--border);
316
316
-
"
317
317
-
>
318
318
-
<h3 class="selector-title" style="margin: 0; font-size: 14px">
319
319
-
Add to Collection
320
320
-
</h3>
321
321
-
<button id="close-collection-selector" class="btn-icon">×</button>
322
322
-
</div>
323
323
-
<div
324
324
-
id="collection-loading"
325
325
-
class="spinner"
326
326
-
style="display: none; margin: 20px auto"
327
327
-
></div>
328
328
-
<div
329
329
-
id="collection-list"
330
330
-
class="collection-list"
331
331
-
style="
332
332
-
display: flex;
333
333
-
flex-direction: column;
334
334
-
gap: 4px;
335
335
-
max-height: 200px;
336
336
-
overflow-y: auto;
337
337
-
"
338
338
-
></div>
339
339
-
<div id="collections-empty" class="empty-state" style="display: none">
340
340
-
<p class="empty-text">No collections found</p>
341
341
-
</div>
342
342
-
</div>
343
343
-
</div>
344
344
-
345
345
-
<script src="sidepanel.js"></script>
346
346
-
</body>
347
347
-
</html>
-973
extension/sidepanel/sidepanel.js
···
1
1
-
document.addEventListener("DOMContentLoaded", async () => {
2
2
-
const views = {
3
3
-
loading: document.getElementById("loading"),
4
4
-
login: document.getElementById("login-prompt"),
5
5
-
main: document.getElementById("main-content"),
6
6
-
settings: document.getElementById("settings-view"),
7
7
-
collectionSelector: document.getElementById("collection-selector"),
8
8
-
};
9
9
-
10
10
-
const els = {
11
11
-
userHandle: document.getElementById("user-handle"),
12
12
-
userInfo: document.getElementById("user-info"),
13
13
-
signInBtn: document.getElementById("sign-in"),
14
14
-
openWebBtn: document.getElementById("open-web"),
15
15
-
submitBtn: document.getElementById("submit-annotation"),
16
16
-
textInput: document.getElementById("annotation-text"),
17
17
-
currentPageUrl: document.getElementById("current-page-url"),
18
18
-
annotationsList: document.getElementById("annotations"),
19
19
-
annotationCount: document.getElementById("annotation-count"),
20
20
-
emptyState: document.getElementById("empty"),
21
21
-
toggleSettings: document.getElementById("toggle-settings"),
22
22
-
closeSettings: document.getElementById("close-settings"),
23
23
-
saveSettings: document.getElementById("save-settings"),
24
24
-
signOutBtn: document.getElementById("sign-out"),
25
25
-
apiUrlInput: document.getElementById("api-url"),
26
26
-
bookmarkBtn: document.getElementById("bookmark-page"),
27
27
-
refreshBtn: document.getElementById("refresh-annotations"),
28
28
-
tabs: document.querySelectorAll(".tab-btn"),
29
29
-
tabContents: document.querySelectorAll(".tab-content"),
30
30
-
bookmarksList: document.getElementById("bookmarks-list"),
31
31
-
bookmarksEmpty: document.getElementById("bookmarks-empty"),
32
32
-
highlightsList: document.getElementById("highlights-list"),
33
33
-
highlightsEmpty: document.getElementById("highlights-empty"),
34
34
-
closeCollectionSelector: document.getElementById(
35
35
-
"close-collection-selector",
36
36
-
),
37
37
-
collectionList: document.getElementById("collection-list"),
38
38
-
collectionLoading: document.getElementById("collection-loading"),
39
39
-
collectionsEmpty: document.getElementById("collections-empty"),
40
40
-
overlayToggle: document.getElementById("overlay-toggle"),
41
41
-
};
42
42
-
43
43
-
let currentTab = null;
44
44
-
let apiUrl = "";
45
45
-
let currentUserDid = null;
46
46
-
let pendingSelector = null;
47
47
-
48
48
-
const storage = await chrome.storage.local.get(["apiUrl"]);
49
49
-
if (storage.apiUrl) {
50
50
-
apiUrl = storage.apiUrl;
51
51
-
}
52
52
-
53
53
-
els.apiUrlInput.value = apiUrl;
54
54
-
55
55
-
const overlayStorage = await chrome.storage.local.get(["showOverlay"]);
56
56
-
if (els.overlayToggle) {
57
57
-
els.overlayToggle.checked = overlayStorage.showOverlay !== false;
58
58
-
}
59
59
-
60
60
-
chrome.storage.onChanged.addListener((changes, area) => {
61
61
-
if (area === "local") {
62
62
-
if (changes.apiUrl) {
63
63
-
apiUrl = changes.apiUrl.newValue || "";
64
64
-
els.apiUrlInput.value = apiUrl;
65
65
-
checkSession();
66
66
-
}
67
67
-
if (changes.theme) {
68
68
-
const newTheme = changes.theme.newValue || "system";
69
69
-
applyTheme(newTheme);
70
70
-
updateThemeUI(newTheme);
71
71
-
}
72
72
-
}
73
73
-
});
74
74
-
75
75
-
chrome.storage.local.get(["theme"], (result) => {
76
76
-
const currentTheme = result.theme || "system";
77
77
-
applyTheme(currentTheme);
78
78
-
updateThemeUI(currentTheme);
79
79
-
});
80
80
-
81
81
-
const themeBtns = document.querySelectorAll(".theme-btn");
82
82
-
themeBtns.forEach((btn) => {
83
83
-
btn.addEventListener("click", () => {
84
84
-
const theme = btn.getAttribute("data-theme");
85
85
-
chrome.storage.local.set({ theme });
86
86
-
applyTheme(theme);
87
87
-
updateThemeUI(theme);
88
88
-
});
89
89
-
});
90
90
-
91
91
-
try {
92
92
-
const [tab] = await chrome.tabs.query({
93
93
-
active: true,
94
94
-
currentWindow: true,
95
95
-
});
96
96
-
currentTab = tab;
97
97
-
if (els.currentPageUrl) {
98
98
-
try {
99
99
-
els.currentPageUrl.textContent = new URL(tab.url).hostname;
100
100
-
} catch {
101
101
-
els.currentPageUrl.textContent = tab.url;
102
102
-
}
103
103
-
}
104
104
-
105
105
-
let pendingData = null;
106
106
-
if (chrome.storage.session) {
107
107
-
const sessionData = await chrome.storage.session.get([
108
108
-
"pendingAnnotation",
109
109
-
]);
110
110
-
if (sessionData.pendingAnnotation) {
111
111
-
pendingData = sessionData.pendingAnnotation;
112
112
-
await chrome.storage.session.remove(["pendingAnnotation"]);
113
113
-
}
114
114
-
}
115
115
-
116
116
-
if (!pendingData) {
117
117
-
const localData = await chrome.storage.local.get([
118
118
-
"pendingAnnotation",
119
119
-
"pendingAnnotationExpiry",
120
120
-
]);
121
121
-
if (
122
122
-
localData.pendingAnnotation &&
123
123
-
localData.pendingAnnotationExpiry > Date.now()
124
124
-
) {
125
125
-
pendingData = localData.pendingAnnotation;
126
126
-
}
127
127
-
await chrome.storage.local.remove([
128
128
-
"pendingAnnotation",
129
129
-
"pendingAnnotationExpiry",
130
130
-
]);
131
131
-
}
132
132
-
133
133
-
if (pendingData?.selector) {
134
134
-
pendingSelector = pendingData.selector;
135
135
-
showQuotePreview(pendingSelector);
136
136
-
}
137
137
-
138
138
-
checkSession();
139
139
-
} catch (err) {
140
140
-
console.error("Init error:", err);
141
141
-
showView("login");
142
142
-
}
143
143
-
144
144
-
chrome.tabs.onActivated.addListener(async (activeInfo) => {
145
145
-
try {
146
146
-
const tab = await chrome.tabs.get(activeInfo.tabId);
147
147
-
currentTab = tab;
148
148
-
if (els.currentPageUrl) {
149
149
-
try {
150
150
-
els.currentPageUrl.textContent = new URL(tab.url).hostname;
151
151
-
} catch {
152
152
-
els.currentPageUrl.textContent = tab.url;
153
153
-
}
154
154
-
}
155
155
-
loadAnnotations();
156
156
-
} catch (err) {
157
157
-
console.error("Tab change error:", err);
158
158
-
}
159
159
-
});
160
160
-
161
161
-
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
162
162
-
if (currentTab && tabId === currentTab.id && changeInfo.url) {
163
163
-
currentTab = tab;
164
164
-
if (els.currentPageUrl) {
165
165
-
try {
166
166
-
els.currentPageUrl.textContent = new URL(tab.url).hostname;
167
167
-
} catch {
168
168
-
els.currentPageUrl.textContent = tab.url;
169
169
-
}
170
170
-
}
171
171
-
loadAnnotations();
172
172
-
}
173
173
-
});
174
174
-
175
175
-
els.signInBtn?.addEventListener("click", () => {
176
176
-
chrome.runtime.sendMessage({ type: "OPEN_LOGIN" });
177
177
-
});
178
178
-
179
179
-
els.openWebBtn?.addEventListener("click", (e) => {
180
180
-
e.preventDefault();
181
181
-
chrome.runtime.sendMessage({ type: "OPEN_WEB" });
182
182
-
});
183
183
-
184
184
-
els.tabs.forEach((btn) => {
185
185
-
btn.addEventListener("click", () => {
186
186
-
els.tabs.forEach((t) => t.classList.remove("active"));
187
187
-
els.tabContents.forEach((c) => c.classList.remove("active"));
188
188
-
const tabId = btn.getAttribute("data-tab");
189
189
-
btn.classList.add("active");
190
190
-
document.getElementById(`tab-${tabId}`).classList.add("active");
191
191
-
if (tabId === "bookmarks") loadBookmarks();
192
192
-
if (tabId === "highlights") loadHighlights();
193
193
-
});
194
194
-
});
195
195
-
196
196
-
els.submitBtn?.addEventListener("click", async () => {
197
197
-
const text = els.textInput.value.trim();
198
198
-
if (!text) return;
199
199
-
200
200
-
els.submitBtn.disabled = true;
201
201
-
els.submitBtn.textContent = "Posting...";
202
202
-
203
203
-
try {
204
204
-
const annotationData = {
205
205
-
url: currentTab.url,
206
206
-
text: text,
207
207
-
title: currentTab.title,
208
208
-
};
209
209
-
210
210
-
if (pendingSelector) {
211
211
-
annotationData.selector = pendingSelector;
212
212
-
}
213
213
-
214
214
-
const res = await sendMessage({
215
215
-
type: "CREATE_ANNOTATION",
216
216
-
data: annotationData,
217
217
-
});
218
218
-
219
219
-
if (res.success) {
220
220
-
els.textInput.value = "";
221
221
-
pendingSelector = null;
222
222
-
hideQuotePreview();
223
223
-
loadAnnotations();
224
224
-
} else {
225
225
-
alert("Failed to post annotation: " + (res.error || "Unknown error"));
226
226
-
}
227
227
-
} catch (err) {
228
228
-
console.error("Post error:", err);
229
229
-
alert("Error posting annotation");
230
230
-
} finally {
231
231
-
els.submitBtn.disabled = false;
232
232
-
els.submitBtn.textContent = "Post";
233
233
-
}
234
234
-
});
235
235
-
236
236
-
els.bookmarkBtn?.addEventListener("click", async () => {
237
237
-
els.bookmarkBtn.disabled = true;
238
238
-
const originalText = els.bookmarkBtn.textContent;
239
239
-
els.bookmarkBtn.textContent = "Saving...";
240
240
-
241
241
-
try {
242
242
-
const res = await sendMessage({
243
243
-
type: "CREATE_BOOKMARK",
244
244
-
data: {
245
245
-
url: currentTab.url,
246
246
-
title: currentTab.title,
247
247
-
},
248
248
-
});
249
249
-
250
250
-
if (res.success) {
251
251
-
els.bookmarkBtn.textContent = "✓ Bookmarked";
252
252
-
setTimeout(() => {
253
253
-
els.bookmarkBtn.textContent = originalText;
254
254
-
els.bookmarkBtn.disabled = false;
255
255
-
}, 2000);
256
256
-
} else {
257
257
-
alert("Failed to bookmark page: " + (res.error || "Unknown error"));
258
258
-
els.bookmarkBtn.textContent = originalText;
259
259
-
els.bookmarkBtn.disabled = false;
260
260
-
}
261
261
-
} catch (err) {
262
262
-
console.error("Bookmark error:", err);
263
263
-
alert("Error bookmarking page");
264
264
-
els.bookmarkBtn.textContent = originalText;
265
265
-
els.bookmarkBtn.disabled = false;
266
266
-
}
267
267
-
});
268
268
-
269
269
-
els.refreshBtn?.addEventListener("click", () => {
270
270
-
loadAnnotations();
271
271
-
});
272
272
-
273
273
-
els.toggleSettings?.addEventListener("click", () => {
274
274
-
views.settings.style.display = "flex";
275
275
-
});
276
276
-
277
277
-
els.closeSettings?.addEventListener("click", () => {
278
278
-
views.settings.style.display = "none";
279
279
-
});
280
280
-
281
281
-
els.closeCollectionSelector?.addEventListener("click", () => {
282
282
-
views.collectionSelector.style.display = "none";
283
283
-
});
284
284
-
285
285
-
els.saveSettings?.addEventListener("click", async () => {
286
286
-
const newUrl = els.apiUrlInput.value.replace(/\/$/, "");
287
287
-
const showOverlay = els.overlayToggle?.checked ?? true;
288
288
-
289
289
-
await chrome.storage.local.set({
290
290
-
apiUrl: newUrl,
291
291
-
showOverlay,
292
292
-
});
293
293
-
if (newUrl) {
294
294
-
apiUrl = newUrl;
295
295
-
}
296
296
-
await sendMessage({ type: "UPDATE_SETTINGS" });
297
297
-
298
298
-
const tabs = await chrome.tabs.query({});
299
299
-
for (const tab of tabs) {
300
300
-
if (tab.id) {
301
301
-
try {
302
302
-
await chrome.tabs.sendMessage(tab.id, {
303
303
-
type: "UPDATE_OVERLAY_VISIBILITY",
304
304
-
show: showOverlay,
305
305
-
});
306
306
-
} catch {
307
307
-
/* ignore */
308
308
-
}
309
309
-
}
310
310
-
}
311
311
-
312
312
-
views.settings.style.display = "none";
313
313
-
checkSession();
314
314
-
});
315
315
-
316
316
-
els.signOutBtn?.addEventListener("click", async () => {
317
317
-
if (apiUrl) {
318
318
-
await chrome.cookies.remove({
319
319
-
url: apiUrl,
320
320
-
name: "margin_session",
321
321
-
});
322
322
-
}
323
323
-
views.settings.style.display = "none";
324
324
-
showView("login");
325
325
-
els.userInfo.style.display = "none";
326
326
-
});
327
327
-
328
328
-
async function checkSession() {
329
329
-
showView("loading");
330
330
-
try {
331
331
-
const res = await sendMessage({ type: "CHECK_SESSION" });
332
332
-
333
333
-
if (res.success && res.data?.authenticated) {
334
334
-
if (els.userHandle) els.userHandle.textContent = "@" + res.data.handle;
335
335
-
els.userInfo.style.display = "flex";
336
336
-
currentUserDid = res.data.did;
337
337
-
showView("main");
338
338
-
loadAnnotations();
339
339
-
} else {
340
340
-
els.userInfo.style.display = "none";
341
341
-
showView("login");
342
342
-
}
343
343
-
} catch (err) {
344
344
-
console.error("Session check error:", err);
345
345
-
els.userInfo.style.display = "none";
346
346
-
showView("login");
347
347
-
}
348
348
-
}
349
349
-
350
350
-
async function loadAnnotations() {
351
351
-
if (!currentTab?.url) return;
352
352
-
353
353
-
try {
354
354
-
const res = await sendMessage({
355
355
-
type: "GET_ANNOTATIONS",
356
356
-
data: { url: currentTab.url },
357
357
-
});
358
358
-
359
359
-
if (res.success) {
360
360
-
if (currentUserDid) {
361
361
-
const isBookmarked = res.data.some(
362
362
-
(item) =>
363
363
-
item.type === "Bookmark" && item.creator.did === currentUserDid,
364
364
-
);
365
365
-
if (els.bookmarkBtn) {
366
366
-
if (isBookmarked) {
367
367
-
els.bookmarkBtn.textContent = "✓ Bookmarked";
368
368
-
els.bookmarkBtn.disabled = true;
369
369
-
} else {
370
370
-
els.bookmarkBtn.textContent = "Bookmark Page";
371
371
-
els.bookmarkBtn.disabled = false;
372
372
-
}
373
373
-
}
374
374
-
}
375
375
-
376
376
-
const listItems = res.data.filter((item) => item.type !== "Bookmark");
377
377
-
renderAnnotations(listItems);
378
378
-
}
379
379
-
} catch (err) {
380
380
-
console.error("Load annotations error:", err);
381
381
-
}
382
382
-
}
383
383
-
384
384
-
async function loadBookmarks() {
385
385
-
if (!currentUserDid) return;
386
386
-
els.bookmarksList.innerHTML =
387
387
-
'<div class="loading"><div class="spinner"></div></div>';
388
388
-
els.bookmarksEmpty.style.display = "none";
389
389
-
390
390
-
try {
391
391
-
const res = await sendMessage({
392
392
-
type: "GET_USER_BOOKMARKS",
393
393
-
data: { did: currentUserDid },
394
394
-
});
395
395
-
396
396
-
if (res.success) {
397
397
-
renderBookmarks(res.data);
398
398
-
}
399
399
-
} catch (err) {
400
400
-
console.error("Load bookmarks error:", err);
401
401
-
els.bookmarksList.innerHTML =
402
402
-
'<p style="color: #ef4444; text-align: center;">Failed to load bookmarks</p>';
403
403
-
}
404
404
-
}
405
405
-
406
406
-
async function loadHighlights() {
407
407
-
if (!currentUserDid) return;
408
408
-
els.highlightsList.innerHTML =
409
409
-
'<div class="loading"><div class="spinner"></div></div>';
410
410
-
els.highlightsEmpty.style.display = "none";
411
411
-
412
412
-
try {
413
413
-
const res = await sendMessage({
414
414
-
type: "GET_USER_HIGHLIGHTS",
415
415
-
data: { did: currentUserDid },
416
416
-
});
417
417
-
418
418
-
if (res.success) {
419
419
-
renderHighlights(res.data);
420
420
-
}
421
421
-
} catch (err) {
422
422
-
console.error("Load highlights error:", err);
423
423
-
els.highlightsList.innerHTML =
424
424
-
'<p style="color: #ef4444; text-align: center;">Failed to load highlights</p>';
425
425
-
}
426
426
-
}
427
427
-
428
428
-
async function openCollectionSelector(annotationUri) {
429
429
-
if (!currentUserDid) {
430
430
-
console.error("No currentUserDid, returning early");
431
431
-
return;
432
432
-
}
433
433
-
views.collectionSelector.style.display = "flex";
434
434
-
els.collectionList.innerHTML = "";
435
435
-
els.collectionLoading.style.display = "block";
436
436
-
els.collectionsEmpty.style.display = "none";
437
437
-
438
438
-
try {
439
439
-
const [collectionsRes, containingRes] = await Promise.all([
440
440
-
sendMessage({
441
441
-
type: "GET_USER_COLLECTIONS",
442
442
-
data: { did: currentUserDid },
443
443
-
}),
444
444
-
sendMessage({
445
445
-
type: "GET_CONTAINING_COLLECTIONS",
446
446
-
data: { uri: annotationUri },
447
447
-
}),
448
448
-
]);
449
449
-
450
450
-
if (collectionsRes.success) {
451
451
-
const containingUris = containingRes.success
452
452
-
? new Set(containingRes.data)
453
453
-
: new Set();
454
454
-
renderCollectionList(
455
455
-
collectionsRes.data,
456
456
-
annotationUri,
457
457
-
containingUris,
458
458
-
);
459
459
-
}
460
460
-
} catch (err) {
461
461
-
console.error("Load collections error:", err);
462
462
-
els.collectionList.innerHTML =
463
463
-
'<p class="error">Failed to load collections</p>';
464
464
-
} finally {
465
465
-
els.collectionLoading.style.display = "none";
466
466
-
}
467
467
-
}
468
468
-
469
469
-
function renderCollectionList(
470
470
-
items,
471
471
-
annotationUri,
472
472
-
containingUris = new Set(),
473
473
-
) {
474
474
-
els.collectionList.innerHTML = "";
475
475
-
els.collectionList.dataset.annotationUri = annotationUri;
476
476
-
477
477
-
if (!items || items.length === 0) {
478
478
-
els.collectionsEmpty.style.display = "block";
479
479
-
return;
480
480
-
}
481
481
-
482
482
-
items.forEach((collection) => {
483
483
-
const btn = document.createElement("button");
484
484
-
btn.className = "collection-select-btn";
485
485
-
const isAdded = containingUris.has(collection.uri);
486
486
-
487
487
-
const icon = document.createElement("span");
488
488
-
if (isAdded) {
489
489
-
icon.textContent = "✓";
490
490
-
} else {
491
491
-
icon.innerHTML =
492
492
-
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
493
493
-
}
494
494
-
btn.appendChild(icon);
495
495
-
496
496
-
const name = document.createElement("span");
497
497
-
name.textContent = collection.name;
498
498
-
btn.appendChild(name);
499
499
-
500
500
-
if (isAdded) {
501
501
-
btn.classList.add("added");
502
502
-
btn.disabled = true;
503
503
-
}
504
504
-
505
505
-
btn.addEventListener("click", async () => {
506
506
-
if (btn.disabled) return;
507
507
-
const annUri = els.collectionList.dataset.annotationUri;
508
508
-
await handleAddToCollection(collection.uri, btn, annUri);
509
509
-
});
510
510
-
511
511
-
els.collectionList.appendChild(btn);
512
512
-
});
513
513
-
}
514
514
-
515
515
-
async function handleAddToCollection(
516
516
-
collectionUri,
517
517
-
btnElement,
518
518
-
annotationUri,
519
519
-
) {
520
520
-
if (!annotationUri) {
521
521
-
console.error("No annotationUri provided!");
522
522
-
alert("Error: No item selected to add");
523
523
-
return;
524
524
-
}
525
525
-
526
526
-
const originalText = btnElement.textContent;
527
527
-
btnElement.disabled = true;
528
528
-
btnElement.textContent = "Adding...";
529
529
-
530
530
-
try {
531
531
-
const res = await sendMessage({
532
532
-
type: "ADD_TO_COLLECTION",
533
533
-
data: {
534
534
-
collectionUri: collectionUri,
535
535
-
annotationUri: annotationUri,
536
536
-
},
537
537
-
});
538
538
-
539
539
-
if (res && res.success) {
540
540
-
btnElement.textContent = "✓ Added";
541
541
-
setTimeout(() => {
542
542
-
btnElement.textContent = originalText;
543
543
-
btnElement.disabled = false;
544
544
-
}, 2000);
545
545
-
} else {
546
546
-
alert(
547
547
-
"Failed to add to collection: " + (res?.error || "Unknown error"),
548
548
-
);
549
549
-
btnElement.textContent = originalText;
550
550
-
btnElement.disabled = false;
551
551
-
}
552
552
-
} catch (err) {
553
553
-
console.error("Add to collection error:", err);
554
554
-
alert("Error adding to collection: " + err.message);
555
555
-
btnElement.textContent = originalText;
556
556
-
btnElement.disabled = false;
557
557
-
}
558
558
-
}
559
559
-
560
560
-
function renderAnnotations(items) {
561
561
-
els.annotationsList.innerHTML = "";
562
562
-
els.annotationCount.textContent = items?.length || 0;
563
563
-
564
564
-
if (!items || items.length === 0) {
565
565
-
els.emptyState.style.display = "flex";
566
566
-
return;
567
567
-
}
568
568
-
569
569
-
els.emptyState.style.display = "none";
570
570
-
items.forEach((item) => {
571
571
-
const el = document.createElement("div");
572
572
-
el.className = "annotation-item";
573
573
-
574
574
-
const author = item.creator || item.author || {};
575
575
-
const authorName = author.handle || author.displayName || "Unknown";
576
576
-
const authorInitial = authorName[0]?.toUpperCase() || "?";
577
577
-
const createdAt = item.created || item.createdAt;
578
578
-
const text = item.body?.value || item.text || "";
579
579
-
const selector = item.target?.selector;
580
580
-
const isHighlight = item.type === "Highlight";
581
581
-
const quote = selector?.exact || "";
582
582
-
583
583
-
const header = document.createElement("div");
584
584
-
header.className = "annotation-item-header";
585
585
-
586
586
-
const avatar = document.createElement("div");
587
587
-
avatar.className = "annotation-item-avatar";
588
588
-
589
589
-
if (author.avatar) {
590
590
-
const img = document.createElement("img");
591
591
-
img.src = author.avatar;
592
592
-
img.alt = authorName;
593
593
-
img.style.width = "100%";
594
594
-
img.style.height = "100%";
595
595
-
img.style.borderRadius = "50%";
596
596
-
img.style.objectFit = "cover";
597
597
-
avatar.appendChild(img);
598
598
-
avatar.style.background = "none";
599
599
-
} else {
600
600
-
avatar.textContent = authorInitial;
601
601
-
}
602
602
-
header.appendChild(avatar);
603
603
-
604
604
-
const meta = document.createElement("div");
605
605
-
meta.className = "annotation-item-meta";
606
606
-
607
607
-
const authorEl = document.createElement("div");
608
608
-
authorEl.className = "annotation-item-author";
609
609
-
authorEl.textContent = "@" + authorName;
610
610
-
meta.appendChild(authorEl);
611
611
-
612
612
-
const timeEl = document.createElement("div");
613
613
-
timeEl.className = "annotation-item-time";
614
614
-
timeEl.textContent = formatDate(createdAt);
615
615
-
meta.appendChild(timeEl);
616
616
-
617
617
-
header.appendChild(meta);
618
618
-
619
619
-
if (isHighlight) {
620
620
-
const badge = document.createElement("span");
621
621
-
badge.className = "annotation-type-badge highlight";
622
622
-
badge.textContent = "Highlight";
623
623
-
header.appendChild(badge);
624
624
-
}
625
625
-
626
626
-
if (currentUserDid) {
627
627
-
const actions = document.createElement("div");
628
628
-
actions.className = "annotation-item-actions";
629
629
-
630
630
-
const folderBtn = document.createElement("button");
631
631
-
folderBtn.className = "btn-icon";
632
632
-
folderBtn.innerHTML =
633
633
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
634
634
-
folderBtn.title = "Add to Collection";
635
635
-
folderBtn.addEventListener("click", (e) => {
636
636
-
e.stopPropagation();
637
637
-
openCollectionSelector(item.uri);
638
638
-
});
639
639
-
actions.appendChild(folderBtn);
640
640
-
header.appendChild(actions);
641
641
-
}
642
642
-
643
643
-
el.appendChild(header);
644
644
-
645
645
-
if (quote) {
646
646
-
const quoteEl = document.createElement("div");
647
647
-
quoteEl.className = "annotation-item-quote";
648
648
-
quoteEl.textContent = '"' + quote + '"';
649
649
-
el.appendChild(quoteEl);
650
650
-
}
651
651
-
652
652
-
if (text) {
653
653
-
const textEl = document.createElement("div");
654
654
-
textEl.className = "annotation-item-text";
655
655
-
textEl.textContent = text;
656
656
-
el.appendChild(textEl);
657
657
-
}
658
658
-
659
659
-
if (selector) {
660
660
-
const jumpBtn = document.createElement("button");
661
661
-
jumpBtn.className = "scroll-to-btn";
662
662
-
jumpBtn.textContent = "Jump to text →";
663
663
-
el.appendChild(jumpBtn);
664
664
-
}
665
665
-
666
666
-
if (selector) {
667
667
-
el.querySelector(".scroll-to-btn")?.addEventListener("click", (e) => {
668
668
-
e.stopPropagation();
669
669
-
scrollToText(selector);
670
670
-
});
671
671
-
}
672
672
-
673
673
-
els.annotationsList.appendChild(el);
674
674
-
});
675
675
-
}
676
676
-
677
677
-
function renderBookmarks(items) {
678
678
-
els.bookmarksList.innerHTML = "";
679
679
-
680
680
-
if (!items || items.length === 0) {
681
681
-
els.bookmarksEmpty.style.display = "flex";
682
682
-
return;
683
683
-
}
684
684
-
685
685
-
els.bookmarksEmpty.style.display = "none";
686
686
-
items.forEach((item) => {
687
687
-
const el = document.createElement("div");
688
688
-
el.className = "bookmark-item";
689
689
-
el.style.cursor = "pointer";
690
690
-
el.addEventListener("click", () => {
691
691
-
window.open(item.source, "_blank");
692
692
-
});
693
693
-
694
694
-
let hostname = item.source;
695
695
-
try {
696
696
-
hostname = new URL(item.source).hostname;
697
697
-
} catch {
698
698
-
/* ignore */
699
699
-
}
700
700
-
701
701
-
const row = document.createElement("div");
702
702
-
row.style.display = "flex";
703
703
-
row.style.justifyContent = "space-between";
704
704
-
row.style.alignItems = "center";
705
705
-
706
706
-
const content = document.createElement("div");
707
707
-
content.style.flex = "1";
708
708
-
content.style.overflow = "hidden";
709
709
-
710
710
-
const titleEl = document.createElement("div");
711
711
-
titleEl.className = "bookmark-title";
712
712
-
titleEl.textContent = item.title || item.source;
713
713
-
content.appendChild(titleEl);
714
714
-
715
715
-
const urlEl = document.createElement("div");
716
716
-
urlEl.className = "bookmark-url";
717
717
-
urlEl.textContent = hostname;
718
718
-
content.appendChild(urlEl);
719
719
-
720
720
-
row.appendChild(content);
721
721
-
722
722
-
if (currentUserDid) {
723
723
-
const folderBtn = document.createElement("button");
724
724
-
folderBtn.className = "btn-icon";
725
725
-
folderBtn.innerHTML =
726
726
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
727
727
-
folderBtn.title = "Add to Collection";
728
728
-
folderBtn.addEventListener("click", (e) => {
729
729
-
e.preventDefault();
730
730
-
e.stopPropagation();
731
731
-
openCollectionSelector(item.uri);
732
732
-
});
733
733
-
row.appendChild(folderBtn);
734
734
-
}
735
735
-
736
736
-
el.appendChild(row);
737
737
-
els.bookmarksList.appendChild(el);
738
738
-
});
739
739
-
}
740
740
-
741
741
-
function renderHighlights(items) {
742
742
-
els.highlightsList.innerHTML = "";
743
743
-
744
744
-
if (!items || items.length === 0) {
745
745
-
els.highlightsEmpty.style.display = "flex";
746
746
-
return;
747
747
-
}
748
748
-
749
749
-
els.highlightsEmpty.style.display = "none";
750
750
-
items.forEach((item) => {
751
751
-
const el = document.createElement("div");
752
752
-
el.className = "annotation-item";
753
753
-
754
754
-
const target = item.target || {};
755
755
-
const selector = target.selector || {};
756
756
-
const quote = selector.exact || "";
757
757
-
const url = target.source || "";
758
758
-
759
759
-
let hostname = url;
760
760
-
try {
761
761
-
hostname = new URL(url).hostname;
762
762
-
} catch {
763
763
-
/* ignore */
764
764
-
}
765
765
-
766
766
-
const header = document.createElement("div");
767
767
-
header.className = "annotation-item-header";
768
768
-
769
769
-
const meta = document.createElement("div");
770
770
-
meta.className = "annotation-item-meta";
771
771
-
772
772
-
const authorEl = document.createElement("div");
773
773
-
authorEl.className = "annotation-item-author";
774
774
-
authorEl.textContent = hostname;
775
775
-
meta.appendChild(authorEl);
776
776
-
777
777
-
const timeEl = document.createElement("div");
778
778
-
timeEl.className = "annotation-item-time";
779
779
-
timeEl.textContent = formatDate(item.created);
780
780
-
meta.appendChild(timeEl);
781
781
-
782
782
-
header.appendChild(meta);
783
783
-
784
784
-
if (currentUserDid) {
785
785
-
const actions = document.createElement("div");
786
786
-
actions.className = "annotation-item-actions";
787
787
-
788
788
-
const folderBtn = document.createElement("button");
789
789
-
folderBtn.className = "btn-icon";
790
790
-
folderBtn.innerHTML =
791
791
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>';
792
792
-
folderBtn.title = "Add to Collection";
793
793
-
folderBtn.addEventListener("click", (e) => {
794
794
-
e.stopPropagation();
795
795
-
openCollectionSelector(item.uri);
796
796
-
});
797
797
-
actions.appendChild(folderBtn);
798
798
-
header.appendChild(actions);
799
799
-
}
800
800
-
801
801
-
el.appendChild(header);
802
802
-
803
803
-
if (quote) {
804
804
-
const quoteEl = document.createElement("div");
805
805
-
quoteEl.className = "annotation-item-quote";
806
806
-
quoteEl.style.borderColor = item.color || "#fcd34d";
807
807
-
quoteEl.textContent = '"' + quote + '"';
808
808
-
el.appendChild(quoteEl);
809
809
-
}
810
810
-
811
811
-
const openBtn = document.createElement("button");
812
812
-
openBtn.className = "scroll-to-btn";
813
813
-
openBtn.textContent = "Open page →";
814
814
-
el.appendChild(openBtn);
815
815
-
816
816
-
el.querySelector(".scroll-to-btn")?.addEventListener("click", (e) => {
817
817
-
e.stopPropagation();
818
818
-
const textFragment = createTextFragment(url, selector);
819
819
-
chrome.tabs.create({ url: textFragment });
820
820
-
});
821
821
-
822
822
-
els.highlightsList.appendChild(el);
823
823
-
});
824
824
-
}
825
825
-
826
826
-
async function scrollToText(selector) {
827
827
-
let tabId = currentTab?.id;
828
828
-
if (!tabId) {
829
829
-
try {
830
830
-
const [tab] = await chrome.tabs.query({
831
831
-
active: true,
832
832
-
currentWindow: true,
833
833
-
});
834
834
-
tabId = tab?.id;
835
835
-
} catch (e) {
836
836
-
console.error("Could not get active tab:", e);
837
837
-
}
838
838
-
}
839
839
-
840
840
-
if (!tabId) {
841
841
-
console.error("No tab ID available for scroll");
842
842
-
return;
843
843
-
}
844
844
-
845
845
-
try {
846
846
-
await chrome.tabs.sendMessage(tabId, {
847
847
-
type: "SCROLL_TO_TEXT",
848
848
-
selector: selector,
849
849
-
});
850
850
-
} catch (err) {
851
851
-
console.error("Error sending SCROLL_TO_TEXT:", err);
852
852
-
}
853
853
-
}
854
854
-
855
855
-
function createTextFragment(url, selector) {
856
856
-
if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact)
857
857
-
return url;
858
858
-
859
859
-
let fragment = ":~:text=";
860
860
-
if (selector.prefix) fragment += encodeURIComponent(selector.prefix) + "-,";
861
861
-
fragment += encodeURIComponent(selector.exact);
862
862
-
if (selector.suffix) fragment += ",-" + encodeURIComponent(selector.suffix);
863
863
-
864
864
-
return url + "#" + fragment;
865
865
-
}
866
866
-
867
867
-
function formatDate(dateString) {
868
868
-
if (!dateString) return "";
869
869
-
try {
870
870
-
const date = new Date(dateString);
871
871
-
const now = new Date();
872
872
-
const diffMs = now - date;
873
873
-
const diffMins = Math.floor(diffMs / 60000);
874
874
-
const diffHours = Math.floor(diffMs / 3600000);
875
875
-
const diffDays = Math.floor(diffMs / 86400000);
876
876
-
877
877
-
if (diffMins < 1) return "Just now";
878
878
-
if (diffMins < 60) return `${diffMins}m ago`;
879
879
-
if (diffHours < 24) return `${diffHours}h ago`;
880
880
-
if (diffDays < 7) return `${diffDays}d ago`;
881
881
-
return date.toLocaleDateString();
882
882
-
} catch {
883
883
-
return dateString;
884
884
-
}
885
885
-
}
886
886
-
887
887
-
function showQuotePreview(selector) {
888
888
-
if (!selector?.exact) return;
889
889
-
890
890
-
let preview = document.getElementById("quote-preview");
891
891
-
if (!preview) {
892
892
-
preview = document.createElement("div");
893
893
-
preview.id = "quote-preview";
894
894
-
preview.className = "quote-preview";
895
895
-
const form = document.querySelector(".create-form");
896
896
-
if (form) {
897
897
-
form.insertBefore(preview, form.querySelector(".annotation-input"));
898
898
-
}
899
899
-
}
900
900
-
901
901
-
const header = document.createElement("div");
902
902
-
header.className = "quote-preview-header";
903
903
-
904
904
-
const label = document.createElement("span");
905
905
-
label.textContent = "Annotating selection:";
906
906
-
header.appendChild(label);
907
907
-
908
908
-
const clearBtn = document.createElement("button");
909
909
-
clearBtn.className = "quote-preview-clear";
910
910
-
clearBtn.title = "Clear selection";
911
911
-
clearBtn.textContent = "×";
912
912
-
clearBtn.addEventListener("click", () => {
913
913
-
pendingSelector = null;
914
914
-
hideQuotePreview();
915
915
-
});
916
916
-
header.appendChild(clearBtn);
917
917
-
918
918
-
preview.appendChild(header);
919
919
-
920
920
-
const text = document.createElement("div");
921
921
-
text.className = "quote-preview-text";
922
922
-
text.textContent = '"' + selector.exact + '"';
923
923
-
preview.appendChild(text);
924
924
-
925
925
-
els.textInput?.focus();
926
926
-
}
927
927
-
928
928
-
function hideQuotePreview() {
929
929
-
const preview = document.getElementById("quote-preview");
930
930
-
if (preview) {
931
931
-
preview.remove();
932
932
-
}
933
933
-
}
934
934
-
935
935
-
function showView(viewName) {
936
936
-
Object.keys(views).forEach((key) => {
937
937
-
if (views[key]) views[key].style.display = "none";
938
938
-
});
939
939
-
if (views[viewName]) {
940
940
-
views[viewName].style.display =
941
941
-
viewName === "loading" || viewName === "settings" ? "flex" : "block";
942
942
-
}
943
943
-
}
944
944
-
945
945
-
function sendMessage(message) {
946
946
-
return new Promise((resolve, reject) => {
947
947
-
chrome.runtime.sendMessage(message, (response) => {
948
948
-
if (chrome.runtime.lastError) {
949
949
-
reject(chrome.runtime.lastError);
950
950
-
} else {
951
951
-
resolve(response);
952
952
-
}
953
953
-
});
954
954
-
});
955
955
-
}
956
956
-
957
957
-
function applyTheme(theme) {
958
958
-
document.body.classList.remove("light", "dark");
959
959
-
if (theme === "system") return;
960
960
-
document.body.classList.add(theme);
961
961
-
}
962
962
-
963
963
-
function updateThemeUI(theme) {
964
964
-
const btns = document.querySelectorAll(".theme-btn");
965
965
-
btns.forEach((btn) => {
966
966
-
if (btn.getAttribute("data-theme") === theme) {
967
967
-
btn.classList.add("active");
968
968
-
} else {
969
969
-
btn.classList.remove("active");
970
970
-
}
971
971
-
});
972
972
-
}
973
973
-
});
+161
extension/src/assets/styles.css
···
1
1
+
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap');
2
2
+
3
3
+
@tailwind base;
4
4
+
@tailwind components;
5
5
+
@tailwind utilities;
6
6
+
7
7
+
:root {
8
8
+
--bg-primary: #0a0a0d;
9
9
+
--bg-secondary: #121216;
10
10
+
--bg-tertiary: #1a1a1f;
11
11
+
--bg-card: #0f0f13;
12
12
+
--bg-elevated: #18181d;
13
13
+
--bg-hover: #1e1e24;
14
14
+
--text-primary: #eaeaee;
15
15
+
--text-secondary: #b7b6c5;
16
16
+
--text-tertiary: #6e6d7a;
17
17
+
--border: rgba(183, 182, 197, 0.12);
18
18
+
--border-strong: rgba(183, 182, 197, 0.2);
19
19
+
--accent: #957a86;
20
20
+
--accent-hover: #a98d98;
21
21
+
--accent-subtle: rgba(149, 122, 134, 0.15);
22
22
+
--success: #34d399;
23
23
+
--warning: #fbbf24;
24
24
+
}
25
25
+
26
26
+
.light {
27
27
+
--bg-primary: #f8f8fa;
28
28
+
--bg-secondary: #ffffff;
29
29
+
--bg-tertiary: #f0f0f4;
30
30
+
--bg-card: #ffffff;
31
31
+
--bg-elevated: #ffffff;
32
32
+
--bg-hover: #eeeef2;
33
33
+
--text-primary: #18171c;
34
34
+
--text-secondary: #5c495a;
35
35
+
--text-tertiary: #8a8494;
36
36
+
--border: rgba(92, 73, 90, 0.12);
37
37
+
--border-strong: rgba(92, 73, 90, 0.2);
38
38
+
--accent: #7a5f6d;
39
39
+
--accent-hover: #664e5b;
40
40
+
--accent-subtle: rgba(149, 122, 134, 0.12);
41
41
+
}
42
42
+
43
43
+
body {
44
44
+
background: var(--bg-primary);
45
45
+
color: var(--text-primary);
46
46
+
font-family:
47
47
+
'IBM Plex Sans',
48
48
+
-apple-system,
49
49
+
BlinkMacSystemFont,
50
50
+
sans-serif;
51
51
+
margin: 0;
52
52
+
padding: 0;
53
53
+
min-width: 320px;
54
54
+
max-width: 100%;
55
55
+
min-height: 100vh;
56
56
+
overflow: hidden;
57
57
+
-webkit-font-smoothing: antialiased;
58
58
+
-moz-osx-font-smoothing: grayscale;
59
59
+
}
60
60
+
61
61
+
html.popup body {
62
62
+
width: 380px;
63
63
+
height: 520px;
64
64
+
min-height: 520px;
65
65
+
}
66
66
+
67
67
+
* {
68
68
+
box-sizing: border-box;
69
69
+
}
70
70
+
71
71
+
::-webkit-scrollbar {
72
72
+
width: 6px;
73
73
+
}
74
74
+
75
75
+
::-webkit-scrollbar-track {
76
76
+
background: transparent;
77
77
+
}
78
78
+
79
79
+
::-webkit-scrollbar-thumb {
80
80
+
background: var(--bg-tertiary);
81
81
+
border-radius: 3px;
82
82
+
}
83
83
+
84
84
+
::-webkit-scrollbar-thumb:hover {
85
85
+
background: var(--text-tertiary);
86
86
+
}
87
87
+
88
88
+
button,
89
89
+
a,
90
90
+
input,
91
91
+
textarea {
92
92
+
transition: all 0.15s ease;
93
93
+
}
94
94
+
95
95
+
input:focus,
96
96
+
textarea:focus,
97
97
+
button:focus-visible {
98
98
+
outline: none;
99
99
+
box-shadow:
100
100
+
0 0 0 2px var(--accent-subtle),
101
101
+
0 0 0 4px var(--accent);
102
102
+
}
103
103
+
104
104
+
@keyframes fadeIn {
105
105
+
from {
106
106
+
opacity: 0;
107
107
+
transform: translateY(-4px);
108
108
+
}
109
109
+
to {
110
110
+
opacity: 1;
111
111
+
transform: translateY(0);
112
112
+
}
113
113
+
}
114
114
+
115
115
+
@keyframes slideUp {
116
116
+
from {
117
117
+
opacity: 0;
118
118
+
transform: translateY(8px);
119
119
+
}
120
120
+
to {
121
121
+
opacity: 1;
122
122
+
transform: translateY(0);
123
123
+
}
124
124
+
}
125
125
+
126
126
+
@keyframes slideDown {
127
127
+
from {
128
128
+
opacity: 0;
129
129
+
transform: translateY(-8px);
130
130
+
}
131
131
+
to {
132
132
+
opacity: 1;
133
133
+
transform: translateY(0);
134
134
+
}
135
135
+
}
136
136
+
137
137
+
@keyframes pulse {
138
138
+
0%,
139
139
+
100% {
140
140
+
opacity: 1;
141
141
+
}
142
142
+
50% {
143
143
+
opacity: 0.5;
144
144
+
}
145
145
+
}
146
146
+
147
147
+
.animate-fadeIn {
148
148
+
animation: fadeIn 0.2s ease forwards;
149
149
+
}
150
150
+
151
151
+
.animate-slideUp {
152
152
+
animation: slideUp 0.25s ease forwards;
153
153
+
}
154
154
+
155
155
+
.animate-slideDown {
156
156
+
animation: slideDown 0.2s ease forwards;
157
157
+
}
158
158
+
159
159
+
.animate-pulse {
160
160
+
animation: pulse 1.5s ease-in-out infinite;
161
161
+
}
+124
extension/src/components/CollectionIcon.tsx
···
1
1
+
import {
2
2
+
Folder,
3
3
+
Star,
4
4
+
Heart,
5
5
+
Bookmark,
6
6
+
Lightbulb,
7
7
+
Zap,
8
8
+
Coffee,
9
9
+
Music,
10
10
+
Camera,
11
11
+
Code,
12
12
+
Globe,
13
13
+
Flag,
14
14
+
Tag,
15
15
+
Box,
16
16
+
Archive,
17
17
+
FileText,
18
18
+
Image,
19
19
+
Video,
20
20
+
Mail,
21
21
+
MapPin,
22
22
+
Calendar,
23
23
+
Clock,
24
24
+
Search,
25
25
+
Settings,
26
26
+
User,
27
27
+
Users,
28
28
+
Home,
29
29
+
Briefcase,
30
30
+
Gift,
31
31
+
Award,
32
32
+
Target,
33
33
+
TrendingUp,
34
34
+
Activity,
35
35
+
Cpu,
36
36
+
Database,
37
37
+
Cloud,
38
38
+
Sun,
39
39
+
Moon,
40
40
+
Flame,
41
41
+
Leaf,
42
42
+
type LucideIcon,
43
43
+
} from 'lucide-react';
44
44
+
45
45
+
const ICON_MAP: Record<string, LucideIcon> = {
46
46
+
folder: Folder,
47
47
+
star: Star,
48
48
+
heart: Heart,
49
49
+
bookmark: Bookmark,
50
50
+
lightbulb: Lightbulb,
51
51
+
zap: Zap,
52
52
+
coffee: Coffee,
53
53
+
music: Music,
54
54
+
camera: Camera,
55
55
+
code: Code,
56
56
+
globe: Globe,
57
57
+
flag: Flag,
58
58
+
tag: Tag,
59
59
+
box: Box,
60
60
+
archive: Archive,
61
61
+
file: FileText,
62
62
+
image: Image,
63
63
+
video: Video,
64
64
+
mail: Mail,
65
65
+
pin: MapPin,
66
66
+
calendar: Calendar,
67
67
+
clock: Clock,
68
68
+
search: Search,
69
69
+
settings: Settings,
70
70
+
user: User,
71
71
+
users: Users,
72
72
+
home: Home,
73
73
+
briefcase: Briefcase,
74
74
+
gift: Gift,
75
75
+
award: Award,
76
76
+
target: Target,
77
77
+
trending: TrendingUp,
78
78
+
activity: Activity,
79
79
+
cpu: Cpu,
80
80
+
database: Database,
81
81
+
cloud: Cloud,
82
82
+
sun: Sun,
83
83
+
moon: Moon,
84
84
+
flame: Flame,
85
85
+
leaf: Leaf,
86
86
+
};
87
87
+
88
88
+
interface CollectionIconProps {
89
89
+
icon?: string;
90
90
+
size?: number;
91
91
+
className?: string;
92
92
+
}
93
93
+
94
94
+
export default function CollectionIcon({ icon, size = 18, className = '' }: CollectionIconProps) {
95
95
+
if (!icon) {
96
96
+
return <Folder size={size} className={className} />;
97
97
+
}
98
98
+
99
99
+
if (icon === 'icon:semble') {
100
100
+
return (
101
101
+
<img
102
102
+
src="/icons/semble-logo.svg"
103
103
+
alt="Semble"
104
104
+
style={{ width: size, height: size, objectFit: 'contain' }}
105
105
+
className={className}
106
106
+
/>
107
107
+
);
108
108
+
}
109
109
+
110
110
+
if (icon.startsWith('icon:')) {
111
111
+
const iconName = icon.replace('icon:', '');
112
112
+
const IconComponent = ICON_MAP[iconName];
113
113
+
if (IconComponent) {
114
114
+
return <IconComponent size={size} className={className} />;
115
115
+
}
116
116
+
return <Folder size={size} className={className} />;
117
117
+
}
118
118
+
119
119
+
return (
120
120
+
<span style={{ fontSize: `${size * 0.065}rem`, lineHeight: 1 }} className={className}>
121
121
+
{icon}
122
122
+
</span>
123
123
+
);
124
124
+
}
+986
extension/src/components/popup/App.tsx
···
1
1
+
import { useState, useEffect } from 'react';
2
2
+
import { sendMessage } from '@/utils/messaging';
3
3
+
import { themeItem, apiUrlItem, overlayEnabledItem } from '@/utils/storage';
4
4
+
import type { MarginSession, Annotation, Bookmark, Highlight, Collection } from '@/utils/types';
5
5
+
import CollectionIcon from '@/components/CollectionIcon';
6
6
+
import {
7
7
+
Settings,
8
8
+
ExternalLink,
9
9
+
Bookmark as BookmarkIcon,
10
10
+
Highlighter,
11
11
+
MessageSquare,
12
12
+
X,
13
13
+
Sun,
14
14
+
Moon,
15
15
+
Monitor,
16
16
+
Check,
17
17
+
Globe,
18
18
+
ChevronRight,
19
19
+
Sparkles,
20
20
+
FolderPlus,
21
21
+
Folder,
22
22
+
} from 'lucide-react';
23
23
+
24
24
+
type Tab = 'page' | 'bookmarks' | 'highlights' | 'collections';
25
25
+
26
26
+
export function App() {
27
27
+
const [session, setSession] = useState<MarginSession | null>(null);
28
28
+
const [loading, setLoading] = useState(true);
29
29
+
const [activeTab, setActiveTab] = useState<Tab>('page');
30
30
+
const [annotations, setAnnotations] = useState<Annotation[]>([]);
31
31
+
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
32
32
+
const [highlights, setHighlights] = useState<Highlight[]>([]);
33
33
+
const [collections, setCollections] = useState<Collection[]>([]);
34
34
+
const [loadingAnnotations, setLoadingAnnotations] = useState(false);
35
35
+
const [loadingBookmarks, setLoadingBookmarks] = useState(false);
36
36
+
const [loadingHighlights, setLoadingHighlights] = useState(false);
37
37
+
const [loadingCollections, setLoadingCollections] = useState(false);
38
38
+
const [collectionModalItem, setCollectionModalItem] = useState<string | null>(null);
39
39
+
const [addingToCollection, setAddingToCollection] = useState<string | null>(null);
40
40
+
const [containingCollections, setContainingCollections] = useState<Set<string>>(new Set());
41
41
+
const [currentUrl, setCurrentUrl] = useState('');
42
42
+
const [currentTitle, setCurrentTitle] = useState('');
43
43
+
const [text, setText] = useState('');
44
44
+
const [posting, setPosting] = useState(false);
45
45
+
const [bookmarking, setBookmarking] = useState(false);
46
46
+
const [bookmarked, setBookmarked] = useState(false);
47
47
+
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
48
48
+
const [showSettings, setShowSettings] = useState(false);
49
49
+
const [apiUrl, setApiUrl] = useState('https://margin.at');
50
50
+
const [overlayEnabled, setOverlayEnabled] = useState(true);
51
51
+
52
52
+
useEffect(() => {
53
53
+
checkSession();
54
54
+
loadCurrentTab();
55
55
+
loadTheme();
56
56
+
loadSettings();
57
57
+
}, []);
58
58
+
59
59
+
useEffect(() => {
60
60
+
if (session?.authenticated && currentUrl) {
61
61
+
if (activeTab === 'page') loadAnnotations();
62
62
+
else if (activeTab === 'bookmarks') loadBookmarks();
63
63
+
else if (activeTab === 'highlights') loadHighlights();
64
64
+
else if (activeTab === 'collections') loadCollections();
65
65
+
}
66
66
+
}, [activeTab, session, currentUrl]);
67
67
+
68
68
+
async function loadSettings() {
69
69
+
const url = await apiUrlItem.getValue();
70
70
+
const overlay = await overlayEnabledItem.getValue();
71
71
+
setApiUrl(url);
72
72
+
setOverlayEnabled(overlay);
73
73
+
}
74
74
+
75
75
+
async function saveSettings() {
76
76
+
const cleanUrl = apiUrl.replace(/\/$/, '');
77
77
+
await apiUrlItem.setValue(cleanUrl);
78
78
+
await overlayEnabledItem.setValue(overlayEnabled);
79
79
+
80
80
+
const tabs = await browser.tabs.query({});
81
81
+
for (const tab of tabs) {
82
82
+
if (tab.id) {
83
83
+
try {
84
84
+
await browser.tabs.sendMessage(tab.id, {
85
85
+
type: 'UPDATE_OVERLAY_VISIBILITY',
86
86
+
show: overlayEnabled,
87
87
+
});
88
88
+
} catch {
89
89
+
/* ignore */
90
90
+
}
91
91
+
}
92
92
+
}
93
93
+
94
94
+
setShowSettings(false);
95
95
+
checkSession();
96
96
+
}
97
97
+
98
98
+
async function loadTheme() {
99
99
+
const t = await themeItem.getValue();
100
100
+
setTheme(t);
101
101
+
applyTheme(t);
102
102
+
103
103
+
themeItem.watch((newTheme) => {
104
104
+
setTheme(newTheme);
105
105
+
applyTheme(newTheme);
106
106
+
});
107
107
+
}
108
108
+
109
109
+
function applyTheme(t: string) {
110
110
+
document.body.classList.remove('light', 'dark');
111
111
+
if (t === 'system') {
112
112
+
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
113
113
+
document.body.classList.add('light');
114
114
+
}
115
115
+
} else {
116
116
+
document.body.classList.add(t);
117
117
+
}
118
118
+
}
119
119
+
120
120
+
async function handleThemeChange(newTheme: 'light' | 'dark' | 'system') {
121
121
+
await themeItem.setValue(newTheme);
122
122
+
setTheme(newTheme);
123
123
+
applyTheme(newTheme);
124
124
+
}
125
125
+
126
126
+
async function checkSession() {
127
127
+
try {
128
128
+
const result = await sendMessage('checkSession', undefined);
129
129
+
setSession(result);
130
130
+
} catch (error) {
131
131
+
console.error('Session check error:', error);
132
132
+
setSession({ authenticated: false });
133
133
+
} finally {
134
134
+
setLoading(false);
135
135
+
}
136
136
+
}
137
137
+
138
138
+
async function loadCurrentTab() {
139
139
+
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
140
140
+
if (tab?.url) {
141
141
+
setCurrentUrl(tab.url);
142
142
+
setCurrentTitle(tab.title || '');
143
143
+
}
144
144
+
}
145
145
+
146
146
+
async function loadAnnotations() {
147
147
+
if (!currentUrl) return;
148
148
+
setLoadingAnnotations(true);
149
149
+
try {
150
150
+
let result = await sendMessage('getCachedAnnotations', { url: currentUrl });
151
151
+
152
152
+
if (!result) {
153
153
+
result = await sendMessage('getAnnotations', { url: currentUrl });
154
154
+
}
155
155
+
156
156
+
const filtered = (result || []).filter((item: any) => item.type !== 'Bookmark');
157
157
+
setAnnotations(filtered);
158
158
+
159
159
+
const isBookmarked = (result || []).some(
160
160
+
(item: any) => item.type === 'Bookmark' && item.creator?.did === session?.did
161
161
+
);
162
162
+
setBookmarked(isBookmarked);
163
163
+
} catch (error) {
164
164
+
console.error('Load annotations error:', error);
165
165
+
} finally {
166
166
+
setLoadingAnnotations(false);
167
167
+
}
168
168
+
}
169
169
+
170
170
+
async function loadBookmarks() {
171
171
+
if (!session?.did) return;
172
172
+
setLoadingBookmarks(true);
173
173
+
try {
174
174
+
const result = await sendMessage('getUserBookmarks', { did: session.did });
175
175
+
setBookmarks(result || []);
176
176
+
} catch (error) {
177
177
+
console.error('Load bookmarks error:', error);
178
178
+
} finally {
179
179
+
setLoadingBookmarks(false);
180
180
+
}
181
181
+
}
182
182
+
183
183
+
async function loadHighlights() {
184
184
+
if (!session?.did) return;
185
185
+
setLoadingHighlights(true);
186
186
+
try {
187
187
+
const result = await sendMessage('getUserHighlights', { did: session.did });
188
188
+
setHighlights(result || []);
189
189
+
} catch (error) {
190
190
+
console.error('Load highlights error:', error);
191
191
+
} finally {
192
192
+
setLoadingHighlights(false);
193
193
+
}
194
194
+
}
195
195
+
196
196
+
async function loadCollections() {
197
197
+
if (!session?.did) return;
198
198
+
setLoadingCollections(true);
199
199
+
try {
200
200
+
const result = await sendMessage('getUserCollections', { did: session.did });
201
201
+
setCollections(result || []);
202
202
+
} catch (error) {
203
203
+
console.error('Load collections error:', error);
204
204
+
} finally {
205
205
+
setLoadingCollections(false);
206
206
+
}
207
207
+
}
208
208
+
209
209
+
async function openCollectionModal(itemUri: string) {
210
210
+
setCollectionModalItem(itemUri);
211
211
+
setContainingCollections(new Set());
212
212
+
213
213
+
if (collections.length === 0) {
214
214
+
await loadCollections();
215
215
+
}
216
216
+
217
217
+
try {
218
218
+
const itemCollectionUris = await sendMessage('getItemCollections', {
219
219
+
annotationUri: itemUri,
220
220
+
});
221
221
+
setContainingCollections(new Set(itemCollectionUris));
222
222
+
} catch (error) {
223
223
+
console.error('Failed to get item collections:', error);
224
224
+
}
225
225
+
}
226
226
+
227
227
+
async function handleAddToCollection(collectionUri: string) {
228
228
+
if (!collectionModalItem) return;
229
229
+
230
230
+
if (containingCollections.has(collectionUri)) {
231
231
+
setCollectionModalItem(null);
232
232
+
return;
233
233
+
}
234
234
+
235
235
+
setAddingToCollection(collectionUri);
236
236
+
try {
237
237
+
const result = await sendMessage('addToCollection', {
238
238
+
collectionUri,
239
239
+
annotationUri: collectionModalItem,
240
240
+
});
241
241
+
if (result.success) {
242
242
+
setContainingCollections((prev) => new Set([...prev, collectionUri]));
243
243
+
} else {
244
244
+
alert('Failed to add to collection');
245
245
+
}
246
246
+
} catch (error) {
247
247
+
console.error('Add to collection error:', error);
248
248
+
alert('Error adding to collection');
249
249
+
} finally {
250
250
+
setAddingToCollection(null);
251
251
+
}
252
252
+
}
253
253
+
254
254
+
async function handlePost() {
255
255
+
if (!text.trim()) return;
256
256
+
setPosting(true);
257
257
+
try {
258
258
+
const result = await sendMessage('createAnnotation', {
259
259
+
url: currentUrl,
260
260
+
text: text.trim(),
261
261
+
title: currentTitle,
262
262
+
});
263
263
+
if (result.success) {
264
264
+
setText('');
265
265
+
loadAnnotations();
266
266
+
} else {
267
267
+
alert('Failed to post annotation');
268
268
+
}
269
269
+
} catch (error) {
270
270
+
console.error('Post error:', error);
271
271
+
alert('Error posting annotation');
272
272
+
} finally {
273
273
+
setPosting(false);
274
274
+
}
275
275
+
}
276
276
+
277
277
+
async function handleBookmark() {
278
278
+
setBookmarking(true);
279
279
+
try {
280
280
+
const result = await sendMessage('createBookmark', {
281
281
+
url: currentUrl,
282
282
+
title: currentTitle,
283
283
+
});
284
284
+
if (result.success) {
285
285
+
setBookmarked(true);
286
286
+
} else {
287
287
+
alert('Failed to bookmark page');
288
288
+
}
289
289
+
} catch (error) {
290
290
+
console.error('Bookmark error:', error);
291
291
+
alert('Error bookmarking page');
292
292
+
} finally {
293
293
+
setBookmarking(false);
294
294
+
}
295
295
+
}
296
296
+
297
297
+
function formatDate(dateString?: string) {
298
298
+
if (!dateString) return '';
299
299
+
try {
300
300
+
return new Date(dateString).toLocaleDateString();
301
301
+
} catch {
302
302
+
return dateString;
303
303
+
}
304
304
+
}
305
305
+
306
306
+
if (loading) {
307
307
+
return (
308
308
+
<div className="flex items-center justify-center h-screen">
309
309
+
<div className="animate-spin rounded-full h-8 w-8 border-2 border-[var(--accent)] border-t-transparent" />
310
310
+
</div>
311
311
+
);
312
312
+
}
313
313
+
314
314
+
if (!session?.authenticated) {
315
315
+
return (
316
316
+
<div className="flex flex-col h-screen">
317
317
+
{showSettings && (
318
318
+
<div className="absolute inset-0 bg-[var(--bg-primary)] z-10 flex flex-col">
319
319
+
<header className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
320
320
+
<span className="font-medium">Settings</span>
321
321
+
<button
322
322
+
onClick={() => setShowSettings(false)}
323
323
+
className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)]"
324
324
+
>
325
325
+
<X size={18} />
326
326
+
</button>
327
327
+
</header>
328
328
+
329
329
+
<div className="flex-1 overflow-y-auto p-4 space-y-6">
330
330
+
<div>
331
331
+
<label className="block text-sm font-medium mb-2">API URL</label>
332
332
+
<input
333
333
+
type="text"
334
334
+
value={apiUrl}
335
335
+
onChange={(e) => setApiUrl(e.target.value)}
336
336
+
className="w-full p-2.5 bg-[var(--bg-card)] border border-[var(--border)] rounded-lg text-sm focus:outline-none focus:border-[var(--accent)]"
337
337
+
placeholder="https://margin.at"
338
338
+
/>
339
339
+
</div>
340
340
+
341
341
+
<div>
342
342
+
<label className="block text-sm font-medium mb-2">Theme</label>
343
343
+
<div className="flex gap-2">
344
344
+
{(['light', 'dark', 'system'] as const).map((t) => (
345
345
+
<button
346
346
+
key={t}
347
347
+
onClick={() => handleThemeChange(t)}
348
348
+
className={`flex-1 py-2 px-3 text-xs rounded-lg border transition-colors flex items-center justify-center gap-1 ${
349
349
+
theme === t
350
350
+
? 'bg-[var(--accent)] text-white border-[var(--accent)]'
351
351
+
: 'bg-[var(--bg-card)] border-[var(--border)] hover:bg-[var(--bg-hover)]'
352
352
+
}`}
353
353
+
>
354
354
+
{t === 'light' ? (
355
355
+
<Sun size={12} />
356
356
+
) : t === 'dark' ? (
357
357
+
<Moon size={12} />
358
358
+
) : (
359
359
+
<Monitor size={12} />
360
360
+
)}
361
361
+
{t.charAt(0).toUpperCase() + t.slice(1)}
362
362
+
</button>
363
363
+
))}
364
364
+
</div>
365
365
+
</div>
366
366
+
</div>
367
367
+
368
368
+
<div className="p-4 border-t border-[var(--border)]">
369
369
+
<button
370
370
+
onClick={saveSettings}
371
371
+
className="w-full py-2.5 bg-[var(--accent)] text-white rounded-lg font-medium hover:bg-[var(--accent-hover)] transition-colors"
372
372
+
>
373
373
+
Save Settings
374
374
+
</button>
375
375
+
</div>
376
376
+
</div>
377
377
+
)}
378
378
+
379
379
+
<div className="flex flex-col items-center justify-center flex-1 p-6 text-center">
380
380
+
<img src="/icons/logo.svg" alt="Margin" className="w-12 h-12 mb-4" />
381
381
+
<h2 className="text-lg font-semibold mb-2">Sign in with AT Protocol</h2>
382
382
+
<p className="text-[var(--text-secondary)] text-sm mb-6">
383
383
+
Connect your Bluesky account to annotate, highlight, and bookmark the web.
384
384
+
</p>
385
385
+
<button
386
386
+
onClick={() => browser.tabs.create({ url: `${apiUrl}/login` })}
387
387
+
className="px-6 py-2.5 bg-[var(--accent)] text-white rounded-lg font-medium hover:bg-[var(--accent-hover)] transition-colors"
388
388
+
>
389
389
+
Continue
390
390
+
</button>
391
391
+
<button
392
392
+
onClick={() => setShowSettings(true)}
393
393
+
className="mt-4 text-xs text-[var(--text-tertiary)] hover:text-[var(--text-primary)] flex items-center gap-1"
394
394
+
>
395
395
+
<Settings size={12} /> Settings
396
396
+
</button>
397
397
+
</div>
398
398
+
</div>
399
399
+
);
400
400
+
}
401
401
+
402
402
+
return (
403
403
+
<div className="flex flex-col h-screen">
404
404
+
{showSettings && (
405
405
+
<div className="absolute inset-0 bg-[var(--bg-primary)] z-10 flex flex-col">
406
406
+
<header className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
407
407
+
<span className="font-medium">Settings</span>
408
408
+
<button
409
409
+
onClick={() => setShowSettings(false)}
410
410
+
className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)]"
411
411
+
>
412
412
+
<X size={18} />
413
413
+
</button>
414
414
+
</header>
415
415
+
416
416
+
<div className="flex-1 overflow-y-auto p-4 space-y-6">
417
417
+
<div>
418
418
+
<label className="block text-sm font-medium mb-2">API URL</label>
419
419
+
<input
420
420
+
type="text"
421
421
+
value={apiUrl}
422
422
+
onChange={(e) => setApiUrl(e.target.value)}
423
423
+
className="w-full p-2.5 bg-[var(--bg-card)] border border-[var(--border)] rounded-lg text-sm focus:outline-none focus:border-[var(--accent)]"
424
424
+
placeholder="https://margin.at"
425
425
+
/>
426
426
+
<p className="text-xs text-[var(--text-tertiary)] mt-1">
427
427
+
Change this for development or self-hosted instances
428
428
+
</p>
429
429
+
</div>
430
430
+
431
431
+
<div className="flex items-center justify-between">
432
432
+
<div>
433
433
+
<label className="text-sm font-medium">Show page overlays</label>
434
434
+
<p className="text-xs text-[var(--text-tertiary)]">
435
435
+
Highlights, badges, and tooltips on pages
436
436
+
</p>
437
437
+
</div>
438
438
+
<input
439
439
+
type="checkbox"
440
440
+
checked={overlayEnabled}
441
441
+
onChange={(e) => setOverlayEnabled(e.target.checked)}
442
442
+
className="w-5 h-5 rounded accent-[var(--accent)]"
443
443
+
/>
444
444
+
</div>
445
445
+
446
446
+
<div>
447
447
+
<label className="block text-sm font-medium mb-2">Theme</label>
448
448
+
<div className="flex gap-2">
449
449
+
{(['light', 'dark', 'system'] as const).map((t) => (
450
450
+
<button
451
451
+
key={t}
452
452
+
onClick={() => handleThemeChange(t)}
453
453
+
className={`flex-1 py-2 px-3 text-xs rounded-lg border transition-colors flex items-center justify-center gap-1 ${
454
454
+
theme === t
455
455
+
? 'bg-[var(--accent)] text-white border-[var(--accent)]'
456
456
+
: 'bg-[var(--bg-card)] border-[var(--border)] hover:bg-[var(--bg-hover)]'
457
457
+
}`}
458
458
+
>
459
459
+
{t === 'light' ? (
460
460
+
<Sun size={12} />
461
461
+
) : t === 'dark' ? (
462
462
+
<Moon size={12} />
463
463
+
) : (
464
464
+
<Monitor size={12} />
465
465
+
)}
466
466
+
{t.charAt(0).toUpperCase() + t.slice(1)}
467
467
+
</button>
468
468
+
))}
469
469
+
</div>
470
470
+
</div>
471
471
+
</div>
472
472
+
473
473
+
<div className="p-4 border-t border-[var(--border)]">
474
474
+
<button
475
475
+
onClick={saveSettings}
476
476
+
className="w-full py-2.5 bg-[var(--accent)] text-white rounded-lg font-medium hover:bg-[var(--accent-hover)] transition-colors"
477
477
+
>
478
478
+
Save
479
479
+
</button>
480
480
+
</div>
481
481
+
</div>
482
482
+
)}
483
483
+
484
484
+
<header className="flex items-center justify-between px-4 py-2.5 border-b border-[var(--border)] bg-[var(--bg-secondary)]">
485
485
+
<div className="flex items-center gap-2.5">
486
486
+
<img src="/icons/logo.svg" alt="Margin" className="w-6 h-6" />
487
487
+
<span className="font-bold text-sm tracking-tight">Margin</span>
488
488
+
</div>
489
489
+
<div className="flex items-center gap-2">
490
490
+
<div className="text-xs text-[var(--text-secondary)] bg-[var(--bg-card)] px-2.5 py-1.5 rounded-full border border-[var(--border)]">
491
491
+
@{session.handle}
492
492
+
</div>
493
493
+
</div>
494
494
+
</header>
495
495
+
496
496
+
<div className="flex border-b border-[var(--border)] px-2 gap-0.5 bg-[var(--bg-secondary)]">
497
497
+
{(['page', 'bookmarks', 'highlights', 'collections'] as Tab[]).map((tab) => {
498
498
+
const icons: Record<Tab, JSX.Element> = {
499
499
+
page: <Globe size={13} />,
500
500
+
bookmarks: <BookmarkIcon size={13} />,
501
501
+
highlights: <Highlighter size={13} />,
502
502
+
collections: <Folder size={13} />,
503
503
+
};
504
504
+
const labels: Record<Tab, string> = {
505
505
+
page: 'Page',
506
506
+
bookmarks: 'Bookmarks',
507
507
+
highlights: 'Highlights',
508
508
+
collections: 'Collections',
509
509
+
};
510
510
+
return (
511
511
+
<button
512
512
+
key={tab}
513
513
+
onClick={() => setActiveTab(tab)}
514
514
+
className={`flex-1 py-2.5 text-[11px] font-medium flex items-center justify-center gap-1 border-b-2 transition-all ${
515
515
+
activeTab === tab
516
516
+
? 'border-[var(--accent)] text-[var(--accent)]'
517
517
+
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]'
518
518
+
}`}
519
519
+
>
520
520
+
{icons[tab]}
521
521
+
{labels[tab]}
522
522
+
</button>
523
523
+
);
524
524
+
})}
525
525
+
</div>
526
526
+
527
527
+
<div className="flex-1 overflow-y-auto">
528
528
+
{activeTab === 'page' && (
529
529
+
<div>
530
530
+
<div className="p-4 border-b border-[var(--border)]">
531
531
+
<div className="flex items-start gap-3 p-3 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl">
532
532
+
<div className="w-10 h-10 rounded-lg bg-[var(--bg-hover)] flex items-center justify-center flex-shrink-0 overflow-hidden">
533
533
+
{currentUrl ? (
534
534
+
<img
535
535
+
src={`https://www.google.com/s2/favicons?domain=${new URL(currentUrl).hostname}&sz=64`}
536
536
+
alt=""
537
537
+
className="w-6 h-6"
538
538
+
onError={(e) => {
539
539
+
(e.target as HTMLImageElement).style.display = 'none';
540
540
+
(e.target as HTMLImageElement).nextElementSibling?.classList.remove(
541
541
+
'hidden'
542
542
+
);
543
543
+
}}
544
544
+
/>
545
545
+
) : null}
546
546
+
<Globe
547
547
+
size={18}
548
548
+
className={`text-[var(--text-tertiary)] ${currentUrl ? 'hidden' : ''}`}
549
549
+
/>
550
550
+
</div>
551
551
+
<div className="flex-1 min-w-0">
552
552
+
<div className="text-sm font-semibold truncate mb-0.5">
553
553
+
{currentTitle || 'Untitled'}
554
554
+
</div>
555
555
+
<div className="text-xs text-[var(--text-tertiary)] truncate">
556
556
+
{currentUrl ? new URL(currentUrl).hostname : ''}
557
557
+
</div>
558
558
+
</div>
559
559
+
<button
560
560
+
onClick={handleBookmark}
561
561
+
disabled={bookmarking || bookmarked}
562
562
+
className={`p-2 rounded-lg transition-all flex-shrink-0 ${
563
563
+
bookmarked
564
564
+
? 'bg-[var(--success)]/15 text-[var(--success)]'
565
565
+
: 'bg-[var(--bg-hover)] hover:bg-[var(--accent-subtle)] text-[var(--text-secondary)] hover:text-[var(--accent)]'
566
566
+
}`}
567
567
+
title={bookmarked ? 'Bookmarked' : 'Bookmark page'}
568
568
+
>
569
569
+
{bookmarked ? <Check size={16} /> : <BookmarkIcon size={16} />}
570
570
+
</button>
571
571
+
</div>
572
572
+
</div>
573
573
+
574
574
+
<div className="p-4 border-b border-[var(--border)]">
575
575
+
<div className="relative">
576
576
+
<textarea
577
577
+
value={text}
578
578
+
onChange={(e) => setText(e.target.value)}
579
579
+
placeholder="Share your thoughts on this page..."
580
580
+
className="w-full p-3 pb-12 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl text-sm resize-none focus:outline-none focus:border-[var(--accent)] focus:ring-2 focus:ring-[var(--accent-subtle)] min-h-[90px]"
581
581
+
/>
582
582
+
<div className="absolute bottom-3 right-3">
583
583
+
<button
584
584
+
onClick={handlePost}
585
585
+
disabled={posting || !text.trim()}
586
586
+
className="px-4 py-1.5 bg-[var(--accent)] text-white text-xs rounded-lg font-semibold hover:bg-[var(--accent-hover)] disabled:opacity-40 disabled:cursor-not-allowed transition-all hover:-translate-y-0.5 active:translate-y-0"
587
587
+
>
588
588
+
{posting ? 'Posting...' : 'Post'}
589
589
+
</button>
590
590
+
</div>
591
591
+
</div>
592
592
+
</div>
593
593
+
594
594
+
<div>
595
595
+
<div className="flex justify-between items-center px-4 py-3">
596
596
+
<div className="flex items-center gap-2">
597
597
+
<MessageSquare size={14} className="text-[var(--text-tertiary)]" />
598
598
+
<span className="text-xs font-semibold text-[var(--text-secondary)]">
599
599
+
Annotations
600
600
+
</span>
601
601
+
</div>
602
602
+
<span className="text-xs font-semibold bg-[var(--accent-subtle)] text-[var(--accent)] px-2.5 py-1 rounded-full">
603
603
+
{annotations.length}
604
604
+
</span>
605
605
+
</div>
606
606
+
607
607
+
{loadingAnnotations ? (
608
608
+
<div className="flex items-center justify-center py-12">
609
609
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" />
610
610
+
</div>
611
611
+
) : annotations.length === 0 ? (
612
612
+
<div className="flex flex-col items-center justify-center py-12 text-[var(--text-tertiary)]">
613
613
+
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mb-4">
614
614
+
<Sparkles size={24} className="opacity-40" />
615
615
+
</div>
616
616
+
<p className="text-sm font-medium mb-1">No annotations yet</p>
617
617
+
<p className="text-xs text-[var(--text-tertiary)]">
618
618
+
Be the first to annotate this page
619
619
+
</p>
620
620
+
</div>
621
621
+
) : (
622
622
+
<div className="divide-y divide-[var(--border)]">
623
623
+
{annotations.map((item) => (
624
624
+
<AnnotationCard
625
625
+
key={item.uri || item.id}
626
626
+
item={item}
627
627
+
formatDate={formatDate}
628
628
+
onAddToCollection={() => openCollectionModal(item.uri || item.id || '')}
629
629
+
/>
630
630
+
))}
631
631
+
</div>
632
632
+
)}
633
633
+
</div>
634
634
+
</div>
635
635
+
)}
636
636
+
637
637
+
{activeTab === 'bookmarks' && (
638
638
+
<div className="p-4">
639
639
+
{loadingBookmarks ? (
640
640
+
<div className="flex items-center justify-center py-16">
641
641
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" />
642
642
+
</div>
643
643
+
) : bookmarks.length === 0 ? (
644
644
+
<div className="flex flex-col items-center justify-center py-16 text-[var(--text-tertiary)]">
645
645
+
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mb-4">
646
646
+
<BookmarkIcon size={24} className="opacity-40" />
647
647
+
</div>
648
648
+
<p className="text-sm font-medium mb-1">No bookmarks yet</p>
649
649
+
<p className="text-xs text-[var(--text-tertiary)]">Save pages to read later</p>
650
650
+
</div>
651
651
+
) : (
652
652
+
<div className="space-y-2">
653
653
+
{bookmarks.map((item) => (
654
654
+
<div
655
655
+
key={item.uri || item.id}
656
656
+
className="flex items-center gap-3 p-3 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] hover:border-[var(--border-strong)] transition-all group"
657
657
+
>
658
658
+
<div className="w-9 h-9 rounded-lg bg-[var(--accent-subtle)] flex items-center justify-center flex-shrink-0">
659
659
+
<BookmarkIcon size={16} className="text-[var(--accent)]" />
660
660
+
</div>
661
661
+
<a
662
662
+
href={item.source}
663
663
+
target="_blank"
664
664
+
rel="noopener noreferrer"
665
665
+
className="flex-1 min-w-0"
666
666
+
>
667
667
+
<div className="text-sm font-medium truncate group-hover:text-[var(--accent)] transition-colors">
668
668
+
{item.title || 'Untitled'}
669
669
+
</div>
670
670
+
<div className="text-xs text-[var(--text-tertiary)] truncate">
671
671
+
{item.source ? new URL(item.source).hostname : ''}
672
672
+
</div>
673
673
+
</a>
674
674
+
<button
675
675
+
onClick={() => openCollectionModal(item.uri || item.id || '')}
676
676
+
className="p-1.5 text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent-subtle)] rounded-lg transition-all"
677
677
+
title="Add to collection"
678
678
+
>
679
679
+
<FolderPlus size={14} />
680
680
+
</button>
681
681
+
</div>
682
682
+
))}
683
683
+
</div>
684
684
+
)}
685
685
+
</div>
686
686
+
)}
687
687
+
688
688
+
{activeTab === 'highlights' && (
689
689
+
<div className="p-4">
690
690
+
{loadingHighlights ? (
691
691
+
<div className="flex items-center justify-center py-16">
692
692
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" />
693
693
+
</div>
694
694
+
) : highlights.length === 0 ? (
695
695
+
<div className="flex flex-col items-center justify-center py-16 text-[var(--text-tertiary)]">
696
696
+
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mb-4">
697
697
+
<Highlighter size={24} className="opacity-40" />
698
698
+
</div>
699
699
+
<p className="text-sm font-medium mb-1">No highlights yet</p>
700
700
+
<p className="text-xs text-[var(--text-tertiary)]">
701
701
+
Select text on any page to highlight
702
702
+
</p>
703
703
+
</div>
704
704
+
) : (
705
705
+
<div className="space-y-3">
706
706
+
{highlights.map((item) => (
707
707
+
<div
708
708
+
key={item.uri || item.id}
709
709
+
className="p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] hover:border-[var(--border-strong)] transition-all group"
710
710
+
>
711
711
+
{item.target?.selector?.exact && (
712
712
+
<div
713
713
+
className="text-sm leading-relaxed border-l-3 pl-3 mb-3 py-1"
714
714
+
style={{
715
715
+
borderColor: item.color || '#fbbf24',
716
716
+
background: `linear-gradient(90deg, ${item.color || '#fbbf24'}15, transparent)`,
717
717
+
}}
718
718
+
>
719
719
+
"
720
720
+
{item.target.selector.exact.length > 120
721
721
+
? item.target.selector.exact.slice(0, 120) + '...'
722
722
+
: item.target.selector.exact}
723
723
+
"
724
724
+
</div>
725
725
+
)}
726
726
+
<div className="flex items-center justify-between">
727
727
+
<div
728
728
+
className="flex items-center gap-2 text-xs text-[var(--text-tertiary)] flex-1 cursor-pointer hover:text-[var(--accent)]"
729
729
+
onClick={() => {
730
730
+
if (item.target?.source) {
731
731
+
browser.tabs.create({ url: item.target.source });
732
732
+
}
733
733
+
}}
734
734
+
>
735
735
+
<Globe size={12} />
736
736
+
{item.target?.source ? new URL(item.target.source).hostname : ''}
737
737
+
<ChevronRight
738
738
+
size={14}
739
739
+
className="ml-auto text-[var(--text-tertiary)] group-hover:text-[var(--accent)] transition-colors"
740
740
+
/>
741
741
+
</div>
742
742
+
<button
743
743
+
onClick={(e) => {
744
744
+
e.stopPropagation();
745
745
+
openCollectionModal(item.uri || item.id || '');
746
746
+
}}
747
747
+
className="p-1.5 text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent-subtle)] rounded-lg transition-all ml-2"
748
748
+
title="Add to collection"
749
749
+
>
750
750
+
<FolderPlus size={14} />
751
751
+
</button>
752
752
+
</div>
753
753
+
</div>
754
754
+
))}
755
755
+
</div>
756
756
+
)}
757
757
+
</div>
758
758
+
)}
759
759
+
760
760
+
{activeTab === 'collections' && (
761
761
+
<div className="p-4">
762
762
+
{loadingCollections ? (
763
763
+
<div className="flex items-center justify-center py-16">
764
764
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" />
765
765
+
</div>
766
766
+
) : collections.length === 0 ? (
767
767
+
<div className="flex flex-col items-center justify-center py-16 text-[var(--text-tertiary)]">
768
768
+
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mb-4">
769
769
+
<Folder size={24} className="opacity-40" />
770
770
+
</div>
771
771
+
<p className="text-sm font-medium mb-1">No collections yet</p>
772
772
+
<p className="text-xs text-[var(--text-tertiary)]">
773
773
+
Organize your annotations into collections
774
774
+
</p>
775
775
+
</div>
776
776
+
) : (
777
777
+
<div className="space-y-2">
778
778
+
{collections.map((item) => (
779
779
+
<button
780
780
+
key={item.uri || item.id}
781
781
+
onClick={() =>
782
782
+
browser.tabs.create({
783
783
+
url: `${apiUrl}/collection/${encodeURIComponent(item.uri || item.id || '')}`,
784
784
+
})
785
785
+
}
786
786
+
className="w-full text-left p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] hover:border-[var(--border-strong)] transition-all group flex items-center gap-3"
787
787
+
>
788
788
+
<div className="w-10 h-10 rounded-lg bg-[var(--accent)]/15 flex items-center justify-center flex-shrink-0 text-[var(--accent)] text-lg">
789
789
+
<CollectionIcon icon={item.icon} size={18} />
790
790
+
</div>
791
791
+
<div className="flex-1 min-w-0">
792
792
+
<div className="text-sm font-medium group-hover:text-[var(--accent)] transition-colors">
793
793
+
{item.name}
794
794
+
</div>
795
795
+
{item.description && (
796
796
+
<div className="text-xs text-[var(--text-tertiary)] truncate">
797
797
+
{item.description}
798
798
+
</div>
799
799
+
)}
800
800
+
</div>
801
801
+
<ChevronRight
802
802
+
size={16}
803
803
+
className="text-[var(--text-tertiary)] group-hover:text-[var(--accent)] transition-colors"
804
804
+
/>
805
805
+
</button>
806
806
+
))}
807
807
+
</div>
808
808
+
)}
809
809
+
</div>
810
810
+
)}
811
811
+
</div>
812
812
+
813
813
+
{collectionModalItem && (
814
814
+
<div
815
815
+
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 animate-fadeIn"
816
816
+
onClick={() => setCollectionModalItem(null)}
817
817
+
>
818
818
+
<div
819
819
+
className="bg-[var(--bg-primary)] rounded-2xl w-[90%] max-w-[340px] max-h-[80vh] overflow-hidden shadow-2xl animate-scaleIn"
820
820
+
onClick={(e) => e.stopPropagation()}
821
821
+
>
822
822
+
<div className="flex items-center justify-between p-4 border-b border-[var(--border)]">
823
823
+
<h3 className="text-sm font-bold">Add to Collection</h3>
824
824
+
<button
825
825
+
onClick={() => setCollectionModalItem(null)}
826
826
+
className="p-1.5 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-lg transition-all"
827
827
+
>
828
828
+
<X size={16} />
829
829
+
</button>
830
830
+
</div>
831
831
+
<div className="p-4 max-h-[300px] overflow-y-auto">
832
832
+
{loadingCollections ? (
833
833
+
<div className="flex items-center justify-center py-8">
834
834
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" />
835
835
+
</div>
836
836
+
) : collections.length === 0 ? (
837
837
+
<div className="text-center py-8 text-[var(--text-tertiary)]">
838
838
+
<Folder size={32} className="mx-auto mb-2 opacity-40" />
839
839
+
<p className="text-sm">No collections yet</p>
840
840
+
<p className="text-xs mt-1">Create collections on margin.at</p>
841
841
+
</div>
842
842
+
) : (
843
843
+
<div className="space-y-2">
844
844
+
{collections.map((col) => {
845
845
+
const colUri = col.uri || col.id || '';
846
846
+
const isInCollection = containingCollections.has(colUri);
847
847
+
const isAdding = addingToCollection === colUri;
848
848
+
return (
849
849
+
<button
850
850
+
key={colUri}
851
851
+
onClick={() => !isInCollection && handleAddToCollection(colUri)}
852
852
+
disabled={isAdding || isInCollection}
853
853
+
className={`w-full text-left p-3 border rounded-xl transition-all flex items-center gap-3 ${
854
854
+
isInCollection
855
855
+
? 'bg-emerald-400/10 border-emerald-400/30 cursor-default'
856
856
+
: 'bg-[var(--bg-card)] border-[var(--border)] hover:bg-[var(--bg-hover)] hover:border-[var(--accent)]'
857
857
+
}`}
858
858
+
>
859
859
+
<div
860
860
+
className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 text-base ${
861
861
+
isInCollection
862
862
+
? 'bg-emerald-400/15 text-emerald-400'
863
863
+
: 'bg-[var(--accent)]/15 text-[var(--accent)]'
864
864
+
}`}
865
865
+
>
866
866
+
<CollectionIcon icon={col.icon} size={16} />
867
867
+
</div>
868
868
+
<div className="flex-1 min-w-0">
869
869
+
<div className="text-sm font-medium">{col.name}</div>
870
870
+
</div>
871
871
+
{isAdding ? (
872
872
+
<div className="animate-spin rounded-full h-4 w-4 border-2 border-[var(--accent)] border-t-transparent" />
873
873
+
) : isInCollection ? (
874
874
+
<Check size={16} className="text-emerald-400" />
875
875
+
) : (
876
876
+
<FolderPlus size={16} className="text-[var(--text-tertiary)]" />
877
877
+
)}
878
878
+
</button>
879
879
+
);
880
880
+
})}
881
881
+
</div>
882
882
+
)}
883
883
+
</div>
884
884
+
</div>
885
885
+
</div>
886
886
+
)}
887
887
+
888
888
+
<footer className="flex items-center justify-between px-4 py-2.5 border-t border-[var(--border)] bg-[var(--bg-secondary)]">
889
889
+
<button
890
890
+
onClick={() => browser.tabs.create({ url: apiUrl })}
891
891
+
className="text-xs text-[var(--text-tertiary)] hover:text-[var(--accent)] flex items-center gap-1.5 py-1.5 px-2.5 rounded-lg hover:bg-[var(--accent-subtle)] transition-all"
892
892
+
>
893
893
+
Open Margin <ExternalLink size={12} />
894
894
+
</button>
895
895
+
<button
896
896
+
onClick={() => setShowSettings(true)}
897
897
+
className="p-2 text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent-subtle)] rounded-lg transition-all"
898
898
+
title="Settings"
899
899
+
>
900
900
+
<Settings size={16} />
901
901
+
</button>
902
902
+
</footer>
903
903
+
</div>
904
904
+
);
905
905
+
}
906
906
+
907
907
+
function AnnotationCard({
908
908
+
item,
909
909
+
formatDate,
910
910
+
onAddToCollection,
911
911
+
}: {
912
912
+
item: Annotation;
913
913
+
formatDate: (d?: string) => string;
914
914
+
onAddToCollection?: () => void;
915
915
+
}) {
916
916
+
const author = item.author || item.creator || {};
917
917
+
const handle = author.handle || 'User';
918
918
+
const text = item.body?.value || item.text || '';
919
919
+
const selector = item.target?.selector;
920
920
+
const quote = selector?.exact || '';
921
921
+
const isHighlight = (item as any).type === 'Highlight';
922
922
+
923
923
+
return (
924
924
+
<div className="px-4 py-4 hover:bg-[var(--bg-hover)] transition-colors">
925
925
+
<div className="flex items-start gap-3">
926
926
+
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-[var(--accent)] to-[var(--accent-hover)] flex items-center justify-center text-white text-xs font-bold flex-shrink-0 overflow-hidden shadow-sm">
927
927
+
{author.avatar ? (
928
928
+
<img src={author.avatar} alt={handle} className="w-full h-full object-cover" />
929
929
+
) : (
930
930
+
handle[0]?.toUpperCase() || 'U'
931
931
+
)}
932
932
+
</div>
933
933
+
<div className="flex-1 min-w-0">
934
934
+
<div className="flex items-center gap-2 mb-1.5">
935
935
+
<span className="text-sm font-semibold hover:text-[var(--accent)] cursor-pointer transition-colors">
936
936
+
@{handle}
937
937
+
</span>
938
938
+
<span className="text-[11px] text-[var(--text-tertiary)]">
939
939
+
{formatDate(item.created || item.createdAt)}
940
940
+
</span>
941
941
+
{isHighlight && (
942
942
+
<span className="text-[10px] font-semibold bg-[var(--warning)]/15 text-[var(--warning)] px-2 py-0.5 rounded-full flex items-center gap-1">
943
943
+
<Highlighter size={10} /> Highlight
944
944
+
</span>
945
945
+
)}
946
946
+
{onAddToCollection && (
947
947
+
<button
948
948
+
onClick={(e) => {
949
949
+
e.stopPropagation();
950
950
+
onAddToCollection();
951
951
+
}}
952
952
+
className="ml-auto p-1 text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent-subtle)] rounded transition-all"
953
953
+
title="Add to collection"
954
954
+
>
955
955
+
<FolderPlus size={14} />
956
956
+
</button>
957
957
+
)}
958
958
+
</div>
959
959
+
960
960
+
{quote && (
961
961
+
<div
962
962
+
className="text-sm text-[var(--text-secondary)] border-l-2 border-[var(--accent)] pl-3 mb-2.5 py-1.5 rounded-r bg-[var(--accent-subtle)] italic cursor-pointer hover:bg-[var(--accent)]/20 transition-colors"
963
963
+
onClick={async (e) => {
964
964
+
e.stopPropagation();
965
965
+
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
966
966
+
if (tab?.id) {
967
967
+
browser.tabs.sendMessage(tab.id, { type: 'SCROLL_TO_TEXT', text: quote });
968
968
+
window.close();
969
969
+
}
970
970
+
}}
971
971
+
title="Jump to text on page"
972
972
+
>
973
973
+
"{quote.length > 100 ? quote.slice(0, 100) + '...' : quote}"
974
974
+
</div>
975
975
+
)}
976
976
+
977
977
+
{text && (
978
978
+
<div className="text-[13px] leading-relaxed text-[var(--text-primary)]">{text}</div>
979
979
+
)}
980
980
+
</div>
981
981
+
</div>
982
982
+
</div>
983
983
+
);
984
984
+
}
985
985
+
986
986
+
export default App;
+913
extension/src/components/sidepanel/App.tsx
···
1
1
+
import { useState, useEffect } from 'react';
2
2
+
import { sendMessage } from '@/utils/messaging';
3
3
+
import { themeItem, overlayEnabledItem } from '@/utils/storage';
4
4
+
import type { MarginSession, Annotation, Bookmark, Highlight, Collection } from '@/utils/types';
5
5
+
import { APP_URL } from '@/utils/types';
6
6
+
import CollectionIcon from '@/components/CollectionIcon';
7
7
+
8
8
+
type Tab = 'page' | 'bookmarks' | 'highlights' | 'collections';
9
9
+
10
10
+
const Icons = {
11
11
+
settings: (
12
12
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
13
13
+
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z" />
14
14
+
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z" />
15
15
+
</svg>
16
16
+
),
17
17
+
globe: (
18
18
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
19
19
+
<circle cx="12" cy="12" r="10" />
20
20
+
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
21
21
+
<path d="M2 12h20" />
22
22
+
</svg>
23
23
+
),
24
24
+
bookmark: (
25
25
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
26
26
+
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
27
27
+
</svg>
28
28
+
),
29
29
+
highlighter: (
30
30
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
31
31
+
<path d="m9 11-6 6v3h9l3-3" />
32
32
+
<path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" />
33
33
+
</svg>
34
34
+
),
35
35
+
folder: (
36
36
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
37
37
+
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h16z" />
38
38
+
</svg>
39
39
+
),
40
40
+
folderPlus: (
41
41
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
42
42
+
<path d="M12 10v6" />
43
43
+
<path d="M9 13h6" />
44
44
+
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
45
45
+
</svg>
46
46
+
),
47
47
+
check: (
48
48
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
49
49
+
<polyline points="20 6 9 17 4 12" />
50
50
+
</svg>
51
51
+
),
52
52
+
chevronRight: (
53
53
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
54
54
+
<polyline points="9 18 15 12 9 6" />
55
55
+
</svg>
56
56
+
),
57
57
+
sparkles: (
58
58
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
59
59
+
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
60
60
+
</svg>
61
61
+
),
62
62
+
externalLink: (
63
63
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
64
64
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
65
65
+
<polyline points="15 3 21 3 21 9" />
66
66
+
<line x1="10" x2="21" y1="14" y2="3" />
67
67
+
</svg>
68
68
+
),
69
69
+
x: (
70
70
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
71
71
+
<path d="M18 6 6 18" />
72
72
+
<path d="m6 6 12 12" />
73
73
+
</svg>
74
74
+
),
75
75
+
};
76
76
+
77
77
+
export function App() {
78
78
+
const [session, setSession] = useState<MarginSession | null>(null);
79
79
+
const [loading, setLoading] = useState(true);
80
80
+
const [activeTab, setActiveTab] = useState<Tab>('page');
81
81
+
const [annotations, setAnnotations] = useState<Annotation[]>([]);
82
82
+
const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
83
83
+
const [highlights, setHighlights] = useState<Highlight[]>([]);
84
84
+
const [collections, setCollections] = useState<Collection[]>([]);
85
85
+
const [loadingAnnotations, setLoadingAnnotations] = useState(false);
86
86
+
const [loadingBookmarks, setLoadingBookmarks] = useState(false);
87
87
+
const [loadingHighlights, setLoadingHighlights] = useState(false);
88
88
+
const [loadingCollections, setLoadingCollections] = useState(false);
89
89
+
const [currentUrl, setCurrentUrl] = useState('');
90
90
+
const [currentTitle, setCurrentTitle] = useState('');
91
91
+
const [text, setText] = useState('');
92
92
+
const [posting, setPosting] = useState(false);
93
93
+
const [bookmarking, setBookmarking] = useState(false);
94
94
+
const [bookmarked, setBookmarked] = useState(false);
95
95
+
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system');
96
96
+
const [overlayEnabled, setOverlayEnabled] = useState(true);
97
97
+
const [showSettings, setShowSettings] = useState(false);
98
98
+
const [collectionModalItem, setCollectionModalItem] = useState<string | null>(null);
99
99
+
const [addingToCollection, setAddingToCollection] = useState<string | null>(null);
100
100
+
const [containingCollections, setContainingCollections] = useState<Set<string>>(new Set());
101
101
+
102
102
+
useEffect(() => {
103
103
+
checkSession();
104
104
+
loadCurrentTab();
105
105
+
loadSettings();
106
106
+
107
107
+
browser.tabs.onActivated.addListener(loadCurrentTab);
108
108
+
browser.tabs.onUpdated.addListener((_, info) => {
109
109
+
if (info.url) loadCurrentTab();
110
110
+
});
111
111
+
112
112
+
return () => {
113
113
+
browser.tabs.onActivated.removeListener(loadCurrentTab);
114
114
+
};
115
115
+
}, []);
116
116
+
117
117
+
useEffect(() => {
118
118
+
if (session?.authenticated && currentUrl) {
119
119
+
if (activeTab === 'page') loadAnnotations();
120
120
+
else if (activeTab === 'bookmarks') loadBookmarks();
121
121
+
else if (activeTab === 'highlights') loadHighlights();
122
122
+
else if (activeTab === 'collections') loadCollections();
123
123
+
}
124
124
+
}, [activeTab, session, currentUrl]);
125
125
+
126
126
+
async function loadSettings() {
127
127
+
const t = await themeItem.getValue();
128
128
+
setTheme(t);
129
129
+
applyTheme(t);
130
130
+
131
131
+
const overlay = await overlayEnabledItem.getValue();
132
132
+
setOverlayEnabled(overlay);
133
133
+
134
134
+
themeItem.watch((newTheme) => {
135
135
+
setTheme(newTheme);
136
136
+
applyTheme(newTheme);
137
137
+
});
138
138
+
139
139
+
overlayEnabledItem.watch((enabled) => {
140
140
+
setOverlayEnabled(enabled);
141
141
+
});
142
142
+
}
143
143
+
144
144
+
function applyTheme(t: string) {
145
145
+
document.body.classList.remove('light', 'dark');
146
146
+
if (t === 'system') {
147
147
+
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
148
148
+
document.body.classList.add('light');
149
149
+
}
150
150
+
} else {
151
151
+
document.body.classList.add(t);
152
152
+
}
153
153
+
}
154
154
+
155
155
+
async function checkSession() {
156
156
+
try {
157
157
+
const result = await sendMessage('checkSession', undefined);
158
158
+
setSession(result);
159
159
+
} catch (error) {
160
160
+
console.error('Session check error:', error);
161
161
+
setSession({ authenticated: false });
162
162
+
} finally {
163
163
+
setLoading(false);
164
164
+
}
165
165
+
}
166
166
+
167
167
+
async function loadCurrentTab() {
168
168
+
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
169
169
+
if (tab?.url) {
170
170
+
setCurrentUrl(tab.url);
171
171
+
setCurrentTitle(tab.title || '');
172
172
+
}
173
173
+
}
174
174
+
175
175
+
async function loadAnnotations() {
176
176
+
if (!currentUrl) return;
177
177
+
setLoadingAnnotations(true);
178
178
+
try {
179
179
+
let result = await sendMessage('getCachedAnnotations', { url: currentUrl });
180
180
+
181
181
+
if (!result) {
182
182
+
result = await sendMessage('getAnnotations', { url: currentUrl });
183
183
+
}
184
184
+
185
185
+
const filtered = (result || []).filter((item: any) => item.type !== 'Bookmark');
186
186
+
setAnnotations(filtered);
187
187
+
188
188
+
const isBookmarked = (result || []).some(
189
189
+
(item: any) => item.type === 'Bookmark' && item.creator?.did === session?.did
190
190
+
);
191
191
+
setBookmarked(isBookmarked);
192
192
+
} catch (error) {
193
193
+
console.error('Load annotations error:', error);
194
194
+
} finally {
195
195
+
setLoadingAnnotations(false);
196
196
+
}
197
197
+
}
198
198
+
199
199
+
async function loadBookmarks() {
200
200
+
if (!session?.did) return;
201
201
+
setLoadingBookmarks(true);
202
202
+
try {
203
203
+
const result = await sendMessage('getUserBookmarks', { did: session.did });
204
204
+
setBookmarks(result || []);
205
205
+
} catch (error) {
206
206
+
console.error('Load bookmarks error:', error);
207
207
+
} finally {
208
208
+
setLoadingBookmarks(false);
209
209
+
}
210
210
+
}
211
211
+
212
212
+
async function loadHighlights() {
213
213
+
if (!session?.did) return;
214
214
+
setLoadingHighlights(true);
215
215
+
try {
216
216
+
const result = await sendMessage('getUserHighlights', { did: session.did });
217
217
+
setHighlights(result || []);
218
218
+
} catch (error) {
219
219
+
console.error('Load highlights error:', error);
220
220
+
} finally {
221
221
+
setLoadingHighlights(false);
222
222
+
}
223
223
+
}
224
224
+
225
225
+
async function loadCollections() {
226
226
+
if (!session?.did) return;
227
227
+
setLoadingCollections(true);
228
228
+
try {
229
229
+
const result = await sendMessage('getUserCollections', { did: session.did });
230
230
+
setCollections(result || []);
231
231
+
} catch (error) {
232
232
+
console.error('Load collections error:', error);
233
233
+
} finally {
234
234
+
setLoadingCollections(false);
235
235
+
}
236
236
+
}
237
237
+
238
238
+
async function handlePost() {
239
239
+
if (!text.trim()) return;
240
240
+
setPosting(true);
241
241
+
try {
242
242
+
const result = await sendMessage('createAnnotation', {
243
243
+
url: currentUrl,
244
244
+
text: text.trim(),
245
245
+
title: currentTitle,
246
246
+
});
247
247
+
if (result.success) {
248
248
+
setText('');
249
249
+
loadAnnotations();
250
250
+
} else {
251
251
+
alert('Failed to post annotation');
252
252
+
}
253
253
+
} catch (error) {
254
254
+
console.error('Post error:', error);
255
255
+
alert('Error posting annotation');
256
256
+
} finally {
257
257
+
setPosting(false);
258
258
+
}
259
259
+
}
260
260
+
261
261
+
async function handleBookmark() {
262
262
+
setBookmarking(true);
263
263
+
try {
264
264
+
const result = await sendMessage('createBookmark', {
265
265
+
url: currentUrl,
266
266
+
title: currentTitle,
267
267
+
});
268
268
+
if (result.success) {
269
269
+
setBookmarked(true);
270
270
+
} else {
271
271
+
alert('Failed to bookmark page');
272
272
+
}
273
273
+
} catch (error) {
274
274
+
console.error('Bookmark error:', error);
275
275
+
alert('Error bookmarking page');
276
276
+
} finally {
277
277
+
setBookmarking(false);
278
278
+
}
279
279
+
}
280
280
+
281
281
+
async function handleThemeChange(newTheme: 'light' | 'dark' | 'system') {
282
282
+
await themeItem.setValue(newTheme);
283
283
+
}
284
284
+
285
285
+
async function handleOverlayToggle() {
286
286
+
await overlayEnabledItem.setValue(!overlayEnabled);
287
287
+
}
288
288
+
289
289
+
async function openCollectionModal(itemUri: string) {
290
290
+
setCollectionModalItem(itemUri);
291
291
+
setContainingCollections(new Set());
292
292
+
293
293
+
if (collections.length === 0) {
294
294
+
await loadCollections();
295
295
+
}
296
296
+
297
297
+
try {
298
298
+
const itemCollectionUris = await sendMessage('getItemCollections', {
299
299
+
annotationUri: itemUri,
300
300
+
});
301
301
+
setContainingCollections(new Set(itemCollectionUris));
302
302
+
} catch (error) {
303
303
+
console.error('Failed to get item collections:', error);
304
304
+
}
305
305
+
}
306
306
+
307
307
+
async function handleAddToCollection(collectionUri: string) {
308
308
+
if (!collectionModalItem) return;
309
309
+
310
310
+
if (containingCollections.has(collectionUri)) {
311
311
+
setCollectionModalItem(null);
312
312
+
return;
313
313
+
}
314
314
+
315
315
+
setAddingToCollection(collectionUri);
316
316
+
try {
317
317
+
const result = await sendMessage('addToCollection', {
318
318
+
collectionUri,
319
319
+
annotationUri: collectionModalItem,
320
320
+
});
321
321
+
if (result.success) {
322
322
+
setContainingCollections((prev) => new Set([...prev, collectionUri]));
323
323
+
} else {
324
324
+
alert('Failed to add to collection');
325
325
+
}
326
326
+
} catch (error) {
327
327
+
console.error('Add to collection error:', error);
328
328
+
alert('Error adding to collection');
329
329
+
} finally {
330
330
+
setAddingToCollection(null);
331
331
+
}
332
332
+
}
333
333
+
334
334
+
function formatDate(dateString?: string) {
335
335
+
if (!dateString) return '';
336
336
+
try {
337
337
+
return new Date(dateString).toLocaleDateString();
338
338
+
} catch {
339
339
+
return dateString;
340
340
+
}
341
341
+
}
342
342
+
343
343
+
if (loading) {
344
344
+
return (
345
345
+
<div className="flex items-center justify-center h-screen">
346
346
+
<div className="animate-spin rounded-full h-8 w-8 border-2 border-[var(--accent)] border-t-transparent" />
347
347
+
</div>
348
348
+
);
349
349
+
}
350
350
+
351
351
+
if (!session?.authenticated) {
352
352
+
return (
353
353
+
<div className="flex flex-col items-center justify-center h-screen p-8 text-center">
354
354
+
<img src="/icons/logo.svg" alt="Margin" className="w-16 h-16 mb-6" />
355
355
+
<h2 className="text-xl font-bold mb-3">Welcome to Margin</h2>
356
356
+
<p className="text-[var(--text-secondary)] mb-8 max-w-xs leading-relaxed">
357
357
+
Sign in to annotate, bookmark, and highlight web pages using the AT Protocol.
358
358
+
</p>
359
359
+
<button
360
360
+
onClick={() => browser.tabs.create({ url: `${APP_URL}/login` })}
361
361
+
className="px-8 py-3 bg-[var(--accent)] text-white rounded-xl font-semibold hover:bg-[var(--accent-hover)] transition-all hover:-translate-y-0.5 active:translate-y-0"
362
362
+
>
363
363
+
Sign In
364
364
+
</button>
365
365
+
</div>
366
366
+
);
367
367
+
}
368
368
+
369
369
+
return (
370
370
+
<div className="flex flex-col h-screen">
371
371
+
<header className="flex items-center justify-between px-3 py-2.5 border-b border-[var(--border)] bg-[var(--bg-secondary)] gap-2">
372
372
+
<div className="flex items-center gap-2 flex-shrink-0">
373
373
+
<img src="/icons/logo.svg" alt="Margin" className="w-5 h-5" />
374
374
+
<span className="font-bold text-sm hidden sm:inline">Margin</span>
375
375
+
</div>
376
376
+
<div className="flex items-center gap-2 min-w-0">
377
377
+
<div className="text-xs text-[var(--text-secondary)] bg-[var(--bg-card)] px-2 py-1 rounded-full border border-[var(--border)] truncate max-w-[120px]">
378
378
+
@{session.handle}
379
379
+
</div>
380
380
+
<button
381
381
+
onClick={() => setShowSettings(!showSettings)}
382
382
+
className="p-1.5 hover:bg-[var(--bg-hover)] rounded-lg transition-colors text-[var(--text-tertiary)] hover:text-[var(--text-primary)] flex-shrink-0"
383
383
+
title="Settings"
384
384
+
>
385
385
+
{Icons.settings}
386
386
+
</button>
387
387
+
</div>
388
388
+
</header>
389
389
+
390
390
+
{showSettings && (
391
391
+
<div className="p-4 border-b border-[var(--border)] bg-[var(--bg-card)] animate-slideDown">
392
392
+
<h3 className="text-sm font-bold mb-4 flex items-center gap-2">
393
393
+
{Icons.settings}
394
394
+
Settings
395
395
+
</h3>
396
396
+
397
397
+
<div className="mb-5">
398
398
+
<label className="text-xs font-medium text-[var(--text-secondary)] mb-2 block">
399
399
+
Theme
400
400
+
</label>
401
401
+
<div className="flex gap-2">
402
402
+
{(['system', 'light', 'dark'] as const).map((t) => (
403
403
+
<button
404
404
+
key={t}
405
405
+
onClick={() => handleThemeChange(t)}
406
406
+
className={`flex-1 px-3 py-2 text-xs font-medium rounded-lg transition-all ${
407
407
+
theme === t
408
408
+
? 'bg-[var(--accent)] text-white'
409
409
+
: 'bg-[var(--bg-elevated)] border border-[var(--border)] hover:bg-[var(--bg-hover)]'
410
410
+
}`}
411
411
+
>
412
412
+
{t.charAt(0).toUpperCase() + t.slice(1)}
413
413
+
</button>
414
414
+
))}
415
415
+
</div>
416
416
+
</div>
417
417
+
418
418
+
<div className="flex items-center justify-between py-2">
419
419
+
<div>
420
420
+
<span className="text-sm font-medium">Page Overlay</span>
421
421
+
<p className="text-xs text-[var(--text-tertiary)]">Show highlights on pages</p>
422
422
+
</div>
423
423
+
<button
424
424
+
onClick={handleOverlayToggle}
425
425
+
className={`w-11 h-6 rounded-full transition-colors relative ${
426
426
+
overlayEnabled
427
427
+
? 'bg-[var(--accent)]'
428
428
+
: 'bg-[var(--bg-hover)] border border-[var(--border)]'
429
429
+
}`}
430
430
+
>
431
431
+
<div
432
432
+
className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${
433
433
+
overlayEnabled ? 'left-6' : 'left-1'
434
434
+
}`}
435
435
+
/>
436
436
+
</button>
437
437
+
</div>
438
438
+
439
439
+
<div className="mt-4 pt-4 border-t border-[var(--border)]">
440
440
+
<button
441
441
+
onClick={() => browser.tabs.create({ url: APP_URL })}
442
442
+
className="w-full py-2.5 text-sm font-medium text-[var(--accent)] bg-[var(--accent)]/10 rounded-lg hover:bg-[var(--accent)]/20 transition-colors flex items-center justify-center gap-2"
443
443
+
>
444
444
+
Open Margin App {Icons.externalLink}
445
445
+
</button>
446
446
+
</div>
447
447
+
</div>
448
448
+
)}
449
449
+
450
450
+
<div className="flex border-b border-[var(--border)] bg-[var(--bg-secondary)]">
451
451
+
{(['page', 'bookmarks', 'highlights', 'collections'] as Tab[]).map((tab) => {
452
452
+
const icons = {
453
453
+
page: Icons.globe,
454
454
+
bookmarks: Icons.bookmark,
455
455
+
highlights: Icons.highlighter,
456
456
+
collections: Icons.folder,
457
457
+
};
458
458
+
const labels = {
459
459
+
page: 'Page',
460
460
+
bookmarks: 'Bookmarks',
461
461
+
highlights: 'Highlights',
462
462
+
collections: 'Collections',
463
463
+
};
464
464
+
return (
465
465
+
<button
466
466
+
key={tab}
467
467
+
onClick={() => setActiveTab(tab)}
468
468
+
className={`flex-1 py-2.5 text-[11px] font-medium flex items-center justify-center gap-1 border-b-2 transition-all ${
469
469
+
activeTab === tab
470
470
+
? 'border-[var(--accent)] text-[var(--accent)]'
471
471
+
: 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]'
472
472
+
}`}
473
473
+
>
474
474
+
{icons[tab]}
475
475
+
<span className="hidden min-[340px]:inline">{labels[tab]}</span>
476
476
+
</button>
477
477
+
);
478
478
+
})}
479
479
+
</div>
480
480
+
481
481
+
<div className="flex-1 overflow-y-auto">
482
482
+
{activeTab === 'page' && (
483
483
+
<div className="p-4">
484
484
+
<div className="mb-4 p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl">
485
485
+
<div className="flex items-start gap-3">
486
486
+
<div className="w-10 h-10 rounded-lg bg-[var(--bg-hover)] flex items-center justify-center flex-shrink-0 overflow-hidden">
487
487
+
{currentUrl ? (
488
488
+
<img
489
489
+
src={`https://www.google.com/s2/favicons?domain=${new URL(currentUrl).hostname}&sz=64`}
490
490
+
alt=""
491
491
+
className="w-6 h-6"
492
492
+
onError={(e) => {
493
493
+
(e.target as HTMLImageElement).style.display = 'none';
494
494
+
(e.target as HTMLImageElement).nextElementSibling?.classList.remove(
495
495
+
'hidden'
496
496
+
);
497
497
+
}}
498
498
+
/>
499
499
+
) : null}
500
500
+
<span className={`text-[var(--text-tertiary)] ${currentUrl ? 'hidden' : ''}`}>
501
501
+
{Icons.globe}
502
502
+
</span>
503
503
+
</div>
504
504
+
<div className="flex-1 min-w-0">
505
505
+
<div className="text-sm font-semibold truncate">{currentTitle || 'Untitled'}</div>
506
506
+
<div className="text-xs text-[var(--text-tertiary)] truncate">
507
507
+
{currentUrl ? new URL(currentUrl).hostname : ''}
508
508
+
</div>
509
509
+
</div>
510
510
+
<button
511
511
+
onClick={handleBookmark}
512
512
+
disabled={bookmarking || bookmarked}
513
513
+
className={`p-2 rounded-lg transition-all ${
514
514
+
bookmarked
515
515
+
? 'bg-emerald-400/15 text-emerald-400'
516
516
+
: 'bg-[var(--bg-hover)] hover:bg-[var(--accent)]/15 text-[var(--text-secondary)] hover:text-[var(--accent)]'
517
517
+
}`}
518
518
+
>
519
519
+
{bookmarked ? Icons.check : Icons.bookmark}
520
520
+
</button>
521
521
+
</div>
522
522
+
</div>
523
523
+
524
524
+
<div className="mb-6">
525
525
+
<textarea
526
526
+
value={text}
527
527
+
onChange={(e) => setText(e.target.value)}
528
528
+
placeholder="Share your thoughts on this page..."
529
529
+
className="w-full p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl text-sm resize-none focus:outline-none focus:border-[var(--accent)] focus:ring-2 focus:ring-[var(--accent)]/20 min-h-[100px]"
530
530
+
/>
531
531
+
<div className="flex gap-2 mt-3">
532
532
+
<button
533
533
+
onClick={handlePost}
534
534
+
disabled={posting || !text.trim()}
535
535
+
className="flex-1 px-4 py-2.5 bg-[var(--accent)] text-white text-sm rounded-xl font-semibold hover:bg-[var(--accent-hover)] disabled:opacity-40 disabled:cursor-not-allowed transition-all hover:-translate-y-0.5 active:translate-y-0"
536
536
+
>
537
537
+
{posting ? 'Posting...' : 'Post Annotation'}
538
538
+
</button>
539
539
+
</div>
540
540
+
</div>
541
541
+
542
542
+
<div className="flex items-center justify-between text-xs text-[var(--text-tertiary)] mb-3">
543
543
+
<span className="font-semibold">
544
544
+
{annotations.length} annotation{annotations.length !== 1 ? 's' : ''}
545
545
+
</span>
546
546
+
</div>
547
547
+
548
548
+
{loadingAnnotations ? (
549
549
+
<div className="flex items-center justify-center py-16">
550
550
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" />
551
551
+
</div>
552
552
+
) : annotations.length === 0 ? (
553
553
+
<div className="text-center py-16 text-[var(--text-tertiary)]">
554
554
+
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mx-auto mb-4">
555
555
+
{Icons.sparkles}
556
556
+
</div>
557
557
+
<p className="font-medium mb-1">No annotations yet</p>
558
558
+
<p className="text-xs">Be the first to annotate</p>
559
559
+
</div>
560
560
+
) : (
561
561
+
<div className="space-y-3">
562
562
+
{annotations.map((item) => (
563
563
+
<AnnotationCard
564
564
+
key={item.uri || item.id}
565
565
+
item={item}
566
566
+
formatDate={formatDate}
567
567
+
onAddToCollection={() => openCollectionModal(item.uri || item.id || '')}
568
568
+
/>
569
569
+
))}
570
570
+
</div>
571
571
+
)}
572
572
+
</div>
573
573
+
)}
574
574
+
575
575
+
{activeTab === 'bookmarks' && (
576
576
+
<div className="p-4">
577
577
+
{loadingBookmarks ? (
578
578
+
<div className="flex items-center justify-center py-16">
579
579
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" />
580
580
+
</div>
581
581
+
) : bookmarks.length === 0 ? (
582
582
+
<div className="text-center py-16 text-[var(--text-tertiary)]">
583
583
+
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mx-auto mb-4">
584
584
+
{Icons.bookmark}
585
585
+
</div>
586
586
+
<p className="font-medium mb-1">No bookmarks yet</p>
587
587
+
<p className="text-xs">Save pages to read later</p>
588
588
+
</div>
589
589
+
) : (
590
590
+
<div className="space-y-2">
591
591
+
{bookmarks.map((item) => (
592
592
+
<div
593
593
+
key={item.uri || item.id}
594
594
+
className="flex items-center gap-3 p-3 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all group"
595
595
+
>
596
596
+
<div className="w-9 h-9 rounded-lg bg-[var(--accent)]/15 flex items-center justify-center flex-shrink-0 text-[var(--accent)]">
597
597
+
{Icons.bookmark}
598
598
+
</div>
599
599
+
<a
600
600
+
href={item.source}
601
601
+
target="_blank"
602
602
+
rel="noopener noreferrer"
603
603
+
className="flex-1 min-w-0"
604
604
+
>
605
605
+
<div className="text-sm font-medium truncate group-hover:text-[var(--accent)] transition-colors">
606
606
+
{item.title || item.source}
607
607
+
</div>
608
608
+
<div className="text-xs text-[var(--text-tertiary)] truncate">
609
609
+
{item.source ? new URL(item.source).hostname : ''}
610
610
+
</div>
611
611
+
</a>
612
612
+
<button
613
613
+
onClick={(e) => {
614
614
+
e.stopPropagation();
615
615
+
if (item.uri) openCollectionModal(item.uri);
616
616
+
}}
617
617
+
className="p-1.5 rounded-lg text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors"
618
618
+
title="Add to collection"
619
619
+
>
620
620
+
{Icons.folderPlus}
621
621
+
</button>
622
622
+
<a
623
623
+
href={item.source}
624
624
+
target="_blank"
625
625
+
rel="noopener noreferrer"
626
626
+
className="text-[var(--text-tertiary)] group-hover:text-[var(--accent)]"
627
627
+
>
628
628
+
{Icons.chevronRight}
629
629
+
</a>
630
630
+
</div>
631
631
+
))}
632
632
+
</div>
633
633
+
)}
634
634
+
</div>
635
635
+
)}
636
636
+
637
637
+
{activeTab === 'highlights' && (
638
638
+
<div className="p-4">
639
639
+
{loadingHighlights ? (
640
640
+
<div className="flex items-center justify-center py-16">
641
641
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" />
642
642
+
</div>
643
643
+
) : highlights.length === 0 ? (
644
644
+
<div className="text-center py-16 text-[var(--text-tertiary)]">
645
645
+
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mx-auto mb-4">
646
646
+
{Icons.highlighter}
647
647
+
</div>
648
648
+
<p className="font-medium mb-1">No highlights yet</p>
649
649
+
<p className="text-xs">Select text on any page to highlight</p>
650
650
+
</div>
651
651
+
) : (
652
652
+
<div className="space-y-3">
653
653
+
{highlights.map((item) => (
654
654
+
<div
655
655
+
key={item.uri || item.id}
656
656
+
className="p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all group"
657
657
+
>
658
658
+
{item.target?.selector?.exact && (
659
659
+
<div
660
660
+
className="text-sm leading-relaxed border-l-3 pl-3 mb-3 cursor-pointer"
661
661
+
style={{
662
662
+
borderColor: item.color || '#fbbf24',
663
663
+
background: `linear-gradient(90deg, ${item.color || '#fbbf24'}10, transparent)`,
664
664
+
}}
665
665
+
onClick={() => {
666
666
+
if (item.target?.source) {
667
667
+
browser.tabs.create({ url: item.target.source });
668
668
+
}
669
669
+
}}
670
670
+
>
671
671
+
"
672
672
+
{item.target.selector.exact.length > 150
673
673
+
? item.target.selector.exact.slice(0, 150) + '...'
674
674
+
: item.target.selector.exact}
675
675
+
"
676
676
+
</div>
677
677
+
)}
678
678
+
<div className="flex items-center justify-between text-xs text-[var(--text-tertiary)]">
679
679
+
<span className="flex items-center gap-1.5">
680
680
+
{Icons.globe}{' '}
681
681
+
{item.target?.source ? new URL(item.target.source).hostname : ''}
682
682
+
</span>
683
683
+
<div className="flex items-center gap-2">
684
684
+
<button
685
685
+
onClick={(e) => {
686
686
+
e.stopPropagation();
687
687
+
if (item.uri) openCollectionModal(item.uri);
688
688
+
}}
689
689
+
className="p-1 rounded text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors"
690
690
+
title="Add to collection"
691
691
+
>
692
692
+
{Icons.folderPlus}
693
693
+
</button>
694
694
+
<button
695
695
+
onClick={() => {
696
696
+
if (item.target?.source) {
697
697
+
browser.tabs.create({ url: item.target.source });
698
698
+
}
699
699
+
}}
700
700
+
className="group-hover:text-[var(--accent)]"
701
701
+
>
702
702
+
{Icons.chevronRight}
703
703
+
</button>
704
704
+
</div>
705
705
+
</div>
706
706
+
</div>
707
707
+
))}
708
708
+
</div>
709
709
+
)}
710
710
+
</div>
711
711
+
)}
712
712
+
713
713
+
{activeTab === 'collections' && (
714
714
+
<div className="p-4">
715
715
+
{loadingCollections ? (
716
716
+
<div className="flex items-center justify-center py-16">
717
717
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" />
718
718
+
</div>
719
719
+
) : collections.length === 0 ? (
720
720
+
<div className="text-center py-16 text-[var(--text-tertiary)]">
721
721
+
<div className="w-14 h-14 rounded-2xl bg-[var(--bg-card)] border border-[var(--border)] flex items-center justify-center mx-auto mb-4">
722
722
+
{Icons.folder}
723
723
+
</div>
724
724
+
<p className="font-medium mb-1">No collections yet</p>
725
725
+
<p className="text-xs">Organize your bookmarks into collections</p>
726
726
+
</div>
727
727
+
) : (
728
728
+
<div className="space-y-2">
729
729
+
{collections.map((item) => (
730
730
+
<button
731
731
+
key={item.uri || item.id}
732
732
+
onClick={() =>
733
733
+
browser.tabs.create({
734
734
+
url: `${APP_URL}/collection/${encodeURIComponent(item.uri || item.id || '')}`,
735
735
+
})
736
736
+
}
737
737
+
className="w-full text-left p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all group flex items-center gap-3"
738
738
+
>
739
739
+
<div className="w-9 h-9 rounded-lg bg-[var(--accent)]/15 flex items-center justify-center flex-shrink-0 text-[var(--accent)] text-lg">
740
740
+
<CollectionIcon icon={item.icon} size={18} />
741
741
+
</div>
742
742
+
<div className="flex-1 min-w-0">
743
743
+
<div className="text-sm font-medium group-hover:text-[var(--accent)] transition-colors">
744
744
+
{item.name}
745
745
+
</div>
746
746
+
{item.description && (
747
747
+
<div className="text-xs text-[var(--text-tertiary)] truncate">
748
748
+
{item.description}
749
749
+
</div>
750
750
+
)}
751
751
+
</div>
752
752
+
<div className="text-[var(--text-tertiary)] group-hover:text-[var(--accent)]">
753
753
+
{Icons.chevronRight}
754
754
+
</div>
755
755
+
</button>
756
756
+
))}
757
757
+
</div>
758
758
+
)}
759
759
+
</div>
760
760
+
)}
761
761
+
</div>
762
762
+
763
763
+
{collectionModalItem && (
764
764
+
<div
765
765
+
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
766
766
+
onClick={() => setCollectionModalItem(null)}
767
767
+
>
768
768
+
<div
769
769
+
className="bg-[var(--bg-primary)] border border-[var(--border)] rounded-2xl w-full max-w-sm max-h-[70vh] overflow-hidden shadow-2xl"
770
770
+
onClick={(e) => e.stopPropagation()}
771
771
+
>
772
772
+
<div className="flex items-center justify-between p-4 border-b border-[var(--border)]">
773
773
+
<h3 className="font-semibold">Add to Collection</h3>
774
774
+
<button
775
775
+
onClick={() => setCollectionModalItem(null)}
776
776
+
className="p-1 rounded-lg hover:bg-[var(--bg-hover)] text-[var(--text-tertiary)]"
777
777
+
>
778
778
+
{Icons.x}
779
779
+
</button>
780
780
+
</div>
781
781
+
<div className="p-2 max-h-[50vh] overflow-y-auto">
782
782
+
{collections.length === 0 ? (
783
783
+
<div className="text-center py-8 text-[var(--text-tertiary)]">
784
784
+
<p className="text-sm">No collections yet</p>
785
785
+
<p className="text-xs mt-1">Create a collection on the web app first</p>
786
786
+
</div>
787
787
+
) : (
788
788
+
collections.map((col) => {
789
789
+
const colUri = col.uri || col.id || '';
790
790
+
const isAdding = addingToCollection === colUri;
791
791
+
const isInCollection = containingCollections.has(colUri);
792
792
+
return (
793
793
+
<button
794
794
+
key={colUri}
795
795
+
onClick={() => !isInCollection && handleAddToCollection(colUri)}
796
796
+
disabled={isAdding || isInCollection}
797
797
+
className={`w-full text-left p-3 rounded-xl flex items-center gap-3 transition-all ${
798
798
+
isInCollection
799
799
+
? 'bg-emerald-400/10 cursor-default'
800
800
+
: 'hover:bg-[var(--bg-hover)]'
801
801
+
}`}
802
802
+
>
803
803
+
<div
804
804
+
className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 text-base ${
805
805
+
isInCollection
806
806
+
? 'bg-emerald-400/15 text-emerald-400'
807
807
+
: 'bg-[var(--accent)]/15 text-[var(--accent)]'
808
808
+
}`}
809
809
+
>
810
810
+
<CollectionIcon icon={col.icon} size={16} />
811
811
+
</div>
812
812
+
<div className="flex-1 min-w-0">
813
813
+
<div className="text-sm font-medium truncate">{col.name}</div>
814
814
+
{col.description && (
815
815
+
<div className="text-xs text-[var(--text-tertiary)] truncate">
816
816
+
{col.description}
817
817
+
</div>
818
818
+
)}
819
819
+
</div>
820
820
+
{isAdding ? (
821
821
+
<div className="animate-spin rounded-full h-4 w-4 border-2 border-[var(--accent)] border-t-transparent" />
822
822
+
) : isInCollection ? (
823
823
+
<span className="text-emerald-400">{Icons.check}</span>
824
824
+
) : (
825
825
+
Icons.folderPlus
826
826
+
)}
827
827
+
</button>
828
828
+
);
829
829
+
})
830
830
+
)}
831
831
+
</div>
832
832
+
</div>
833
833
+
</div>
834
834
+
)}
835
835
+
</div>
836
836
+
);
837
837
+
}
838
838
+
839
839
+
function AnnotationCard({
840
840
+
item,
841
841
+
formatDate,
842
842
+
onAddToCollection,
843
843
+
}: {
844
844
+
item: Annotation;
845
845
+
formatDate: (d?: string) => string;
846
846
+
onAddToCollection?: () => void;
847
847
+
}) {
848
848
+
const author = item.author || item.creator || {};
849
849
+
const handle = author.handle || 'User';
850
850
+
const text = item.body?.value || item.text || '';
851
851
+
const selector = item.target?.selector;
852
852
+
const quote = selector?.exact || '';
853
853
+
const isHighlight = (item as any).type === 'Highlight';
854
854
+
855
855
+
return (
856
856
+
<div className="p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all">
857
857
+
<div className="flex items-start gap-3">
858
858
+
<div className="w-9 h-9 rounded-full bg-gradient-to-br from-[var(--accent)] to-[var(--accent-hover)] flex items-center justify-center text-white text-xs font-bold overflow-hidden flex-shrink-0">
859
859
+
{author.avatar ? (
860
860
+
<img src={author.avatar} alt={handle} className="w-full h-full object-cover" />
861
861
+
) : (
862
862
+
handle[0]?.toUpperCase() || 'U'
863
863
+
)}
864
864
+
</div>
865
865
+
<div className="flex-1 min-w-0">
866
866
+
<div className="flex items-center gap-2 mb-2">
867
867
+
<span className="text-sm font-semibold">@{handle}</span>
868
868
+
<span className="text-xs text-[var(--text-tertiary)]">
869
869
+
{formatDate(item.created || item.createdAt)}
870
870
+
</span>
871
871
+
{isHighlight && (
872
872
+
<span className="text-[10px] font-semibold bg-amber-400/15 text-amber-400 px-2 py-0.5 rounded-full">
873
873
+
Highlight
874
874
+
</span>
875
875
+
)}
876
876
+
{onAddToCollection && (
877
877
+
<button
878
878
+
onClick={(e) => {
879
879
+
e.stopPropagation();
880
880
+
onAddToCollection();
881
881
+
}}
882
882
+
className="ml-auto p-1 rounded text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors"
883
883
+
title="Add to collection"
884
884
+
>
885
885
+
{Icons.folderPlus}
886
886
+
</button>
887
887
+
)}
888
888
+
</div>
889
889
+
890
890
+
{quote && (
891
891
+
<div
892
892
+
className="text-sm text-[var(--text-secondary)] border-l-2 border-[var(--accent)] pl-3 mb-2 py-1 rounded-r bg-[var(--accent)]/10 italic cursor-pointer hover:bg-[var(--accent)]/20 transition-colors"
893
893
+
onClick={async (e) => {
894
894
+
e.stopPropagation();
895
895
+
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
896
896
+
if (tab?.id) {
897
897
+
browser.tabs.sendMessage(tab.id, { type: 'SCROLL_TO_TEXT', text: quote });
898
898
+
}
899
899
+
}}
900
900
+
title="Jump to text on page"
901
901
+
>
902
902
+
"{quote.length > 150 ? quote.slice(0, 150) + '...' : quote}"
903
903
+
</div>
904
904
+
)}
905
905
+
906
906
+
{text && <div className="text-sm leading-relaxed">{text}</div>}
907
907
+
</div>
908
908
+
</div>
909
909
+
</div>
910
910
+
);
911
911
+
}
912
912
+
913
913
+
export default App;
+366
extension/src/entrypoints/background.ts
···
1
1
+
import { onMessage } from '@/utils/messaging';
2
2
+
import type { Annotation } from '@/utils/types';
3
3
+
import {
4
4
+
checkSession,
5
5
+
getAnnotations,
6
6
+
createAnnotation,
7
7
+
createBookmark,
8
8
+
createHighlight,
9
9
+
getUserBookmarks,
10
10
+
getUserHighlights,
11
11
+
getUserCollections,
12
12
+
addToCollection,
13
13
+
getItemCollections,
14
14
+
getReplies,
15
15
+
createReply,
16
16
+
} from '@/utils/api';
17
17
+
import { overlayEnabledItem, apiUrlItem } from '@/utils/storage';
18
18
+
19
19
+
export default defineBackground(() => {
20
20
+
console.log('Margin extension loaded');
21
21
+
22
22
+
const annotationCache = new Map<string, { annotations: Annotation[]; timestamp: number }>();
23
23
+
const CACHE_TTL = 60000;
24
24
+
25
25
+
onMessage('checkSession', async () => {
26
26
+
return await checkSession();
27
27
+
});
28
28
+
29
29
+
onMessage('getAnnotations', async ({ data }) => {
30
30
+
return await getAnnotations(data.url);
31
31
+
});
32
32
+
33
33
+
onMessage('createAnnotation', async ({ data }) => {
34
34
+
return await createAnnotation(data);
35
35
+
});
36
36
+
37
37
+
onMessage('createBookmark', async ({ data }) => {
38
38
+
return await createBookmark(data);
39
39
+
});
40
40
+
41
41
+
onMessage('createHighlight', async ({ data }) => {
42
42
+
return await createHighlight(data);
43
43
+
});
44
44
+
45
45
+
onMessage('getUserBookmarks', async ({ data }) => {
46
46
+
return await getUserBookmarks(data.did);
47
47
+
});
48
48
+
49
49
+
onMessage('getUserHighlights', async ({ data }) => {
50
50
+
return await getUserHighlights(data.did);
51
51
+
});
52
52
+
53
53
+
onMessage('getUserCollections', async ({ data }) => {
54
54
+
return await getUserCollections(data.did);
55
55
+
});
56
56
+
57
57
+
onMessage('addToCollection', async ({ data }) => {
58
58
+
return await addToCollection(data.collectionUri, data.annotationUri);
59
59
+
});
60
60
+
61
61
+
onMessage('getItemCollections', async ({ data }) => {
62
62
+
return await getItemCollections(data.annotationUri);
63
63
+
});
64
64
+
65
65
+
onMessage('getReplies', async ({ data }) => {
66
66
+
return await getReplies(data.uri);
67
67
+
});
68
68
+
69
69
+
onMessage('createReply', async ({ data }) => {
70
70
+
return await createReply(data);
71
71
+
});
72
72
+
73
73
+
onMessage('getOverlayEnabled', async () => {
74
74
+
return await overlayEnabledItem.getValue();
75
75
+
});
76
76
+
77
77
+
onMessage('openAppUrl', async ({ data }) => {
78
78
+
const apiUrl = await apiUrlItem.getValue();
79
79
+
await browser.tabs.create({ url: `${apiUrl}${data.path}` });
80
80
+
});
81
81
+
82
82
+
onMessage('updateBadge', async ({ data }) => {
83
83
+
const { count, tabId } = data;
84
84
+
const text = count > 0 ? String(count > 99 ? '99+' : count) : '';
85
85
+
86
86
+
if (tabId) {
87
87
+
await browser.action.setBadgeText({ text, tabId });
88
88
+
await browser.action.setBadgeBackgroundColor({ color: '#6366f1', tabId });
89
89
+
} else {
90
90
+
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
91
91
+
if (tab?.id) {
92
92
+
await browser.action.setBadgeText({ text, tabId: tab.id });
93
93
+
await browser.action.setBadgeBackgroundColor({ color: '#6366f1', tabId: tab.id });
94
94
+
}
95
95
+
}
96
96
+
});
97
97
+
98
98
+
browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
99
99
+
if (changeInfo.status === 'loading' && changeInfo.url) {
100
100
+
await browser.action.setBadgeText({ text: '', tabId });
101
101
+
}
102
102
+
});
103
103
+
104
104
+
onMessage('cacheAnnotations', async ({ data }) => {
105
105
+
const { url, annotations } = data;
106
106
+
const normalizedUrl = normalizeUrl(url);
107
107
+
annotationCache.set(normalizedUrl, { annotations, timestamp: Date.now() });
108
108
+
});
109
109
+
110
110
+
onMessage('getCachedAnnotations', async ({ data }) => {
111
111
+
const normalizedUrl = normalizeUrl(data.url);
112
112
+
const cached = annotationCache.get(normalizedUrl);
113
113
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
114
114
+
return cached.annotations;
115
115
+
}
116
116
+
return null;
117
117
+
});
118
118
+
119
119
+
function normalizeUrl(url: string): string {
120
120
+
try {
121
121
+
const u = new URL(url);
122
122
+
u.hash = '';
123
123
+
const path = u.pathname.replace(/\/$/, '') || '/';
124
124
+
return `${u.origin}${path}${u.search}`;
125
125
+
} catch {
126
126
+
return url;
127
127
+
}
128
128
+
}
129
129
+
130
130
+
async function ensureContextMenus() {
131
131
+
await browser.contextMenus.removeAll();
132
132
+
133
133
+
browser.contextMenus.create({
134
134
+
id: 'margin-annotate',
135
135
+
title: 'Annotate "%s"',
136
136
+
contexts: ['selection'],
137
137
+
});
138
138
+
139
139
+
browser.contextMenus.create({
140
140
+
id: 'margin-highlight',
141
141
+
title: 'Highlight "%s"',
142
142
+
contexts: ['selection'],
143
143
+
});
144
144
+
145
145
+
browser.contextMenus.create({
146
146
+
id: 'margin-bookmark',
147
147
+
title: 'Bookmark this page',
148
148
+
contexts: ['page'],
149
149
+
});
150
150
+
151
151
+
browser.contextMenus.create({
152
152
+
id: 'margin-open-sidebar',
153
153
+
title: 'Open Margin Sidebar',
154
154
+
contexts: ['page', 'selection', 'link'],
155
155
+
});
156
156
+
}
157
157
+
158
158
+
browser.runtime.onInstalled.addListener(async () => {
159
159
+
await ensureContextMenus();
160
160
+
});
161
161
+
162
162
+
browser.runtime.onStartup.addListener(async () => {
163
163
+
await ensureContextMenus();
164
164
+
});
165
165
+
166
166
+
browser.contextMenus.onClicked.addListener((info, tab) => {
167
167
+
if (info.menuItemId === 'margin-open-sidebar') {
168
168
+
const browserAny = browser as any;
169
169
+
if (browserAny.sidePanel && tab?.windowId) {
170
170
+
browserAny.sidePanel.open({ windowId: tab.windowId }).catch((err: Error) => {
171
171
+
console.error('Could not open side panel:', err);
172
172
+
});
173
173
+
} else if (browserAny.sidebarAction) {
174
174
+
browserAny.sidebarAction.open().catch((err: Error) => {
175
175
+
console.warn('Could not open Firefox sidebar:', err);
176
176
+
});
177
177
+
}
178
178
+
return;
179
179
+
}
180
180
+
181
181
+
handleContextMenuAction(info, tab);
182
182
+
});
183
183
+
184
184
+
async function handleContextMenuAction(info: any, tab?: any) {
185
185
+
const apiUrl = await apiUrlItem.getValue();
186
186
+
187
187
+
if (info.menuItemId === 'margin-bookmark' && tab?.url) {
188
188
+
const session = await checkSession();
189
189
+
if (!session.authenticated) {
190
190
+
await browser.tabs.create({ url: `${apiUrl}/login` });
191
191
+
return;
192
192
+
}
193
193
+
194
194
+
const result = await createBookmark({
195
195
+
url: tab.url,
196
196
+
title: tab.title,
197
197
+
});
198
198
+
199
199
+
if (result.success) {
200
200
+
showNotification('Margin', 'Page bookmarked!');
201
201
+
}
202
202
+
return;
203
203
+
}
204
204
+
205
205
+
if (info.menuItemId === 'margin-annotate' && tab?.url && info.selectionText) {
206
206
+
const session = await checkSession();
207
207
+
if (!session.authenticated) {
208
208
+
await browser.tabs.create({ url: `${apiUrl}/login` });
209
209
+
return;
210
210
+
}
211
211
+
212
212
+
try {
213
213
+
await browser.tabs.sendMessage(tab.id!, {
214
214
+
type: 'SHOW_INLINE_ANNOTATE',
215
215
+
data: {
216
216
+
url: tab.url,
217
217
+
title: tab.title,
218
218
+
selector: {
219
219
+
type: 'TextQuoteSelector',
220
220
+
exact: info.selectionText,
221
221
+
},
222
222
+
},
223
223
+
});
224
224
+
} catch {
225
225
+
let composeUrl = `${apiUrl}/new?url=${encodeURIComponent(tab.url)}`;
226
226
+
composeUrl += `&selector=${encodeURIComponent(
227
227
+
JSON.stringify({
228
228
+
type: 'TextQuoteSelector',
229
229
+
exact: info.selectionText,
230
230
+
})
231
231
+
)}`;
232
232
+
await browser.tabs.create({ url: composeUrl });
233
233
+
}
234
234
+
return;
235
235
+
}
236
236
+
237
237
+
if (info.menuItemId === 'margin-highlight' && tab?.url && info.selectionText) {
238
238
+
const session = await checkSession();
239
239
+
if (!session.authenticated) {
240
240
+
await browser.tabs.create({ url: `${apiUrl}/login` });
241
241
+
return;
242
242
+
}
243
243
+
244
244
+
const result = await createHighlight({
245
245
+
url: tab.url,
246
246
+
title: tab.title,
247
247
+
selector: {
248
248
+
type: 'TextQuoteSelector',
249
249
+
exact: info.selectionText,
250
250
+
},
251
251
+
});
252
252
+
253
253
+
if (result.success) {
254
254
+
showNotification('Margin', 'Text highlighted!');
255
255
+
try {
256
256
+
await browser.tabs.sendMessage(tab.id!, { type: 'REFRESH_ANNOTATIONS' });
257
257
+
} catch {
258
258
+
/* ignore */
259
259
+
}
260
260
+
}
261
261
+
return;
262
262
+
}
263
263
+
}
264
264
+
265
265
+
function showNotification(title: string, message: string) {
266
266
+
const browserAny = browser as any;
267
267
+
if (browserAny.notifications) {
268
268
+
browserAny.notifications.create({
269
269
+
type: 'basic',
270
270
+
iconUrl: '/icons/icon-128.png',
271
271
+
title,
272
272
+
message,
273
273
+
});
274
274
+
}
275
275
+
}
276
276
+
277
277
+
browser.commands?.onCommand.addListener((command) => {
278
278
+
if (command === 'open-sidebar') {
279
279
+
const browserAny = browser as any;
280
280
+
if (browserAny.sidePanel) {
281
281
+
chrome.windows.getCurrent((win) => {
282
282
+
if (win?.id) {
283
283
+
browserAny.sidePanel.open({ windowId: win.id }).catch((err: Error) => {
284
284
+
console.error('Could not open side panel:', err);
285
285
+
});
286
286
+
}
287
287
+
});
288
288
+
} else if (browserAny.sidebarAction) {
289
289
+
browserAny.sidebarAction.open().catch((err: Error) => {
290
290
+
console.warn('Could not open Firefox sidebar:', err);
291
291
+
});
292
292
+
}
293
293
+
return;
294
294
+
}
295
295
+
296
296
+
handleCommandAsync(command);
297
297
+
});
298
298
+
299
299
+
async function handleCommandAsync(command: string) {
300
300
+
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
301
301
+
302
302
+
if (command === 'toggle-overlay') {
303
303
+
const current = await overlayEnabledItem.getValue();
304
304
+
await overlayEnabledItem.setValue(!current);
305
305
+
return;
306
306
+
}
307
307
+
308
308
+
if (command === 'bookmark-page' && tab?.url) {
309
309
+
const session = await checkSession();
310
310
+
if (!session.authenticated) {
311
311
+
const apiUrl = await apiUrlItem.getValue();
312
312
+
await browser.tabs.create({ url: `${apiUrl}/login` });
313
313
+
return;
314
314
+
}
315
315
+
316
316
+
const result = await createBookmark({
317
317
+
url: tab.url,
318
318
+
title: tab.title,
319
319
+
});
320
320
+
321
321
+
if (result.success) {
322
322
+
showNotification('Margin', 'Page bookmarked!');
323
323
+
}
324
324
+
return;
325
325
+
}
326
326
+
327
327
+
if ((command === 'annotate-selection' || command === 'highlight-selection') && tab?.id) {
328
328
+
try {
329
329
+
const selection = (await browser.tabs.sendMessage(tab.id, { type: 'GET_SELECTION' })) as
330
330
+
| { text?: string }
331
331
+
| undefined;
332
332
+
if (!selection?.text) return;
333
333
+
334
334
+
const session = await checkSession();
335
335
+
if (!session.authenticated) {
336
336
+
const apiUrl = await apiUrlItem.getValue();
337
337
+
await browser.tabs.create({ url: `${apiUrl}/login` });
338
338
+
return;
339
339
+
}
340
340
+
341
341
+
if (command === 'annotate-selection') {
342
342
+
await browser.tabs.sendMessage(tab.id, {
343
343
+
type: 'SHOW_INLINE_ANNOTATE',
344
344
+
data: { selector: { exact: selection.text } },
345
345
+
});
346
346
+
} else if (command === 'highlight-selection') {
347
347
+
const result = await createHighlight({
348
348
+
url: tab.url!,
349
349
+
title: tab.title,
350
350
+
selector: {
351
351
+
type: 'TextQuoteSelector',
352
352
+
exact: selection.text,
353
353
+
},
354
354
+
});
355
355
+
356
356
+
if (result.success) {
357
357
+
showNotification('Margin', 'Text highlighted!');
358
358
+
await browser.tabs.sendMessage(tab.id, { type: 'REFRESH_ANNOTATIONS' });
359
359
+
}
360
360
+
}
361
361
+
} catch (err) {
362
362
+
console.error('Error handling keyboard shortcut:', err);
363
363
+
}
364
364
+
}
365
365
+
}
366
366
+
});
+754
extension/src/entrypoints/content.ts
···
1
1
+
import { sendMessage } from '@/utils/messaging';
2
2
+
import { overlayEnabledItem, themeItem } from '@/utils/storage';
3
3
+
import { overlayStyles } from '@/utils/overlay-styles';
4
4
+
import { DOMTextMatcher } from '@/utils/text-matcher';
5
5
+
import type { Annotation } from '@/utils/types';
6
6
+
import { APP_URL } from '@/utils/types';
7
7
+
8
8
+
const Icons = {
9
9
+
annotate: `<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>`,
10
10
+
highlight: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"/></svg>`,
11
11
+
bookmark: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg>`,
12
12
+
close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,
13
13
+
reply: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>`,
14
14
+
share: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" x2="12" y1="2" y2="15"/></svg>`,
15
15
+
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>`,
16
16
+
highlightMarker: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5Z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/></svg>`,
17
17
+
};
18
18
+
19
19
+
export default defineContentScript({
20
20
+
matches: ['<all_urls>'],
21
21
+
runAt: 'document_idle',
22
22
+
cssInjectionMode: 'ui',
23
23
+
24
24
+
async main(ctx) {
25
25
+
let overlayHost: HTMLElement | null = null;
26
26
+
let shadowRoot: ShadowRoot | null = null;
27
27
+
let popoverEl: HTMLElement | null = null;
28
28
+
let hoverIndicator: HTMLElement | null = null;
29
29
+
let composeModal: HTMLElement | null = null;
30
30
+
let activeItems: Array<{ range: Range; item: Annotation }> = [];
31
31
+
let cachedMatcher: DOMTextMatcher | null = null;
32
32
+
const injectedStyles = new Set<string>();
33
33
+
let overlayEnabled = true;
34
34
+
35
35
+
function initOverlay() {
36
36
+
overlayHost = document.createElement('div');
37
37
+
overlayHost.id = 'margin-overlay-host';
38
38
+
overlayHost.style.cssText = `
39
39
+
position: absolute; top: 0; left: 0; width: 100%;
40
40
+
height: 0; overflow: visible;
41
41
+
pointer-events: none; z-index: 2147483647;
42
42
+
`;
43
43
+
if (document.body) {
44
44
+
document.body.appendChild(overlayHost);
45
45
+
} else {
46
46
+
document.documentElement.appendChild(overlayHost);
47
47
+
}
48
48
+
49
49
+
shadowRoot = overlayHost.attachShadow({ mode: 'open' });
50
50
+
51
51
+
const styleEl = document.createElement('style');
52
52
+
styleEl.textContent = overlayStyles;
53
53
+
shadowRoot.appendChild(styleEl);
54
54
+
const overlayContainer = document.createElement('div');
55
55
+
overlayContainer.className = 'margin-overlay';
56
56
+
overlayContainer.id = 'margin-overlay-container';
57
57
+
shadowRoot.appendChild(overlayContainer);
58
58
+
59
59
+
document.addEventListener('mousemove', handleMouseMove);
60
60
+
document.addEventListener('click', handleDocumentClick, true);
61
61
+
document.addEventListener('keydown', handleKeyDown);
62
62
+
}
63
63
+
if (document.body) {
64
64
+
initOverlay();
65
65
+
} else {
66
66
+
document.addEventListener('DOMContentLoaded', initOverlay);
67
67
+
}
68
68
+
69
69
+
overlayEnabledItem.getValue().then((enabled) => {
70
70
+
overlayEnabled = enabled;
71
71
+
if (!enabled && overlayHost) {
72
72
+
overlayHost.style.display = 'none';
73
73
+
sendMessage('updateBadge', { count: 0 });
74
74
+
} else {
75
75
+
applyTheme();
76
76
+
if ('requestIdleCallback' in window) {
77
77
+
requestIdleCallback(() => fetchAnnotations(), { timeout: 2000 });
78
78
+
} else {
79
79
+
setTimeout(() => fetchAnnotations(), 100);
80
80
+
}
81
81
+
}
82
82
+
});
83
83
+
84
84
+
ctx.onInvalidated(() => {
85
85
+
document.removeEventListener('mousemove', handleMouseMove);
86
86
+
document.removeEventListener('click', handleDocumentClick, true);
87
87
+
document.removeEventListener('keydown', handleKeyDown);
88
88
+
overlayHost?.remove();
89
89
+
});
90
90
+
91
91
+
async function applyTheme() {
92
92
+
if (!overlayHost) return;
93
93
+
const theme = await themeItem.getValue();
94
94
+
overlayHost.classList.remove('light', 'dark');
95
95
+
if (theme === 'system' || !theme) {
96
96
+
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
97
97
+
overlayHost.classList.add('light');
98
98
+
}
99
99
+
} else {
100
100
+
overlayHost.classList.add(theme);
101
101
+
}
102
102
+
}
103
103
+
104
104
+
themeItem.watch((newTheme) => {
105
105
+
if (overlayHost) {
106
106
+
overlayHost.classList.remove('light', 'dark');
107
107
+
if (newTheme === 'system') {
108
108
+
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
109
109
+
overlayHost.classList.add('light');
110
110
+
}
111
111
+
} else {
112
112
+
overlayHost.classList.add(newTheme);
113
113
+
}
114
114
+
}
115
115
+
});
116
116
+
117
117
+
overlayEnabledItem.watch((enabled) => {
118
118
+
overlayEnabled = enabled;
119
119
+
if (overlayHost) {
120
120
+
overlayHost.style.display = enabled ? '' : 'none';
121
121
+
if (enabled) {
122
122
+
fetchAnnotations();
123
123
+
} else {
124
124
+
activeItems = [];
125
125
+
if (typeof CSS !== 'undefined' && CSS.highlights) {
126
126
+
CSS.highlights.clear();
127
127
+
}
128
128
+
sendMessage('updateBadge', { count: 0 });
129
129
+
}
130
130
+
}
131
131
+
});
132
132
+
133
133
+
function handleKeyDown(e: KeyboardEvent) {
134
134
+
if (e.key === 'Escape') {
135
135
+
if (composeModal) {
136
136
+
composeModal.remove();
137
137
+
composeModal = null;
138
138
+
}
139
139
+
if (popoverEl) {
140
140
+
popoverEl.remove();
141
141
+
popoverEl = null;
142
142
+
}
143
143
+
}
144
144
+
}
145
145
+
146
146
+
function showComposeModal(quoteText: string) {
147
147
+
if (!shadowRoot) return;
148
148
+
149
149
+
const container = shadowRoot.getElementById('margin-overlay-container');
150
150
+
if (!container) return;
151
151
+
152
152
+
if (composeModal) composeModal.remove();
153
153
+
154
154
+
composeModal = document.createElement('div');
155
155
+
composeModal.className = 'inline-compose-modal';
156
156
+
157
157
+
const left = Math.max(20, (window.innerWidth - 380) / 2);
158
158
+
const top = Math.max(60, window.innerHeight * 0.2);
159
159
+
160
160
+
composeModal.style.left = `${left}px`;
161
161
+
composeModal.style.top = `${top}px`;
162
162
+
163
163
+
const truncatedQuote = quoteText.length > 150 ? quoteText.slice(0, 150) + '...' : quoteText;
164
164
+
165
165
+
composeModal.innerHTML = `
166
166
+
<div class="compose-header">
167
167
+
<span class="compose-title">New Annotation</span>
168
168
+
<button class="compose-close">${Icons.close}</button>
169
169
+
</div>
170
170
+
<div class="compose-body">
171
171
+
<div class="inline-compose-quote">"${escapeHtml(truncatedQuote)}"</div>
172
172
+
<textarea class="inline-compose-textarea" placeholder="Write your annotation..."></textarea>
173
173
+
</div>
174
174
+
<div class="compose-footer">
175
175
+
<button class="btn-cancel">Cancel</button>
176
176
+
<button class="btn-submit">Post</button>
177
177
+
</div>
178
178
+
`;
179
179
+
180
180
+
composeModal.querySelector('.compose-close')?.addEventListener('click', () => {
181
181
+
composeModal?.remove();
182
182
+
composeModal = null;
183
183
+
});
184
184
+
185
185
+
composeModal.querySelector('.btn-cancel')?.addEventListener('click', () => {
186
186
+
composeModal?.remove();
187
187
+
composeModal = null;
188
188
+
});
189
189
+
190
190
+
const textarea = composeModal.querySelector(
191
191
+
'.inline-compose-textarea'
192
192
+
) as HTMLTextAreaElement;
193
193
+
const submitBtn = composeModal.querySelector('.btn-submit') as HTMLButtonElement;
194
194
+
195
195
+
submitBtn.addEventListener('click', async () => {
196
196
+
const text = textarea?.value.trim();
197
197
+
if (!text) return;
198
198
+
199
199
+
submitBtn.disabled = true;
200
200
+
submitBtn.textContent = 'Posting...';
201
201
+
202
202
+
try {
203
203
+
await sendMessage('createAnnotation', {
204
204
+
url: window.location.href,
205
205
+
title: document.title,
206
206
+
text,
207
207
+
selector: { type: 'TextQuoteSelector', exact: quoteText },
208
208
+
});
209
209
+
210
210
+
showToast('Annotation created!', 'success');
211
211
+
composeModal?.remove();
212
212
+
composeModal = null;
213
213
+
214
214
+
setTimeout(() => fetchAnnotations(), 500);
215
215
+
} catch (error) {
216
216
+
console.error('Failed to create annotation:', error);
217
217
+
showToast('Failed to create annotation', 'error');
218
218
+
submitBtn.disabled = false;
219
219
+
submitBtn.textContent = 'Post';
220
220
+
}
221
221
+
});
222
222
+
223
223
+
container.appendChild(composeModal);
224
224
+
setTimeout(() => textarea?.focus(), 100);
225
225
+
}
226
226
+
browser.runtime.onMessage.addListener((message: any) => {
227
227
+
if (message.type === 'SHOW_INLINE_ANNOTATE' && message.data?.selector?.exact) {
228
228
+
showComposeModal(message.data.selector.exact);
229
229
+
}
230
230
+
if (message.type === 'REFRESH_ANNOTATIONS') {
231
231
+
fetchAnnotations();
232
232
+
}
233
233
+
if (message.type === 'SCROLL_TO_TEXT' && message.text) {
234
234
+
scrollToText(message.text);
235
235
+
}
236
236
+
if (message.type === 'GET_SELECTION') {
237
237
+
const selection = window.getSelection();
238
238
+
const text = selection?.toString().trim() || '';
239
239
+
return Promise.resolve({ text });
240
240
+
}
241
241
+
});
242
242
+
243
243
+
function scrollToText(text: string) {
244
244
+
if (!text || text.length < 10) return;
245
245
+
246
246
+
const searchText = text.slice(0, 150);
247
247
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
248
248
+
249
249
+
let node: Text | null;
250
250
+
while ((node = walker.nextNode() as Text | null)) {
251
251
+
const content = node.textContent || '';
252
252
+
const index = content.indexOf(searchText.slice(0, 50));
253
253
+
if (index !== -1) {
254
254
+
const range = document.createRange();
255
255
+
range.setStart(node, index);
256
256
+
range.setEnd(node, Math.min(index + searchText.length, content.length));
257
257
+
258
258
+
const rect = range.getBoundingClientRect();
259
259
+
const scrollY = window.scrollY + rect.top - window.innerHeight / 3;
260
260
+
261
261
+
window.scrollTo({ top: scrollY, behavior: 'smooth' });
262
262
+
263
263
+
const highlight = document.createElement('mark');
264
264
+
highlight.style.cssText =
265
265
+
'background: #6366f1; color: white; padding: 2px 0; border-radius: 2px; transition: background 0.5s;';
266
266
+
range.surroundContents(highlight);
267
267
+
268
268
+
setTimeout(() => {
269
269
+
highlight.style.background = 'transparent';
270
270
+
highlight.style.color = 'inherit';
271
271
+
setTimeout(() => {
272
272
+
const parent = highlight.parentNode;
273
273
+
if (parent) {
274
274
+
parent.replaceChild(
275
275
+
document.createTextNode(highlight.textContent || ''),
276
276
+
highlight
277
277
+
);
278
278
+
parent.normalize();
279
279
+
}
280
280
+
}, 500);
281
281
+
}, 1500);
282
282
+
283
283
+
return;
284
284
+
}
285
285
+
}
286
286
+
}
287
287
+
288
288
+
function showToast(message: string, type: 'success' | 'error' = 'success') {
289
289
+
if (!shadowRoot) return;
290
290
+
291
291
+
const container = shadowRoot.getElementById('margin-overlay-container');
292
292
+
if (!container) return;
293
293
+
294
294
+
container.querySelectorAll('.margin-toast').forEach((el) => el.remove());
295
295
+
296
296
+
const toast = document.createElement('div');
297
297
+
toast.className = `margin-toast ${type === 'success' ? 'toast-success' : ''}`;
298
298
+
toast.innerHTML = `
299
299
+
<span class="toast-icon">${type === 'success' ? Icons.check : Icons.close}</span>
300
300
+
<span>${message}</span>
301
301
+
`;
302
302
+
303
303
+
container.appendChild(toast);
304
304
+
305
305
+
setTimeout(() => {
306
306
+
toast.classList.add('toast-out');
307
307
+
setTimeout(() => toast.remove(), 200);
308
308
+
}, 2500);
309
309
+
}
310
310
+
311
311
+
async function fetchAnnotations(retryCount = 0) {
312
312
+
if (!overlayEnabled) {
313
313
+
sendMessage('updateBadge', { count: 0 });
314
314
+
return;
315
315
+
}
316
316
+
317
317
+
try {
318
318
+
const _citedUrls = Array.from(document.querySelectorAll('[cite]'))
319
319
+
.map((el) => el.getAttribute('cite'))
320
320
+
.filter((url): url is string => !!url && url.startsWith('http'));
321
321
+
322
322
+
const annotations = await sendMessage('getAnnotations', { url: window.location.href });
323
323
+
324
324
+
sendMessage('updateBadge', { count: annotations?.length || 0 });
325
325
+
326
326
+
if (annotations) {
327
327
+
sendMessage('cacheAnnotations', { url: window.location.href, annotations });
328
328
+
}
329
329
+
330
330
+
if (annotations && annotations.length > 0) {
331
331
+
renderBadges(annotations);
332
332
+
} else if (retryCount < 3) {
333
333
+
setTimeout(() => fetchAnnotations(retryCount + 1), 1000 * (retryCount + 1));
334
334
+
}
335
335
+
} catch (error) {
336
336
+
console.error('Failed to fetch annotations:', error);
337
337
+
if (retryCount < 3) {
338
338
+
setTimeout(() => fetchAnnotations(retryCount + 1), 1000 * (retryCount + 1));
339
339
+
}
340
340
+
}
341
341
+
}
342
342
+
343
343
+
function renderBadges(annotations: Annotation[]) {
344
344
+
if (!shadowRoot) return;
345
345
+
346
346
+
activeItems = [];
347
347
+
const rangesByColor: Record<string, Range[]> = {};
348
348
+
349
349
+
if (!cachedMatcher) {
350
350
+
cachedMatcher = new DOMTextMatcher();
351
351
+
}
352
352
+
const matcher = cachedMatcher;
353
353
+
354
354
+
annotations.forEach((item) => {
355
355
+
const selector = item.target?.selector || item.selector;
356
356
+
if (!selector?.exact) return;
357
357
+
358
358
+
const range = matcher.findRange(selector.exact);
359
359
+
if (range) {
360
360
+
activeItems.push({ range, item });
361
361
+
362
362
+
const isHighlight = (item as any).type === 'Highlight';
363
363
+
const defaultColor = isHighlight ? '#f59e0b' : '#6366f1';
364
364
+
const color = item.color || defaultColor;
365
365
+
if (!rangesByColor[color]) rangesByColor[color] = [];
366
366
+
rangesByColor[color].push(range);
367
367
+
}
368
368
+
});
369
369
+
370
370
+
if (typeof CSS !== 'undefined' && CSS.highlights) {
371
371
+
CSS.highlights.clear();
372
372
+
for (const [color, ranges] of Object.entries(rangesByColor)) {
373
373
+
const highlight = new Highlight(...ranges);
374
374
+
const safeColor = color.replace(/[^a-zA-Z0-9]/g, '');
375
375
+
const name = `margin-hl-${safeColor}`;
376
376
+
CSS.highlights.set(name, highlight);
377
377
+
injectHighlightStyle(name, color);
378
378
+
}
379
379
+
}
380
380
+
}
381
381
+
382
382
+
function injectHighlightStyle(name: string, color: string) {
383
383
+
if (injectedStyles.has(name)) return;
384
384
+
const style = document.createElement('style');
385
385
+
style.textContent = `
386
386
+
::highlight(${name}) {
387
387
+
text-decoration: underline;
388
388
+
text-decoration-color: ${color};
389
389
+
text-decoration-thickness: 2px;
390
390
+
text-underline-offset: 2px;
391
391
+
cursor: pointer;
392
392
+
}
393
393
+
`;
394
394
+
document.head.appendChild(style);
395
395
+
injectedStyles.add(name);
396
396
+
}
397
397
+
398
398
+
function handleMouseMove(e: MouseEvent) {
399
399
+
if (!overlayEnabled || !overlayHost) return;
400
400
+
401
401
+
const x = e.clientX;
402
402
+
const y = e.clientY;
403
403
+
404
404
+
const foundItems: Array<{ range: Range; item: Annotation; rect: DOMRect }> = [];
405
405
+
let firstRange: Range | null = null;
406
406
+
407
407
+
for (const { range, item } of activeItems) {
408
408
+
const rects = range.getClientRects();
409
409
+
for (const rect of rects) {
410
410
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
411
411
+
let container: Node | null = range.commonAncestorContainer;
412
412
+
if (container.nodeType === Node.TEXT_NODE) {
413
413
+
container = container.parentNode;
414
414
+
}
415
415
+
416
416
+
if (
417
417
+
container &&
418
418
+
((e.target as Node).contains(container) || container.contains(e.target as Node))
419
419
+
) {
420
420
+
if (!firstRange) firstRange = range;
421
421
+
if (!foundItems.some((f) => f.item === item)) {
422
422
+
foundItems.push({ range, item, rect });
423
423
+
}
424
424
+
}
425
425
+
break;
426
426
+
}
427
427
+
}
428
428
+
}
429
429
+
430
430
+
if (foundItems.length > 0 && shadowRoot) {
431
431
+
document.body.style.cursor = 'pointer';
432
432
+
433
433
+
if (!hoverIndicator) {
434
434
+
const container = shadowRoot.getElementById('margin-overlay-container');
435
435
+
if (container) {
436
436
+
hoverIndicator = document.createElement('div');
437
437
+
hoverIndicator.className = 'margin-hover-indicator';
438
438
+
container.appendChild(hoverIndicator);
439
439
+
}
440
440
+
}
441
441
+
442
442
+
if (hoverIndicator && firstRange) {
443
443
+
const authorsMap = new Map<string, any>();
444
444
+
foundItems.forEach(({ item }) => {
445
445
+
const author = item.author || item.creator || {};
446
446
+
const id = author.did || author.handle || 'unknown';
447
447
+
if (!authorsMap.has(id)) {
448
448
+
authorsMap.set(id, author);
449
449
+
}
450
450
+
});
451
451
+
452
452
+
const uniqueAuthors = Array.from(authorsMap.values());
453
453
+
const maxShow = 3;
454
454
+
const displayAuthors = uniqueAuthors.slice(0, maxShow);
455
455
+
const overflow = uniqueAuthors.length - maxShow;
456
456
+
457
457
+
let html = displayAuthors
458
458
+
.map((author, i) => {
459
459
+
const avatar = author.avatar;
460
460
+
const handle = author.handle || 'U';
461
461
+
const marginLeft = i === 0 ? '0' : '-8px';
462
462
+
463
463
+
if (avatar) {
464
464
+
return `<img src="${avatar}" style="width: 24px; height: 24px; border-radius: 50%; object-fit: cover; border: 2px solid #09090b; margin-left: ${marginLeft};">`;
465
465
+
} else {
466
466
+
return `<div style="width: 24px; height: 24px; border-radius: 50%; background: #6366f1; color: white; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; font-family: -apple-system, sans-serif; border: 2px solid #09090b; margin-left: ${marginLeft};">${handle[0]?.toUpperCase() || 'U'}</div>`;
467
467
+
}
468
468
+
})
469
469
+
.join('');
470
470
+
471
471
+
if (overflow > 0) {
472
472
+
html += `<div style="width: 24px; height: 24px; border-radius: 50%; background: #27272a; color: #a1a1aa; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; font-family: -apple-system, sans-serif; border: 2px solid #09090b; margin-left: -8px;">+${overflow}</div>`;
473
473
+
}
474
474
+
475
475
+
hoverIndicator.innerHTML = html;
476
476
+
477
477
+
const firstRect = firstRange.getClientRects()[0];
478
478
+
const totalWidth =
479
479
+
Math.min(uniqueAuthors.length, maxShow + (overflow > 0 ? 1 : 0)) * 18 + 8;
480
480
+
const leftPos = firstRect.left - totalWidth;
481
481
+
const topPos = firstRect.top + firstRect.height / 2 - 12;
482
482
+
483
483
+
hoverIndicator.style.left = `${leftPos}px`;
484
484
+
hoverIndicator.style.top = `${topPos}px`;
485
485
+
hoverIndicator.classList.add('visible');
486
486
+
}
487
487
+
} else {
488
488
+
document.body.style.cursor = '';
489
489
+
if (hoverIndicator) {
490
490
+
hoverIndicator.classList.remove('visible');
491
491
+
}
492
492
+
}
493
493
+
}
494
494
+
495
495
+
function handleDocumentClick(e: MouseEvent) {
496
496
+
if (!overlayEnabled || !overlayHost) return;
497
497
+
498
498
+
const x = e.clientX;
499
499
+
const y = e.clientY;
500
500
+
501
501
+
if (popoverEl) {
502
502
+
const rect = popoverEl.getBoundingClientRect();
503
503
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
504
504
+
return;
505
505
+
}
506
506
+
}
507
507
+
508
508
+
if (composeModal) {
509
509
+
const rect = composeModal.getBoundingClientRect();
510
510
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
511
511
+
return;
512
512
+
}
513
513
+
composeModal.remove();
514
514
+
composeModal = null;
515
515
+
}
516
516
+
517
517
+
const clickedItems: Annotation[] = [];
518
518
+
for (const { range, item } of activeItems) {
519
519
+
const rects = range.getClientRects();
520
520
+
for (const rect of rects) {
521
521
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
522
522
+
let container: Node | null = range.commonAncestorContainer;
523
523
+
if (container.nodeType === Node.TEXT_NODE) {
524
524
+
container = container.parentNode;
525
525
+
}
526
526
+
527
527
+
if (
528
528
+
container &&
529
529
+
((e.target as Node).contains(container) || container.contains(e.target as Node))
530
530
+
) {
531
531
+
if (!clickedItems.includes(item)) {
532
532
+
clickedItems.push(item);
533
533
+
}
534
534
+
}
535
535
+
break;
536
536
+
}
537
537
+
}
538
538
+
}
539
539
+
540
540
+
if (clickedItems.length > 0) {
541
541
+
e.preventDefault();
542
542
+
e.stopPropagation();
543
543
+
544
544
+
if (popoverEl) {
545
545
+
const currentIds = popoverEl.dataset.itemIds;
546
546
+
const newIds = clickedItems
547
547
+
.map((i) => i.uri || i.id)
548
548
+
.sort()
549
549
+
.join(',');
550
550
+
if (currentIds === newIds) {
551
551
+
popoverEl.remove();
552
552
+
popoverEl = null;
553
553
+
return;
554
554
+
}
555
555
+
}
556
556
+
557
557
+
const firstItem = clickedItems[0];
558
558
+
const match = activeItems.find((x) => x.item === firstItem);
559
559
+
if (match) {
560
560
+
const rects = match.range.getClientRects();
561
561
+
if (rects.length > 0) {
562
562
+
const rect = rects[0];
563
563
+
const top = rect.top + window.scrollY;
564
564
+
const left = rect.left + window.scrollX;
565
565
+
showPopover(clickedItems, top, left);
566
566
+
}
567
567
+
}
568
568
+
} else {
569
569
+
if (popoverEl) {
570
570
+
popoverEl.remove();
571
571
+
popoverEl = null;
572
572
+
}
573
573
+
}
574
574
+
}
575
575
+
576
576
+
function showPopover(items: Annotation[], top: number, left: number) {
577
577
+
if (!shadowRoot) return;
578
578
+
if (popoverEl) popoverEl.remove();
579
579
+
580
580
+
const container = shadowRoot.getElementById('margin-overlay-container');
581
581
+
if (!container) return;
582
582
+
583
583
+
popoverEl = document.createElement('div');
584
584
+
popoverEl.className = 'margin-popover';
585
585
+
586
586
+
const ids = items
587
587
+
.map((i) => i.uri || i.id)
588
588
+
.sort()
589
589
+
.join(',');
590
590
+
popoverEl.dataset.itemIds = ids;
591
591
+
592
592
+
const popWidth = 320;
593
593
+
const screenWidth = window.innerWidth;
594
594
+
let finalLeft = left;
595
595
+
if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20;
596
596
+
if (finalLeft < 10) finalLeft = 10;
597
597
+
598
598
+
popoverEl.style.top = `${top + 24}px`;
599
599
+
popoverEl.style.left = `${finalLeft}px`;
600
600
+
601
601
+
const count = items.length;
602
602
+
const title = count === 1 ? 'Annotation' : `Annotations`;
603
603
+
604
604
+
const contentHtml = items
605
605
+
.map((item) => {
606
606
+
const author = item.author || item.creator || {};
607
607
+
const handle = author.handle || 'User';
608
608
+
const avatar = author.avatar;
609
609
+
const text = item.body?.value || item.text || '';
610
610
+
const id = item.id || item.uri;
611
611
+
const isHighlight = (item as any).type === 'Highlight';
612
612
+
const createdAt = item.createdAt ? formatRelativeTime(item.createdAt) : '';
613
613
+
614
614
+
let avatarHtml = `<div class="comment-avatar">${handle[0]?.toUpperCase() || 'U'}</div>`;
615
615
+
if (avatar) {
616
616
+
avatarHtml = `<img src="${avatar}" class="comment-avatar" style="object-fit: cover;">`;
617
617
+
}
618
618
+
619
619
+
let bodyHtml = '';
620
620
+
if (isHighlight && !text) {
621
621
+
bodyHtml = `<div class="highlight-badge">${Icons.highlightMarker} Highlighted</div>`;
622
622
+
} else {
623
623
+
bodyHtml = `<div class="comment-text">${escapeHtml(text)}</div>`;
624
624
+
}
625
625
+
626
626
+
return `
627
627
+
<div class="comment-item">
628
628
+
<div class="comment-header">
629
629
+
${avatarHtml}
630
630
+
<div class="comment-meta">
631
631
+
<span class="comment-handle">@${handle}</span>
632
632
+
${createdAt ? `<span class="comment-time">${createdAt}</span>` : ''}
633
633
+
</div>
634
634
+
</div>
635
635
+
${bodyHtml}
636
636
+
<div class="comment-actions">
637
637
+
${!isHighlight ? `<button class="comment-action-btn btn-reply" data-id="${id}">${Icons.reply} Reply</button>` : ''}
638
638
+
<button class="comment-action-btn btn-share" data-id="${id}" data-text="${escapeHtml(text)}">${Icons.share} Share</button>
639
639
+
</div>
640
640
+
</div>
641
641
+
`;
642
642
+
})
643
643
+
.join('');
644
644
+
645
645
+
popoverEl.innerHTML = `
646
646
+
<div class="popover-header">
647
647
+
<span class="popover-title">${title} <span class="popover-count">${count}</span></span>
648
648
+
<button class="popover-close">${Icons.close}</button>
649
649
+
</div>
650
650
+
<div class="popover-scroll-area">
651
651
+
${contentHtml}
652
652
+
</div>
653
653
+
`;
654
654
+
655
655
+
popoverEl.querySelector('.popover-close')?.addEventListener('click', (e) => {
656
656
+
e.stopPropagation();
657
657
+
popoverEl?.remove();
658
658
+
popoverEl = null;
659
659
+
});
660
660
+
661
661
+
popoverEl.querySelectorAll('.btn-reply').forEach((btn) => {
662
662
+
btn.addEventListener('click', (e) => {
663
663
+
e.stopPropagation();
664
664
+
const id = (btn as HTMLElement).getAttribute('data-id');
665
665
+
if (id) {
666
666
+
window.open(`${APP_URL}/annotation/${encodeURIComponent(id)}`, '_blank');
667
667
+
}
668
668
+
});
669
669
+
});
670
670
+
671
671
+
popoverEl.querySelectorAll('.btn-share').forEach((btn) => {
672
672
+
btn.addEventListener('click', async (e) => {
673
673
+
e.stopPropagation();
674
674
+
const id = (btn as HTMLElement).getAttribute('data-id');
675
675
+
const text = (btn as HTMLElement).getAttribute('data-text');
676
676
+
const url = `${APP_URL}/annotation/${encodeURIComponent(id || '')}`;
677
677
+
const shareText = text ? `${text}\n${url}` : url;
678
678
+
679
679
+
try {
680
680
+
await navigator.clipboard.writeText(shareText);
681
681
+
const originalHtml = btn.innerHTML;
682
682
+
btn.innerHTML = `${Icons.check} Copied!`;
683
683
+
setTimeout(() => (btn.innerHTML = originalHtml), 2000);
684
684
+
} catch (err) {
685
685
+
console.error('Failed to copy', err);
686
686
+
}
687
687
+
});
688
688
+
});
689
689
+
690
690
+
container.appendChild(popoverEl);
691
691
+
}
692
692
+
693
693
+
function formatRelativeTime(dateStr: string): string {
694
694
+
const date = new Date(dateStr);
695
695
+
const now = new Date();
696
696
+
const diffMs = now.getTime() - date.getTime();
697
697
+
const diffMins = Math.floor(diffMs / 60000);
698
698
+
const diffHours = Math.floor(diffMs / 3600000);
699
699
+
const diffDays = Math.floor(diffMs / 86400000);
700
700
+
701
701
+
if (diffMins < 1) return 'just now';
702
702
+
if (diffMins < 60) return `${diffMins}m`;
703
703
+
if (diffHours < 24) return `${diffHours}h`;
704
704
+
if (diffDays < 7) return `${diffDays}d`;
705
705
+
return date.toLocaleDateString();
706
706
+
}
707
707
+
708
708
+
function escapeHtml(text: string): string {
709
709
+
const div = document.createElement('div');
710
710
+
div.textContent = text;
711
711
+
return div.innerHTML;
712
712
+
}
713
713
+
714
714
+
let lastUrl = window.location.href;
715
715
+
function checkUrlChange() {
716
716
+
if (window.location.href !== lastUrl) {
717
717
+
lastUrl = window.location.href;
718
718
+
onUrlChange();
719
719
+
}
720
720
+
}
721
721
+
722
722
+
function onUrlChange() {
723
723
+
if (typeof CSS !== 'undefined' && CSS.highlights) {
724
724
+
CSS.highlights.clear();
725
725
+
}
726
726
+
activeItems = [];
727
727
+
sendMessage('updateBadge', { count: 0 });
728
728
+
if (overlayEnabled) {
729
729
+
fetchAnnotations();
730
730
+
}
731
731
+
}
732
732
+
733
733
+
window.addEventListener('popstate', onUrlChange);
734
734
+
735
735
+
const originalPushState = history.pushState;
736
736
+
const originalReplaceState = history.replaceState;
737
737
+
738
738
+
history.pushState = function (...args) {
739
739
+
originalPushState.apply(this, args);
740
740
+
checkUrlChange();
741
741
+
};
742
742
+
743
743
+
history.replaceState = function (...args) {
744
744
+
originalReplaceState.apply(this, args);
745
745
+
checkUrlChange();
746
746
+
};
747
747
+
748
748
+
setInterval(checkUrlChange, 1000);
749
749
+
750
750
+
window.addEventListener('load', () => {
751
751
+
setTimeout(() => fetchAnnotations(), 500);
752
752
+
});
753
753
+
},
754
754
+
});
+12
extension/src/entrypoints/popup/index.html
···
1
1
+
<!DOCTYPE html>
2
2
+
<html lang="en" class="popup">
3
3
+
<head>
4
4
+
<meta charset="UTF-8" />
5
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
+
<title>Margin</title>
7
7
+
</head>
8
8
+
<body>
9
9
+
<div id="root"></div>
10
10
+
<script type="module" src="./main.tsx"></script>
11
11
+
</body>
12
12
+
</html>
+10
extension/src/entrypoints/popup/main.tsx
···
1
1
+
import React from 'react';
2
2
+
import ReactDOM from 'react-dom/client';
3
3
+
import App from '@/components/popup/App';
4
4
+
import '@/assets/styles.css';
5
5
+
6
6
+
ReactDOM.createRoot(document.getElementById('root')!).render(
7
7
+
<React.StrictMode>
8
8
+
<App />
9
9
+
</React.StrictMode>
10
10
+
);
+13
extension/src/entrypoints/sidepanel/index.html
···
1
1
+
<!DOCTYPE html>
2
2
+
<html lang="en">
3
3
+
<head>
4
4
+
<meta charset="UTF-8" />
5
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
+
<title>Margin</title>
7
7
+
<meta name="manifest.default_icon" content="{ '16': '/icons/icon-16.png', '32': '/icons/icon-32.png', '48': '/icons/icon-48.png' }" />
8
8
+
</head>
9
9
+
<body>
10
10
+
<div id="root"></div>
11
11
+
<script type="module" src="./main.tsx"></script>
12
12
+
</body>
13
13
+
</html>
+10
extension/src/entrypoints/sidepanel/main.tsx
···
1
1
+
import React from 'react';
2
2
+
import ReactDOM from 'react-dom/client';
3
3
+
import App from '@/components/sidepanel/App';
4
4
+
import '@/assets/styles.css';
5
5
+
6
6
+
ReactDOM.createRoot(document.getElementById('root')!).render(
7
7
+
<React.StrictMode>
8
8
+
<App />
9
9
+
</React.StrictMode>
10
10
+
);
+304
extension/src/utils/api.ts
···
1
1
+
import type { MarginSession, TextSelector } from './types';
2
2
+
import { apiUrlItem } from './storage';
3
3
+
4
4
+
async function getApiUrl(): Promise<string> {
5
5
+
return await apiUrlItem.getValue();
6
6
+
}
7
7
+
8
8
+
async function getSessionCookie(): Promise<string | null> {
9
9
+
try {
10
10
+
const apiUrl = await getApiUrl();
11
11
+
const cookie = await browser.cookies.get({
12
12
+
url: apiUrl,
13
13
+
name: 'margin_session',
14
14
+
});
15
15
+
return cookie?.value || null;
16
16
+
} catch (error) {
17
17
+
console.error('Get cookie error:', error);
18
18
+
return null;
19
19
+
}
20
20
+
}
21
21
+
22
22
+
export async function checkSession(): Promise<MarginSession> {
23
23
+
try {
24
24
+
const apiUrl = await getApiUrl();
25
25
+
const cookie = await getSessionCookie();
26
26
+
27
27
+
if (!cookie) {
28
28
+
return { authenticated: false };
29
29
+
}
30
30
+
31
31
+
const res = await fetch(`${apiUrl}/auth/session`, {
32
32
+
headers: {
33
33
+
'X-Session-Token': cookie,
34
34
+
},
35
35
+
});
36
36
+
37
37
+
if (!res.ok) {
38
38
+
return { authenticated: false };
39
39
+
}
40
40
+
41
41
+
const sessionData = await res.json();
42
42
+
43
43
+
if (!sessionData.did || !sessionData.handle) {
44
44
+
return { authenticated: false };
45
45
+
}
46
46
+
47
47
+
return {
48
48
+
authenticated: true,
49
49
+
did: sessionData.did,
50
50
+
handle: sessionData.handle,
51
51
+
accessJwt: sessionData.accessJwt,
52
52
+
refreshJwt: sessionData.refreshJwt,
53
53
+
};
54
54
+
} catch (error) {
55
55
+
console.error('Session check error:', error);
56
56
+
return { authenticated: false };
57
57
+
}
58
58
+
}
59
59
+
60
60
+
async function apiRequest(path: string, options: RequestInit = {}): Promise<Response> {
61
61
+
const apiUrl = await getApiUrl();
62
62
+
const cookie = await getSessionCookie();
63
63
+
64
64
+
const headers: Record<string, string> = {
65
65
+
'Content-Type': 'application/json',
66
66
+
...(options.headers as Record<string, string>),
67
67
+
};
68
68
+
69
69
+
if (cookie) {
70
70
+
headers['X-Session-Token'] = cookie;
71
71
+
}
72
72
+
73
73
+
const apiPath = path.startsWith('/api') ? path : `/api${path}`;
74
74
+
75
75
+
const response = await fetch(`${apiUrl}${apiPath}`, {
76
76
+
...options,
77
77
+
headers,
78
78
+
credentials: 'include',
79
79
+
});
80
80
+
81
81
+
return response;
82
82
+
}
83
83
+
84
84
+
export async function getAnnotations(url: string, citedUrls: string[] = []) {
85
85
+
try {
86
86
+
const apiUrl = await getApiUrl();
87
87
+
const uniqueUrls = [...new Set([url, ...citedUrls])];
88
88
+
89
89
+
const fetchPromises = uniqueUrls.map(async (u) => {
90
90
+
try {
91
91
+
const res = await fetch(`${apiUrl}/api/targets?source=${encodeURIComponent(u)}`);
92
92
+
if (!res.ok) return { annotations: [], highlights: [], bookmarks: [] };
93
93
+
return await res.json();
94
94
+
} catch {
95
95
+
return { annotations: [], highlights: [], bookmarks: [] };
96
96
+
}
97
97
+
});
98
98
+
99
99
+
const results = await Promise.all(fetchPromises);
100
100
+
const allItems: any[] = [];
101
101
+
const seenIds = new Set<string>();
102
102
+
103
103
+
results.forEach((data) => {
104
104
+
const items = [
105
105
+
...(data.annotations || []),
106
106
+
...(data.highlights || []),
107
107
+
...(data.bookmarks || []),
108
108
+
];
109
109
+
items.forEach((item: any) => {
110
110
+
const id = item.uri || item.id;
111
111
+
if (id && !seenIds.has(id)) {
112
112
+
seenIds.add(id);
113
113
+
allItems.push(item);
114
114
+
}
115
115
+
});
116
116
+
});
117
117
+
118
118
+
return allItems;
119
119
+
} catch (error) {
120
120
+
console.error('Get annotations error:', error);
121
121
+
return [];
122
122
+
}
123
123
+
}
124
124
+
125
125
+
export async function createAnnotation(data: {
126
126
+
url: string;
127
127
+
text: string;
128
128
+
title?: string;
129
129
+
selector?: TextSelector;
130
130
+
}) {
131
131
+
try {
132
132
+
const res = await apiRequest('/annotations', {
133
133
+
method: 'POST',
134
134
+
body: JSON.stringify({
135
135
+
target: {
136
136
+
source: data.url,
137
137
+
selector: data.selector,
138
138
+
},
139
139
+
body: { type: 'TextualBody', value: data.text, format: 'text/plain' },
140
140
+
motivation: 'commenting',
141
141
+
title: data.title,
142
142
+
}),
143
143
+
});
144
144
+
145
145
+
if (!res.ok) {
146
146
+
const error = await res.text();
147
147
+
return { success: false, error };
148
148
+
}
149
149
+
150
150
+
return { success: true, data: await res.json() };
151
151
+
} catch (error) {
152
152
+
return { success: false, error: String(error) };
153
153
+
}
154
154
+
}
155
155
+
156
156
+
export async function createBookmark(data: { url: string; title?: string }) {
157
157
+
try {
158
158
+
const res = await apiRequest('/bookmarks', {
159
159
+
method: 'POST',
160
160
+
body: JSON.stringify({ url: data.url, title: data.title }),
161
161
+
});
162
162
+
163
163
+
if (!res.ok) {
164
164
+
const error = await res.text();
165
165
+
return { success: false, error };
166
166
+
}
167
167
+
168
168
+
return { success: true, data: await res.json() };
169
169
+
} catch (error) {
170
170
+
return { success: false, error: String(error) };
171
171
+
}
172
172
+
}
173
173
+
174
174
+
export async function createHighlight(data: {
175
175
+
url: string;
176
176
+
title?: string;
177
177
+
selector: TextSelector;
178
178
+
color?: string;
179
179
+
}) {
180
180
+
try {
181
181
+
const res = await apiRequest('/highlights', {
182
182
+
method: 'POST',
183
183
+
body: JSON.stringify({
184
184
+
url: data.url,
185
185
+
title: data.title,
186
186
+
selector: data.selector,
187
187
+
color: data.color,
188
188
+
}),
189
189
+
});
190
190
+
191
191
+
if (!res.ok) {
192
192
+
const error = await res.text();
193
193
+
return { success: false, error };
194
194
+
}
195
195
+
196
196
+
return { success: true, data: await res.json() };
197
197
+
} catch (error) {
198
198
+
return { success: false, error: String(error) };
199
199
+
}
200
200
+
}
201
201
+
202
202
+
export async function getUserBookmarks(did: string) {
203
203
+
try {
204
204
+
const res = await apiRequest(`/users/${did}/bookmarks`);
205
205
+
if (!res.ok) return [];
206
206
+
const data = await res.json();
207
207
+
return data.items || data || [];
208
208
+
} catch (error) {
209
209
+
console.error('Get bookmarks error:', error);
210
210
+
return [];
211
211
+
}
212
212
+
}
213
213
+
214
214
+
export async function getUserHighlights(did: string) {
215
215
+
try {
216
216
+
const res = await apiRequest(`/users/${did}/highlights`);
217
217
+
if (!res.ok) return [];
218
218
+
const data = await res.json();
219
219
+
return data.items || data || [];
220
220
+
} catch (error) {
221
221
+
console.error('Get highlights error:', error);
222
222
+
return [];
223
223
+
}
224
224
+
}
225
225
+
226
226
+
export async function getUserCollections(did: string) {
227
227
+
try {
228
228
+
const res = await apiRequest(`/collections?author=${encodeURIComponent(did)}`);
229
229
+
if (!res.ok) return [];
230
230
+
const data = await res.json();
231
231
+
return data.items || data || [];
232
232
+
} catch (error) {
233
233
+
console.error('Get collections error:', error);
234
234
+
return [];
235
235
+
}
236
236
+
}
237
237
+
238
238
+
export async function addToCollection(collectionUri: string, annotationUri: string) {
239
239
+
try {
240
240
+
const res = await apiRequest(`/collections/${encodeURIComponent(collectionUri)}/items`, {
241
241
+
method: 'POST',
242
242
+
body: JSON.stringify({ annotationUri, position: 0 }),
243
243
+
});
244
244
+
245
245
+
if (!res.ok) {
246
246
+
const error = await res.text();
247
247
+
return { success: false, error };
248
248
+
}
249
249
+
250
250
+
return { success: true };
251
251
+
} catch (error) {
252
252
+
return { success: false, error: String(error) };
253
253
+
}
254
254
+
}
255
255
+
256
256
+
export async function getItemCollections(annotationUri: string): Promise<string[]> {
257
257
+
try {
258
258
+
const res = await apiRequest(
259
259
+
`/collections/containing?uri=${encodeURIComponent(annotationUri)}`
260
260
+
);
261
261
+
if (!res.ok) return [];
262
262
+
const data = await res.json();
263
263
+
return Array.isArray(data) ? data : [];
264
264
+
} catch (error) {
265
265
+
console.error('Get item collections error:', error);
266
266
+
return [];
267
267
+
}
268
268
+
}
269
269
+
270
270
+
export async function getReplies(uri: string) {
271
271
+
try {
272
272
+
const res = await apiRequest(`/annotations/${encodeURIComponent(uri)}/replies`);
273
273
+
if (!res.ok) return [];
274
274
+
const data = await res.json();
275
275
+
return data.items || data || [];
276
276
+
} catch (error) {
277
277
+
console.error('Get replies error:', error);
278
278
+
return [];
279
279
+
}
280
280
+
}
281
281
+
282
282
+
export async function createReply(data: {
283
283
+
parentUri: string;
284
284
+
parentCid: string;
285
285
+
rootUri: string;
286
286
+
rootCid: string;
287
287
+
text: string;
288
288
+
}) {
289
289
+
try {
290
290
+
const res = await apiRequest('/replies', {
291
291
+
method: 'POST',
292
292
+
body: JSON.stringify(data),
293
293
+
});
294
294
+
295
295
+
if (!res.ok) {
296
296
+
const error = await res.text();
297
297
+
return { success: false, error };
298
298
+
}
299
299
+
300
300
+
return { success: true };
301
301
+
} catch (error) {
302
302
+
return { success: false, error: String(error) };
303
303
+
}
304
304
+
}
+61
extension/src/utils/messaging.ts
···
1
1
+
import { defineExtensionMessaging } from '@webext-core/messaging';
2
2
+
import type {
3
3
+
MarginSession,
4
4
+
Annotation,
5
5
+
Bookmark,
6
6
+
Highlight,
7
7
+
Collection,
8
8
+
TextSelector,
9
9
+
} from './types';
10
10
+
11
11
+
interface ProtocolMap {
12
12
+
checkSession(): MarginSession;
13
13
+
14
14
+
getAnnotations(data: { url: string }): Annotation[];
15
15
+
createAnnotation(data: { url: string; text: string; title?: string; selector?: TextSelector }): {
16
16
+
success: boolean;
17
17
+
data?: Annotation;
18
18
+
error?: string;
19
19
+
};
20
20
+
21
21
+
createBookmark(data: { url: string; title?: string }): {
22
22
+
success: boolean;
23
23
+
data?: Bookmark;
24
24
+
error?: string;
25
25
+
};
26
26
+
getUserBookmarks(data: { did: string }): Bookmark[];
27
27
+
28
28
+
createHighlight(data: { url: string; title?: string; selector: TextSelector; color?: string }): {
29
29
+
success: boolean;
30
30
+
data?: Highlight;
31
31
+
error?: string;
32
32
+
};
33
33
+
getUserHighlights(data: { did: string }): Highlight[];
34
34
+
35
35
+
getUserCollections(data: { did: string }): Collection[];
36
36
+
addToCollection(data: { collectionUri: string; annotationUri: string }): {
37
37
+
success: boolean;
38
38
+
error?: string;
39
39
+
};
40
40
+
getItemCollections(data: { annotationUri: string }): string[];
41
41
+
42
42
+
getReplies(data: { uri: string }): Annotation[];
43
43
+
createReply(data: {
44
44
+
parentUri: string;
45
45
+
parentCid: string;
46
46
+
rootUri: string;
47
47
+
rootCid: string;
48
48
+
text: string;
49
49
+
}): { success: boolean; error?: string };
50
50
+
51
51
+
getOverlayEnabled(): boolean;
52
52
+
53
53
+
openAppUrl(data: { path: string }): void;
54
54
+
55
55
+
updateBadge(data: { count: number; tabId?: number }): void;
56
56
+
57
57
+
cacheAnnotations(data: { url: string; annotations: Annotation[] }): void;
58
58
+
getCachedAnnotations(data: { url: string }): Annotation[] | null;
59
59
+
}
60
60
+
61
61
+
export const { sendMessage, onMessage } = defineExtensionMessaging<ProtocolMap>();
+576
extension/src/utils/overlay-styles.ts
···
1
1
+
export const overlayStyles = /* css */ `
2
2
+
:host {
3
3
+
all: initial;
4
4
+
--bg-primary: #0a0a0d;
5
5
+
--bg-secondary: #121216;
6
6
+
--bg-tertiary: #1a1a1f;
7
7
+
--bg-card: #0f0f13;
8
8
+
--bg-elevated: #18181d;
9
9
+
--bg-hover: #1e1e24;
10
10
+
11
11
+
--text-primary: #eaeaee;
12
12
+
--text-secondary: #b7b6c5;
13
13
+
--text-tertiary: #6e6d7a;
14
14
+
--border: rgba(183, 182, 197, 0.12);
15
15
+
16
16
+
--accent: #957a86;
17
17
+
--accent-hover: #a98d98;
18
18
+
--accent-subtle: rgba(149, 122, 134, 0.15);
19
19
+
20
20
+
--highlight-yellow: #fbbf24;
21
21
+
--highlight-green: #34d399;
22
22
+
--highlight-blue: #60a5fa;
23
23
+
--highlight-pink: #f472b6;
24
24
+
--highlight-purple: #a78bfa;
25
25
+
}
26
26
+
27
27
+
:host(.light) {
28
28
+
--bg-primary: #f8f8fa;
29
29
+
--bg-secondary: #ffffff;
30
30
+
--bg-tertiary: #f0f0f4;
31
31
+
--bg-card: #ffffff;
32
32
+
--bg-elevated: #ffffff;
33
33
+
--bg-hover: #eeeef2;
34
34
+
35
35
+
--text-primary: #18171c;
36
36
+
--text-secondary: #5c495a;
37
37
+
--text-tertiary: #8a8494;
38
38
+
--border: rgba(92, 73, 90, 0.12);
39
39
+
40
40
+
--accent: #7a5f6d;
41
41
+
--accent-hover: #664e5b;
42
42
+
--accent-subtle: rgba(149, 122, 134, 0.12);
43
43
+
}
44
44
+
45
45
+
.margin-overlay {
46
46
+
position: absolute;
47
47
+
top: 0;
48
48
+
left: 0;
49
49
+
width: 100%;
50
50
+
height: 100%;
51
51
+
pointer-events: none;
52
52
+
}
53
53
+
54
54
+
.margin-selection-toolbar {
55
55
+
position: fixed;
56
56
+
display: flex;
57
57
+
align-items: center;
58
58
+
gap: 2px;
59
59
+
padding: 4px;
60
60
+
background: var(--bg-card);
61
61
+
border: 1px solid var(--border);
62
62
+
border-radius: 10px;
63
63
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255,255,255,0.05);
64
64
+
z-index: 2147483647;
65
65
+
pointer-events: auto;
66
66
+
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
67
67
+
opacity: 0;
68
68
+
transform: translateY(8px) scale(0.95);
69
69
+
animation: toolbar-in 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
70
70
+
}
71
71
+
72
72
+
@keyframes toolbar-in {
73
73
+
to { opacity: 1; transform: translateY(0) scale(1); }
74
74
+
}
75
75
+
76
76
+
.toolbar-btn {
77
77
+
display: flex;
78
78
+
align-items: center;
79
79
+
justify-content: center;
80
80
+
gap: 6px;
81
81
+
padding: 8px 12px;
82
82
+
background: transparent;
83
83
+
border: none;
84
84
+
border-radius: 6px;
85
85
+
color: var(--text-primary);
86
86
+
font-size: 12px;
87
87
+
font-weight: 500;
88
88
+
cursor: pointer;
89
89
+
transition: all 0.15s ease;
90
90
+
white-space: nowrap;
91
91
+
}
92
92
+
93
93
+
.toolbar-btn:hover {
94
94
+
background: var(--bg-hover);
95
95
+
}
96
96
+
97
97
+
.toolbar-btn:active {
98
98
+
transform: scale(0.96);
99
99
+
}
100
100
+
101
101
+
.toolbar-btn svg {
102
102
+
width: 16px;
103
103
+
height: 16px;
104
104
+
flex-shrink: 0;
105
105
+
}
106
106
+
107
107
+
.toolbar-btn.highlight-btn {
108
108
+
color: var(--highlight-yellow);
109
109
+
}
110
110
+
111
111
+
.toolbar-btn.highlight-btn:hover {
112
112
+
background: rgba(251, 191, 36, 0.15);
113
113
+
}
114
114
+
115
115
+
.toolbar-divider {
116
116
+
width: 1px;
117
117
+
height: 20px;
118
118
+
background: var(--border);
119
119
+
margin: 0 2px;
120
120
+
}
121
121
+
122
122
+
.color-picker {
123
123
+
position: absolute;
124
124
+
top: 100%;
125
125
+
left: 50%;
126
126
+
transform: translateX(-50%);
127
127
+
margin-top: 6px;
128
128
+
display: flex;
129
129
+
gap: 6px;
130
130
+
padding: 8px;
131
131
+
background: var(--bg-card);
132
132
+
border: 1px solid var(--border);
133
133
+
border-radius: 10px;
134
134
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
135
135
+
animation: toolbar-in 0.15s ease forwards;
136
136
+
}
137
137
+
138
138
+
.color-dot {
139
139
+
width: 24px;
140
140
+
height: 24px;
141
141
+
border-radius: 50%;
142
142
+
border: 2px solid transparent;
143
143
+
cursor: pointer;
144
144
+
transition: all 0.15s ease;
145
145
+
}
146
146
+
147
147
+
.color-dot:hover {
148
148
+
transform: scale(1.15);
149
149
+
border-color: var(--text-primary);
150
150
+
}
151
151
+
152
152
+
.margin-popover {
153
153
+
position: absolute;
154
154
+
width: 320px;
155
155
+
background: var(--bg-card);
156
156
+
border: 1px solid var(--border);
157
157
+
border-radius: 14px;
158
158
+
padding: 0;
159
159
+
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255,255,255,0.05);
160
160
+
display: flex;
161
161
+
flex-direction: column;
162
162
+
pointer-events: auto;
163
163
+
z-index: 2147483647;
164
164
+
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
165
165
+
color: var(--text-primary);
166
166
+
opacity: 0;
167
167
+
transform: translateY(-8px) scale(0.96);
168
168
+
animation: popover-in 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
169
169
+
max-height: 450px;
170
170
+
overflow: hidden;
171
171
+
}
172
172
+
173
173
+
@keyframes popover-in {
174
174
+
to { opacity: 1; transform: translateY(0) scale(1); }
175
175
+
}
176
176
+
177
177
+
.popover-header {
178
178
+
padding: 12px 16px;
179
179
+
border-bottom: 1px solid var(--border);
180
180
+
display: flex;
181
181
+
justify-content: space-between;
182
182
+
align-items: center;
183
183
+
background: var(--bg-primary);
184
184
+
border-radius: 14px 14px 0 0;
185
185
+
}
186
186
+
187
187
+
.popover-title {
188
188
+
font-weight: 600;
189
189
+
font-size: 13px;
190
190
+
color: var(--text-primary);
191
191
+
display: flex;
192
192
+
align-items: center;
193
193
+
gap: 6px;
194
194
+
}
195
195
+
196
196
+
.popover-count {
197
197
+
font-size: 11px;
198
198
+
color: var(--text-tertiary);
199
199
+
background: var(--bg-tertiary);
200
200
+
padding: 2px 8px;
201
201
+
border-radius: 10px;
202
202
+
}
203
203
+
204
204
+
.popover-close {
205
205
+
background: none;
206
206
+
border: none;
207
207
+
color: var(--text-tertiary);
208
208
+
cursor: pointer;
209
209
+
padding: 4px;
210
210
+
border-radius: 6px;
211
211
+
display: flex;
212
212
+
align-items: center;
213
213
+
justify-content: center;
214
214
+
transition: all 0.15s;
215
215
+
}
216
216
+
217
217
+
.popover-close:hover {
218
218
+
background: var(--bg-hover);
219
219
+
color: var(--text-primary);
220
220
+
}
221
221
+
222
222
+
.popover-close svg {
223
223
+
width: 16px;
224
224
+
height: 16px;
225
225
+
}
226
226
+
227
227
+
.popover-scroll-area {
228
228
+
overflow-y: auto;
229
229
+
max-height: 350px;
230
230
+
overscroll-behavior: contain;
231
231
+
scrollbar-width: thin;
232
232
+
scrollbar-color: var(--bg-tertiary) transparent;
233
233
+
}
234
234
+
235
235
+
.popover-scroll-area::-webkit-scrollbar {
236
236
+
width: 6px;
237
237
+
}
238
238
+
239
239
+
.popover-scroll-area::-webkit-scrollbar-track {
240
240
+
background: transparent;
241
241
+
margin: 4px 0;
242
242
+
}
243
243
+
244
244
+
.popover-scroll-area::-webkit-scrollbar-thumb {
245
245
+
background: var(--bg-tertiary);
246
246
+
border-radius: 3px;
247
247
+
}
248
248
+
249
249
+
.popover-scroll-area::-webkit-scrollbar-thumb:hover {
250
250
+
background: var(--text-tertiary);
251
251
+
}
252
252
+
253
253
+
.comment-item {
254
254
+
padding: 14px 16px;
255
255
+
border-bottom: 1px solid var(--border);
256
256
+
transition: background 0.15s;
257
257
+
}
258
258
+
259
259
+
.comment-item:hover {
260
260
+
background: var(--bg-hover);
261
261
+
}
262
262
+
263
263
+
.comment-item:last-child {
264
264
+
border-bottom: none;
265
265
+
}
266
266
+
267
267
+
.comment-header {
268
268
+
display: flex;
269
269
+
align-items: center;
270
270
+
gap: 10px;
271
271
+
margin-bottom: 8px;
272
272
+
}
273
273
+
274
274
+
.comment-avatar {
275
275
+
width: 28px;
276
276
+
height: 28px;
277
277
+
border-radius: 50%;
278
278
+
background: linear-gradient(135deg, var(--accent), var(--accent-hover));
279
279
+
display: flex;
280
280
+
align-items: center;
281
281
+
justify-content: center;
282
282
+
font-size: 11px;
283
283
+
font-weight: 600;
284
284
+
color: white;
285
285
+
flex-shrink: 0;
286
286
+
}
287
287
+
288
288
+
.comment-meta {
289
289
+
flex: 1;
290
290
+
min-width: 0;
291
291
+
}
292
292
+
293
293
+
.comment-handle {
294
294
+
font-size: 13px;
295
295
+
font-weight: 600;
296
296
+
color: var(--text-primary);
297
297
+
}
298
298
+
299
299
+
.comment-time {
300
300
+
font-size: 11px;
301
301
+
color: var(--text-tertiary);
302
302
+
}
303
303
+
304
304
+
.comment-text {
305
305
+
font-size: 13px;
306
306
+
line-height: 1.55;
307
307
+
color: var(--text-primary);
308
308
+
}
309
309
+
310
310
+
.highlight-badge {
311
311
+
display: inline-flex;
312
312
+
align-items: center;
313
313
+
gap: 5px;
314
314
+
font-size: 11px;
315
315
+
color: var(--text-tertiary);
316
316
+
background: var(--bg-tertiary);
317
317
+
padding: 4px 10px;
318
318
+
border-radius: 12px;
319
319
+
font-weight: 500;
320
320
+
}
321
321
+
322
322
+
.highlight-badge svg {
323
323
+
width: 12px;
324
324
+
height: 12px;
325
325
+
}
326
326
+
327
327
+
.comment-actions {
328
328
+
display: flex;
329
329
+
gap: 4px;
330
330
+
margin-top: 10px;
331
331
+
padding-top: 10px;
332
332
+
border-top: 1px solid var(--border);
333
333
+
}
334
334
+
335
335
+
.comment-action-btn {
336
336
+
background: none;
337
337
+
border: none;
338
338
+
padding: 6px 10px;
339
339
+
color: var(--text-tertiary);
340
340
+
font-size: 12px;
341
341
+
font-weight: 500;
342
342
+
cursor: pointer;
343
343
+
border-radius: 6px;
344
344
+
transition: all 0.15s;
345
345
+
display: flex;
346
346
+
align-items: center;
347
347
+
gap: 5px;
348
348
+
}
349
349
+
350
350
+
.comment-action-btn svg {
351
351
+
width: 14px;
352
352
+
height: 14px;
353
353
+
}
354
354
+
355
355
+
.comment-action-btn:hover {
356
356
+
background: var(--bg-tertiary);
357
357
+
color: var(--text-primary);
358
358
+
}
359
359
+
360
360
+
.inline-compose-modal {
361
361
+
position: fixed;
362
362
+
width: 380px;
363
363
+
max-width: calc(100vw - 32px);
364
364
+
background: var(--bg-card);
365
365
+
border: 1px solid var(--border);
366
366
+
border-radius: 16px;
367
367
+
padding: 0;
368
368
+
box-sizing: border-box;
369
369
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255,255,255,0.05);
370
370
+
z-index: 2147483647;
371
371
+
pointer-events: auto;
372
372
+
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
373
373
+
color: var(--text-primary);
374
374
+
animation: modal-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
375
375
+
overflow: hidden;
376
376
+
}
377
377
+
378
378
+
@keyframes modal-in {
379
379
+
from { opacity: 0; transform: scale(0.95) translateY(10px); }
380
380
+
to { opacity: 1; transform: scale(1) translateY(0); }
381
381
+
}
382
382
+
383
383
+
.inline-compose-modal * {
384
384
+
box-sizing: border-box;
385
385
+
}
386
386
+
387
387
+
.compose-header {
388
388
+
padding: 14px 16px;
389
389
+
border-bottom: 1px solid var(--border);
390
390
+
display: flex;
391
391
+
justify-content: space-between;
392
392
+
align-items: center;
393
393
+
background: var(--bg-primary);
394
394
+
}
395
395
+
396
396
+
.compose-title {
397
397
+
font-weight: 600;
398
398
+
font-size: 14px;
399
399
+
color: var(--text-primary);
400
400
+
}
401
401
+
402
402
+
.compose-close {
403
403
+
background: none;
404
404
+
border: none;
405
405
+
color: var(--text-tertiary);
406
406
+
cursor: pointer;
407
407
+
padding: 4px;
408
408
+
border-radius: 6px;
409
409
+
display: flex;
410
410
+
transition: all 0.15s;
411
411
+
}
412
412
+
413
413
+
.compose-close:hover {
414
414
+
background: var(--bg-hover);
415
415
+
color: var(--text-primary);
416
416
+
}
417
417
+
418
418
+
.compose-body {
419
419
+
padding: 16px;
420
420
+
}
421
421
+
422
422
+
.inline-compose-quote {
423
423
+
padding: 12px 14px;
424
424
+
background: var(--accent-subtle);
425
425
+
border-left: 3px solid var(--accent);
426
426
+
border-radius: 0 8px 8px 0;
427
427
+
font-size: 13px;
428
428
+
color: var(--text-secondary);
429
429
+
font-style: italic;
430
430
+
margin-bottom: 14px;
431
431
+
max-height: 80px;
432
432
+
overflow: hidden;
433
433
+
word-break: break-word;
434
434
+
line-height: 1.5;
435
435
+
}
436
436
+
437
437
+
.inline-compose-textarea {
438
438
+
width: 100%;
439
439
+
min-height: 100px;
440
440
+
padding: 12px 14px;
441
441
+
background: var(--bg-elevated);
442
442
+
border: 1px solid var(--border);
443
443
+
border-radius: 10px;
444
444
+
color: var(--text-primary);
445
445
+
font-family: inherit;
446
446
+
font-size: 14px;
447
447
+
line-height: 1.5;
448
448
+
resize: none;
449
449
+
box-sizing: border-box;
450
450
+
transition: border-color 0.15s;
451
451
+
}
452
452
+
453
453
+
.inline-compose-textarea::placeholder {
454
454
+
color: var(--text-tertiary);
455
455
+
}
456
456
+
457
457
+
.inline-compose-textarea:focus {
458
458
+
outline: none;
459
459
+
border-color: var(--accent);
460
460
+
}
461
461
+
462
462
+
.compose-footer {
463
463
+
padding: 12px 16px;
464
464
+
border-top: 1px solid var(--border);
465
465
+
display: flex;
466
466
+
justify-content: flex-end;
467
467
+
gap: 8px;
468
468
+
background: var(--bg-primary);
469
469
+
}
470
470
+
471
471
+
.btn-cancel {
472
472
+
padding: 9px 18px;
473
473
+
background: transparent;
474
474
+
border: 1px solid var(--border);
475
475
+
border-radius: 8px;
476
476
+
color: var(--text-secondary);
477
477
+
font-size: 13px;
478
478
+
font-weight: 500;
479
479
+
cursor: pointer;
480
480
+
transition: all 0.15s;
481
481
+
}
482
482
+
483
483
+
.btn-cancel:hover {
484
484
+
background: var(--bg-hover);
485
485
+
color: var(--text-primary);
486
486
+
border-color: var(--border);
487
487
+
}
488
488
+
489
489
+
.btn-submit {
490
490
+
padding: 9px 20px;
491
491
+
background: var(--accent);
492
492
+
border: none;
493
493
+
border-radius: 8px;
494
494
+
color: white;
495
495
+
font-size: 13px;
496
496
+
font-weight: 600;
497
497
+
cursor: pointer;
498
498
+
transition: all 0.15s;
499
499
+
}
500
500
+
501
501
+
.btn-submit:hover {
502
502
+
background: var(--accent-hover);
503
503
+
transform: translateY(-1px);
504
504
+
}
505
505
+
506
506
+
.btn-submit:active {
507
507
+
transform: translateY(0);
508
508
+
}
509
509
+
510
510
+
.btn-submit:disabled {
511
511
+
opacity: 0.5;
512
512
+
cursor: not-allowed;
513
513
+
transform: none;
514
514
+
}
515
515
+
516
516
+
.margin-hover-indicator {
517
517
+
position: fixed;
518
518
+
display: flex;
519
519
+
align-items: center;
520
520
+
pointer-events: none;
521
521
+
z-index: 2147483647;
522
522
+
opacity: 0;
523
523
+
transition: opacity 0.2s ease, transform 0.2s ease;
524
524
+
transform: scale(0.8) translateX(4px);
525
525
+
}
526
526
+
527
527
+
.margin-hover-indicator.visible {
528
528
+
opacity: 1;
529
529
+
transform: scale(1) translateX(0);
530
530
+
}
531
531
+
532
532
+
.margin-toast {
533
533
+
position: fixed;
534
534
+
bottom: 24px;
535
535
+
left: 50%;
536
536
+
transform: translateX(-50%) translateY(20px);
537
537
+
padding: 12px 20px;
538
538
+
background: var(--bg-card);
539
539
+
border: 1px solid var(--border);
540
540
+
border-radius: 10px;
541
541
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
542
542
+
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
543
543
+
font-size: 13px;
544
544
+
font-weight: 500;
545
545
+
color: var(--text-primary);
546
546
+
z-index: 2147483647;
547
547
+
pointer-events: auto;
548
548
+
display: flex;
549
549
+
align-items: center;
550
550
+
gap: 10px;
551
551
+
opacity: 0;
552
552
+
animation: toast-in 0.3s ease forwards;
553
553
+
}
554
554
+
555
555
+
@keyframes toast-in {
556
556
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
557
557
+
}
558
558
+
559
559
+
.margin-toast.toast-out {
560
560
+
animation: toast-out 0.2s ease forwards;
561
561
+
}
562
562
+
563
563
+
@keyframes toast-out {
564
564
+
to { opacity: 0; transform: translateX(-50%) translateY(10px); }
565
565
+
}
566
566
+
567
567
+
.toast-icon {
568
568
+
width: 18px;
569
569
+
height: 18px;
570
570
+
color: var(--accent);
571
571
+
}
572
572
+
573
573
+
.toast-success .toast-icon {
574
574
+
color: #34d399;
575
575
+
}
576
576
+
`;
+11
extension/src/utils/storage.ts
···
1
1
+
export const apiUrlItem = storage.defineItem<string>('local:apiUrl', {
2
2
+
fallback: 'https://margin.at',
3
3
+
});
4
4
+
5
5
+
export const overlayEnabledItem = storage.defineItem<boolean>('local:overlayEnabled', {
6
6
+
fallback: true,
7
7
+
});
8
8
+
9
9
+
export const themeItem = storage.defineItem<'light' | 'dark' | 'system'>('local:theme', {
10
10
+
fallback: 'system',
11
11
+
});
+183
extension/src/utils/text-matcher.ts
···
1
1
+
interface TextNodeIndex {
2
2
+
start: number;
3
3
+
node: Text;
4
4
+
length: number;
5
5
+
}
6
6
+
7
7
+
interface TextPoint {
8
8
+
node: Text;
9
9
+
offset: number;
10
10
+
}
11
11
+
12
12
+
export class DOMTextMatcher {
13
13
+
private textNodes: Text[] = [];
14
14
+
private corpus = '';
15
15
+
private indices: TextNodeIndex[] = [];
16
16
+
private built = false;
17
17
+
18
18
+
constructor() {}
19
19
+
20
20
+
private ensureBuilt(): void {
21
21
+
if (!this.built) {
22
22
+
this.buildMap();
23
23
+
this.built = true;
24
24
+
}
25
25
+
}
26
26
+
27
27
+
private buildMap(): void {
28
28
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
29
29
+
acceptNode: (node: Text) => {
30
30
+
if (!node.parentNode) return NodeFilter.FILTER_REJECT;
31
31
+
const parent = node.parentNode as Element;
32
32
+
const tag = parent.tagName;
33
33
+
if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT'].includes(tag)) {
34
34
+
return NodeFilter.FILTER_REJECT;
35
35
+
}
36
36
+
if (!node.textContent?.trim()) return NodeFilter.FILTER_SKIP;
37
37
+
const htmlParent = parent as HTMLElement;
38
38
+
if (
39
39
+
htmlParent.style &&
40
40
+
(htmlParent.style.display === 'none' || htmlParent.style.visibility === 'hidden')
41
41
+
) {
42
42
+
return NodeFilter.FILTER_REJECT;
43
43
+
}
44
44
+
return NodeFilter.FILTER_ACCEPT;
45
45
+
},
46
46
+
});
47
47
+
48
48
+
let currentNode: Text | null;
49
49
+
let index = 0;
50
50
+
const parts: string[] = [];
51
51
+
52
52
+
while ((currentNode = walker.nextNode() as Text | null)) {
53
53
+
const text = currentNode.textContent || '';
54
54
+
this.textNodes.push(currentNode);
55
55
+
parts.push(text);
56
56
+
this.indices.push({
57
57
+
start: index,
58
58
+
node: currentNode,
59
59
+
length: text.length,
60
60
+
});
61
61
+
index += text.length;
62
62
+
}
63
63
+
64
64
+
this.corpus = parts.join('');
65
65
+
}
66
66
+
67
67
+
findRange(searchText: string): Range | null {
68
68
+
if (!searchText) return null;
69
69
+
70
70
+
this.ensureBuilt();
71
71
+
72
72
+
let matchIndex = this.corpus.indexOf(searchText);
73
73
+
74
74
+
if (matchIndex === -1) {
75
75
+
const normalizedSearch = searchText.replace(/\s+/g, ' ').trim();
76
76
+
matchIndex = this.corpus.indexOf(normalizedSearch);
77
77
+
78
78
+
if (matchIndex === -1) {
79
79
+
const fuzzyMatch = this.fuzzyFindInCorpus(searchText);
80
80
+
if (fuzzyMatch) {
81
81
+
const start = this.mapIndexToPoint(fuzzyMatch.start);
82
82
+
const end = this.mapIndexToPoint(fuzzyMatch.end);
83
83
+
if (start && end) {
84
84
+
const range = document.createRange();
85
85
+
range.setStart(start.node, start.offset);
86
86
+
range.setEnd(end.node, end.offset);
87
87
+
return range;
88
88
+
}
89
89
+
}
90
90
+
return null;
91
91
+
}
92
92
+
}
93
93
+
94
94
+
const start = this.mapIndexToPoint(matchIndex);
95
95
+
const end = this.mapIndexToPoint(matchIndex + searchText.length);
96
96
+
97
97
+
if (start && end) {
98
98
+
const range = document.createRange();
99
99
+
range.setStart(start.node, start.offset);
100
100
+
range.setEnd(end.node, end.offset);
101
101
+
return range;
102
102
+
}
103
103
+
return null;
104
104
+
}
105
105
+
106
106
+
private fuzzyFindInCorpus(searchText: string): { start: number; end: number } | null {
107
107
+
const searchWords = searchText
108
108
+
.trim()
109
109
+
.split(/\s+/)
110
110
+
.filter((w) => w.length > 0);
111
111
+
if (searchWords.length === 0) return null;
112
112
+
113
113
+
const corpusLower = this.corpus.toLowerCase();
114
114
+
const firstWord = searchWords[0].toLowerCase();
115
115
+
let searchStart = 0;
116
116
+
117
117
+
while (searchStart < corpusLower.length) {
118
118
+
const wordStart = corpusLower.indexOf(firstWord, searchStart);
119
119
+
if (wordStart === -1) break;
120
120
+
121
121
+
let corpusPos = wordStart;
122
122
+
let matched = true;
123
123
+
let lastMatchEnd = wordStart;
124
124
+
125
125
+
for (const word of searchWords) {
126
126
+
const wordLower = word.toLowerCase();
127
127
+
while (corpusPos < corpusLower.length && /\s/.test(this.corpus[corpusPos])) {
128
128
+
corpusPos++;
129
129
+
}
130
130
+
const corpusSlice = corpusLower.slice(corpusPos, corpusPos + wordLower.length);
131
131
+
if (corpusSlice !== wordLower) {
132
132
+
matched = false;
133
133
+
break;
134
134
+
}
135
135
+
corpusPos += wordLower.length;
136
136
+
lastMatchEnd = corpusPos;
137
137
+
}
138
138
+
139
139
+
if (matched) {
140
140
+
return { start: wordStart, end: lastMatchEnd };
141
141
+
}
142
142
+
searchStart = wordStart + 1;
143
143
+
}
144
144
+
145
145
+
return null;
146
146
+
}
147
147
+
148
148
+
private mapIndexToPoint(corpusIndex: number): TextPoint | null {
149
149
+
for (const info of this.indices) {
150
150
+
if (corpusIndex >= info.start && corpusIndex < info.start + info.length) {
151
151
+
return { node: info.node, offset: corpusIndex - info.start };
152
152
+
}
153
153
+
}
154
154
+
if (this.indices.length > 0) {
155
155
+
const last = this.indices[this.indices.length - 1];
156
156
+
if (corpusIndex === last.start + last.length) {
157
157
+
return { node: last.node, offset: last.length };
158
158
+
}
159
159
+
}
160
160
+
return null;
161
161
+
}
162
162
+
}
163
163
+
164
164
+
export function findCanonicalUrl(range: Range): string | null {
165
165
+
let node: Node | null = range.commonAncestorContainer;
166
166
+
if (node.nodeType === Node.TEXT_NODE) {
167
167
+
node = node.parentNode;
168
168
+
}
169
169
+
170
170
+
while (node && node !== document.body) {
171
171
+
const element = node as Element;
172
172
+
if (
173
173
+
(element.tagName === 'BLOCKQUOTE' || element.tagName === 'Q') &&
174
174
+
element.hasAttribute('cite')
175
175
+
) {
176
176
+
if (element.contains(range.commonAncestorContainer)) {
177
177
+
return element.getAttribute('cite');
178
178
+
}
179
179
+
}
180
180
+
node = node.parentNode;
181
181
+
}
182
182
+
return null;
183
183
+
}
+76
extension/src/utils/types.ts
···
1
1
+
export interface MarginSession {
2
2
+
authenticated: boolean;
3
3
+
did?: string;
4
4
+
handle?: string;
5
5
+
accessJwt?: string;
6
6
+
refreshJwt?: string;
7
7
+
}
8
8
+
9
9
+
export interface TextSelector {
10
10
+
type?: string;
11
11
+
exact: string;
12
12
+
prefix?: string;
13
13
+
suffix?: string;
14
14
+
}
15
15
+
16
16
+
export interface Annotation {
17
17
+
uri?: string;
18
18
+
id?: string;
19
19
+
cid?: string;
20
20
+
type?: 'Annotation' | 'Bookmark' | 'Highlight';
21
21
+
body?: { value: string };
22
22
+
text?: string;
23
23
+
target?: {
24
24
+
source?: string;
25
25
+
selector?: TextSelector;
26
26
+
};
27
27
+
selector?: TextSelector;
28
28
+
color?: string;
29
29
+
created?: string;
30
30
+
createdAt?: string;
31
31
+
creator?: Author;
32
32
+
author?: Author;
33
33
+
}
34
34
+
35
35
+
export interface Author {
36
36
+
did?: string;
37
37
+
handle?: string;
38
38
+
displayName?: string;
39
39
+
avatar?: string;
40
40
+
}
41
41
+
42
42
+
export interface Bookmark {
43
43
+
uri?: string;
44
44
+
id?: string;
45
45
+
source?: string;
46
46
+
url?: string;
47
47
+
title?: string;
48
48
+
description?: string;
49
49
+
image?: string;
50
50
+
createdAt?: string;
51
51
+
}
52
52
+
53
53
+
export interface Highlight {
54
54
+
uri?: string;
55
55
+
id?: string;
56
56
+
target?: {
57
57
+
source?: string;
58
58
+
selector?: TextSelector;
59
59
+
};
60
60
+
color?: string;
61
61
+
title?: string;
62
62
+
createdAt?: string;
63
63
+
}
64
64
+
65
65
+
export interface Collection {
66
66
+
uri?: string;
67
67
+
id?: string;
68
68
+
name: string;
69
69
+
description?: string;
70
70
+
icon?: string;
71
71
+
createdAt?: string;
72
72
+
itemCount?: number;
73
73
+
}
74
74
+
75
75
+
export const DEFAULT_API_URL = 'https://margin.at';
76
76
+
export const APP_URL = 'https://margin.at';
+12
extension/tailwind.config.js
···
1
1
+
/** @type {import('tailwindcss').Config} */
2
2
+
export default {
3
3
+
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
4
4
+
theme: {
5
5
+
extend: {
6
6
+
fontFamily: {
7
7
+
sans: ['IBM Plex Sans', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
8
8
+
},
9
9
+
},
10
10
+
},
11
11
+
plugins: [],
12
12
+
};
+16
extension/tsconfig.json
···
1
1
+
{
2
2
+
"extends": "./.wxt/tsconfig.json",
3
3
+
"compilerOptions": {
4
4
+
"jsx": "react-jsx",
5
5
+
"jsxImportSource": "react",
6
6
+
"moduleResolution": "bundler",
7
7
+
"strict": true,
8
8
+
"skipLibCheck": true,
9
9
+
"esModuleInterop": true,
10
10
+
"allowSyntheticDefaultImports": true,
11
11
+
"paths": {
12
12
+
"@/*": ["./src/*"]
13
13
+
}
14
14
+
},
15
15
+
"include": ["src/**/*", ".wxt/wxt.d.ts"]
16
16
+
}
+87
extension/wxt.config.ts
···
1
1
+
import { defineConfig } from 'wxt';
2
2
+
import { cp } from 'fs/promises';
3
3
+
import { existsSync } from 'fs';
4
4
+
import { resolve } from 'path';
5
5
+
6
6
+
export default defineConfig({
7
7
+
srcDir: 'src',
8
8
+
modules: ['@wxt-dev/module-react'],
9
9
+
manifestVersion: 3,
10
10
+
hooks: {
11
11
+
'build:done': async (wxt) => {
12
12
+
const publicDir = resolve(__dirname, 'public');
13
13
+
const outDir = wxt.config.outDir;
14
14
+
15
15
+
if (existsSync(publicDir)) {
16
16
+
await cp(publicDir, outDir, { recursive: true });
17
17
+
}
18
18
+
},
19
19
+
},
20
20
+
manifest: ({ browser }) => {
21
21
+
const basePermissions = ['storage', 'activeTab', 'tabs', 'cookies', 'contextMenus'];
22
22
+
const chromePermissions = [...basePermissions, 'sidePanel'];
23
23
+
24
24
+
return {
25
25
+
name: 'Margin',
26
26
+
description: 'Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.',
27
27
+
permissions: browser === 'firefox' ? basePermissions : chromePermissions,
28
28
+
host_permissions: ['https://margin.at/*', '*://*/*'],
29
29
+
icons: {
30
30
+
16: '/icons/icon-16.png',
31
31
+
32: '/icons/icon-32.png',
32
32
+
48: '/icons/icon-48.png',
33
33
+
128: '/icons/icon-128.png',
34
34
+
},
35
35
+
commands: {
36
36
+
'open-sidebar': {
37
37
+
suggested_key: {
38
38
+
default: 'Alt+M',
39
39
+
mac: 'Alt+M',
40
40
+
},
41
41
+
description: 'Open Margin sidebar',
42
42
+
},
43
43
+
'annotate-selection': {
44
44
+
suggested_key: {
45
45
+
default: 'Alt+A',
46
46
+
mac: 'Alt+A',
47
47
+
},
48
48
+
description: 'Annotate selected text',
49
49
+
},
50
50
+
'highlight-selection': {
51
51
+
suggested_key: {
52
52
+
default: 'Alt+H',
53
53
+
mac: 'Alt+H',
54
54
+
},
55
55
+
description: 'Highlight selected text',
56
56
+
},
57
57
+
'bookmark-page': {
58
58
+
suggested_key: {
59
59
+
default: 'Alt+B',
60
60
+
mac: 'Alt+B',
61
61
+
},
62
62
+
description: 'Bookmark current page',
63
63
+
},
64
64
+
},
65
65
+
action: {
66
66
+
default_title: 'Margin',
67
67
+
default_popup: 'popup.html',
68
68
+
default_icon: {
69
69
+
16: '/icons/icon-16.png',
70
70
+
32: '/icons/icon-32.png',
71
71
+
48: '/icons/icon-48.png',
72
72
+
128: '/icons/icon-128.png',
73
73
+
},
74
74
+
},
75
75
+
...(browser === 'chrome' ? {
76
76
+
side_panel: {
77
77
+
default_path: 'sidepanel.html',
78
78
+
},
79
79
+
} : {
80
80
+
sidebar_action: {
81
81
+
default_title: 'Margin',
82
82
+
default_panel: 'sidepanel.html',
83
83
+
},
84
84
+
}),
85
85
+
};
86
86
+
},
87
87
+
});