Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

New extension using wxt.dev

+6065 -7528
+27 -22
.github/workflows/release-extension.yml
··· 15 15 - name: Checkout code 16 16 uses: actions/checkout@v4 17 17 18 - - name: Update Manifest Version 18 + - name: Setup Node.js 19 + uses: actions/setup-node@v4 20 + with: 21 + node-version: '20' 22 + cache: 'npm' 23 + cache-dependency-path: extension/package-lock.json 24 + 25 + - name: Install dependencies 26 + run: | 27 + cd extension 28 + npm ci 29 + 30 + - name: Update Version 19 31 run: | 20 32 VERSION=${GITHUB_REF_NAME#v} 21 - echo "Updating manifests to version $VERSION" 22 - 33 + echo "Updating to version $VERSION" 23 34 cd extension 24 - for manifest in manifest.json manifest.chrome.json manifest.firefox.json; do 25 - if [ -f "$manifest" ]; then 26 - tmp=$(mktemp) 27 - jq --arg v "$VERSION" '.version = $v' "$manifest" > "$tmp" && mv "$tmp" "$manifest" 28 - echo "Updated $manifest" 29 - fi 30 - done 31 - cd .. 35 + tmp=$(mktemp) 36 + jq --arg v "$VERSION" '.version = $v' package.json > "$tmp" && mv "$tmp" package.json 32 37 33 - - name: Build Extension (Chrome) 38 + - name: Build Chrome Extension 34 39 run: | 35 40 cd extension 36 - cp manifest.chrome.json manifest.json 37 - zip -r ../margin-extension-chrome.zip . -x "*.DS_Store" -x "*.git*" -x "manifest.*.json" 38 - cd .. 41 + npm run zip 42 + cp .output/margin-extension-*.zip ../margin-extension-chrome.zip 39 43 40 - - name: Build Extension (Firefox) 44 + - name: Build Firefox Extension 41 45 run: | 42 46 cd extension 43 - cp manifest.firefox.json manifest.json 44 - zip -r ../margin-extension-firefox.xpi . -x "*.DS_Store" -x "*.git*" -x "manifest.*.json" 45 - cd .. 47 + npm run zip:firefox 48 + cp .output/margin-extension-*-firefox.zip ../margin-extension-firefox.xpi 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 - cd extension 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 - 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 + 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 - cd .. 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 + 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 + # WXT 12 + extension/.output/ 13 + extension/.wxt/ 14 + 11 15 # Environment 12 16 .env 13 17 .env.local
+7
extension/.prettierrc
··· 1 + { 2 + "semi": true, 3 + "singleQuote": true, 4 + "tabWidth": 2, 5 + "trailingComma": "es5", 6 + "printWidth": 100 7 + }
-764
extension/background/service-worker.js
··· 1 - let API_BASE = "https://margin.at"; 2 - let WEB_BASE = "https://margin.at"; 3 - 4 - const hasSidePanel = 5 - typeof chrome !== "undefined" && typeof chrome.sidePanel !== "undefined"; 6 - const hasSidebarAction = 7 - typeof browser !== "undefined" && 8 - typeof browser.sidebarAction !== "undefined"; 9 - const hasNotifications = 10 - typeof chrome !== "undefined" && typeof chrome.notifications !== "undefined"; 11 - 12 - chrome.storage.local.get(["apiUrl"], (result) => { 13 - if (result.apiUrl) { 14 - updateBaseUrls(result.apiUrl); 15 - } 16 - }); 17 - 18 - function updateBaseUrls(url) { 19 - let cleanUrl = url.replace(/\/$/, ""); 20 - 21 - if (cleanUrl.includes("ngrok") && cleanUrl.startsWith("http://")) { 22 - cleanUrl = cleanUrl.replace("http://", "https://"); 23 - } 24 - 25 - API_BASE = cleanUrl; 26 - WEB_BASE = cleanUrl; 27 - API_BASE = cleanUrl; 28 - WEB_BASE = cleanUrl; 29 - } 30 - 31 - function showNotification(title, message) { 32 - if (hasNotifications) { 33 - chrome.notifications.create({ 34 - type: "basic", 35 - iconUrl: "icons/android-chrome-192x192.png", 36 - title: title, 37 - message: message, 38 - }); 39 - } 40 - } 41 - 42 - async function createHighlight(payload) { 43 - if (!API_BASE) { 44 - throw new Error("API URL not configured"); 45 - } 46 - 47 - const cookie = await chrome.cookies.get({ 48 - url: API_BASE, 49 - name: "margin_session", 50 - }); 51 - 52 - if (!cookie) { 53 - throw new Error("Not authenticated"); 54 - } 55 - 56 - const res = await fetch(`${API_BASE}/api/highlights`, { 57 - method: "POST", 58 - headers: { 59 - "Content-Type": "application/json", 60 - "X-Session-Token": cookie.value, 61 - }, 62 - credentials: "include", 63 - body: JSON.stringify(payload), 64 - }); 65 - 66 - if (!res.ok) { 67 - const errText = await res.text(); 68 - throw new Error(`Failed to create highlight: ${res.status} ${errText}`); 69 - } 70 - 71 - return res.json(); 72 - } 73 - 74 - function refreshTabAnnotations(tabId) { 75 - if (!tabId) return; 76 - chrome.tabs.sendMessage(tabId, { type: "REFRESH_ANNOTATIONS" }).catch(() => { 77 - /* ignore missing content script */ 78 - }); 79 - } 80 - 81 - async function openAnnotationUI(tabId, windowId) { 82 - if (hasSidePanel) { 83 - try { 84 - let targetWindowId = windowId; 85 - 86 - if (!targetWindowId) { 87 - const tab = await chrome.tabs.get(tabId); 88 - targetWindowId = tab.windowId; 89 - } 90 - 91 - await chrome.sidePanel.open({ windowId: targetWindowId }); 92 - return true; 93 - } catch (err) { 94 - console.error("Could not open Chrome side panel:", err); 95 - } 96 - } 97 - 98 - if (hasSidebarAction) { 99 - try { 100 - await browser.sidebarAction.open(); 101 - return true; 102 - } catch (err) { 103 - console.warn("Could not open Firefox sidebar:", err); 104 - } 105 - } 106 - 107 - return false; 108 - } 109 - 110 - chrome.runtime.onInstalled.addListener(async () => { 111 - const stored = await chrome.storage.local.get(["apiUrl"]); 112 - if (!stored.apiUrl) { 113 - await chrome.storage.local.set({ apiUrl: "https://margin.at" }); 114 - updateBaseUrls("https://margin.at"); 115 - } 116 - 117 - await ensureContextMenus(); 118 - 119 - if (hasSidebarAction) { 120 - try { 121 - await browser.sidebarAction.close(); 122 - } catch { 123 - /* ignore */ 124 - } 125 - } 126 - }); 127 - 128 - chrome.runtime.onStartup.addListener(async () => { 129 - await ensureContextMenus(); 130 - }); 131 - 132 - async function ensureContextMenus() { 133 - await chrome.contextMenus.removeAll(); 134 - 135 - chrome.contextMenus.create({ 136 - id: "margin-annotate", 137 - title: 'Annotate "%s"', 138 - contexts: ["selection"], 139 - }); 140 - 141 - chrome.contextMenus.create({ 142 - id: "margin-highlight", 143 - title: 'Highlight "%s"', 144 - contexts: ["selection"], 145 - }); 146 - 147 - chrome.contextMenus.create({ 148 - id: "margin-bookmark", 149 - title: "Bookmark this page", 150 - contexts: ["page"], 151 - }); 152 - 153 - chrome.contextMenus.create({ 154 - id: "margin-open-sidebar", 155 - title: "Open Margin Sidebar", 156 - contexts: ["page", "selection", "link"], 157 - }); 158 - } 159 - 160 - chrome.action.onClicked.addListener(async () => { 161 - const stored = await chrome.storage.local.get(["apiUrl"]); 162 - const webUrl = stored.apiUrl || WEB_BASE; 163 - chrome.tabs.create({ url: webUrl }); 164 - }); 165 - 166 - chrome.contextMenus.onClicked.addListener(async (info, tab) => { 167 - if (info.menuItemId === "margin-open-sidebar") { 168 - await openAnnotationUI(tab.id, tab.windowId); 169 - return; 170 - } 171 - 172 - if (info.menuItemId === "margin-bookmark") { 173 - const cookie = await chrome.cookies.get({ 174 - url: API_BASE, 175 - name: "margin_session", 176 - }); 177 - 178 - if (!cookie) { 179 - showNotification("Margin", "Please sign in to bookmark pages"); 180 - return; 181 - } 182 - 183 - try { 184 - const res = await fetch(`${API_BASE}/api/bookmarks`, { 185 - method: "POST", 186 - credentials: "include", 187 - headers: { 188 - "Content-Type": "application/json", 189 - "X-Session-Token": cookie.value, 190 - }, 191 - body: JSON.stringify({ 192 - url: tab.url, 193 - title: tab.title, 194 - }), 195 - }); 196 - 197 - if (res.ok) { 198 - showNotification("Margin", "Page bookmarked!"); 199 - } 200 - } catch (err) { 201 - console.error("Bookmark error:", err); 202 - } 203 - return; 204 - } 205 - 206 - if (info.menuItemId === "margin-annotate") { 207 - let selector = null; 208 - let canonicalUrl = null; 209 - 210 - try { 211 - const response = await chrome.tabs.sendMessage(tab.id, { 212 - type: "GET_SELECTOR_FOR_ANNOTATE_INLINE", 213 - selectionText: info.selectionText, 214 - }); 215 - selector = response?.selector; 216 - canonicalUrl = response?.canonicalUrl; 217 - } catch { 218 - /* ignore */ 219 - } 220 - 221 - if (!selector && info.selectionText) { 222 - selector = { 223 - type: "TextQuoteSelector", 224 - exact: info.selectionText, 225 - }; 226 - } 227 - 228 - const targetUrl = canonicalUrl || tab.url; 229 - 230 - if (selector) { 231 - try { 232 - await chrome.tabs.sendMessage(tab.id, { 233 - type: "SHOW_INLINE_ANNOTATE", 234 - data: { 235 - url: targetUrl, 236 - title: tab.title, 237 - selector: selector, 238 - }, 239 - }); 240 - return; 241 - } catch (e) { 242 - console.debug("Inline annotate failed, falling back to new tab:", e); 243 - } 244 - } 245 - 246 - if (WEB_BASE) { 247 - let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(targetUrl)}`; 248 - if (selector) { 249 - composeUrl += `&selector=${encodeURIComponent(JSON.stringify(selector))}`; 250 - } 251 - chrome.tabs.create({ url: composeUrl }); 252 - } 253 - return; 254 - } 255 - 256 - if (info.menuItemId === "margin-highlight") { 257 - let selector = null; 258 - let canonicalUrl = null; 259 - 260 - try { 261 - const response = await chrome.tabs.sendMessage(tab.id, { 262 - type: "GET_SELECTOR_FOR_HIGHLIGHT", 263 - selectionText: info.selectionText, 264 - }); 265 - if (response?.selector) { 266 - selector = response.selector; 267 - canonicalUrl = response.canonicalUrl; 268 - } 269 - if (response && response.success) return; 270 - } catch { 271 - /* ignore */ 272 - } 273 - 274 - if (!selector && info.selectionText) { 275 - selector = { 276 - type: "TextQuoteSelector", 277 - exact: info.selectionText, 278 - }; 279 - } 280 - 281 - const targetUrl = canonicalUrl || tab.url; 282 - 283 - if (selector) { 284 - try { 285 - await createHighlight({ 286 - url: targetUrl, 287 - title: tab.title, 288 - selector: selector, 289 - }); 290 - showNotification("Margin", "Text highlighted!"); 291 - refreshTabAnnotations(tab.id); 292 - } catch (err) { 293 - console.error("Highlight API error:", err); 294 - if (err?.message === "Not authenticated") { 295 - showNotification("Margin", "Please sign in to create highlights"); 296 - return; 297 - } 298 - showNotification("Margin", "Error creating highlight"); 299 - } 300 - } else { 301 - showNotification("Margin", "No text selected"); 302 - } 303 - } 304 - }); 305 - 306 - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 307 - handleMessage(request, sender, sendResponse); 308 - return true; 309 - }); 310 - 311 - async function handleMessage(request, sender, sendResponse) { 312 - try { 313 - if (request.type === "UPDATE_SETTINGS") { 314 - const result = await chrome.storage.local.get(["apiUrl"]); 315 - if (result.apiUrl) updateBaseUrls(result.apiUrl); 316 - sendResponse({ success: true }); 317 - return; 318 - } 319 - 320 - switch (request.type) { 321 - case "CHECK_SESSION": { 322 - if (!API_BASE) { 323 - sendResponse({ success: true, data: { authenticated: false } }); 324 - return; 325 - } 326 - 327 - const cookie = await chrome.cookies.get({ 328 - url: API_BASE, 329 - name: "margin_session", 330 - }); 331 - 332 - if (!cookie) { 333 - sendResponse({ success: true, data: { authenticated: false } }); 334 - return; 335 - } 336 - 337 - try { 338 - const response = await fetch(`${API_BASE}/auth/session`, { 339 - credentials: "include", 340 - }); 341 - 342 - if (!response.ok) { 343 - sendResponse({ success: true, data: { authenticated: false } }); 344 - return; 345 - } 346 - 347 - const sessionData = await response.json(); 348 - sendResponse({ 349 - success: true, 350 - data: { 351 - authenticated: true, 352 - did: sessionData.did, 353 - handle: sessionData.handle, 354 - }, 355 - }); 356 - } catch (err) { 357 - console.error("Session check error:", err); 358 - sendResponse({ success: true, data: { authenticated: false } }); 359 - } 360 - break; 361 - } 362 - 363 - case "GET_ANNOTATIONS": { 364 - const stored = await chrome.storage.local.get(["apiUrl"]); 365 - const currentApiUrl = stored.apiUrl 366 - ? stored.apiUrl.replace(/\/$/, "") 367 - : API_BASE; 368 - 369 - const pageUrl = request.data.url; 370 - const citedUrls = request.data.citedUrls || []; 371 - const uniqueUrls = [...new Set([pageUrl, ...citedUrls])]; 372 - 373 - const fetchPromises = uniqueUrls.map((u) => 374 - fetch( 375 - `${currentApiUrl}/api/targets?source=${encodeURIComponent(u)}`, 376 - ).then((r) => r.json().catch(() => ({}))), 377 - ); 378 - 379 - const results = await Promise.all(fetchPromises); 380 - let allItems = []; 381 - const seenIds = new Set(); 382 - 383 - results.forEach((data) => { 384 - const items = [ 385 - ...(data.annotations || []), 386 - ...(data.highlights || []), 387 - ...(data.bookmarks || []), 388 - ]; 389 - items.forEach((item) => { 390 - const id = item.uri || item.id; 391 - if (id && !seenIds.has(id)) { 392 - seenIds.add(id); 393 - allItems.push(item); 394 - } 395 - }); 396 - }); 397 - 398 - sendResponse({ success: true, data: allItems }); 399 - 400 - if (sender.tab) { 401 - const count = allItems.length; 402 - chrome.action 403 - .setBadgeText({ 404 - text: count > 0 ? count.toString() : "", 405 - tabId: sender.tab.id, 406 - }) 407 - .catch(() => { 408 - /* ignore */ 409 - }); 410 - chrome.action 411 - .setBadgeBackgroundColor({ 412 - color: "#6366f1", 413 - tabId: sender.tab.id, 414 - }) 415 - .catch(() => { 416 - /* ignore */ 417 - }); 418 - } 419 - break; 420 - } 421 - 422 - case "CREATE_ANNOTATION": { 423 - const cookie = await chrome.cookies.get({ 424 - url: API_BASE, 425 - name: "margin_session", 426 - }); 427 - 428 - if (!cookie) { 429 - sendResponse({ success: false, error: "Not authenticated" }); 430 - return; 431 - } 432 - 433 - const payload = { 434 - url: request.data.url, 435 - text: request.data.text, 436 - title: request.data.title, 437 - }; 438 - 439 - if (request.data.selector) { 440 - payload.selector = request.data.selector; 441 - } 442 - 443 - const createRes = await fetch(`${API_BASE}/api/annotations`, { 444 - method: "POST", 445 - credentials: "include", 446 - headers: { 447 - "Content-Type": "application/json", 448 - "X-Session-Token": cookie.value, 449 - }, 450 - body: JSON.stringify(payload), 451 - }); 452 - 453 - if (!createRes.ok) { 454 - const errorText = await createRes.text(); 455 - throw new Error( 456 - `Failed to create annotation: ${createRes.status} ${errorText}`, 457 - ); 458 - } 459 - 460 - const createData = await createRes.json(); 461 - sendResponse({ success: true, data: createData }); 462 - break; 463 - } 464 - 465 - case "OPEN_LOGIN": 466 - if (!WEB_BASE) { 467 - chrome.runtime.openOptionsPage(); 468 - return; 469 - } 470 - chrome.tabs.create({ url: `${WEB_BASE}/login` }); 471 - break; 472 - 473 - case "OPEN_WEB": 474 - if (!WEB_BASE) { 475 - chrome.runtime.openOptionsPage(); 476 - return; 477 - } 478 - chrome.tabs.create({ url: `${WEB_BASE}` }); 479 - break; 480 - 481 - case "OPEN_COMPOSE": { 482 - if (!WEB_BASE) { 483 - chrome.runtime.openOptionsPage(); 484 - return; 485 - } 486 - const { url, selector } = request.data; 487 - let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(url)}`; 488 - if (selector) { 489 - composeUrl += `&selector=${encodeURIComponent(JSON.stringify(selector))}`; 490 - } 491 - chrome.tabs.create({ url: composeUrl }); 492 - break; 493 - } 494 - 495 - case "OPEN_APP_URL": { 496 - if (!WEB_BASE) { 497 - chrome.runtime.openOptionsPage(); 498 - return; 499 - } 500 - const path = request.data.path; 501 - const safePath = path.startsWith("/") ? path : `/${path}`; 502 - chrome.tabs.create({ url: `${WEB_BASE}${safePath}` }); 503 - break; 504 - } 505 - 506 - case "OPEN_SIDE_PANEL": 507 - if (sender.tab && sender.tab.windowId) { 508 - chrome.sidePanel 509 - .open({ windowId: sender.tab.windowId }) 510 - .catch((err) => console.error("Failed to open side panel", err)); 511 - } 512 - break; 513 - 514 - case "CREATE_BOOKMARK": { 515 - if (!API_BASE) { 516 - sendResponse({ success: false, error: "API URL not configured" }); 517 - return; 518 - } 519 - 520 - const cookie = await chrome.cookies.get({ 521 - url: API_BASE, 522 - name: "margin_session", 523 - }); 524 - 525 - if (!cookie) { 526 - sendResponse({ success: false, error: "Not authenticated" }); 527 - return; 528 - } 529 - 530 - const bookmarkRes = await fetch(`${API_BASE}/api/bookmarks`, { 531 - method: "POST", 532 - credentials: "include", 533 - headers: { 534 - "Content-Type": "application/json", 535 - "X-Session-Token": cookie.value, 536 - }, 537 - body: JSON.stringify({ 538 - url: request.data.url, 539 - title: request.data.title, 540 - }), 541 - }); 542 - 543 - if (!bookmarkRes.ok) { 544 - const errorText = await bookmarkRes.text(); 545 - throw new Error( 546 - `Failed to create bookmark: ${bookmarkRes.status} ${errorText}`, 547 - ); 548 - } 549 - 550 - const bookmarkData = await bookmarkRes.json(); 551 - sendResponse({ success: true, data: bookmarkData }); 552 - break; 553 - } 554 - 555 - case "CREATE_HIGHLIGHT": { 556 - try { 557 - const highlightData = await createHighlight({ 558 - url: request.data.url, 559 - title: request.data.title, 560 - selector: request.data.selector, 561 - color: request.data.color || "yellow", 562 - }); 563 - sendResponse({ success: true, data: highlightData }); 564 - refreshTabAnnotations(sender.tab?.id); 565 - } catch (error) { 566 - sendResponse({ success: false, error: error.message }); 567 - } 568 - break; 569 - } 570 - 571 - case "GET_USER_BOOKMARKS": { 572 - if (!API_BASE) { 573 - sendResponse({ success: false, error: "API URL not configured" }); 574 - return; 575 - } 576 - 577 - const did = request.data.did; 578 - const res = await fetch( 579 - `${API_BASE}/api/users/${encodeURIComponent(did)}/bookmarks`, 580 - ); 581 - 582 - if (!res.ok) { 583 - throw new Error(`Failed to fetch bookmarks: ${res.status}`); 584 - } 585 - 586 - const data = await res.json(); 587 - sendResponse({ success: true, data: data.items || [] }); 588 - break; 589 - } 590 - 591 - case "GET_USER_HIGHLIGHTS": { 592 - if (!API_BASE) { 593 - sendResponse({ success: false, error: "API URL not configured" }); 594 - return; 595 - } 596 - 597 - const did = request.data.did; 598 - const res = await fetch( 599 - `${API_BASE}/api/users/${encodeURIComponent(did)}/highlights`, 600 - ); 601 - 602 - if (!res.ok) { 603 - throw new Error(`Failed to fetch highlights: ${res.status}`); 604 - } 605 - 606 - const data = await res.json(); 607 - sendResponse({ success: true, data: data.items || [] }); 608 - break; 609 - } 610 - 611 - case "GET_USER_COLLECTIONS": { 612 - if (!API_BASE) { 613 - sendResponse({ success: false, error: "API URL not configured" }); 614 - return; 615 - } 616 - 617 - const did = request.data.did; 618 - const res = await fetch( 619 - `${API_BASE}/api/collections?author=${encodeURIComponent(did)}`, 620 - ); 621 - 622 - if (!res.ok) { 623 - throw new Error(`Failed to fetch collections: ${res.status}`); 624 - } 625 - 626 - const data = await res.json(); 627 - sendResponse({ success: true, data: data.items || [] }); 628 - break; 629 - } 630 - 631 - case "GET_CONTAINING_COLLECTIONS": { 632 - if (!API_BASE) { 633 - sendResponse({ success: false, error: "API URL not configured" }); 634 - return; 635 - } 636 - 637 - const uri = request.data.uri; 638 - const res = await fetch( 639 - `${API_BASE}/api/collections/containing?uri=${encodeURIComponent(uri)}`, 640 - ); 641 - 642 - if (!res.ok) { 643 - throw new Error( 644 - `Failed to fetch containing collections: ${res.status}`, 645 - ); 646 - } 647 - 648 - const data = await res.json(); 649 - sendResponse({ success: true, data: data || [] }); 650 - break; 651 - } 652 - 653 - case "ADD_TO_COLLECTION": { 654 - if (!API_BASE) { 655 - sendResponse({ success: false, error: "API URL not configured" }); 656 - return; 657 - } 658 - 659 - const cookie = await chrome.cookies.get({ 660 - url: API_BASE, 661 - name: "margin_session", 662 - }); 663 - 664 - if (!cookie) { 665 - sendResponse({ success: false, error: "Not authenticated" }); 666 - return; 667 - } 668 - 669 - const { collectionUri, annotationUri } = request.data; 670 - const res = await fetch( 671 - `${API_BASE}/api/collections/${encodeURIComponent(collectionUri)}/items`, 672 - { 673 - method: "POST", 674 - credentials: "include", 675 - headers: { 676 - "Content-Type": "application/json", 677 - "X-Session-Token": cookie.value, 678 - }, 679 - body: JSON.stringify({ 680 - annotationUri: annotationUri, 681 - }), 682 - }, 683 - ); 684 - 685 - if (!res.ok) { 686 - const errText = await res.text(); 687 - throw new Error( 688 - `Failed to add to collection: ${res.status} ${errText}`, 689 - ); 690 - } 691 - 692 - const data = await res.json(); 693 - sendResponse({ success: true, data }); 694 - break; 695 - } 696 - 697 - case "GET_REPLIES": { 698 - if (!API_BASE) { 699 - sendResponse({ success: false, error: "API URL not configured" }); 700 - return; 701 - } 702 - 703 - const uri = request.data.uri; 704 - const res = await fetch( 705 - `${API_BASE}/api/replies?uri=${encodeURIComponent(uri)}`, 706 - ); 707 - 708 - if (!res.ok) { 709 - throw new Error(`Failed to fetch replies: ${res.status}`); 710 - } 711 - 712 - const data = await res.json(); 713 - sendResponse({ success: true, data: data.items || [] }); 714 - break; 715 - } 716 - 717 - case "CREATE_REPLY": { 718 - if (!API_BASE) { 719 - sendResponse({ success: false, error: "API URL not configured" }); 720 - return; 721 - } 722 - 723 - const cookie = await chrome.cookies.get({ 724 - url: API_BASE, 725 - name: "margin_session", 726 - }); 727 - 728 - if (!cookie) { 729 - sendResponse({ success: false, error: "Not authenticated" }); 730 - return; 731 - } 732 - 733 - const { parentUri, parentCid, rootUri, rootCid, text } = request.data; 734 - const res = await fetch(`${API_BASE}/api/annotations/reply`, { 735 - method: "POST", 736 - credentials: "include", 737 - headers: { 738 - "Content-Type": "application/json", 739 - "X-Session-Token": cookie.value, 740 - }, 741 - body: JSON.stringify({ 742 - parentUri, 743 - parentCid, 744 - rootUri, 745 - rootCid, 746 - text, 747 - }), 748 - }); 749 - 750 - if (!res.ok) { 751 - const errText = await res.text(); 752 - throw new Error(`Failed to create reply: ${res.status} ${errText}`); 753 - } 754 - 755 - const data = await res.json(); 756 - sendResponse({ success: true, data }); 757 - break; 758 - } 759 - } 760 - } catch (error) { 761 - console.error("Service worker error:", error); 762 - sendResponse({ success: false, error: error.message }); 763 - } 764 - }
+1301
extension/bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "workspaces": { 4 + "": { 5 + "name": "margin-extension", 6 + "dependencies": { 7 + "@webext-core/messaging": "^1.4.0", 8 + "clsx": "^2.1.1", 9 + "lucide-react": "^0.563.0", 10 + "react": "^18.3.1", 11 + "react-dom": "^18.3.1", 12 + }, 13 + "devDependencies": { 14 + "@eslint/js": "^9.39.2", 15 + "@types/react": "^18.3.12", 16 + "@types/react-dom": "^18.3.1", 17 + "@wxt-dev/module-react": "^1.1.1", 18 + "autoprefixer": "^10.4.20", 19 + "eslint": "^9.39.2", 20 + "eslint-config-prettier": "^10.1.8", 21 + "postcss": "^8.4.49", 22 + "prettier": "^3.8.1", 23 + "tailwindcss": "^3.4.15", 24 + "typescript": "^5.6.3", 25 + "typescript-eslint": "^8.54.0", 26 + "wxt": "^0.19.13", 27 + }, 28 + }, 29 + }, 30 + "packages": { 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 + 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 + 35 + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], 36 + 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 + 39 + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], 40 + 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 + 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 + 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 + 47 + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], 48 + 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 + 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 + 53 + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], 54 + 55 + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], 56 + 57 + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], 58 + 59 + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], 60 + 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 + 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 + 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 + 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 + 69 + "@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], 70 + 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 + 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 + 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 + 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 + 79 + "@devicefarmer/adbkit-logcat": ["@devicefarmer/adbkit-logcat@2.1.3", "", {}, "sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw=="], 80 + 81 + "@devicefarmer/adbkit-monkey": ["@devicefarmer/adbkit-monkey@1.2.1", "", {}, "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg=="], 82 + 83 + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], 84 + 85 + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], 86 + 87 + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], 88 + 89 + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], 90 + 91 + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], 92 + 93 + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], 94 + 95 + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], 96 + 97 + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], 98 + 99 + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], 100 + 101 + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], 102 + 103 + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], 104 + 105 + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], 106 + 107 + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], 108 + 109 + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], 110 + 111 + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], 112 + 113 + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], 114 + 115 + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], 116 + 117 + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], 118 + 119 + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], 120 + 121 + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], 122 + 123 + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], 124 + 125 + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], 126 + 127 + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], 128 + 129 + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], 130 + 131 + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], 132 + 133 + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], 134 + 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 + 137 + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], 138 + 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 + 141 + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], 142 + 143 + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], 144 + 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 + 147 + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], 148 + 149 + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], 150 + 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 + 153 + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], 154 + 155 + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], 156 + 157 + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], 158 + 159 + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], 160 + 161 + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], 162 + 163 + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], 164 + 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 + 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 + 169 + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 170 + 171 + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 172 + 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 + 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 + 177 + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], 178 + 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 + 181 + "@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="], 182 + 183 + "@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="], 184 + 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 + 187 + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], 188 + 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 + 191 + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], 192 + 193 + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], 194 + 195 + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], 196 + 197 + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], 198 + 199 + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], 200 + 201 + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], 202 + 203 + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], 204 + 205 + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], 206 + 207 + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], 208 + 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 + 211 + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], 212 + 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 + 215 + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], 216 + 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 + 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 + 221 + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], 222 + 223 + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], 224 + 225 + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], 226 + 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 + 229 + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], 230 + 231 + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], 232 + 233 + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], 234 + 235 + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], 236 + 237 + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], 238 + 239 + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], 240 + 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 + 243 + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], 244 + 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 + 247 + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], 248 + 249 + "@types/chrome": ["@types/chrome@0.0.280", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-AotSmZrL9bcZDDmSI1D9dE7PGbhOur5L0cKxXd7IqbVizQWCY4gcvupPUVsQ4FfDj3V2tt/iOpomT9EY0s+w1g=="], 250 + 251 + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 252 + 253 + "@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="], 254 + 255 + "@types/filewriter": ["@types/filewriter@0.0.33", "", {}, "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="], 256 + 257 + "@types/har-format": ["@types/har-format@1.2.16", "", {}, "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A=="], 258 + 259 + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], 260 + 261 + "@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="], 262 + 263 + "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], 264 + 265 + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], 266 + 267 + "@types/react": ["@types/react@18.3.27", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w=="], 268 + 269 + "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], 270 + 271 + "@types/webextension-polyfill": ["@types/webextension-polyfill@0.12.4", "", {}, "sha512-wK8YdSI0pDiaehSLDIvtvonYmLwUUivg4Z6JCJO8rkyssMAG82cFJgwPK/V7NO61mJBLg/tXeoXQL8AFzpXZmQ=="], 272 + 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 + 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 + 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 + 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 + 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 + 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 + 285 + "@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="], 286 + 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 + 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 + 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 + 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 + 295 + "@webext-core/fake-browser": ["@webext-core/fake-browser@1.3.4", "", { "dependencies": { "lodash.merge": "^4.6.2" } }, "sha512-nZcVWr3JpwpS5E6hKpbAwAMBM/AXZShnfW0F76udW8oLd6Kv0nbW6vFS07md4Na/0ntQonk3hFnlQYGYBAlTrA=="], 296 + 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 + 299 + "@webext-core/match-patterns": ["@webext-core/match-patterns@1.0.3", "", {}, "sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A=="], 300 + 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 + 303 + "@wxt-dev/browser": ["@wxt-dev/browser@0.1.32", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-jvfSppeLzlH4sOkIvMBJoA1pKoI+U5gTkjDwMKdkTWh0P/fj+KDyze3lzo3S6372viCm8tXUKNez+VKyVz2ZDw=="], 304 + 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 + 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 + 309 + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], 310 + 311 + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], 312 + 313 + "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], 314 + 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 + 317 + "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], 318 + 319 + "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], 320 + 321 + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], 322 + 323 + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], 324 + 325 + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], 326 + 327 + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], 328 + 329 + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], 330 + 331 + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], 332 + 333 + "array-differ": ["array-differ@4.0.0", "", {}, "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw=="], 334 + 335 + "array-union": ["array-union@3.0.1", "", {}, "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw=="], 336 + 337 + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], 338 + 339 + "async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="], 340 + 341 + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], 342 + 343 + "atomically": ["atomically@2.1.0", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q=="], 344 + 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 + 347 + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], 348 + 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 + 351 + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], 352 + 353 + "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], 354 + 355 + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], 356 + 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 + 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 + 361 + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], 362 + 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 + 365 + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], 366 + 367 + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], 368 + 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 + 371 + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], 372 + 373 + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], 374 + 375 + "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], 376 + 377 + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], 378 + 379 + "caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="], 380 + 381 + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], 382 + 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 + 385 + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], 386 + 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 + 389 + "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], 390 + 391 + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], 392 + 393 + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], 394 + 395 + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], 396 + 397 + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], 398 + 399 + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], 400 + 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 + 403 + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], 404 + 405 + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 406 + 407 + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 408 + 409 + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], 410 + 411 + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], 412 + 413 + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], 414 + 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 + 417 + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], 418 + 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 + 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 + 423 + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], 424 + 425 + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 426 + 427 + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], 428 + 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 + 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 + 433 + "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], 434 + 435 + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], 436 + 437 + "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], 438 + 439 + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], 440 + 441 + "debounce": ["debounce@1.2.1", "", {}, "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="], 442 + 443 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 444 + 445 + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], 446 + 447 + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], 448 + 449 + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], 450 + 451 + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], 452 + 453 + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], 454 + 455 + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], 456 + 457 + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], 458 + 459 + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], 460 + 461 + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], 462 + 463 + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], 464 + 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 + 467 + "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], 468 + 469 + "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], 470 + 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 + 473 + "dot-prop": ["dot-prop@9.0.0", "", { "dependencies": { "type-fest": "^4.18.2" } }, "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ=="], 474 + 475 + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], 476 + 477 + "dotenv-expand": ["dotenv-expand@12.0.3", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA=="], 478 + 479 + "electron-to-chromium": ["electron-to-chromium@1.5.283", "", {}, "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w=="], 480 + 481 + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], 482 + 483 + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], 484 + 485 + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], 486 + 487 + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], 488 + 489 + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], 490 + 491 + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], 492 + 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 + 495 + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 496 + 497 + "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="], 498 + 499 + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], 500 + 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 + 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 + 505 + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], 506 + 507 + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 508 + 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 + 511 + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], 512 + 513 + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], 514 + 515 + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], 516 + 517 + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], 518 + 519 + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 520 + 521 + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], 522 + 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 + 525 + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], 526 + 527 + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 528 + 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 + 531 + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], 532 + 533 + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], 534 + 535 + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], 536 + 537 + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], 538 + 539 + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 540 + 541 + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], 542 + 543 + "filesize": ["filesize@10.1.6", "", {}, "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w=="], 544 + 545 + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], 546 + 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 + 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 + 551 + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], 552 + 553 + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], 554 + 555 + "form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="], 556 + 557 + "formdata-node": ["formdata-node@6.0.3", "", {}, "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg=="], 558 + 559 + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], 560 + 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 + 563 + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], 564 + 565 + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], 566 + 567 + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 568 + 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 + 571 + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], 572 + 573 + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], 574 + 575 + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], 576 + 577 + "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], 578 + 579 + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], 580 + 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 + 583 + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], 584 + 585 + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], 586 + 587 + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], 588 + 589 + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], 590 + 591 + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 592 + 593 + "graceful-readlink": ["graceful-readlink@1.0.1", "", {}, "sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w=="], 594 + 595 + "growly": ["growly@1.3.0", "", {}, "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw=="], 596 + 597 + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], 598 + 599 + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 600 + 601 + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], 602 + 603 + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], 604 + 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 + 607 + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], 608 + 609 + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], 610 + 611 + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], 612 + 613 + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], 614 + 615 + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], 616 + 617 + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], 618 + 619 + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 620 + 621 + "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], 622 + 623 + "is-absolute": ["is-absolute@0.1.7", "", { "dependencies": { "is-relative": "^0.1.0" } }, "sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA=="], 624 + 625 + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], 626 + 627 + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], 628 + 629 + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], 630 + 631 + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], 632 + 633 + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], 634 + 635 + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], 636 + 637 + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], 638 + 639 + "is-in-ci": ["is-in-ci@1.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg=="], 640 + 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 + 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 + 645 + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], 646 + 647 + "is-npm": ["is-npm@6.1.0", "", {}, "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA=="], 648 + 649 + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], 650 + 651 + "is-path-inside": ["is-path-inside@4.0.0", "", {}, "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA=="], 652 + 653 + "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], 654 + 655 + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], 656 + 657 + "is-primitive": ["is-primitive@3.0.1", "", {}, "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w=="], 658 + 659 + "is-relative": ["is-relative@0.1.3", "", {}, "sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA=="], 660 + 661 + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], 662 + 663 + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], 664 + 665 + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], 666 + 667 + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], 668 + 669 + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 670 + 671 + "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], 672 + 673 + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], 674 + 675 + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 676 + 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 + 679 + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], 680 + 681 + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], 682 + 683 + "json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="], 684 + 685 + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], 686 + 687 + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], 688 + 689 + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], 690 + 691 + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], 692 + 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 + 695 + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], 696 + 697 + "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], 698 + 699 + "ky": ["ky@1.14.3", "", {}, "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw=="], 700 + 701 + "latest-version": ["latest-version@9.0.0", "", { "dependencies": { "package-json": "^10.0.0" } }, "sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA=="], 702 + 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 + 705 + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], 706 + 707 + "lighthouse-logger": ["lighthouse-logger@2.0.2", "", { "dependencies": { "debug": "^4.4.1", "marky": "^1.2.2" } }, "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg=="], 708 + 709 + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], 710 + 711 + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], 712 + 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 + 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 + 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 + 719 + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], 720 + 721 + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], 722 + 723 + "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], 724 + 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 + 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 + 729 + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], 730 + 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 + 733 + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 734 + 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 + 737 + "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], 738 + 739 + "many-keys-map": ["many-keys-map@2.0.1", "", {}, "sha512-DHnZAD4phTbZ+qnJdjoNEVU1NecYoSdbOOoVmTDH46AuxDkEVh3MxTVpXq10GtcTC6mndN9dkv1rNfpjRcLnOw=="], 740 + 741 + "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], 742 + 743 + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], 744 + 745 + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], 746 + 747 + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], 748 + 749 + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], 750 + 751 + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], 752 + 753 + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], 754 + 755 + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], 756 + 757 + "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], 758 + 759 + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], 760 + 761 + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], 762 + 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 + 765 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 766 + 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 + 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 + 771 + "nano-spawn": ["nano-spawn@0.2.1", "", {}, "sha512-/pULofvsF8mOVcl/nUeVXL/GYOEvc7eJWSIxa+K4OYUolvXa5zwSgevsn4eoHs1xvh/BO3vx/PZiD9+Ow2ZVuw=="], 772 + 773 + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 774 + 775 + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], 776 + 777 + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], 778 + 779 + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], 780 + 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 + 783 + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], 784 + 785 + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], 786 + 787 + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], 788 + 789 + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], 790 + 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 + 793 + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], 794 + 795 + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], 796 + 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 + 799 + "ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], 800 + 801 + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], 802 + 803 + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], 804 + 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 + 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 + 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 + 811 + "os-shim": ["os-shim@0.1.3", "", {}, "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A=="], 812 + 813 + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], 814 + 815 + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], 816 + 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 + 819 + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], 820 + 821 + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], 822 + 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 + 825 + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], 826 + 827 + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 828 + 829 + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], 830 + 831 + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 832 + 833 + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], 834 + 835 + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 836 + 837 + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], 838 + 839 + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], 840 + 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 + 843 + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], 844 + 845 + "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], 846 + 847 + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], 848 + 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 + 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 + 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 + 855 + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], 856 + 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 + 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 + 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 + 863 + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], 864 + 865 + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], 866 + 867 + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], 868 + 869 + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], 870 + 871 + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], 872 + 873 + "promise-toolbox": ["promise-toolbox@0.21.0", "", { "dependencies": { "make-error": "^1.3.2" } }, "sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg=="], 874 + 875 + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], 876 + 877 + "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], 878 + 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 + 881 + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], 882 + 883 + "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], 884 + 885 + "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], 886 + 887 + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], 888 + 889 + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], 890 + 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 + 893 + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], 894 + 895 + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], 896 + 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 + 899 + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], 900 + 901 + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], 902 + 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 + 905 + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], 906 + 907 + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], 908 + 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 + 911 + "registry-url": ["registry-url@6.0.1", "", { "dependencies": { "rc": "1.2.8" } }, "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q=="], 912 + 913 + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], 914 + 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 + 917 + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], 918 + 919 + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], 920 + 921 + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], 922 + 923 + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], 924 + 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 + 927 + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], 928 + 929 + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], 930 + 931 + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], 932 + 933 + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], 934 + 935 + "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], 936 + 937 + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], 938 + 939 + "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], 940 + 941 + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 942 + 943 + "serialize-error": ["serialize-error@11.0.3", "", { "dependencies": { "type-fest": "^2.12.2" } }, "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g=="], 944 + 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 + 947 + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], 948 + 949 + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 950 + 951 + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 952 + 953 + "shell-quote": ["shell-quote@1.7.3", "", {}, "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="], 954 + 955 + "shellwords": ["shellwords@0.1.1", "", {}, "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww=="], 956 + 957 + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], 958 + 959 + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], 960 + 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 + 963 + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], 964 + 965 + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], 966 + 967 + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 968 + 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 + 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 + 973 + "split": ["split@1.0.1", "", { "dependencies": { "through": "2" } }, "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg=="], 974 + 975 + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], 976 + 977 + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], 978 + 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 + 981 + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], 982 + 983 + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], 984 + 985 + "strip-bom": ["strip-bom@5.0.0", "", {}, "sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A=="], 986 + 987 + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], 988 + 989 + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], 990 + 991 + "strip-literal": ["strip-literal@2.1.1", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q=="], 992 + 993 + "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], 994 + 995 + "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], 996 + 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 + 999 + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], 1000 + 1001 + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], 1002 + 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 + 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 + 1007 + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], 1008 + 1009 + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], 1010 + 1011 + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], 1012 + 1013 + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], 1014 + 1015 + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], 1016 + 1017 + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 1018 + 1019 + "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], 1020 + 1021 + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], 1022 + 1023 + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], 1024 + 1025 + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], 1026 + 1027 + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 1028 + 1029 + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 1030 + 1031 + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], 1032 + 1033 + "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], 1034 + 1035 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 1036 + 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 + 1039 + "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], 1040 + 1041 + "uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="], 1042 + 1043 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 1044 + 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 + 1047 + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], 1048 + 1049 + "unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="], 1050 + 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 + 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 + 1055 + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], 1056 + 1057 + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], 1058 + 1059 + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], 1060 + 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 + 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 + 1065 + "watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="], 1066 + 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 + 1069 + "webextension-polyfill": ["webextension-polyfill@0.12.0", "", {}, "sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q=="], 1070 + 1071 + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], 1072 + 1073 + "when": ["when@3.7.7", "", {}, "sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw=="], 1074 + 1075 + "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], 1076 + 1077 + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 1078 + 1079 + "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], 1080 + 1081 + "winreg": ["winreg@0.0.12", "", {}, "sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ=="], 1082 + 1083 + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], 1084 + 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 + 1087 + "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], 1088 + 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 + 1091 + "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], 1092 + 1093 + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], 1094 + 1095 + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], 1096 + 1097 + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 1098 + 1099 + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], 1100 + 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 + 1103 + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], 1104 + 1105 + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 1106 + 1107 + "zip-dir": ["zip-dir@2.0.0", "", { "dependencies": { "async": "^3.2.0", "jszip": "^3.2.2" } }, "sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg=="], 1108 + 1109 + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], 1110 + 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 + 1113 + "@babel/core/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], 1114 + 1115 + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1116 + 1117 + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], 1118 + 1119 + "@devicefarmer/adbkit/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], 1120 + 1121 + "@devicefarmer/adbkit/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="], 1122 + 1123 + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], 1124 + 1125 + "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], 1126 + 1127 + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], 1128 + 1129 + "@rollup/pluginutils/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1130 + 1131 + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], 1132 + 1133 + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], 1134 + 1135 + "@webext-core/messaging/webextension-polyfill": ["webextension-polyfill@0.10.0", "", {}, "sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g=="], 1136 + 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 + 1139 + "boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], 1140 + 1141 + "boxen/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], 1142 + 1143 + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], 1144 + 1145 + "c12/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], 1146 + 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 + 1149 + "c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], 1150 + 1151 + "c12/ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], 1152 + 1153 + "c12/perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], 1154 + 1155 + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 1156 + 1157 + "chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], 1158 + 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 + 1161 + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1162 + 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 + 1165 + "config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], 1166 + 1167 + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], 1168 + 1169 + "dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], 1170 + 1171 + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 1172 + 1173 + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], 1174 + 1175 + "fx-runner/commander": ["commander@2.9.0", "", { "dependencies": { "graceful-readlink": ">= 1.0.0" } }, "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A=="], 1176 + 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 + 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 + 1181 + "global-directory/ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], 1182 + 1183 + "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], 1184 + 1185 + "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], 1186 + 1187 + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], 1188 + 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 + 1191 + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], 1192 + 1193 + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], 1194 + 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 + 1197 + "node-notifier/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], 1198 + 1199 + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], 1200 + 1201 + "nypm/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], 1202 + 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 + 1205 + "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], 1206 + 1207 + "parse-json/lines-and-columns": ["lines-and-columns@2.0.4", "", {}, "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A=="], 1208 + 1209 + "parse-json/type-fest": ["type-fest@3.13.1", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="], 1210 + 1211 + "publish-browser-extension/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], 1212 + 1213 + "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], 1214 + 1215 + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], 1216 + 1217 + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], 1218 + 1219 + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], 1220 + 1221 + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], 1222 + 1223 + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], 1224 + 1225 + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], 1226 + 1227 + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1228 + 1229 + "unimport/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], 1230 + 1231 + "unimport/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1232 + 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 + 1235 + "update-notifier/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], 1236 + 1237 + "vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 1238 + 1239 + "web-ext-run/strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="], 1240 + 1241 + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], 1242 + 1243 + "wxt/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], 1244 + 1245 + "wxt/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], 1246 + 1247 + "wxt/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], 1248 + 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 + 1251 + "@aklinker1/rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], 1252 + 1253 + "@aklinker1/rollup-plugin-visualizer/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], 1254 + 1255 + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], 1256 + 1257 + "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 1258 + 1259 + "ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1260 + 1261 + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], 1262 + 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 + 1265 + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 1266 + 1267 + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 1268 + 1269 + "fx-runner/which/isexe": ["isexe@1.1.2", "", {}, "sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw=="], 1270 + 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 + 1273 + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], 1274 + 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 + 1277 + "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], 1278 + 1279 + "nypm/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], 1280 + 1281 + "nypm/pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 1282 + 1283 + "unimport/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], 1284 + 1285 + "wxt/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], 1286 + 1287 + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], 1288 + 1289 + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1290 + 1291 + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 1292 + 1293 + "c12/giget/nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], 1294 + 1295 + "c12/giget/nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], 1296 + 1297 + "giget/nypm/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], 1298 + 1299 + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], 1300 + } 1301 + }
-21
extension/content/content.css
··· 1 - ::highlight(margin-highlight-preview) { 2 - background-color: rgba(149, 122, 134, 0.3); 3 - color: inherit; 4 - } 5 - 6 - ::highlight(margin-scroll-highlight) { 7 - background-color: rgba(149, 122, 134, 0.5); 8 - color: inherit; 9 - } 10 - 11 - ::highlight(margin-page-highlights) { 12 - background-color: rgba(149, 122, 134, 0.25); 13 - color: inherit; 14 - } 15 - 16 - .margin-notification { 17 - position: fixed; 18 - bottom: 24px; 19 - right: 24px; 20 - z-index: 999999; 21 - }
-1263
extension/content/content.js
··· 1 - (() => { 2 - let sidebarHost = null; 3 - let sidebarShadow = null; 4 - let popoverEl = null; 5 - 6 - let activeItems = []; 7 - let currentSelection = null; 8 - 9 - const OVERLAY_STYLES = ` 10 - :host { 11 - all: initial; 12 - --bg-primary: #0a0a0d; 13 - --bg-secondary: #121216; 14 - --bg-tertiary: #1a1a1f; 15 - --bg-card: #0f0f13; 16 - --bg-elevated: #18181d; 17 - --bg-hover: #1e1e24; 18 - 19 - --text-primary: #eaeaee; 20 - --text-secondary: #b7b6c5; 21 - --text-tertiary: #6e6d7a; 22 - --border: rgba(183, 182, 197, 0.12); 23 - 24 - --accent: #957a86; 25 - --accent-hover: #a98d98; 26 - --accent-subtle: rgba(149, 122, 134, 0.15); 27 - } 28 - 29 - :host(.light) { 30 - --bg-primary: #f8f8fa; 31 - --bg-secondary: #ffffff; 32 - --bg-tertiary: #f0f0f4; 33 - --bg-card: #ffffff; 34 - --bg-elevated: #ffffff; 35 - --bg-hover: #eeeef2; 36 - 37 - --text-primary: #18171c; 38 - --text-secondary: #5c495a; 39 - --text-tertiary: #8a8494; 40 - --border: rgba(92, 73, 90, 0.12); 41 - 42 - --accent: #7a5f6d; 43 - --accent-hover: #664e5b; 44 - --accent-subtle: rgba(149, 122, 134, 0.12); 45 - } 46 - 47 - .margin-overlay { 48 - position: absolute; 49 - top: 0; 50 - left: 0; 51 - width: 100%; 52 - height: 100%; 53 - pointer-events: none; 54 - } 55 - 56 - .margin-popover { 57 - position: absolute; 58 - width: 300px; 59 - background: var(--bg-card); 60 - border: 1px solid var(--border); 61 - border-radius: 12px; 62 - padding: 0; 63 - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); 64 - display: flex; 65 - flex-direction: column; 66 - pointer-events: auto; 67 - z-index: 2147483647; 68 - font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 69 - color: var(--text-primary); 70 - opacity: 0; 71 - transform: translateY(-4px); 72 - animation: popover-in 0.15s forwards; 73 - max-height: 400px; 74 - overflow: hidden; 75 - } 76 - @keyframes popover-in { to { opacity: 1; transform: translateY(0); } } 77 - 78 - .popover-header { 79 - padding: 10px 14px; 80 - border-bottom: 1px solid var(--border); 81 - display: flex; 82 - justify-content: space-between; 83 - align-items: center; 84 - background: var(--bg-primary); 85 - border-radius: 12px 12px 0 0; 86 - font-weight: 500; 87 - font-size: 11px; 88 - color: var(--text-tertiary); 89 - text-transform: uppercase; 90 - letter-spacing: 0.5px; 91 - } 92 - .popover-close { 93 - background: none; 94 - border: none; 95 - color: var(--text-tertiary); 96 - cursor: pointer; 97 - padding: 2px; 98 - font-size: 16px; 99 - line-height: 1; 100 - opacity: 0.6; 101 - transition: opacity 0.15s; 102 - } 103 - .popover-close:hover { opacity: 1; } 104 - 105 - .popover-scroll-area { 106 - overflow-y: auto; 107 - max-height: 340px; 108 - } 109 - 110 - .comment-item { 111 - padding: 12px 14px; 112 - border-bottom: 1px solid var(--border); 113 - } 114 - .comment-item:last-child { 115 - border-bottom: none; 116 - } 117 - 118 - .comment-header { 119 - display: flex; 120 - align-items: center; 121 - gap: 8px; 122 - margin-bottom: 6px; 123 - } 124 - .comment-avatar { 125 - width: 22px; 126 - height: 22px; 127 - border-radius: 50%; 128 - background: var(--accent); 129 - display: flex; 130 - align-items: center; 131 - justify-content: center; 132 - font-size: 9px; 133 - font-weight: 600; 134 - color: white; 135 - } 136 - .comment-handle { 137 - font-size: 12px; 138 - font-weight: 600; 139 - color: var(--text-primary); 140 - } 141 - .comment-time { 142 - font-size: 11px; 143 - color: var(--text-tertiary); 144 - margin-left: auto; 145 - } 146 - 147 - .comment-text { 148 - font-size: 13px; 149 - line-height: 1.5; 150 - color: var(--text-primary); 151 - margin-bottom: 8px; 152 - } 153 - 154 - .highlight-only-badge { 155 - display: inline-flex; 156 - align-items: center; 157 - gap: 4px; 158 - font-size: 11px; 159 - color: var(--text-tertiary); 160 - font-style: italic; 161 - } 162 - 163 - .comment-actions { 164 - display: flex; 165 - gap: 8px; 166 - margin-top: 8px; 167 - } 168 - .highlight-only-badge { 169 - font-size: 11px; 170 - color: var(--text-tertiary); 171 - font-style: italic; 172 - opacity: 0.7; 173 - } 174 - .comment-action-btn { 175 - background: none; 176 - border: none; 177 - padding: 4px 8px; 178 - color: var(--text-tertiary); 179 - font-size: 11px; 180 - cursor: pointer; 181 - border-radius: 4px; 182 - transition: all 0.15s; 183 - } 184 - .comment-action-btn:hover { 185 - background: var(--bg-hover); 186 - color: var(--text-secondary); 187 - } 188 - 189 - .margin-selection-popup { 190 - position: fixed; 191 - display: flex; 192 - gap: 4px; 193 - padding: 6px; 194 - background: var(--bg-card); 195 - border: 1px solid var(--border); 196 - border-radius: 8px; 197 - box-shadow: 0 8px 24px rgba(0,0,0,0.3); 198 - z-index: 2147483647; 199 - pointer-events: auto; 200 - font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 201 - animation: popover-in 0.15s forwards; 202 - } 203 - .selection-btn { 204 - display: flex; 205 - align-items: center; 206 - gap: 6px; 207 - padding: 6px 12px; 208 - background: transparent; 209 - border: none; 210 - border-radius: 6px; 211 - color: var(--text-primary); 212 - font-size: 12px; 213 - font-weight: 500; 214 - cursor: pointer; 215 - transition: background 0.15s; 216 - } 217 - .selection-btn:hover { 218 - background: var(--bg-hover); 219 - } 220 - .selection-btn svg { 221 - width: 14px; 222 - height: 14px; 223 - } 224 - .inline-compose-modal { 225 - position: fixed; 226 - width: 340px; 227 - max-width: calc(100vw - 40px); 228 - background: var(--bg-card); 229 - border: 1px solid var(--border); 230 - border-radius: 12px; 231 - padding: 16px; 232 - box-sizing: border-box; 233 - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); 234 - z-index: 2147483647; 235 - pointer-events: auto; 236 - font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 237 - color: var(--text-primary); 238 - animation: popover-in 0.15s forwards; 239 - overflow: hidden; 240 - } 241 - .inline-compose-modal * { 242 - box-sizing: border-box; 243 - } 244 - .inline-compose-quote { 245 - padding: 8px 12px; 246 - background: var(--accent-subtle); 247 - border-left: 2px solid var(--accent); 248 - border-radius: 4px; 249 - font-size: 12px; 250 - color: var(--text-secondary); 251 - font-style: italic; 252 - margin-bottom: 12px; 253 - max-height: 60px; 254 - overflow: hidden; 255 - word-break: break-word; 256 - } 257 - .inline-compose-textarea { 258 - width: 100%; 259 - min-height: 80px; 260 - padding: 10px 12px; 261 - background: var(--bg-elevated); 262 - border: 1px solid var(--border); 263 - border-radius: 8px; 264 - color: var(--text-primary); 265 - font-family: inherit; 266 - font-size: 13px; 267 - resize: vertical; 268 - margin-bottom: 12px; 269 - box-sizing: border-box; 270 - } 271 - .inline-compose-textarea:focus { 272 - outline: none; 273 - border-color: var(--accent); 274 - } 275 - .inline-compose-actions { 276 - display: flex; 277 - justify-content: flex-end; 278 - gap: 8px; 279 - } 280 - .btn-cancel { 281 - padding: 8px 16px; 282 - background: transparent; 283 - border: 1px solid var(--border); 284 - border-radius: 6px; 285 - color: var(--text-secondary); 286 - font-size: 13px; 287 - cursor: pointer; 288 - } 289 - .btn-cancel:hover { 290 - background: var(--bg-hover); 291 - color: var(--text-primary); 292 - } 293 - .btn-submit { 294 - padding: 8px 16px; 295 - background: var(--accent); 296 - border: none; 297 - border-radius: 6px; 298 - color: white; 299 - font-size: 13px; 300 - font-weight: 500; 301 - cursor: pointer; 302 - } 303 - .btn-submit:hover { 304 - background: var(--accent-hover); 305 - } 306 - .btn-submit:disabled { 307 - opacity: 0.5; 308 - cursor: not-allowed; 309 - } 310 - .reply-section { 311 - border-top: 1px solid var(--border); 312 - padding: 10px 14px; 313 - background: var(--bg-primary); 314 - border-radius: 0 0 12px 12px; 315 - } 316 - .reply-textarea { 317 - width: 100%; 318 - min-height: 50px; 319 - padding: 8px 10px; 320 - background: var(--bg-elevated); 321 - border: 1px solid var(--border); 322 - border-radius: 6px; 323 - color: var(--text-primary); 324 - font-family: inherit; 325 - font-size: 12px; 326 - resize: none; 327 - margin-bottom: 8px; 328 - } 329 - .reply-textarea:focus { 330 - outline: none; 331 - border-color: var(--accent); 332 - } 333 - .reply-submit { 334 - padding: 6px 12px; 335 - background: var(--accent); 336 - border: none; 337 - border-radius: 4px; 338 - color: white; 339 - font-size: 11px; 340 - font-weight: 500; 341 - cursor: pointer; 342 - float: right; 343 - } 344 - .reply-submit:disabled { 345 - opacity: 0.5; 346 - } 347 - .reply-item { 348 - padding: 8px 0; 349 - border-top: 1px solid var(--border); 350 - } 351 - .reply-item:first-child { 352 - border-top: none; 353 - } 354 - .reply-author { 355 - font-size: 11px; 356 - font-weight: 600; 357 - color: var(--text-secondary); 358 - margin-bottom: 4px; 359 - } 360 - .reply-text { 361 - font-size: 12px; 362 - color: var(--text-primary); 363 - line-height: 1.4; 364 - } 365 - `; 366 - 367 - class DOMTextMatcher { 368 - constructor() { 369 - this.textNodes = []; 370 - this.corpus = ""; 371 - this.indices = []; 372 - this.buildMap(); 373 - } 374 - 375 - buildMap() { 376 - const walker = document.createTreeWalker( 377 - document.body, 378 - NodeFilter.SHOW_TEXT, 379 - { 380 - acceptNode: (node) => { 381 - if (!node.parentNode) return NodeFilter.FILTER_REJECT; 382 - const tag = node.parentNode.tagName; 383 - if ( 384 - ["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "INPUT"].includes(tag) 385 - ) 386 - return NodeFilter.FILTER_REJECT; 387 - if (node.textContent.trim().length === 0) 388 - return NodeFilter.FILTER_SKIP; 389 - 390 - if (node.parentNode.offsetParent === null) 391 - return NodeFilter.FILTER_REJECT; 392 - 393 - return NodeFilter.FILTER_ACCEPT; 394 - }, 395 - }, 396 - ); 397 - 398 - let currentNode; 399 - let index = 0; 400 - while ((currentNode = walker.nextNode())) { 401 - const text = currentNode.textContent; 402 - this.textNodes.push(currentNode); 403 - this.corpus += text; 404 - this.indices.push({ 405 - start: index, 406 - node: currentNode, 407 - length: text.length, 408 - }); 409 - index += text.length; 410 - } 411 - } 412 - 413 - findRange(searchText) { 414 - if (!searchText) return null; 415 - 416 - let matchIndex = this.corpus.indexOf(searchText); 417 - 418 - if (matchIndex === -1) { 419 - const normalizedSearch = searchText.replace(/\s+/g, " ").trim(); 420 - matchIndex = this.corpus.indexOf(normalizedSearch); 421 - 422 - if (matchIndex === -1) { 423 - const fuzzyMatch = this.fuzzyFindInCorpus(searchText); 424 - if (fuzzyMatch) { 425 - const start = this.mapIndexToPoint(fuzzyMatch.start); 426 - const end = this.mapIndexToPoint(fuzzyMatch.end); 427 - if (start && end) { 428 - const range = document.createRange(); 429 - range.setStart(start.node, start.offset); 430 - range.setEnd(end.node, end.offset); 431 - return range; 432 - } 433 - } 434 - return null; 435 - } 436 - } 437 - 438 - const start = this.mapIndexToPoint(matchIndex); 439 - const end = this.mapIndexToPoint(matchIndex + searchText.length); 440 - 441 - if (start && end) { 442 - const range = document.createRange(); 443 - range.setStart(start.node, start.offset); 444 - range.setEnd(end.node, end.offset); 445 - return range; 446 - } 447 - return null; 448 - } 449 - 450 - fuzzyFindInCorpus(searchText) { 451 - const searchWords = searchText 452 - .trim() 453 - .split(/\s+/) 454 - .filter((w) => w.length > 0); 455 - if (searchWords.length === 0) return null; 456 - 457 - const corpusLower = this.corpus.toLowerCase(); 458 - 459 - const firstWord = searchWords[0].toLowerCase(); 460 - let searchStart = 0; 461 - 462 - while (searchStart < corpusLower.length) { 463 - const wordStart = corpusLower.indexOf(firstWord, searchStart); 464 - if (wordStart === -1) break; 465 - 466 - let corpusPos = wordStart; 467 - let matched = true; 468 - let lastMatchEnd = wordStart; 469 - 470 - for (const word of searchWords) { 471 - const wordLower = word.toLowerCase(); 472 - while ( 473 - corpusPos < corpusLower.length && 474 - /\s/.test(this.corpus[corpusPos]) 475 - ) { 476 - corpusPos++; 477 - } 478 - const corpusSlice = corpusLower.slice( 479 - corpusPos, 480 - corpusPos + wordLower.length, 481 - ); 482 - if (corpusSlice !== wordLower) { 483 - matched = false; 484 - break; 485 - } 486 - 487 - corpusPos += wordLower.length; 488 - lastMatchEnd = corpusPos; 489 - } 490 - 491 - if (matched) { 492 - return { start: wordStart, end: lastMatchEnd }; 493 - } 494 - 495 - searchStart = wordStart + 1; 496 - } 497 - 498 - return null; 499 - } 500 - 501 - mapIndexToPoint(corpusIndex) { 502 - for (const info of this.indices) { 503 - if ( 504 - corpusIndex >= info.start && 505 - corpusIndex < info.start + info.length 506 - ) { 507 - return { node: info.node, offset: corpusIndex - info.start }; 508 - } 509 - } 510 - if (this.indices.length > 0) { 511 - const last = this.indices[this.indices.length - 1]; 512 - if (corpusIndex === last.start + last.length) { 513 - return { node: last.node, offset: last.length }; 514 - } 515 - } 516 - return null; 517 - } 518 - } 519 - 520 - function applyTheme(theme) { 521 - if (!sidebarHost) return; 522 - sidebarHost.classList.remove("light", "dark"); 523 - if (theme === "system" || !theme) { 524 - if (window.matchMedia("(prefers-color-scheme: light)").matches) { 525 - sidebarHost.classList.add("light"); 526 - } 527 - } else { 528 - sidebarHost.classList.add(theme); 529 - } 530 - } 531 - 532 - window 533 - .matchMedia("(prefers-color-scheme: light)") 534 - .addEventListener("change", (e) => { 535 - chrome.storage.local.get(["theme"], (result) => { 536 - if (!result.theme || result.theme === "system") { 537 - if (e.matches) { 538 - sidebarHost?.classList.add("light"); 539 - } else { 540 - sidebarHost?.classList.remove("light"); 541 - } 542 - } 543 - }); 544 - }); 545 - 546 - function initOverlay() { 547 - sidebarHost = document.createElement("div"); 548 - sidebarHost.id = "margin-overlay-host"; 549 - sidebarHost.style.cssText = ` 550 - position: absolute; top: 0; left: 0; width: 100%; 551 - height: 0; 552 - overflow: visible; 553 - pointer-events: none; z-index: 2147483647; 554 - `; 555 - document.body?.appendChild(sidebarHost) || 556 - document.documentElement.appendChild(sidebarHost); 557 - 558 - sidebarShadow = sidebarHost.attachShadow({ mode: "open" }); 559 - const styleEl = document.createElement("style"); 560 - styleEl.textContent = OVERLAY_STYLES; 561 - sidebarShadow.appendChild(styleEl); 562 - 563 - const container = document.createElement("div"); 564 - container.className = "margin-overlay"; 565 - container.id = "margin-overlay-container"; 566 - sidebarShadow.appendChild(container); 567 - 568 - if (typeof chrome !== "undefined" && chrome.storage) { 569 - chrome.storage.local.get(["showOverlay", "theme"], (result) => { 570 - applyTheme(result.theme); 571 - if (result.showOverlay === false) { 572 - sidebarHost.style.display = "none"; 573 - } else { 574 - fetchAnnotations(); 575 - } 576 - }); 577 - } else { 578 - fetchAnnotations(); 579 - } 580 - 581 - document.addEventListener("mousemove", handleMouseMove); 582 - document.addEventListener("click", handleDocumentClick, true); 583 - 584 - chrome.storage.onChanged.addListener((changes, area) => { 585 - if (area === "local") { 586 - if (changes.theme) { 587 - applyTheme(changes.theme.newValue); 588 - } 589 - if (changes.showOverlay) { 590 - if (changes.showOverlay.newValue === false) { 591 - sidebarHost.style.display = "none"; 592 - activeItems = []; 593 - if (typeof CSS !== "undefined" && CSS.highlights) { 594 - CSS.highlights.clear(); 595 - } 596 - } else { 597 - sidebarHost.style.display = ""; 598 - fetchAnnotations(); 599 - } 600 - } 601 - } 602 - }); 603 - } 604 - 605 - function showInlineComposeModal() { 606 - if (!sidebarShadow || !currentSelection) return; 607 - 608 - const container = sidebarShadow.getElementById("margin-overlay-container"); 609 - if (!container) return; 610 - 611 - const existingModal = container.querySelector(".inline-compose-modal"); 612 - if (existingModal) existingModal.remove(); 613 - 614 - const modal = document.createElement("div"); 615 - modal.className = "inline-compose-modal"; 616 - 617 - modal.style.left = `${Math.max(20, (window.innerWidth - 340) / 2)}px`; 618 - modal.style.top = `${Math.min(200, window.innerHeight / 4)}px`; 619 - 620 - const truncatedQuote = 621 - currentSelection.text.length > 100 622 - ? currentSelection.text.substring(0, 100) + "..." 623 - : currentSelection.text; 624 - 625 - modal.innerHTML = ` 626 - <div class="inline-compose-quote">"${truncatedQuote}"</div> 627 - <textarea class="inline-compose-textarea" placeholder="Add your annotation..." autofocus></textarea> 628 - <div class="inline-compose-actions"> 629 - <button class="btn-cancel">Cancel</button> 630 - <button class="btn-submit">Post Annotation</button> 631 - </div> 632 - `; 633 - 634 - const textarea = modal.querySelector("textarea"); 635 - const submitBtn = modal.querySelector(".btn-submit"); 636 - const cancelBtn = modal.querySelector(".btn-cancel"); 637 - 638 - cancelBtn.addEventListener("click", () => { 639 - modal.remove(); 640 - }); 641 - 642 - submitBtn.addEventListener("click", async () => { 643 - const text = textarea.value.trim(); 644 - if (!text) return; 645 - 646 - submitBtn.disabled = true; 647 - submitBtn.textContent = "Posting..."; 648 - 649 - chrome.runtime.sendMessage( 650 - { 651 - type: "CREATE_ANNOTATION", 652 - data: { 653 - url: currentSelection.url || window.location.href, 654 - title: currentSelection.title || document.title, 655 - text: text, 656 - selector: currentSelection.selector, 657 - }, 658 - }, 659 - (res) => { 660 - if (res && res.success) { 661 - modal.remove(); 662 - fetchAnnotations(); 663 - } else { 664 - submitBtn.disabled = false; 665 - submitBtn.textContent = "Post Annotation"; 666 - alert( 667 - "Failed to create annotation: " + (res?.error || "Unknown error"), 668 - ); 669 - } 670 - }, 671 - ); 672 - }); 673 - 674 - container.appendChild(modal); 675 - textarea.focus(); 676 - 677 - const handleEscape = (e) => { 678 - if (e.key === "Escape") { 679 - modal.remove(); 680 - document.removeEventListener("keydown", handleEscape); 681 - } 682 - }; 683 - document.addEventListener("keydown", handleEscape); 684 - } 685 - 686 - let hoverIndicator = null; 687 - 688 - function handleMouseMove(e) { 689 - const x = e.clientX; 690 - const y = e.clientY; 691 - 692 - if (sidebarHost && sidebarHost.style.display === "none") return; 693 - 694 - let foundItems = []; 695 - let firstRange = null; 696 - for (const { range, item } of activeItems) { 697 - const rects = range.getClientRects(); 698 - for (const rect of rects) { 699 - if ( 700 - x >= rect.left && 701 - x <= rect.right && 702 - y >= rect.top && 703 - y <= rect.bottom 704 - ) { 705 - let container = range.commonAncestorContainer; 706 - if (container.nodeType === Node.TEXT_NODE) { 707 - container = container.parentNode; 708 - } 709 - 710 - if ( 711 - container && 712 - (e.target.contains(container) || container.contains(e.target)) 713 - ) { 714 - if (!firstRange) firstRange = range; 715 - if (!foundItems.some((f) => f.item === item)) { 716 - foundItems.push({ range, item, rect }); 717 - } 718 - } 719 - break; 720 - } 721 - } 722 - } 723 - 724 - if (foundItems.length > 0) { 725 - document.body.style.cursor = "pointer"; 726 - 727 - if (!hoverIndicator && sidebarShadow) { 728 - const container = sidebarShadow.getElementById( 729 - "margin-overlay-container", 730 - ); 731 - if (container) { 732 - hoverIndicator = document.createElement("div"); 733 - hoverIndicator.className = "margin-hover-indicator"; 734 - hoverIndicator.style.cssText = ` 735 - position: fixed; 736 - display: flex; 737 - align-items: center; 738 - pointer-events: none; 739 - z-index: 2147483647; 740 - opacity: 0; 741 - transition: opacity 0.15s, transform 0.15s; 742 - transform: scale(0.8); 743 - `; 744 - container.appendChild(hoverIndicator); 745 - } 746 - } 747 - 748 - if (hoverIndicator) { 749 - const authorsMap = new Map(); 750 - foundItems.forEach(({ item }) => { 751 - const author = item.author || item.creator || {}; 752 - const id = author.did || author.handle || "unknown"; 753 - if (!authorsMap.has(id)) { 754 - authorsMap.set(id, author); 755 - } 756 - }); 757 - const uniqueAuthors = Array.from(authorsMap.values()); 758 - 759 - const maxShow = 3; 760 - const displayAuthors = uniqueAuthors.slice(0, maxShow); 761 - const overflow = uniqueAuthors.length - maxShow; 762 - 763 - let html = displayAuthors 764 - .map((author, i) => { 765 - const avatar = author.avatar; 766 - const handle = author.handle || "U"; 767 - const marginLeft = i === 0 ? "0" : "-8px"; 768 - 769 - if (avatar) { 770 - return `<img src="${avatar}" style="width: 24px; height: 24px; border-radius: 50%; object-fit: cover; border: 2px solid #09090b; margin-left: ${marginLeft};">`; 771 - } else { 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 - } 774 - }) 775 - .join(""); 776 - 777 - if (overflow > 0) { 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 - } 780 - 781 - hoverIndicator.innerHTML = html; 782 - 783 - const firstRect = firstRange.getClientRects()[0]; 784 - const totalWidth = 785 - Math.min(uniqueAuthors.length, maxShow + (overflow > 0 ? 1 : 0)) * 786 - 18 + 787 - 8; 788 - const leftPos = firstRect.left - totalWidth; 789 - const topPos = firstRect.top + firstRect.height / 2 - 12; 790 - 791 - hoverIndicator.style.left = `${leftPos}px`; 792 - hoverIndicator.style.top = `${topPos}px`; 793 - hoverIndicator.style.opacity = "1"; 794 - hoverIndicator.style.transform = "scale(1)"; 795 - } 796 - } else { 797 - document.body.style.cursor = ""; 798 - if (hoverIndicator) { 799 - hoverIndicator.style.opacity = "0"; 800 - hoverIndicator.style.transform = "scale(0.8)"; 801 - } 802 - } 803 - } 804 - 805 - function handleDocumentClick(e) { 806 - const x = e.clientX; 807 - const y = e.clientY; 808 - 809 - if (sidebarHost && sidebarHost.style.display === "none") return; 810 - 811 - if (popoverEl && sidebarShadow) { 812 - const rect = popoverEl.getBoundingClientRect(); 813 - if ( 814 - x >= rect.left && 815 - x <= rect.right && 816 - y >= rect.top && 817 - y <= rect.bottom 818 - ) { 819 - return; 820 - } 821 - } 822 - 823 - let clickedItems = []; 824 - for (const { range, item } of activeItems) { 825 - const rects = range.getClientRects(); 826 - for (const rect of rects) { 827 - if ( 828 - x >= rect.left && 829 - x <= rect.right && 830 - y >= rect.top && 831 - y <= rect.bottom 832 - ) { 833 - let container = range.commonAncestorContainer; 834 - if (container.nodeType === Node.TEXT_NODE) { 835 - container = container.parentNode; 836 - } 837 - 838 - if ( 839 - container && 840 - (e.target.contains(container) || container.contains(e.target)) 841 - ) { 842 - if (!clickedItems.includes(item)) { 843 - clickedItems.push(item); 844 - } 845 - } 846 - break; 847 - } 848 - } 849 - } 850 - 851 - if (clickedItems.length > 0) { 852 - e.preventDefault(); 853 - e.stopPropagation(); 854 - 855 - if (popoverEl) { 856 - const currentIds = popoverEl.dataset.itemIds; 857 - const newIds = clickedItems 858 - .map((i) => i.uri || i.id) 859 - .sort() 860 - .join(","); 861 - 862 - if (currentIds === newIds) { 863 - popoverEl.remove(); 864 - popoverEl = null; 865 - return; 866 - } 867 - } 868 - 869 - const firstItem = clickedItems[0]; 870 - const match = activeItems.find((x) => x.item === firstItem); 871 - if (match) { 872 - const rects = match.range.getClientRects(); 873 - if (rects.length > 0) { 874 - const rect = rects[0]; 875 - const top = rect.top + window.scrollY; 876 - const left = rect.left + window.scrollX; 877 - showPopover(clickedItems, top, left); 878 - } 879 - } 880 - } else { 881 - if (popoverEl) { 882 - popoverEl.remove(); 883 - popoverEl = null; 884 - } 885 - } 886 - } 887 - 888 - function renderBadges(annotations) { 889 - if (!sidebarShadow) return; 890 - 891 - const itemsToRender = annotations || []; 892 - activeItems = []; 893 - const rangesByColor = {}; 894 - 895 - const matcher = new DOMTextMatcher(); 896 - 897 - itemsToRender.forEach((item) => { 898 - const selector = item.target?.selector || item.selector; 899 - if (!selector?.exact) return; 900 - 901 - const range = matcher.findRange(selector.exact); 902 - if (range) { 903 - activeItems.push({ range, item }); 904 - 905 - const color = item.color || "#6366f1"; 906 - if (!rangesByColor[color]) rangesByColor[color] = []; 907 - rangesByColor[color].push(range); 908 - } 909 - }); 910 - 911 - if (typeof CSS !== "undefined" && CSS.highlights) { 912 - CSS.highlights.clear(); 913 - for (const [color, ranges] of Object.entries(rangesByColor)) { 914 - const highlight = new Highlight(...ranges); 915 - const safeColor = color.replace(/[^a-zA-Z0-9]/g, ""); 916 - const name = `margin-hl-${safeColor}`; 917 - CSS.highlights.set(name, highlight); 918 - injectHighlightStyle(name, color); 919 - } 920 - } 921 - } 922 - 923 - const injectedStyles = new Set(); 924 - function injectHighlightStyle(name, color) { 925 - if (injectedStyles.has(name)) return; 926 - const style = document.createElement("style"); 927 - style.textContent = ` 928 - ::highlight(${name}) { 929 - text-decoration: underline; 930 - text-decoration-color: ${color}; 931 - text-decoration-thickness: 2px; 932 - text-underline-offset: 2px; 933 - cursor: pointer; 934 - } 935 - `; 936 - document.head.appendChild(style); 937 - injectedStyles.add(name); 938 - } 939 - 940 - function showPopover(items, top, left) { 941 - if (popoverEl) popoverEl.remove(); 942 - const container = sidebarShadow.getElementById("margin-overlay-container"); 943 - popoverEl = document.createElement("div"); 944 - popoverEl.className = "margin-popover"; 945 - 946 - const ids = items 947 - .map((i) => i.uri || i.id) 948 - .sort() 949 - .join(","); 950 - popoverEl.dataset.itemIds = ids; 951 - 952 - const popWidth = 300; 953 - const screenWidth = window.innerWidth; 954 - let finalLeft = left; 955 - if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20; 956 - 957 - popoverEl.style.top = `${top + 20}px`; 958 - popoverEl.style.left = `${finalLeft}px`; 959 - 960 - const count = items.length; 961 - const title = count === 1 ? "1 Comment" : `${count} Comments`; 962 - 963 - let contentHtml = items 964 - .map((item) => { 965 - const author = item.author || item.creator || {}; 966 - const handle = author.handle || "User"; 967 - const avatar = author.avatar; 968 - const text = item.body?.value || item.text || ""; 969 - const id = item.id || item.uri; 970 - const isHighlight = item.type === "Highlight"; 971 - 972 - let avatarHtml = `<div class="comment-avatar">${handle[0]?.toUpperCase() || "U"}</div>`; 973 - if (avatar) { 974 - avatarHtml = `<img src="${avatar}" class="comment-avatar" style="object-fit: cover;">`; 975 - } 976 - 977 - let bodyHtml = ""; 978 - if (isHighlight && !text) { 979 - bodyHtml = `<div class="highlight-only-badge">Highlighted</div>`; 980 - } else { 981 - bodyHtml = `<div class="comment-text">${text}</div>`; 982 - } 983 - 984 - return ` 985 - <div class="comment-item"> 986 - <div class="comment-header"> 987 - ${avatarHtml} 988 - <span class="comment-handle">@${handle}</span> 989 - </div> 990 - ${bodyHtml} 991 - <div class="comment-actions"> 992 - ${!isHighlight ? `<button class="comment-action-btn btn-reply" data-id="${id}">Reply</button>` : ""} 993 - <button class="comment-action-btn btn-share" data-id="${id}" data-text="${text}">Share</button> 994 - </div> 995 - </div> 996 - `; 997 - }) 998 - .join(""); 999 - 1000 - popoverEl.innerHTML = ` 1001 - <div class="popover-header"> 1002 - <span>${title}</span> 1003 - <button class="popover-close">✕</button> 1004 - </div> 1005 - <div class="popover-scroll-area"> 1006 - ${contentHtml} 1007 - </div> 1008 - `; 1009 - 1010 - popoverEl.querySelector(".popover-close").addEventListener("click", (e) => { 1011 - e.stopPropagation(); 1012 - popoverEl.remove(); 1013 - popoverEl = null; 1014 - }); 1015 - 1016 - const replyBtns = popoverEl.querySelectorAll(".btn-reply"); 1017 - replyBtns.forEach((btn) => { 1018 - btn.addEventListener("click", (e) => { 1019 - e.stopPropagation(); 1020 - const id = btn.getAttribute("data-id"); 1021 - if (id) { 1022 - chrome.runtime.sendMessage({ 1023 - type: "OPEN_APP_URL", 1024 - data: { path: `/annotation/${encodeURIComponent(id)}` }, 1025 - }); 1026 - } 1027 - }); 1028 - }); 1029 - 1030 - const shareBtns = popoverEl.querySelectorAll(".btn-share"); 1031 - shareBtns.forEach((btn) => { 1032 - btn.addEventListener("click", async () => { 1033 - const id = btn.getAttribute("data-id"); 1034 - const text = btn.getAttribute("data-text"); 1035 - const u = `https://margin.at/annotation/${encodeURIComponent(id)}`; 1036 - const shareText = text ? `${text}\n${u}` : u; 1037 - 1038 - try { 1039 - await navigator.clipboard.writeText(shareText); 1040 - const originalText = btn.innerText; 1041 - btn.innerText = "Copied!"; 1042 - setTimeout(() => (btn.innerText = originalText), 2000); 1043 - } catch (e) { 1044 - console.error("Failed to copy", e); 1045 - } 1046 - }); 1047 - }); 1048 - 1049 - container.appendChild(popoverEl); 1050 - 1051 - setTimeout(() => { 1052 - document.addEventListener("click", closePopoverOutside); 1053 - }, 0); 1054 - } 1055 - 1056 - function closePopoverOutside() { 1057 - if (popoverEl) { 1058 - popoverEl.remove(); 1059 - popoverEl = null; 1060 - document.removeEventListener("click", closePopoverOutside); 1061 - } 1062 - } 1063 - 1064 - function fetchAnnotations(retryCount = 0) { 1065 - if (typeof chrome !== "undefined" && chrome.runtime) { 1066 - const citedUrls = Array.from(document.querySelectorAll("[cite]")) 1067 - .map((el) => el.getAttribute("cite")) 1068 - .filter((url) => url && url.startsWith("http")); 1069 - const uniqueCitedUrls = [...new Set(citedUrls)]; 1070 - 1071 - chrome.runtime.sendMessage( 1072 - { 1073 - type: "GET_ANNOTATIONS", 1074 - data: { 1075 - url: window.location.href, 1076 - citedUrls: uniqueCitedUrls, 1077 - }, 1078 - }, 1079 - (res) => { 1080 - if (res && res.success && res.data && res.data.length > 0) { 1081 - renderBadges(res.data); 1082 - } else if (retryCount < 3) { 1083 - setTimeout( 1084 - () => fetchAnnotations(retryCount + 1), 1085 - 1000 * (retryCount + 1), 1086 - ); 1087 - } 1088 - }, 1089 - ); 1090 - } 1091 - } 1092 - 1093 - function findCanonicalUrl(range) { 1094 - if (!range) return null; 1095 - let node = range.commonAncestorContainer; 1096 - if (node.nodeType === Node.TEXT_NODE) { 1097 - node = node.parentNode; 1098 - } 1099 - 1100 - while (node && node !== document.body) { 1101 - if ( 1102 - (node.tagName === "BLOCKQUOTE" || node.tagName === "Q") && 1103 - node.hasAttribute("cite") 1104 - ) { 1105 - if (node.contains(range.commonAncestorContainer)) { 1106 - return node.getAttribute("cite"); 1107 - } 1108 - } 1109 - node = node.parentNode; 1110 - } 1111 - return null; 1112 - } 1113 - 1114 - chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 1115 - if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") { 1116 - const sel = window.getSelection(); 1117 - if (!sel || !sel.toString()) { 1118 - sendResponse({ selector: null }); 1119 - return true; 1120 - } 1121 - const exact = sel.toString().trim(); 1122 - const canonicalUrl = findCanonicalUrl(sel.getRangeAt(0)); 1123 - 1124 - sendResponse({ 1125 - selector: { type: "TextQuoteSelector", exact }, 1126 - canonicalUrl, 1127 - }); 1128 - return true; 1129 - } 1130 - 1131 - if (request.type === "SHOW_INLINE_ANNOTATE") { 1132 - currentSelection = { 1133 - text: request.data.selector?.exact || "", 1134 - selector: request.data.selector, 1135 - url: request.data.url, 1136 - title: request.data.title, 1137 - }; 1138 - showInlineComposeModal(); 1139 - sendResponse({ success: true }); 1140 - return true; 1141 - } 1142 - 1143 - if (request.type === "GET_SELECTOR_FOR_HIGHLIGHT") { 1144 - const sel = window.getSelection(); 1145 - if (!sel || !sel.toString().trim()) { 1146 - sendResponse({ success: false, selector: null }); 1147 - return true; 1148 - } 1149 - const exact = sel.toString().trim(); 1150 - const canonicalUrl = findCanonicalUrl(sel.getRangeAt(0)); 1151 - 1152 - sendResponse({ 1153 - success: false, 1154 - selector: { type: "TextQuoteSelector", exact }, 1155 - canonicalUrl, 1156 - }); 1157 - return true; 1158 - } 1159 - 1160 - if (request.type === "REFRESH_ANNOTATIONS") { 1161 - fetchAnnotations(); 1162 - sendResponse({ success: true }); 1163 - return true; 1164 - } 1165 - 1166 - if (request.type === "UPDATE_OVERLAY_VISIBILITY") { 1167 - if (sidebarHost) { 1168 - sidebarHost.style.display = request.show ? "block" : "none"; 1169 - } 1170 - if (request.show) { 1171 - fetchAnnotations(); 1172 - } else { 1173 - activeItems = []; 1174 - if (typeof CSS !== "undefined" && CSS.highlights) { 1175 - CSS.highlights.clear(); 1176 - } 1177 - } 1178 - sendResponse({ success: true }); 1179 - return true; 1180 - } 1181 - 1182 - if (request.type === "SCROLL_TO_TEXT") { 1183 - const selector = request.selector; 1184 - if (selector?.exact) { 1185 - const matcher = new DOMTextMatcher(); 1186 - const range = matcher.findRange(selector.exact); 1187 - if (range) { 1188 - const rect = range.getBoundingClientRect(); 1189 - window.scrollTo({ 1190 - top: window.scrollY + rect.top - window.innerHeight / 3, 1191 - behavior: "smooth", 1192 - }); 1193 - const highlight = new Highlight(range); 1194 - CSS.highlights.set("margin-scroll-flash", highlight); 1195 - injectHighlightStyle("margin-scroll-flash", "#8b5cf6"); 1196 - setTimeout(() => CSS.highlights.delete("margin-scroll-flash"), 2000); 1197 - } 1198 - } 1199 - } 1200 - return true; 1201 - }); 1202 - 1203 - if (document.readyState === "loading") { 1204 - document.addEventListener("DOMContentLoaded", initOverlay); 1205 - } else { 1206 - initOverlay(); 1207 - } 1208 - 1209 - window.addEventListener("load", () => { 1210 - if (typeof chrome !== "undefined" && chrome.storage) { 1211 - chrome.storage.local.get(["showOverlay"], (result) => { 1212 - if (result.showOverlay !== false) { 1213 - setTimeout(() => fetchAnnotations(), 500); 1214 - } 1215 - }); 1216 - } else { 1217 - setTimeout(() => fetchAnnotations(), 500); 1218 - } 1219 - }); 1220 - 1221 - let lastUrl = window.location.href; 1222 - 1223 - function checkUrlChange() { 1224 - if (window.location.href !== lastUrl) { 1225 - lastUrl = window.location.href; 1226 - onUrlChange(); 1227 - } 1228 - } 1229 - 1230 - function onUrlChange() { 1231 - if (typeof CSS !== "undefined" && CSS.highlights) { 1232 - CSS.highlights.clear(); 1233 - } 1234 - activeItems = []; 1235 - 1236 - if (typeof chrome !== "undefined" && chrome.storage) { 1237 - chrome.storage.local.get(["showOverlay"], (result) => { 1238 - if (result.showOverlay !== false) { 1239 - fetchAnnotations(); 1240 - } 1241 - }); 1242 - } else { 1243 - fetchAnnotations(); 1244 - } 1245 - } 1246 - 1247 - window.addEventListener("popstate", onUrlChange); 1248 - 1249 - const originalPushState = history.pushState; 1250 - const originalReplaceState = history.replaceState; 1251 - 1252 - history.pushState = function (...args) { 1253 - originalPushState.apply(this, args); 1254 - checkUrlChange(); 1255 - }; 1256 - 1257 - history.replaceState = function (...args) { 1258 - originalReplaceState.apply(this, args); 1259 - checkUrlChange(); 1260 - }; 1261 - 1262 - setInterval(checkUrlChange, 1000); 1263 - })();
+14 -21
extension/eslint.config.js
··· 1 - import js from "@eslint/js"; 2 - import globals from "globals"; 1 + import js from '@eslint/js'; 2 + import tseslint from 'typescript-eslint'; 3 + import eslintConfigPrettier from 'eslint-config-prettier'; 3 4 4 - export default [ 5 - { ignores: ["dist"] }, 5 + export default tseslint.config( 6 + js.configs.recommended, 7 + ...tseslint.configs.recommended, 8 + eslintConfigPrettier, 9 + { 10 + ignores: ['.output/', '.wxt/', 'node_modules/', '*.config.js'], 11 + }, 6 12 { 7 - files: ["**/*.js"], 8 - languageOptions: { 9 - ecmaVersion: 2020, 10 - globals: { 11 - ...globals.browser, 12 - ...globals.webextensions, 13 - }, 14 - parserOptions: { 15 - ecmaVersion: "latest", 16 - sourceType: "module", 17 - }, 18 - }, 19 13 rules: { 20 - ...js.configs.recommended.rules, 21 - "no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], 22 - "no-undef": "warn", 14 + '@typescript-eslint/no-explicit-any': 'off', 15 + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 23 16 }, 24 - }, 25 - ]; 17 + } 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 - { 2 - "name": "Margin", 3 - "short_name": "Margin", 4 - "icons": [ 5 - { 6 - "src": "/android-chrome-192x192.png", 7 - "sizes": "192x192", 8 - "type": "image/png" 9 - }, 10 - { 11 - "src": "/android-chrome-512x512.png", 12 - "sizes": "512x512", 13 - "type": "image/png" 14 - } 15 - ], 16 - "theme_color": "#ffffff", 17 - "background_color": "#ffffff", 18 - "display": "standalone" 19 - }
-56
extension/manifest.chrome.json
··· 1 - { 2 - "manifest_version": 3, 3 - "name": "Margin", 4 - "description": "Write in the margins of the web. Annotate any URL with AT Protocol.", 5 - "version": "0.0.11", 6 - "icons": { 7 - "16": "icons/favicon-16x16.png", 8 - "32": "icons/favicon-32x32.png", 9 - "48": "icons/icon-48x48.png", 10 - "128": "icons/icon-128x128.png" 11 - }, 12 - "action": { 13 - "default_popup": "popup/popup.html", 14 - "default_icon": { 15 - "16": "icons/favicon-16x16.png", 16 - "32": "icons/favicon-32x32.png", 17 - "48": "icons/icon-48x48.png" 18 - }, 19 - "default_title": "Margin - View annotations" 20 - }, 21 - "background": { 22 - "service_worker": "background/service-worker.js", 23 - "type": "module" 24 - }, 25 - "content_scripts": [ 26 - { 27 - "matches": ["<all_urls>"], 28 - "js": ["content/content.js"], 29 - "css": ["content/content.css"], 30 - "run_at": "document_idle" 31 - } 32 - ], 33 - "permissions": [ 34 - "storage", 35 - "activeTab", 36 - "tabs", 37 - "cookies", 38 - "contextMenus", 39 - "sidePanel" 40 - ], 41 - "side_panel": { 42 - "default_path": "sidepanel/sidepanel.html" 43 - }, 44 - "optional_permissions": ["notifications"], 45 - "host_permissions": ["<all_urls>"], 46 - "options_ui": { 47 - "page": "popup/popup.html", 48 - "open_in_tab": true 49 - }, 50 - "browser_specific_settings": { 51 - "gecko": { 52 - "id": "hello@margin.at", 53 - "strict_min_version": "109.0" 54 - } 55 - } 56 - }
-59
extension/manifest.firefox.json
··· 1 - { 2 - "manifest_version": 3, 3 - "name": "Margin", 4 - "description": "Write in the margins of the web. Annotate any URL with AT Protocol.", 5 - "version": "0.0.11", 6 - "icons": { 7 - "16": "icons/favicon-16x16.png", 8 - "32": "icons/favicon-32x32.png", 9 - "48": "icons/icon-48x48.png", 10 - "64": "icons/icon-64x64.png", 11 - "128": "icons/icon-128x128.png" 12 - }, 13 - "action": { 14 - "default_popup": "popup/popup.html", 15 - "default_icon": { 16 - "16": "icons/favicon-16x16.png", 17 - "32": "icons/favicon-32x32.png", 18 - "48": "icons/icon-48x48.png" 19 - }, 20 - "default_title": "Margin - View annotations" 21 - }, 22 - "sidebar_action": { 23 - "default_panel": "sidepanel/sidepanel.html", 24 - "default_icon": { 25 - "16": "icons/favicon-16x16.png", 26 - "32": "icons/favicon-32x32.png" 27 - }, 28 - "default_title": "Margin Sidebar", 29 - "open_at_install": false 30 - }, 31 - "background": { 32 - "scripts": ["background/service-worker.js"], 33 - "type": "module" 34 - }, 35 - "content_scripts": [ 36 - { 37 - "matches": ["<all_urls>"], 38 - "js": ["content/content.js"], 39 - "css": ["content/content.css"], 40 - "run_at": "document_idle" 41 - } 42 - ], 43 - "permissions": ["storage", "activeTab", "tabs", "cookies", "contextMenus"], 44 - "optional_permissions": ["notifications"], 45 - "host_permissions": ["<all_urls>"], 46 - "options_ui": { 47 - "page": "popup/popup.html", 48 - "open_in_tab": true 49 - }, 50 - "browser_specific_settings": { 51 - "gecko": { 52 - "id": "hello@margin.at", 53 - "strict_min_version": "140.0", 54 - "data_collection_permissions": { 55 - "required": ["none"] 56 - } 57 - } 58 - } 59 - }
-52
extension/manifest.json
··· 1 - { 2 - "manifest_version": 3, 3 - "name": "Margin", 4 - "description": "Write in the margins of the web. Annotate any URL with AT Protocol.", 5 - "version": "0.0.11", 6 - "icons": { 7 - "16": "icons/favicon-16x16.png", 8 - "32": "icons/favicon-32x32.png", 9 - "48": "icons/icon-48x48.png", 10 - "128": "icons/icon-128x128.png" 11 - }, 12 - "action": { 13 - "default_popup": "popup/popup.html", 14 - "default_icon": { 15 - "16": "icons/favicon-16x16.png", 16 - "32": "icons/favicon-32x32.png", 17 - "48": "icons/icon-48x48.png" 18 - }, 19 - "default_title": "Margin - View annotations" 20 - }, 21 - "background": { 22 - "service_worker": "background/service-worker.js", 23 - "type": "module" 24 - }, 25 - "content_scripts": [ 26 - { 27 - "matches": ["<all_urls>"], 28 - "js": ["content/content.js"], 29 - "css": ["content/content.css"], 30 - "run_at": "document_idle" 31 - } 32 - ], 33 - "permissions": [ 34 - "storage", 35 - "activeTab", 36 - "tabs", 37 - "cookies", 38 - "contextMenus", 39 - "sidePanel" 40 - ], 41 - "side_panel": { 42 - "default_path": "sidepanel/sidepanel.html" 43 - }, 44 - "optional_permissions": ["notifications"], 45 - "host_permissions": ["<all_urls>"], 46 - "browser_specific_settings": { 47 - "gecko": { 48 - "id": "hello@margin.at", 49 - "strict_min_version": "109.0" 50 - } 51 - } 52 - }
-1091
extension/package-lock.json
··· 1 - { 2 - "name": "margin-extension", 3 - "version": "0.1.0", 4 - "lockfileVersion": 3, 5 - "requires": true, 6 - "packages": { 7 - "": { 8 - "name": "margin-extension", 9 - "version": "0.1.0", 10 - "devDependencies": { 11 - "@eslint/js": "^9.39.2", 12 - "eslint": "^9.39.2", 13 - "globals": "^17.0.0" 14 - } 15 - }, 16 - "node_modules/@eslint-community/eslint-utils": { 17 - "version": "4.9.1", 18 - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", 19 - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", 20 - "dev": true, 21 - "license": "MIT", 22 - "dependencies": { 23 - "eslint-visitor-keys": "^3.4.3" 24 - }, 25 - "engines": { 26 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 27 - }, 28 - "funding": { 29 - "url": "https://opencollective.com/eslint" 30 - }, 31 - "peerDependencies": { 32 - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 33 - } 34 - }, 35 - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 36 - "version": "3.4.3", 37 - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 38 - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 39 - "dev": true, 40 - "license": "Apache-2.0", 41 - "engines": { 42 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 43 - }, 44 - "funding": { 45 - "url": "https://opencollective.com/eslint" 46 - } 47 - }, 48 - "node_modules/@eslint-community/regexpp": { 49 - "version": "4.12.2", 50 - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", 51 - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", 52 - "dev": true, 53 - "license": "MIT", 54 - "engines": { 55 - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 56 - } 57 - }, 58 - "node_modules/@eslint/config-array": { 59 - "version": "0.21.1", 60 - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", 61 - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", 62 - "dev": true, 63 - "license": "Apache-2.0", 64 - "dependencies": { 65 - "@eslint/object-schema": "^2.1.7", 66 - "debug": "^4.3.1", 67 - "minimatch": "^3.1.2" 68 - }, 69 - "engines": { 70 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 71 - } 72 - }, 73 - "node_modules/@eslint/config-helpers": { 74 - "version": "0.4.2", 75 - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", 76 - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", 77 - "dev": true, 78 - "license": "Apache-2.0", 79 - "dependencies": { 80 - "@eslint/core": "^0.17.0" 81 - }, 82 - "engines": { 83 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 84 - } 85 - }, 86 - "node_modules/@eslint/core": { 87 - "version": "0.17.0", 88 - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", 89 - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", 90 - "dev": true, 91 - "license": "Apache-2.0", 92 - "dependencies": { 93 - "@types/json-schema": "^7.0.15" 94 - }, 95 - "engines": { 96 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 97 - } 98 - }, 99 - "node_modules/@eslint/eslintrc": { 100 - "version": "3.3.3", 101 - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", 102 - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", 103 - "dev": true, 104 - "license": "MIT", 105 - "dependencies": { 106 - "ajv": "^6.12.4", 107 - "debug": "^4.3.2", 108 - "espree": "^10.0.1", 109 - "globals": "^14.0.0", 110 - "ignore": "^5.2.0", 111 - "import-fresh": "^3.2.1", 112 - "js-yaml": "^4.1.1", 113 - "minimatch": "^3.1.2", 114 - "strip-json-comments": "^3.1.1" 115 - }, 116 - "engines": { 117 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 118 - }, 119 - "funding": { 120 - "url": "https://opencollective.com/eslint" 121 - } 122 - }, 123 - "node_modules/@eslint/eslintrc/node_modules/globals": { 124 - "version": "14.0.0", 125 - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 126 - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 127 - "dev": true, 128 - "license": "MIT", 129 - "engines": { 130 - "node": ">=18" 131 - }, 132 - "funding": { 133 - "url": "https://github.com/sponsors/sindresorhus" 134 - } 135 - }, 136 - "node_modules/@eslint/js": { 137 - "version": "9.39.2", 138 - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", 139 - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", 140 - "dev": true, 141 - "license": "MIT", 142 - "engines": { 143 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 144 - }, 145 - "funding": { 146 - "url": "https://eslint.org/donate" 147 - } 148 - }, 149 - "node_modules/@eslint/object-schema": { 150 - "version": "2.1.7", 151 - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", 152 - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", 153 - "dev": true, 154 - "license": "Apache-2.0", 155 - "engines": { 156 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 157 - } 158 - }, 159 - "node_modules/@eslint/plugin-kit": { 160 - "version": "0.4.1", 161 - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", 162 - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", 163 - "dev": true, 164 - "license": "Apache-2.0", 165 - "dependencies": { 166 - "@eslint/core": "^0.17.0", 167 - "levn": "^0.4.1" 168 - }, 169 - "engines": { 170 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 171 - } 172 - }, 173 - "node_modules/@humanfs/core": { 174 - "version": "0.19.1", 175 - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 176 - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 177 - "dev": true, 178 - "license": "Apache-2.0", 179 - "engines": { 180 - "node": ">=18.18.0" 181 - } 182 - }, 183 - "node_modules/@humanfs/node": { 184 - "version": "0.16.7", 185 - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 186 - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 187 - "dev": true, 188 - "license": "Apache-2.0", 189 - "dependencies": { 190 - "@humanfs/core": "^0.19.1", 191 - "@humanwhocodes/retry": "^0.4.0" 192 - }, 193 - "engines": { 194 - "node": ">=18.18.0" 195 - } 196 - }, 197 - "node_modules/@humanwhocodes/module-importer": { 198 - "version": "1.0.1", 199 - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 200 - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 201 - "dev": true, 202 - "license": "Apache-2.0", 203 - "engines": { 204 - "node": ">=12.22" 205 - }, 206 - "funding": { 207 - "type": "github", 208 - "url": "https://github.com/sponsors/nzakas" 209 - } 210 - }, 211 - "node_modules/@humanwhocodes/retry": { 212 - "version": "0.4.3", 213 - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 214 - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 215 - "dev": true, 216 - "license": "Apache-2.0", 217 - "engines": { 218 - "node": ">=18.18" 219 - }, 220 - "funding": { 221 - "type": "github", 222 - "url": "https://github.com/sponsors/nzakas" 223 - } 224 - }, 225 - "node_modules/@types/estree": { 226 - "version": "1.0.8", 227 - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 228 - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 229 - "dev": true, 230 - "license": "MIT" 231 - }, 232 - "node_modules/@types/json-schema": { 233 - "version": "7.0.15", 234 - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 235 - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 236 - "dev": true, 237 - "license": "MIT" 238 - }, 239 - "node_modules/acorn": { 240 - "version": "8.15.0", 241 - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 242 - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 243 - "dev": true, 244 - "license": "MIT", 245 - "peer": true, 246 - "bin": { 247 - "acorn": "bin/acorn" 248 - }, 249 - "engines": { 250 - "node": ">=0.4.0" 251 - } 252 - }, 253 - "node_modules/acorn-jsx": { 254 - "version": "5.3.2", 255 - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 256 - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 257 - "dev": true, 258 - "license": "MIT", 259 - "peerDependencies": { 260 - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 261 - } 262 - }, 263 - "node_modules/ajv": { 264 - "version": "6.12.6", 265 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 266 - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 267 - "dev": true, 268 - "license": "MIT", 269 - "dependencies": { 270 - "fast-deep-equal": "^3.1.1", 271 - "fast-json-stable-stringify": "^2.0.0", 272 - "json-schema-traverse": "^0.4.1", 273 - "uri-js": "^4.2.2" 274 - }, 275 - "funding": { 276 - "type": "github", 277 - "url": "https://github.com/sponsors/epoberezkin" 278 - } 279 - }, 280 - "node_modules/ansi-styles": { 281 - "version": "4.3.0", 282 - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 283 - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 284 - "dev": true, 285 - "license": "MIT", 286 - "dependencies": { 287 - "color-convert": "^2.0.1" 288 - }, 289 - "engines": { 290 - "node": ">=8" 291 - }, 292 - "funding": { 293 - "url": "https://github.com/chalk/ansi-styles?sponsor=1" 294 - } 295 - }, 296 - "node_modules/argparse": { 297 - "version": "2.0.1", 298 - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 299 - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 300 - "dev": true, 301 - "license": "Python-2.0" 302 - }, 303 - "node_modules/balanced-match": { 304 - "version": "1.0.2", 305 - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 306 - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 307 - "dev": true, 308 - "license": "MIT" 309 - }, 310 - "node_modules/brace-expansion": { 311 - "version": "1.1.12", 312 - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", 313 - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", 314 - "dev": true, 315 - "license": "MIT", 316 - "dependencies": { 317 - "balanced-match": "^1.0.0", 318 - "concat-map": "0.0.1" 319 - } 320 - }, 321 - "node_modules/callsites": { 322 - "version": "3.1.0", 323 - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 324 - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 325 - "dev": true, 326 - "license": "MIT", 327 - "engines": { 328 - "node": ">=6" 329 - } 330 - }, 331 - "node_modules/chalk": { 332 - "version": "4.1.2", 333 - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 334 - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 335 - "dev": true, 336 - "license": "MIT", 337 - "dependencies": { 338 - "ansi-styles": "^4.1.0", 339 - "supports-color": "^7.1.0" 340 - }, 341 - "engines": { 342 - "node": ">=10" 343 - }, 344 - "funding": { 345 - "url": "https://github.com/chalk/chalk?sponsor=1" 346 - } 347 - }, 348 - "node_modules/color-convert": { 349 - "version": "2.0.1", 350 - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 351 - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 352 - "dev": true, 353 - "license": "MIT", 354 - "dependencies": { 355 - "color-name": "~1.1.4" 356 - }, 357 - "engines": { 358 - "node": ">=7.0.0" 359 - } 360 - }, 361 - "node_modules/color-name": { 362 - "version": "1.1.4", 363 - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 364 - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 365 - "dev": true, 366 - "license": "MIT" 367 - }, 368 - "node_modules/concat-map": { 369 - "version": "0.0.1", 370 - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 371 - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 372 - "dev": true, 373 - "license": "MIT" 374 - }, 375 - "node_modules/cross-spawn": { 376 - "version": "7.0.6", 377 - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 378 - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 379 - "dev": true, 380 - "license": "MIT", 381 - "dependencies": { 382 - "path-key": "^3.1.0", 383 - "shebang-command": "^2.0.0", 384 - "which": "^2.0.1" 385 - }, 386 - "engines": { 387 - "node": ">= 8" 388 - } 389 - }, 390 - "node_modules/debug": { 391 - "version": "4.4.3", 392 - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 393 - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 394 - "dev": true, 395 - "license": "MIT", 396 - "dependencies": { 397 - "ms": "^2.1.3" 398 - }, 399 - "engines": { 400 - "node": ">=6.0" 401 - }, 402 - "peerDependenciesMeta": { 403 - "supports-color": { 404 - "optional": true 405 - } 406 - } 407 - }, 408 - "node_modules/deep-is": { 409 - "version": "0.1.4", 410 - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 411 - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 412 - "dev": true, 413 - "license": "MIT" 414 - }, 415 - "node_modules/escape-string-regexp": { 416 - "version": "4.0.0", 417 - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 418 - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 419 - "dev": true, 420 - "license": "MIT", 421 - "engines": { 422 - "node": ">=10" 423 - }, 424 - "funding": { 425 - "url": "https://github.com/sponsors/sindresorhus" 426 - } 427 - }, 428 - "node_modules/eslint": { 429 - "version": "9.39.2", 430 - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", 431 - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", 432 - "dev": true, 433 - "license": "MIT", 434 - "peer": true, 435 - "dependencies": { 436 - "@eslint-community/eslint-utils": "^4.8.0", 437 - "@eslint-community/regexpp": "^4.12.1", 438 - "@eslint/config-array": "^0.21.1", 439 - "@eslint/config-helpers": "^0.4.2", 440 - "@eslint/core": "^0.17.0", 441 - "@eslint/eslintrc": "^3.3.1", 442 - "@eslint/js": "9.39.2", 443 - "@eslint/plugin-kit": "^0.4.1", 444 - "@humanfs/node": "^0.16.6", 445 - "@humanwhocodes/module-importer": "^1.0.1", 446 - "@humanwhocodes/retry": "^0.4.2", 447 - "@types/estree": "^1.0.6", 448 - "ajv": "^6.12.4", 449 - "chalk": "^4.0.0", 450 - "cross-spawn": "^7.0.6", 451 - "debug": "^4.3.2", 452 - "escape-string-regexp": "^4.0.0", 453 - "eslint-scope": "^8.4.0", 454 - "eslint-visitor-keys": "^4.2.1", 455 - "espree": "^10.4.0", 456 - "esquery": "^1.5.0", 457 - "esutils": "^2.0.2", 458 - "fast-deep-equal": "^3.1.3", 459 - "file-entry-cache": "^8.0.0", 460 - "find-up": "^5.0.0", 461 - "glob-parent": "^6.0.2", 462 - "ignore": "^5.2.0", 463 - "imurmurhash": "^0.1.4", 464 - "is-glob": "^4.0.0", 465 - "json-stable-stringify-without-jsonify": "^1.0.1", 466 - "lodash.merge": "^4.6.2", 467 - "minimatch": "^3.1.2", 468 - "natural-compare": "^1.4.0", 469 - "optionator": "^0.9.3" 470 - }, 471 - "bin": { 472 - "eslint": "bin/eslint.js" 473 - }, 474 - "engines": { 475 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 476 - }, 477 - "funding": { 478 - "url": "https://eslint.org/donate" 479 - }, 480 - "peerDependencies": { 481 - "jiti": "*" 482 - }, 483 - "peerDependenciesMeta": { 484 - "jiti": { 485 - "optional": true 486 - } 487 - } 488 - }, 489 - "node_modules/eslint-scope": { 490 - "version": "8.4.0", 491 - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", 492 - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", 493 - "dev": true, 494 - "license": "BSD-2-Clause", 495 - "dependencies": { 496 - "esrecurse": "^4.3.0", 497 - "estraverse": "^5.2.0" 498 - }, 499 - "engines": { 500 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 501 - }, 502 - "funding": { 503 - "url": "https://opencollective.com/eslint" 504 - } 505 - }, 506 - "node_modules/eslint-visitor-keys": { 507 - "version": "4.2.1", 508 - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", 509 - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", 510 - "dev": true, 511 - "license": "Apache-2.0", 512 - "engines": { 513 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 514 - }, 515 - "funding": { 516 - "url": "https://opencollective.com/eslint" 517 - } 518 - }, 519 - "node_modules/espree": { 520 - "version": "10.4.0", 521 - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", 522 - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", 523 - "dev": true, 524 - "license": "BSD-2-Clause", 525 - "dependencies": { 526 - "acorn": "^8.15.0", 527 - "acorn-jsx": "^5.3.2", 528 - "eslint-visitor-keys": "^4.2.1" 529 - }, 530 - "engines": { 531 - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 532 - }, 533 - "funding": { 534 - "url": "https://opencollective.com/eslint" 535 - } 536 - }, 537 - "node_modules/esquery": { 538 - "version": "1.7.0", 539 - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", 540 - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", 541 - "dev": true, 542 - "license": "BSD-3-Clause", 543 - "dependencies": { 544 - "estraverse": "^5.1.0" 545 - }, 546 - "engines": { 547 - "node": ">=0.10" 548 - } 549 - }, 550 - "node_modules/esrecurse": { 551 - "version": "4.3.0", 552 - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 553 - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 554 - "dev": true, 555 - "license": "BSD-2-Clause", 556 - "dependencies": { 557 - "estraverse": "^5.2.0" 558 - }, 559 - "engines": { 560 - "node": ">=4.0" 561 - } 562 - }, 563 - "node_modules/estraverse": { 564 - "version": "5.3.0", 565 - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 566 - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 567 - "dev": true, 568 - "license": "BSD-2-Clause", 569 - "engines": { 570 - "node": ">=4.0" 571 - } 572 - }, 573 - "node_modules/esutils": { 574 - "version": "2.0.3", 575 - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 576 - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 577 - "dev": true, 578 - "license": "BSD-2-Clause", 579 - "engines": { 580 - "node": ">=0.10.0" 581 - } 582 - }, 583 - "node_modules/fast-deep-equal": { 584 - "version": "3.1.3", 585 - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 586 - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 587 - "dev": true, 588 - "license": "MIT" 589 - }, 590 - "node_modules/fast-json-stable-stringify": { 591 - "version": "2.1.0", 592 - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 593 - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 594 - "dev": true, 595 - "license": "MIT" 596 - }, 597 - "node_modules/fast-levenshtein": { 598 - "version": "2.0.6", 599 - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 600 - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 601 - "dev": true, 602 - "license": "MIT" 603 - }, 604 - "node_modules/file-entry-cache": { 605 - "version": "8.0.0", 606 - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 607 - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 608 - "dev": true, 609 - "license": "MIT", 610 - "dependencies": { 611 - "flat-cache": "^4.0.0" 612 - }, 613 - "engines": { 614 - "node": ">=16.0.0" 615 - } 616 - }, 617 - "node_modules/find-up": { 618 - "version": "5.0.0", 619 - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 620 - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 621 - "dev": true, 622 - "license": "MIT", 623 - "dependencies": { 624 - "locate-path": "^6.0.0", 625 - "path-exists": "^4.0.0" 626 - }, 627 - "engines": { 628 - "node": ">=10" 629 - }, 630 - "funding": { 631 - "url": "https://github.com/sponsors/sindresorhus" 632 - } 633 - }, 634 - "node_modules/flat-cache": { 635 - "version": "4.0.1", 636 - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 637 - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 638 - "dev": true, 639 - "license": "MIT", 640 - "dependencies": { 641 - "flatted": "^3.2.9", 642 - "keyv": "^4.5.4" 643 - }, 644 - "engines": { 645 - "node": ">=16" 646 - } 647 - }, 648 - "node_modules/flatted": { 649 - "version": "3.3.3", 650 - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 651 - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 652 - "dev": true, 653 - "license": "ISC" 654 - }, 655 - "node_modules/glob-parent": { 656 - "version": "6.0.2", 657 - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 658 - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 659 - "dev": true, 660 - "license": "ISC", 661 - "dependencies": { 662 - "is-glob": "^4.0.3" 663 - }, 664 - "engines": { 665 - "node": ">=10.13.0" 666 - } 667 - }, 668 - "node_modules/globals": { 669 - "version": "17.0.0", 670 - "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz", 671 - "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==", 672 - "dev": true, 673 - "license": "MIT", 674 - "engines": { 675 - "node": ">=18" 676 - }, 677 - "funding": { 678 - "url": "https://github.com/sponsors/sindresorhus" 679 - } 680 - }, 681 - "node_modules/has-flag": { 682 - "version": "4.0.0", 683 - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 684 - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 685 - "dev": true, 686 - "license": "MIT", 687 - "engines": { 688 - "node": ">=8" 689 - } 690 - }, 691 - "node_modules/ignore": { 692 - "version": "5.3.2", 693 - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 694 - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 695 - "dev": true, 696 - "license": "MIT", 697 - "engines": { 698 - "node": ">= 4" 699 - } 700 - }, 701 - "node_modules/import-fresh": { 702 - "version": "3.3.1", 703 - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 704 - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 705 - "dev": true, 706 - "license": "MIT", 707 - "dependencies": { 708 - "parent-module": "^1.0.0", 709 - "resolve-from": "^4.0.0" 710 - }, 711 - "engines": { 712 - "node": ">=6" 713 - }, 714 - "funding": { 715 - "url": "https://github.com/sponsors/sindresorhus" 716 - } 717 - }, 718 - "node_modules/imurmurhash": { 719 - "version": "0.1.4", 720 - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 721 - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 722 - "dev": true, 723 - "license": "MIT", 724 - "engines": { 725 - "node": ">=0.8.19" 726 - } 727 - }, 728 - "node_modules/is-extglob": { 729 - "version": "2.1.1", 730 - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 731 - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 732 - "dev": true, 733 - "license": "MIT", 734 - "engines": { 735 - "node": ">=0.10.0" 736 - } 737 - }, 738 - "node_modules/is-glob": { 739 - "version": "4.0.3", 740 - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 741 - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 742 - "dev": true, 743 - "license": "MIT", 744 - "dependencies": { 745 - "is-extglob": "^2.1.1" 746 - }, 747 - "engines": { 748 - "node": ">=0.10.0" 749 - } 750 - }, 751 - "node_modules/isexe": { 752 - "version": "2.0.0", 753 - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 754 - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 755 - "dev": true, 756 - "license": "ISC" 757 - }, 758 - "node_modules/js-yaml": { 759 - "version": "4.1.1", 760 - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", 761 - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", 762 - "dev": true, 763 - "license": "MIT", 764 - "dependencies": { 765 - "argparse": "^2.0.1" 766 - }, 767 - "bin": { 768 - "js-yaml": "bin/js-yaml.js" 769 - } 770 - }, 771 - "node_modules/json-buffer": { 772 - "version": "3.0.1", 773 - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 774 - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 775 - "dev": true, 776 - "license": "MIT" 777 - }, 778 - "node_modules/json-schema-traverse": { 779 - "version": "0.4.1", 780 - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 781 - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 782 - "dev": true, 783 - "license": "MIT" 784 - }, 785 - "node_modules/json-stable-stringify-without-jsonify": { 786 - "version": "1.0.1", 787 - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 788 - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 789 - "dev": true, 790 - "license": "MIT" 791 - }, 792 - "node_modules/keyv": { 793 - "version": "4.5.4", 794 - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 795 - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 796 - "dev": true, 797 - "license": "MIT", 798 - "dependencies": { 799 - "json-buffer": "3.0.1" 800 - } 801 - }, 802 - "node_modules/levn": { 803 - "version": "0.4.1", 804 - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 805 - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 806 - "dev": true, 807 - "license": "MIT", 808 - "dependencies": { 809 - "prelude-ls": "^1.2.1", 810 - "type-check": "~0.4.0" 811 - }, 812 - "engines": { 813 - "node": ">= 0.8.0" 814 - } 815 - }, 816 - "node_modules/locate-path": { 817 - "version": "6.0.0", 818 - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 819 - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 820 - "dev": true, 821 - "license": "MIT", 822 - "dependencies": { 823 - "p-locate": "^5.0.0" 824 - }, 825 - "engines": { 826 - "node": ">=10" 827 - }, 828 - "funding": { 829 - "url": "https://github.com/sponsors/sindresorhus" 830 - } 831 - }, 832 - "node_modules/lodash.merge": { 833 - "version": "4.6.2", 834 - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 835 - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 836 - "dev": true, 837 - "license": "MIT" 838 - }, 839 - "node_modules/minimatch": { 840 - "version": "3.1.2", 841 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 842 - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 843 - "dev": true, 844 - "license": "ISC", 845 - "dependencies": { 846 - "brace-expansion": "^1.1.7" 847 - }, 848 - "engines": { 849 - "node": "*" 850 - } 851 - }, 852 - "node_modules/ms": { 853 - "version": "2.1.3", 854 - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 855 - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 856 - "dev": true, 857 - "license": "MIT" 858 - }, 859 - "node_modules/natural-compare": { 860 - "version": "1.4.0", 861 - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 862 - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 863 - "dev": true, 864 - "license": "MIT" 865 - }, 866 - "node_modules/optionator": { 867 - "version": "0.9.4", 868 - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 869 - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 870 - "dev": true, 871 - "license": "MIT", 872 - "dependencies": { 873 - "deep-is": "^0.1.3", 874 - "fast-levenshtein": "^2.0.6", 875 - "levn": "^0.4.1", 876 - "prelude-ls": "^1.2.1", 877 - "type-check": "^0.4.0", 878 - "word-wrap": "^1.2.5" 879 - }, 880 - "engines": { 881 - "node": ">= 0.8.0" 882 - } 883 - }, 884 - "node_modules/p-limit": { 885 - "version": "3.1.0", 886 - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 887 - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 888 - "dev": true, 889 - "license": "MIT", 890 - "dependencies": { 891 - "yocto-queue": "^0.1.0" 892 - }, 893 - "engines": { 894 - "node": ">=10" 895 - }, 896 - "funding": { 897 - "url": "https://github.com/sponsors/sindresorhus" 898 - } 899 - }, 900 - "node_modules/p-locate": { 901 - "version": "5.0.0", 902 - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 903 - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 904 - "dev": true, 905 - "license": "MIT", 906 - "dependencies": { 907 - "p-limit": "^3.0.2" 908 - }, 909 - "engines": { 910 - "node": ">=10" 911 - }, 912 - "funding": { 913 - "url": "https://github.com/sponsors/sindresorhus" 914 - } 915 - }, 916 - "node_modules/parent-module": { 917 - "version": "1.0.1", 918 - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 919 - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 920 - "dev": true, 921 - "license": "MIT", 922 - "dependencies": { 923 - "callsites": "^3.0.0" 924 - }, 925 - "engines": { 926 - "node": ">=6" 927 - } 928 - }, 929 - "node_modules/path-exists": { 930 - "version": "4.0.0", 931 - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 932 - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 933 - "dev": true, 934 - "license": "MIT", 935 - "engines": { 936 - "node": ">=8" 937 - } 938 - }, 939 - "node_modules/path-key": { 940 - "version": "3.1.1", 941 - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 942 - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 943 - "dev": true, 944 - "license": "MIT", 945 - "engines": { 946 - "node": ">=8" 947 - } 948 - }, 949 - "node_modules/prelude-ls": { 950 - "version": "1.2.1", 951 - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 952 - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 953 - "dev": true, 954 - "license": "MIT", 955 - "engines": { 956 - "node": ">= 0.8.0" 957 - } 958 - }, 959 - "node_modules/punycode": { 960 - "version": "2.3.1", 961 - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 962 - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 963 - "dev": true, 964 - "license": "MIT", 965 - "engines": { 966 - "node": ">=6" 967 - } 968 - }, 969 - "node_modules/resolve-from": { 970 - "version": "4.0.0", 971 - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 972 - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 973 - "dev": true, 974 - "license": "MIT", 975 - "engines": { 976 - "node": ">=4" 977 - } 978 - }, 979 - "node_modules/shebang-command": { 980 - "version": "2.0.0", 981 - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 982 - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 983 - "dev": true, 984 - "license": "MIT", 985 - "dependencies": { 986 - "shebang-regex": "^3.0.0" 987 - }, 988 - "engines": { 989 - "node": ">=8" 990 - } 991 - }, 992 - "node_modules/shebang-regex": { 993 - "version": "3.0.0", 994 - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 995 - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 996 - "dev": true, 997 - "license": "MIT", 998 - "engines": { 999 - "node": ">=8" 1000 - } 1001 - }, 1002 - "node_modules/strip-json-comments": { 1003 - "version": "3.1.1", 1004 - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1005 - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1006 - "dev": true, 1007 - "license": "MIT", 1008 - "engines": { 1009 - "node": ">=8" 1010 - }, 1011 - "funding": { 1012 - "url": "https://github.com/sponsors/sindresorhus" 1013 - } 1014 - }, 1015 - "node_modules/supports-color": { 1016 - "version": "7.2.0", 1017 - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1018 - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1019 - "dev": true, 1020 - "license": "MIT", 1021 - "dependencies": { 1022 - "has-flag": "^4.0.0" 1023 - }, 1024 - "engines": { 1025 - "node": ">=8" 1026 - } 1027 - }, 1028 - "node_modules/type-check": { 1029 - "version": "0.4.0", 1030 - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1031 - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1032 - "dev": true, 1033 - "license": "MIT", 1034 - "dependencies": { 1035 - "prelude-ls": "^1.2.1" 1036 - }, 1037 - "engines": { 1038 - "node": ">= 0.8.0" 1039 - } 1040 - }, 1041 - "node_modules/uri-js": { 1042 - "version": "4.4.1", 1043 - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1044 - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1045 - "dev": true, 1046 - "license": "BSD-2-Clause", 1047 - "dependencies": { 1048 - "punycode": "^2.1.0" 1049 - } 1050 - }, 1051 - "node_modules/which": { 1052 - "version": "2.0.2", 1053 - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1054 - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1055 - "dev": true, 1056 - "license": "ISC", 1057 - "dependencies": { 1058 - "isexe": "^2.0.0" 1059 - }, 1060 - "bin": { 1061 - "node-which": "bin/node-which" 1062 - }, 1063 - "engines": { 1064 - "node": ">= 8" 1065 - } 1066 - }, 1067 - "node_modules/word-wrap": { 1068 - "version": "1.2.5", 1069 - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 1070 - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 1071 - "dev": true, 1072 - "license": "MIT", 1073 - "engines": { 1074 - "node": ">=0.10.0" 1075 - } 1076 - }, 1077 - "node_modules/yocto-queue": { 1078 - "version": "0.1.0", 1079 - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1080 - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1081 - "dev": true, 1082 - "license": "MIT", 1083 - "engines": { 1084 - "node": ">=10" 1085 - }, 1086 - "funding": { 1087 - "url": "https://github.com/sponsors/sindresorhus" 1088 - } 1089 - } 1090 - } 1091 - }
+30 -3
extension/package.json
··· 1 1 { 2 2 "name": "margin-extension", 3 - "version": "0.1.0", 3 + "description": "Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.", 4 4 "private": true, 5 + "version": "1.0.0", 5 6 "type": "module", 6 7 "scripts": { 7 - "lint": "eslint ." 8 + "dev": "wxt", 9 + "dev:firefox": "wxt -b firefox", 10 + "build": "wxt build", 11 + "build:firefox": "wxt build -b firefox", 12 + "zip": "wxt zip", 13 + "zip:firefox": "wxt zip -b firefox", 14 + "lint": "eslint src", 15 + "lint:fix": "eslint src --fix", 16 + "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", 17 + "postinstall": "wxt prepare" 18 + }, 19 + "dependencies": { 20 + "@webext-core/messaging": "^1.4.0", 21 + "clsx": "^2.1.1", 22 + "lucide-react": "^0.563.0", 23 + "react": "^18.3.1", 24 + "react-dom": "^18.3.1" 8 25 }, 9 26 "devDependencies": { 10 27 "@eslint/js": "^9.39.2", 28 + "@types/react": "^18.3.12", 29 + "@types/react-dom": "^18.3.1", 30 + "@wxt-dev/module-react": "^1.1.1", 31 + "autoprefixer": "^10.4.20", 11 32 "eslint": "^9.39.2", 12 - "globals": "^17.0.0" 33 + "eslint-config-prettier": "^10.1.8", 34 + "postcss": "^8.4.49", 35 + "prettier": "^3.8.1", 36 + "tailwindcss": "^3.4.15", 37 + "typescript": "^5.6.3", 38 + "typescript-eslint": "^8.54.0", 39 + "wxt": "^0.19.13" 13 40 } 14 41 }
-817
extension/popup/popup.css
··· 1 - @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap"); 2 - 3 - :root { 4 - --bg-primary: #0a0a0d; 5 - --bg-secondary: #121216; 6 - --bg-tertiary: #1a1a1f; 7 - --bg-card: #0f0f13; 8 - --bg-elevated: #18181d; 9 - --bg-hover: #1e1e24; 10 - 11 - --text-primary: #eaeaee; 12 - --text-secondary: #b7b6c5; 13 - --text-tertiary: #6e6d7a; 14 - 15 - --border: rgba(183, 182, 197, 0.12); 16 - --border-hover: rgba(183, 182, 197, 0.2); 17 - 18 - --accent: #957a86; 19 - --accent-hover: #a98d98; 20 - --accent-subtle: rgba(149, 122, 134, 0.15); 21 - --accent-text: #c4a8b2; 22 - 23 - --success: #7fb069; 24 - --error: #d97766; 25 - --warning: #e8a54b; 26 - 27 - --radius-sm: 6px; 28 - --radius-md: 8px; 29 - --radius-lg: 12px; 30 - --radius-full: 9999px; 31 - 32 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 33 - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 34 - } 35 - 36 - @media (prefers-color-scheme: light) { 37 - :root { 38 - --bg-primary: #f8f8fa; 39 - --bg-secondary: #ffffff; 40 - --bg-tertiary: #f0f0f4; 41 - --bg-card: #ffffff; 42 - --bg-elevated: #ffffff; 43 - --bg-hover: #eeeef2; 44 - 45 - --text-primary: #18171c; 46 - --text-secondary: #5c495a; 47 - --text-tertiary: #8a8494; 48 - 49 - --border: rgba(92, 73, 90, 0.12); 50 - --border-hover: rgba(92, 73, 90, 0.22); 51 - 52 - --accent: #7a5f6d; 53 - --accent-hover: #664e5b; 54 - --accent-subtle: rgba(149, 122, 134, 0.12); 55 - --accent-text: #5c495a; 56 - 57 - --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06); 58 - --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08); 59 - } 60 - } 61 - 62 - body.light { 63 - --bg-primary: #f8f8fa; 64 - --bg-secondary: #ffffff; 65 - --bg-tertiary: #f0f0f4; 66 - --bg-card: #ffffff; 67 - --bg-elevated: #ffffff; 68 - --bg-hover: #eeeef2; 69 - 70 - --text-primary: #18171c; 71 - --text-secondary: #5c495a; 72 - --text-tertiary: #8a8494; 73 - 74 - --border: rgba(92, 73, 90, 0.12); 75 - --border-hover: rgba(92, 73, 90, 0.22); 76 - 77 - --accent: #7a5f6d; 78 - --accent-hover: #664e5b; 79 - --accent-subtle: rgba(149, 122, 134, 0.12); 80 - --accent-text: #5c495a; 81 - 82 - --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06); 83 - --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08); 84 - } 85 - 86 - body.dark { 87 - --bg-primary: #0a0a0d; 88 - --bg-secondary: #121216; 89 - --bg-tertiary: #1a1a1f; 90 - --bg-card: #0f0f13; 91 - --bg-elevated: #18181d; 92 - --bg-hover: #1e1e24; 93 - 94 - --text-primary: #eaeaee; 95 - --text-secondary: #b7b6c5; 96 - --text-tertiary: #6e6d7a; 97 - 98 - --border: rgba(183, 182, 197, 0.12); 99 - --border-hover: rgba(183, 182, 197, 0.2); 100 - 101 - --accent: #957a86; 102 - --accent-hover: #a98d98; 103 - --accent-subtle: rgba(149, 122, 134, 0.15); 104 - --accent-text: #c4a8b2; 105 - 106 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 107 - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 108 - } 109 - 110 - * { 111 - box-sizing: border-box; 112 - margin: 0; 113 - padding: 0; 114 - } 115 - 116 - body { 117 - width: 380px; 118 - height: 520px; 119 - font-family: 120 - "IBM Plex Sans", 121 - -apple-system, 122 - BlinkMacSystemFont, 123 - sans-serif; 124 - color: var(--text-primary); 125 - background-color: var(--bg-primary); 126 - overflow: hidden; 127 - -webkit-font-smoothing: antialiased; 128 - } 129 - 130 - .popup { 131 - display: flex; 132 - flex-direction: column; 133 - height: 100%; 134 - } 135 - 136 - .popup-header { 137 - padding: 14px 16px; 138 - border-bottom: 1px solid var(--border); 139 - display: flex; 140 - justify-content: space-between; 141 - align-items: center; 142 - background: var(--bg-primary); 143 - } 144 - 145 - .popup-brand { 146 - display: flex; 147 - align-items: center; 148 - gap: 10px; 149 - } 150 - 151 - .popup-logo { 152 - color: var(--accent); 153 - } 154 - 155 - .popup-title { 156 - font-weight: 600; 157 - font-size: 15px; 158 - color: var(--text-primary); 159 - letter-spacing: -0.02em; 160 - } 161 - 162 - .user-info { 163 - display: flex; 164 - align-items: center; 165 - gap: 8px; 166 - } 167 - 168 - .user-handle { 169 - font-size: 12px; 170 - color: var(--text-secondary); 171 - background: var(--bg-tertiary); 172 - padding: 4px 10px; 173 - border-radius: var(--radius-full); 174 - } 175 - 176 - .tabs { 177 - display: flex; 178 - border-bottom: 1px solid var(--border); 179 - background: var(--bg-primary); 180 - padding: 4px 8px; 181 - gap: 4px; 182 - } 183 - 184 - .tab-btn { 185 - flex: 1; 186 - padding: 10px 8px; 187 - background: transparent; 188 - border: none; 189 - font-size: 12px; 190 - font-weight: 500; 191 - color: var(--text-tertiary); 192 - cursor: pointer; 193 - border-radius: var(--radius-sm); 194 - transition: all 0.15s; 195 - } 196 - 197 - .tab-btn:hover { 198 - color: var(--text-secondary); 199 - background: var(--bg-hover); 200 - } 201 - 202 - .tab-btn.active { 203 - color: var(--text-primary); 204 - background: var(--bg-tertiary); 205 - } 206 - 207 - .tab-content { 208 - display: none; 209 - flex: 1; 210 - flex-direction: column; 211 - overflow-y: auto; 212 - } 213 - 214 - .tab-content.active { 215 - display: flex; 216 - } 217 - 218 - .popup-content { 219 - flex: 1; 220 - overflow-y: auto; 221 - display: flex; 222 - flex-direction: column; 223 - background: var(--bg-primary); 224 - } 225 - 226 - .loading { 227 - display: none; 228 - flex-direction: column; 229 - align-items: center; 230 - justify-content: center; 231 - height: 100%; 232 - color: var(--text-tertiary); 233 - gap: 12px; 234 - } 235 - 236 - .spinner { 237 - width: 20px; 238 - height: 20px; 239 - border: 2px solid var(--border); 240 - border-top-color: var(--accent); 241 - border-radius: 50%; 242 - animation: spin 1s linear infinite; 243 - } 244 - 245 - @keyframes spin { 246 - to { 247 - transform: rotate(360deg); 248 - } 249 - } 250 - 251 - .login-prompt { 252 - display: flex; 253 - flex-direction: column; 254 - align-items: center; 255 - justify-content: center; 256 - height: 100%; 257 - padding: 32px; 258 - text-align: center; 259 - gap: 20px; 260 - } 261 - 262 - .login-at-logo { 263 - font-size: 3.5rem; 264 - font-weight: 700; 265 - color: var(--accent); 266 - line-height: 1; 267 - } 268 - 269 - .login-title { 270 - font-size: 1rem; 271 - font-weight: 600; 272 - color: var(--text-primary); 273 - } 274 - 275 - .login-text { 276 - font-size: 13px; 277 - color: var(--text-secondary); 278 - line-height: 1.5; 279 - } 280 - 281 - .quick-actions { 282 - padding: 12px 16px; 283 - border-bottom: 1px solid var(--border); 284 - background: var(--bg-primary); 285 - } 286 - 287 - .create-form { 288 - padding: 16px; 289 - border-bottom: 1px solid var(--border); 290 - background: var(--bg-primary); 291 - } 292 - 293 - .form-header { 294 - display: flex; 295 - justify-content: space-between; 296 - align-items: center; 297 - margin-bottom: 10px; 298 - } 299 - 300 - .form-title { 301 - font-size: 12px; 302 - font-weight: 600; 303 - color: var(--text-primary); 304 - letter-spacing: -0.01em; 305 - } 306 - 307 - .current-url { 308 - font-size: 11px; 309 - color: var(--text-tertiary); 310 - max-width: 150px; 311 - overflow: hidden; 312 - text-overflow: ellipsis; 313 - white-space: nowrap; 314 - } 315 - 316 - .annotation-input { 317 - width: 100%; 318 - padding: 12px; 319 - border: 1px solid var(--border); 320 - border-radius: var(--radius-md); 321 - font-family: inherit; 322 - font-size: 13px; 323 - resize: none; 324 - margin-bottom: 10px; 325 - background: var(--bg-elevated); 326 - color: var(--text-primary); 327 - transition: border-color 0.15s; 328 - } 329 - 330 - .annotation-input::placeholder { 331 - color: var(--text-tertiary); 332 - } 333 - 334 - .annotation-input:focus { 335 - outline: none; 336 - border-color: var(--accent); 337 - } 338 - 339 - .form-actions { 340 - display: flex; 341 - justify-content: flex-end; 342 - } 343 - 344 - .quote-preview { 345 - margin-bottom: 12px; 346 - padding: 10px 12px; 347 - background: var(--accent-subtle); 348 - border-left: 2px solid var(--accent); 349 - border-radius: var(--radius-sm); 350 - } 351 - 352 - .quote-preview-header { 353 - display: flex; 354 - justify-content: space-between; 355 - align-items: center; 356 - margin-bottom: 6px; 357 - font-size: 10px; 358 - font-weight: 600; 359 - text-transform: uppercase; 360 - letter-spacing: 0.5px; 361 - color: var(--accent-text); 362 - } 363 - 364 - .quote-preview-clear { 365 - background: none; 366 - border: none; 367 - color: var(--text-tertiary); 368 - font-size: 14px; 369 - cursor: pointer; 370 - padding: 0 4px; 371 - line-height: 1; 372 - } 373 - 374 - .quote-preview-clear:hover { 375 - color: var(--text-primary); 376 - } 377 - 378 - .quote-preview-text { 379 - font-size: 12px; 380 - font-style: italic; 381 - color: var(--text-secondary); 382 - line-height: 1.4; 383 - max-height: 60px; 384 - overflow: hidden; 385 - } 386 - 387 - .annotations-section { 388 - flex: 1; 389 - } 390 - 391 - .section-header { 392 - display: flex; 393 - justify-content: space-between; 394 - align-items: center; 395 - padding: 14px 16px; 396 - background: var(--bg-primary); 397 - } 398 - 399 - .section-title { 400 - font-size: 11px; 401 - font-weight: 600; 402 - text-transform: uppercase; 403 - color: var(--text-tertiary); 404 - letter-spacing: 0.5px; 405 - } 406 - 407 - .annotation-count { 408 - font-size: 11px; 409 - background: var(--bg-tertiary); 410 - padding: 3px 8px; 411 - border-radius: var(--radius-full); 412 - color: var(--text-secondary); 413 - } 414 - 415 - .annotations { 416 - display: flex; 417 - flex-direction: column; 418 - gap: 1px; 419 - background: var(--border); 420 - } 421 - 422 - .annotation-item { 423 - padding: 14px 16px; 424 - background: var(--bg-primary); 425 - transition: background 0.15s; 426 - } 427 - 428 - .annotation-item:hover { 429 - background: var(--bg-hover); 430 - } 431 - 432 - .annotation-item-header { 433 - display: flex; 434 - align-items: center; 435 - margin-bottom: 8px; 436 - gap: 10px; 437 - } 438 - 439 - .annotation-item-avatar { 440 - width: 26px; 441 - height: 26px; 442 - border-radius: 50%; 443 - background: var(--accent); 444 - color: var(--bg-primary); 445 - display: flex; 446 - align-items: center; 447 - justify-content: center; 448 - font-size: 10px; 449 - font-weight: 600; 450 - } 451 - 452 - .annotation-item-meta { 453 - flex: 1; 454 - } 455 - 456 - .annotation-item-author { 457 - font-size: 12px; 458 - font-weight: 600; 459 - color: var(--text-primary); 460 - } 461 - 462 - .annotation-item-time { 463 - font-size: 11px; 464 - color: var(--text-tertiary); 465 - } 466 - 467 - .annotation-type-badge { 468 - font-size: 10px; 469 - padding: 3px 8px; 470 - border-radius: var(--radius-full); 471 - font-weight: 500; 472 - } 473 - 474 - .annotation-type-badge.highlight { 475 - background: var(--accent-subtle); 476 - color: var(--accent-text); 477 - } 478 - 479 - .annotation-item-quote { 480 - padding: 8px 12px; 481 - border-left: 2px solid var(--accent); 482 - margin-bottom: 8px; 483 - font-size: 12px; 484 - color: var(--text-secondary); 485 - font-style: italic; 486 - background: var(--accent-subtle); 487 - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 488 - } 489 - 490 - .annotation-item-text { 491 - font-size: 13px; 492 - line-height: 1.5; 493 - color: var(--text-primary); 494 - } 495 - 496 - .bookmark-item { 497 - padding: 14px 16px; 498 - background: var(--bg-primary); 499 - text-decoration: none; 500 - color: inherit; 501 - display: block; 502 - transition: background 0.15s; 503 - } 504 - 505 - .bookmark-item:hover { 506 - background: var(--bg-hover); 507 - } 508 - 509 - .bookmark-title { 510 - font-size: 13px; 511 - font-weight: 500; 512 - margin-bottom: 4px; 513 - white-space: nowrap; 514 - overflow: hidden; 515 - text-overflow: ellipsis; 516 - color: var(--text-primary); 517 - } 518 - 519 - .bookmark-url { 520 - font-size: 11px; 521 - color: var(--text-tertiary); 522 - white-space: nowrap; 523 - overflow: hidden; 524 - text-overflow: ellipsis; 525 - } 526 - 527 - .empty-state { 528 - display: flex; 529 - flex-direction: column; 530 - align-items: center; 531 - justify-content: center; 532 - padding: 40px 16px; 533 - text-align: center; 534 - color: var(--text-tertiary); 535 - } 536 - 537 - .empty-icon { 538 - margin-bottom: 12px; 539 - color: var(--text-tertiary); 540 - opacity: 0.4; 541 - } 542 - 543 - .empty-text { 544 - font-size: 13px; 545 - color: var(--text-secondary); 546 - } 547 - 548 - .btn { 549 - padding: 10px 18px; 550 - border-radius: var(--radius-md); 551 - border: none; 552 - font-weight: 600; 553 - cursor: pointer; 554 - font-size: 13px; 555 - transition: all 0.15s; 556 - display: inline-flex; 557 - align-items: center; 558 - justify-content: center; 559 - gap: 8px; 560 - } 561 - 562 - .btn-small { 563 - padding: 8px 14px; 564 - font-size: 12px; 565 - } 566 - 567 - .btn-primary { 568 - background: var(--accent); 569 - color: white; 570 - } 571 - 572 - .btn-primary:hover { 573 - background: var(--accent-hover); 574 - } 575 - 576 - .btn-secondary { 577 - background: var(--bg-tertiary); 578 - border: 1px solid var(--border); 579 - color: var(--text-primary); 580 - width: 100%; 581 - } 582 - 583 - .btn-secondary:hover { 584 - background: var(--bg-hover); 585 - border-color: var(--border-hover); 586 - } 587 - 588 - .btn-icon { 589 - background: none; 590 - border: none; 591 - color: var(--text-tertiary); 592 - cursor: pointer; 593 - padding: 6px; 594 - border-radius: var(--radius-sm); 595 - } 596 - 597 - .btn-icon:hover { 598 - color: var(--text-primary); 599 - background: var(--bg-hover); 600 - } 601 - 602 - .popup-link { 603 - font-size: 12px; 604 - color: var(--text-tertiary); 605 - text-decoration: none; 606 - } 607 - 608 - .popup-link:hover { 609 - color: var(--accent-text); 610 - } 611 - 612 - .popup-footer { 613 - padding: 12px 16px; 614 - border-top: 1px solid var(--border); 615 - background: var(--bg-primary); 616 - } 617 - 618 - .settings-view { 619 - position: absolute; 620 - top: 0; 621 - left: 0; 622 - width: 100%; 623 - height: 100%; 624 - background: var(--bg-primary); 625 - z-index: 20; 626 - display: flex; 627 - flex-direction: column; 628 - padding: 16px; 629 - } 630 - 631 - .settings-header { 632 - display: flex; 633 - justify-content: space-between; 634 - align-items: center; 635 - margin-bottom: 24px; 636 - color: var(--text-primary); 637 - } 638 - 639 - .setting-item { 640 - margin-bottom: 20px; 641 - } 642 - 643 - .setting-label { 644 - font-size: 13px; 645 - font-weight: 500; 646 - color: var(--text-primary); 647 - margin-bottom: 6px; 648 - } 649 - 650 - .setting-help { 651 - font-size: 11px; 652 - color: var(--text-tertiary); 653 - margin-top: 4px; 654 - } 655 - 656 - ::-webkit-scrollbar { 657 - width: 8px; 658 - } 659 - 660 - ::-webkit-scrollbar-track { 661 - background: transparent; 662 - } 663 - 664 - ::-webkit-scrollbar-thumb { 665 - background: var(--bg-hover); 666 - border-radius: var(--radius-full); 667 - } 668 - 669 - ::-webkit-scrollbar-thumb:hover { 670 - background: var(--text-tertiary); 671 - } 672 - 673 - .collection-selector { 674 - position: absolute; 675 - top: 0; 676 - left: 0; 677 - width: 100%; 678 - height: 100%; 679 - background: var(--bg-primary); 680 - z-index: 30; 681 - display: flex; 682 - flex-direction: column; 683 - padding: 16px; 684 - } 685 - 686 - .collection-list { 687 - display: flex; 688 - flex-direction: column; 689 - gap: 8px; 690 - overflow-y: auto; 691 - flex: 1; 692 - } 693 - 694 - .collection-select-btn { 695 - display: flex; 696 - align-items: center; 697 - gap: 12px; 698 - padding: 12px; 699 - background: var(--bg-primary); 700 - border: 1px solid var(--border); 701 - border-radius: var(--radius-md); 702 - color: var(--text-primary); 703 - font-size: 14px; 704 - cursor: pointer; 705 - text-align: left; 706 - transition: all 0.15s; 707 - } 708 - 709 - .collection-select-btn:hover { 710 - border-color: var(--accent); 711 - background: var(--bg-hover); 712 - } 713 - 714 - .collection-select-btn:disabled { 715 - opacity: 0.6; 716 - cursor: not-allowed; 717 - } 718 - 719 - .annotation-item-actions { 720 - display: flex; 721 - align-items: center; 722 - gap: 8px; 723 - margin-left: auto; 724 - } 725 - 726 - .toggle-switch { 727 - position: relative; 728 - display: inline-block; 729 - width: 40px; 730 - height: 22px; 731 - flex-shrink: 0; 732 - } 733 - 734 - .toggle-switch input { 735 - opacity: 0; 736 - width: 0; 737 - height: 0; 738 - } 739 - 740 - .toggle-slider { 741 - position: absolute; 742 - cursor: pointer; 743 - top: 0; 744 - left: 0; 745 - right: 0; 746 - bottom: 0; 747 - background-color: var(--bg-tertiary); 748 - transition: 0.2s; 749 - border-radius: 22px; 750 - } 751 - 752 - .toggle-slider:before { 753 - position: absolute; 754 - content: ""; 755 - height: 16px; 756 - width: 16px; 757 - left: 3px; 758 - bottom: 3px; 759 - background-color: var(--text-tertiary); 760 - transition: 0.2s; 761 - border-radius: 50%; 762 - } 763 - 764 - .toggle-switch input:checked + .toggle-slider { 765 - background-color: var(--accent); 766 - } 767 - 768 - .toggle-switch input:checked + .toggle-slider:before { 769 - transform: translateX(18px); 770 - background-color: white; 771 - } 772 - 773 - .settings-input { 774 - width: 100%; 775 - padding: 10px 12px; 776 - background: var(--bg-elevated); 777 - border: 1px solid var(--border); 778 - border-radius: var(--radius-md); 779 - color: var(--text-primary); 780 - font-size: 13px; 781 - } 782 - 783 - .settings-input:focus { 784 - outline: none; 785 - border-color: var(--accent); 786 - } 787 - 788 - .theme-toggle-group { 789 - display: flex; 790 - background: var(--bg-tertiary); 791 - padding: 3px; 792 - border-radius: var(--radius-md); 793 - gap: 2px; 794 - margin-top: 8px; 795 - } 796 - 797 - .theme-btn { 798 - flex: 1; 799 - padding: 6px; 800 - border: none; 801 - background: transparent; 802 - color: var(--text-tertiary); 803 - font-size: 12px; 804 - font-weight: 500; 805 - border-radius: var(--radius-sm); 806 - cursor: pointer; 807 - transition: all 0.15s ease; 808 - } 809 - 810 - .theme-btn:hover { 811 - color: var(--text-secondary); 812 - } 813 - 814 - .theme-btn.active { 815 - background: var(--bg-primary); 816 - color: var(--text-primary); 817 - }
-308
extension/popup/popup.html
··· 1 - <!doctype html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>Margin</title> 7 - <link rel="preconnect" href="https://fonts.googleapis.com" /> 8 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 9 - <link 10 - href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" 11 - rel="stylesheet" 12 - /> 13 - <link rel="stylesheet" href="popup.css" /> 14 - </head> 15 - 16 - <body> 17 - <div class="popup"> 18 - <header class="popup-header"> 19 - <div class="popup-brand"> 20 - <span class="popup-logo"> 21 - <img src="../icons/logo.svg" alt="Margin" width="20" height="20" /> 22 - </span> 23 - <span class="popup-title">Margin</span> 24 - </div> 25 - <div id="user-info" class="user-info" style="display: none"> 26 - <span id="user-handle" class="user-handle"></span> 27 - </div> 28 - </header> 29 - 30 - <div class="popup-content"> 31 - <div id="loading" class="loading"> 32 - <div class="spinner"></div> 33 - <span>Loading...</span> 34 - </div> 35 - 36 - <div id="login-prompt" class="login-prompt" style="display: none"> 37 - <span class="login-at-logo">@</span> 38 - <h2 class="login-title">Sign in with AT Protocol</h2> 39 - <p class="login-text"> 40 - Connect your Bluesky account to annotate, highlight, and bookmark 41 - the web. 42 - </p> 43 - <button id="sign-in" class="btn btn-primary">Continue</button> 44 - </div> 45 - 46 - <div id="main-content" style="display: none"> 47 - <div class="tabs"> 48 - <button class="tab-btn active" data-tab="page">Page</button> 49 - <button class="tab-btn" data-tab="bookmarks">My Bookmarks</button> 50 - <button class="tab-btn" data-tab="highlights">My Highlights</button> 51 - </div> 52 - 53 - <div id="tab-page" class="tab-content active"> 54 - <div class="quick-actions"> 55 - <button 56 - id="bookmark-page" 57 - class="btn btn-secondary btn-small" 58 - title="Bookmark this page" 59 - > 60 - <svg 61 - width="14" 62 - height="14" 63 - viewBox="0 0 24 24" 64 - fill="none" 65 - stroke="currentColor" 66 - stroke-width="2" 67 - stroke-linecap="round" 68 - stroke-linejoin="round" 69 - > 70 - <path 71 - d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" 72 - ></path> 73 - </svg> 74 - Bookmark Page 75 - </button> 76 - </div> 77 - 78 - <div id="create-form" class="create-form"> 79 - <div class="form-header"> 80 - <span class="form-title">New Annotation</span> 81 - <span id="current-url" class="current-url"></span> 82 - </div> 83 - <textarea 84 - id="annotation-text" 85 - class="annotation-input" 86 - placeholder="Write your annotation..." 87 - rows="3" 88 - ></textarea> 89 - <div class="form-actions"> 90 - <button 91 - id="submit-annotation" 92 - class="btn btn-primary btn-small" 93 - > 94 - Post 95 - </button> 96 - </div> 97 - </div> 98 - 99 - <div class="annotations-section"> 100 - <div class="section-header"> 101 - <span class="section-title">Annotations on this page</span> 102 - <span id="annotation-count" class="annotation-count">0</span> 103 - </div> 104 - <div id="annotations" class="annotations"></div> 105 - <div id="empty" class="empty-state" style="display: none"> 106 - <span class="empty-icon"> 107 - <svg 108 - width="32" 109 - height="32" 110 - viewBox="0 0 24 24" 111 - fill="none" 112 - stroke="currentColor" 113 - stroke-width="2" 114 - stroke-linecap="round" 115 - stroke-linejoin="round" 116 - > 117 - <path d="M22 12h-6l-2 3h-4l-2-3H2"></path> 118 - <path 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 - ></path> 121 - </svg> 122 - </span> 123 - <p class="empty-text">No annotations yet</p> 124 - </div> 125 - </div> 126 - </div> 127 - 128 - <div id="tab-bookmarks" class="tab-content"> 129 - <div class="section-header"> 130 - <span class="section-title">My Saved Pages</span> 131 - </div> 132 - <div id="bookmarks-list" class="annotations"></div> 133 - <div id="bookmarks-empty" class="empty-state" style="display: none"> 134 - <span class="empty-icon"> 135 - <svg 136 - width="32" 137 - height="32" 138 - viewBox="0 0 24 24" 139 - fill="none" 140 - stroke="currentColor" 141 - stroke-width="2" 142 - stroke-linecap="round" 143 - stroke-linejoin="round" 144 - > 145 - <path 146 - d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" 147 - ></path> 148 - </svg> 149 - </span> 150 - <p class="empty-text">You haven't bookmarked any pages yet</p> 151 - </div> 152 - </div> 153 - 154 - <div id="tab-highlights" class="tab-content"> 155 - <div class="section-header"> 156 - <span class="section-title">My Highlights</span> 157 - </div> 158 - <div id="highlights-list" class="annotations"></div> 159 - <div 160 - id="highlights-empty" 161 - class="empty-state" 162 - style="display: none" 163 - > 164 - <span class="empty-icon"> 165 - <svg 166 - width="32" 167 - height="32" 168 - viewBox="0 0 24 24" 169 - fill="none" 170 - stroke="currentColor" 171 - stroke-width="2" 172 - stroke-linecap="round" 173 - stroke-linejoin="round" 174 - > 175 - <path 176 - d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" 177 - ></path> 178 - </svg> 179 - </span> 180 - <p class="empty-text">You haven't highlighted anything yet</p> 181 - </div> 182 - </div> 183 - </div> 184 - </div> 185 - 186 - <footer class="popup-footer"> 187 - <div 188 - style=" 189 - display: flex; 190 - justify-content: space-between; 191 - align-items: center; 192 - " 193 - > 194 - <a href="#" id="open-web" class="popup-link">Open Margin</a> 195 - <button id="toggle-settings" class="btn-icon" title="Settings"> 196 - <svg 197 - width="16" 198 - height="16" 199 - viewBox="0 0 24 24" 200 - fill="none" 201 - stroke="currentColor" 202 - stroke-width="2" 203 - stroke-linecap="round" 204 - stroke-linejoin="round" 205 - > 206 - <circle cx="12" cy="12" r="3"></circle> 207 - <path 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 - ></path> 210 - </svg> 211 - </button> 212 - </div> 213 - </footer> 214 - 215 - <div id="settings-view" class="settings-view" style="display: none"> 216 - <div class="settings-header"> 217 - <h3 class="settings-title">Extension Settings</h3> 218 - <button id="close-settings" class="btn-icon">×</button> 219 - </div> 220 - <div class="setting-item"> 221 - <div 222 - style=" 223 - display: flex; 224 - justify-content: space-between; 225 - align-items: center; 226 - " 227 - > 228 - <div> 229 - <label>Show page overlays</label> 230 - <p class="setting-help" style="margin-top: 2px"> 231 - Highlights, badges, and tooltips on pages 232 - </p> 233 - </div> 234 - <label class="toggle-switch"> 235 - <input type="checkbox" id="overlay-toggle" checked /> 236 - <span class="toggle-slider"></span> 237 - </label> 238 - </div> 239 - </div> 240 - <div class="setting-item"> 241 - <label for="api-url">API URL (for self-hosting)</label> 242 - <input 243 - type="url" 244 - id="api-url" 245 - placeholder="http://localhost:8080" 246 - class="settings-input" 247 - /> 248 - <p class="setting-help">Enter your backend URL</p> 249 - </div> 250 - <div class="setting-item"> 251 - <label for="theme-select">Theme</label> 252 - <div class="theme-toggle-group"> 253 - <button class="theme-btn active" data-theme="system">Auto</button> 254 - <button class="theme-btn" data-theme="light">Light</button> 255 - <button class="theme-btn" data-theme="dark">Dark</button> 256 - </div> 257 - </div> 258 - <button id="save-settings" class="btn btn-primary" style="width: 100%"> 259 - Save 260 - </button> 261 - </div> 262 - 263 - <div 264 - id="collection-selector" 265 - class="collection-selector" 266 - style="display: none" 267 - > 268 - <div 269 - class="selector-header" 270 - style=" 271 - display: flex; 272 - justify-content: space-between; 273 - align-items: center; 274 - margin-bottom: 12px; 275 - padding-bottom: 8px; 276 - border-bottom: 1px solid var(--border); 277 - " 278 - > 279 - <h3 class="selector-title" style="margin: 0; font-size: 14px"> 280 - Add to Collection 281 - </h3> 282 - <button id="close-collection-selector" class="btn-icon">×</button> 283 - </div> 284 - <div 285 - id="collection-loading" 286 - class="spinner" 287 - style="display: none; margin: 20px auto" 288 - ></div> 289 - <div 290 - id="collection-list" 291 - class="collection-list" 292 - style=" 293 - display: flex; 294 - flex-direction: column; 295 - gap: 4px; 296 - max-height: 200px; 297 - overflow-y: auto; 298 - " 299 - ></div> 300 - <div id="collections-empty" class="empty-state" style="display: none"> 301 - <p class="empty-text">No collections found</p> 302 - </div> 303 - </div> 304 - </div> 305 - 306 - <script src="popup.js"></script> 307 - </body> 308 - </html>
-835
extension/popup/popup.js
··· 1 - const browserAPI = typeof browser !== "undefined" ? browser : chrome; 2 - 3 - document.addEventListener("DOMContentLoaded", async () => { 4 - const views = { 5 - loading: document.getElementById("loading"), 6 - login: document.getElementById("login-prompt"), 7 - main: document.getElementById("main-content"), 8 - settings: document.getElementById("settings-view"), 9 - collectionSelector: document.getElementById("collection-selector"), 10 - }; 11 - 12 - const els = { 13 - userHandle: document.getElementById("user-handle"), 14 - userInfo: document.getElementById("user-info"), 15 - signInBtn: document.getElementById("sign-in"), 16 - openWebBtn: document.getElementById("open-web"), 17 - submitBtn: document.getElementById("submit-annotation"), 18 - textInput: document.getElementById("annotation-text"), 19 - currentUrl: document.getElementById("current-url"), 20 - annotationsList: document.getElementById("annotations"), 21 - annotationCount: document.getElementById("annotation-count"), 22 - emptyState: document.getElementById("empty"), 23 - toggleSettings: document.getElementById("toggle-settings"), 24 - closeSettings: document.getElementById("close-settings"), 25 - saveSettings: document.getElementById("save-settings"), 26 - apiUrlInput: document.getElementById("api-url"), 27 - bookmarkBtn: document.getElementById("bookmark-page"), 28 - 29 - tabs: document.querySelectorAll(".tab-btn"), 30 - tabContents: document.querySelectorAll(".tab-content"), 31 - bookmarksList: document.getElementById("bookmarks-list"), 32 - bookmarksEmpty: document.getElementById("bookmarks-empty"), 33 - highlightsList: document.getElementById("highlights-list"), 34 - highlightsEmpty: document.getElementById("highlights-empty"), 35 - 36 - closeCollectionSelector: document.getElementById( 37 - "close-collection-selector", 38 - ), 39 - collectionList: document.getElementById("collection-list"), 40 - collectionLoading: document.getElementById("collection-loading"), 41 - collectionsEmpty: document.getElementById("collections-empty"), 42 - overlayToggle: document.getElementById("overlay-toggle"), 43 - themeBtns: document.querySelectorAll(".theme-btn"), 44 - }; 45 - 46 - let currentTab = null; 47 - let apiUrl = "https://margin.at"; 48 - let currentUserDid = null; 49 - let pendingSelector = null; 50 - // let _activeAnnotationUriForCollection = null; 51 - 52 - const storage = await browserAPI.storage.local.get([ 53 - "apiUrl", 54 - "showOverlay", 55 - "theme", 56 - ]); 57 - if (storage.apiUrl) { 58 - apiUrl = storage.apiUrl; 59 - } 60 - els.apiUrlInput.value = apiUrl; 61 - 62 - if (els.overlayToggle) { 63 - els.overlayToggle.checked = storage.showOverlay !== false; 64 - } 65 - 66 - const currentTheme = storage.theme || "system"; 67 - applyTheme(currentTheme); 68 - updateThemeUI(currentTheme); 69 - 70 - try { 71 - const [tab] = await browserAPI.tabs.query({ 72 - active: true, 73 - currentWindow: true, 74 - }); 75 - currentTab = tab; 76 - 77 - if (els.currentUrl) { 78 - els.currentUrl.textContent = new URL(tab.url).hostname; 79 - } 80 - 81 - let pendingData = null; 82 - if (browserAPI.storage.session) { 83 - try { 84 - const sessionData = await browserAPI.storage.session.get([ 85 - "pendingAnnotation", 86 - ]); 87 - if (sessionData.pendingAnnotation) { 88 - pendingData = sessionData.pendingAnnotation; 89 - await browserAPI.storage.session.remove(["pendingAnnotation"]); 90 - } 91 - } catch { 92 - /* ignore */ 93 - } 94 - } 95 - 96 - if (!pendingData) { 97 - const localData = await browserAPI.storage.local.get([ 98 - "pendingAnnotation", 99 - "pendingAnnotationExpiry", 100 - ]); 101 - if ( 102 - localData.pendingAnnotation && 103 - localData.pendingAnnotationExpiry > Date.now() 104 - ) { 105 - pendingData = localData.pendingAnnotation; 106 - } 107 - await browserAPI.storage.local.remove([ 108 - "pendingAnnotation", 109 - "pendingAnnotationExpiry", 110 - ]); 111 - } 112 - 113 - if (pendingData?.selector) { 114 - pendingSelector = pendingData.selector; 115 - showQuotePreview(pendingSelector); 116 - } 117 - 118 - checkSession(); 119 - } catch (err) { 120 - console.error("Init error:", err); 121 - showView("login"); 122 - } 123 - 124 - els.signInBtn?.addEventListener("click", () => { 125 - browserAPI.runtime.sendMessage({ type: "OPEN_LOGIN" }); 126 - }); 127 - 128 - els.openWebBtn?.addEventListener("click", () => { 129 - browserAPI.runtime.sendMessage({ type: "OPEN_WEB" }); 130 - }); 131 - 132 - els.tabs.forEach((btn) => { 133 - btn.addEventListener("click", () => { 134 - els.tabs.forEach((t) => t.classList.remove("active")); 135 - els.tabContents.forEach((c) => c.classList.remove("active")); 136 - 137 - const tabId = btn.getAttribute("data-tab"); 138 - btn.classList.add("active"); 139 - document.getElementById(`tab-${tabId}`).classList.add("active"); 140 - 141 - if (tabId === "bookmarks") loadBookmarks(); 142 - if (tabId === "highlights") loadHighlights(); 143 - }); 144 - }); 145 - 146 - els.submitBtn?.addEventListener("click", async () => { 147 - const text = els.textInput.value.trim(); 148 - if (!text) return; 149 - 150 - els.submitBtn.disabled = true; 151 - els.submitBtn.textContent = "Posting..."; 152 - 153 - try { 154 - const annotationData = { 155 - url: currentTab.url, 156 - text: text, 157 - title: currentTab.title, 158 - }; 159 - 160 - if (pendingSelector) { 161 - annotationData.selector = pendingSelector; 162 - } 163 - 164 - const res = await sendMessage({ 165 - type: "CREATE_ANNOTATION", 166 - data: annotationData, 167 - }); 168 - 169 - if (res.success) { 170 - els.textInput.value = ""; 171 - pendingSelector = null; 172 - hideQuotePreview(); 173 - loadAnnotations(); 174 - } else { 175 - alert("Failed to post annotation"); 176 - } 177 - } catch (err) { 178 - console.error("Post error:", err); 179 - alert("Error posting annotation"); 180 - } finally { 181 - els.submitBtn.disabled = false; 182 - els.submitBtn.textContent = "Post"; 183 - } 184 - }); 185 - 186 - els.bookmarkBtn?.addEventListener("click", async () => { 187 - els.bookmarkBtn.disabled = true; 188 - els.bookmarkBtn.textContent = "Saving..."; 189 - 190 - try { 191 - const res = await sendMessage({ 192 - type: "CREATE_BOOKMARK", 193 - data: { 194 - url: currentTab.url, 195 - title: currentTab.title, 196 - }, 197 - }); 198 - 199 - if (res.success) { 200 - els.bookmarkBtn.textContent = "✓ Bookmarked"; 201 - setTimeout(() => { 202 - els.bookmarkBtn.textContent = "Bookmark Page"; 203 - els.bookmarkBtn.disabled = false; 204 - }, 2000); 205 - } else { 206 - alert("Failed to bookmark page"); 207 - els.bookmarkBtn.textContent = "Bookmark Page"; 208 - els.bookmarkBtn.disabled = false; 209 - } 210 - } catch (err) { 211 - console.error("Bookmark error:", err); 212 - alert("Error bookmarking page"); 213 - els.bookmarkBtn.textContent = "Bookmark Page"; 214 - els.bookmarkBtn.disabled = false; 215 - } 216 - }); 217 - 218 - els.toggleSettings?.addEventListener("click", () => { 219 - views.settings.style.display = "flex"; 220 - }); 221 - 222 - els.closeSettings?.addEventListener("click", () => { 223 - views.settings.style.display = "none"; 224 - }); 225 - 226 - els.saveSettings?.addEventListener("click", async () => { 227 - const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); 228 - const showOverlay = els.overlayToggle?.checked ?? true; 229 - 230 - await browserAPI.storage.local.set({ apiUrl: newUrl, showOverlay }); 231 - if (newUrl) { 232 - apiUrl = newUrl; 233 - } 234 - await sendMessage({ type: "UPDATE_SETTINGS" }); 235 - 236 - const tabs = await browserAPI.tabs.query({}); 237 - for (const tab of tabs) { 238 - if (tab.id) { 239 - try { 240 - await browserAPI.tabs.sendMessage(tab.id, { 241 - type: "UPDATE_OVERLAY_VISIBILITY", 242 - show: showOverlay, 243 - }); 244 - } catch { 245 - /* ignore */ 246 - } 247 - } 248 - } 249 - 250 - views.settings.style.display = "none"; 251 - checkSession(); 252 - }); 253 - 254 - els.themeBtns.forEach((btn) => { 255 - btn.addEventListener("click", async () => { 256 - const theme = btn.getAttribute("data-theme"); 257 - await browserAPI.storage.local.set({ theme }); 258 - applyTheme(theme); 259 - updateThemeUI(theme); 260 - }); 261 - }); 262 - 263 - els.closeCollectionSelector?.addEventListener("click", () => { 264 - views.collectionSelector.style.display = "none"; 265 - }); 266 - 267 - async function openCollectionSelector(annotationUri) { 268 - if (!currentUserDid) { 269 - console.error("No currentUserDid, returning early"); 270 - return; 271 - } 272 - views.collectionSelector.style.display = "flex"; 273 - els.collectionList.innerHTML = ""; 274 - els.collectionLoading.style.display = "block"; 275 - els.collectionsEmpty.style.display = "none"; 276 - 277 - try { 278 - const [collectionsRes, containingRes] = await Promise.all([ 279 - sendMessage({ 280 - type: "GET_USER_COLLECTIONS", 281 - data: { did: currentUserDid }, 282 - }), 283 - sendMessage({ 284 - type: "GET_CONTAINING_COLLECTIONS", 285 - data: { uri: annotationUri }, 286 - }), 287 - ]); 288 - 289 - if (collectionsRes.success) { 290 - const containingUris = containingRes.success 291 - ? new Set(containingRes.data) 292 - : new Set(); 293 - renderCollectionList( 294 - collectionsRes.data, 295 - annotationUri, 296 - containingUris, 297 - ); 298 - } 299 - } catch (err) { 300 - console.error("Load collections error:", err); 301 - els.collectionList.innerHTML = 302 - '<p class="error">Failed to load collections</p>'; 303 - } finally { 304 - els.collectionLoading.style.display = "none"; 305 - } 306 - } 307 - 308 - function renderCollectionList( 309 - items, 310 - annotationUri, 311 - containingUris = new Set(), 312 - ) { 313 - els.collectionList.innerHTML = ""; 314 - els.collectionList.dataset.annotationUri = annotationUri; 315 - 316 - if (!items || items.length === 0) { 317 - els.collectionsEmpty.style.display = "block"; 318 - return; 319 - } 320 - 321 - items.forEach((col) => { 322 - const btn = document.createElement("button"); 323 - btn.className = "collection-select-btn"; 324 - const isAdded = containingUris.has(col.uri); 325 - 326 - const iconSpan = document.createElement("span"); 327 - iconSpan.textContent = isAdded ? "✓" : "📁"; 328 - btn.appendChild(iconSpan); 329 - 330 - const nameSpan = document.createElement("span"); 331 - nameSpan.textContent = col.name; 332 - btn.appendChild(nameSpan); 333 - 334 - if (isAdded) { 335 - btn.classList.add("added"); 336 - btn.disabled = true; 337 - } 338 - 339 - btn.addEventListener("click", async () => { 340 - if (btn.disabled) return; 341 - const annUri = els.collectionList.dataset.annotationUri; 342 - await handleAddToCollection(col.uri, btn, annUri); 343 - }); 344 - 345 - els.collectionList.appendChild(btn); 346 - }); 347 - } 348 - 349 - async function handleAddToCollection( 350 - collectionUri, 351 - btnElement, 352 - annotationUri, 353 - ) { 354 - if (!annotationUri) { 355 - console.error("No annotationUri provided!"); 356 - alert("Error: No item selected to add"); 357 - return; 358 - } 359 - 360 - const originalText = btnElement.textContent; 361 - btnElement.disabled = true; 362 - btnElement.textContent = "Adding..."; 363 - 364 - try { 365 - const res = await sendMessage({ 366 - type: "ADD_TO_COLLECTION", 367 - data: { 368 - collectionUri: collectionUri, 369 - annotationUri: annotationUri, 370 - }, 371 - }); 372 - 373 - if (res && res.success) { 374 - btnElement.textContent = "✓ Added"; 375 - btnElement.textContent = "✓ Added"; 376 - setTimeout(() => { 377 - btnElement.textContent = originalText; 378 - btnElement.disabled = false; 379 - }, 2000); 380 - } else { 381 - alert( 382 - "Failed to add to collection: " + (res?.error || "Unknown error"), 383 - ); 384 - btnElement.textContent = originalText; 385 - btnElement.disabled = false; 386 - } 387 - } catch (err) { 388 - console.error("Add to collection error:", err); 389 - alert("Error adding to collection: " + err.message); 390 - btnElement.textContent = originalText; 391 - btnElement.disabled = false; 392 - } 393 - } 394 - 395 - async function checkSession() { 396 - showView("loading"); 397 - try { 398 - const res = await sendMessage({ type: "CHECK_SESSION" }); 399 - 400 - if (res.success && res.data?.authenticated) { 401 - if (els.userHandle) { 402 - const handle = res.data.handle || res.data.email || "User"; 403 - els.userHandle.textContent = "@" + handle; 404 - } 405 - els.userInfo.style.display = "flex"; 406 - currentUserDid = res.data.did; 407 - showView("main"); 408 - loadAnnotations(); 409 - } else { 410 - els.userInfo.style.display = "none"; 411 - showView("login"); 412 - } 413 - } catch (err) { 414 - console.error("Session check error:", err); 415 - els.userInfo.style.display = "none"; 416 - showView("login"); 417 - } 418 - } 419 - 420 - async function loadAnnotations() { 421 - try { 422 - const res = await sendMessage({ 423 - type: "GET_ANNOTATIONS", 424 - data: { url: currentTab.url }, 425 - }); 426 - 427 - if (res.success) { 428 - if (currentUserDid) { 429 - const isBookmarked = res.data.some( 430 - (item) => 431 - item.type === "Bookmark" && item.creator.did === currentUserDid, 432 - ); 433 - if (els.bookmarkBtn) { 434 - if (isBookmarked) { 435 - els.bookmarkBtn.textContent = "✓ Bookmarked"; 436 - els.bookmarkBtn.disabled = true; 437 - } else { 438 - els.bookmarkBtn.textContent = "Bookmark Page"; 439 - els.bookmarkBtn.disabled = false; 440 - } 441 - } 442 - } 443 - 444 - const listItems = res.data.filter((item) => item.type !== "Bookmark"); 445 - renderAnnotations(listItems); 446 - } 447 - } catch (err) { 448 - console.error("Load annotations error:", err); 449 - } 450 - } 451 - 452 - async function loadBookmarks() { 453 - if (!currentUserDid) return; 454 - els.bookmarksList.innerHTML = 455 - '<div class="spinner" style="margin: 20px auto;"></div>'; 456 - els.bookmarksEmpty.style.display = "none"; 457 - 458 - try { 459 - const res = await sendMessage({ 460 - type: "GET_USER_BOOKMARKS", 461 - data: { did: currentUserDid }, 462 - }); 463 - 464 - if (res.success) { 465 - renderBookmarks(res.data); 466 - } 467 - } catch (err) { 468 - console.error("Load bookmarks error:", err); 469 - els.bookmarksList.innerHTML = 470 - '<p class="error">Failed to load bookmarks</p>'; 471 - } 472 - } 473 - 474 - async function loadHighlights() { 475 - if (!currentUserDid) return; 476 - els.highlightsList.innerHTML = 477 - '<div class="spinner" style="margin: 20px auto;"></div>'; 478 - els.highlightsEmpty.style.display = "none"; 479 - 480 - try { 481 - const res = await sendMessage({ 482 - type: "GET_USER_HIGHLIGHTS", 483 - data: { did: currentUserDid }, 484 - }); 485 - 486 - if (res.success) { 487 - renderHighlights(res.data); 488 - } 489 - } catch (err) { 490 - console.error("Load highlights error:", err); 491 - els.highlightsList.innerHTML = 492 - '<p class="error">Failed to load highlights</p>'; 493 - } 494 - } 495 - 496 - function renderAnnotations(items) { 497 - els.annotationsList.innerHTML = ""; 498 - els.annotationCount.textContent = items?.length || 0; 499 - 500 - if (!items || items.length === 0) { 501 - els.emptyState.style.display = "block"; 502 - return; 503 - } 504 - 505 - els.emptyState.style.display = "none"; 506 - items.forEach((item) => { 507 - const el = document.createElement("div"); 508 - el.className = "annotation-item"; 509 - 510 - const author = item.creator || item.author || {}; 511 - const authorName = author.handle || author.displayName || "Unknown"; 512 - const authorInitial = authorName[0]?.toUpperCase() || "?"; 513 - const createdAt = item.created || item.createdAt; 514 - const text = item.body?.value || item.text || ""; 515 - const selector = item.target?.selector; 516 - 517 - const isHighlight = item.type === "Highlight"; 518 - const quote = selector?.exact || ""; 519 - 520 - const header = document.createElement("div"); 521 - header.className = "annotation-item-header"; 522 - 523 - const avatar = document.createElement("div"); 524 - avatar.className = "annotation-item-avatar"; 525 - if (author.avatar) { 526 - const img = document.createElement("img"); 527 - img.src = author.avatar; 528 - img.alt = authorName; 529 - img.style.width = "100%"; 530 - img.style.height = "100%"; 531 - img.style.borderRadius = "50%"; 532 - img.style.objectFit = "cover"; 533 - avatar.appendChild(img); 534 - avatar.style.background = "none"; 535 - } else { 536 - avatar.textContent = authorInitial; 537 - } 538 - header.appendChild(avatar); 539 - 540 - const meta = document.createElement("div"); 541 - meta.className = "annotation-item-meta"; 542 - 543 - const authorEl = document.createElement("div"); 544 - authorEl.className = "annotation-item-author"; 545 - authorEl.textContent = "@" + authorName; 546 - meta.appendChild(authorEl); 547 - 548 - const timeEl = document.createElement("div"); 549 - timeEl.className = "annotation-item-time"; 550 - timeEl.textContent = formatDate(createdAt); 551 - meta.appendChild(timeEl); 552 - 553 - header.appendChild(meta); 554 - 555 - if (isHighlight) { 556 - const badge = document.createElement("span"); 557 - badge.className = "annotation-type-badge highlight"; 558 - badge.textContent = "Highlight"; 559 - header.appendChild(badge); 560 - } 561 - 562 - el.appendChild(header); 563 - 564 - const actions = document.createElement("div"); 565 - actions.className = "annotation-item-actions"; 566 - 567 - if (currentUserDid) { 568 - const folderBtn = document.createElement("button"); 569 - folderBtn.className = "btn-icon"; 570 - folderBtn.innerHTML = 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 - folderBtn.title = "Add to Collection"; 573 - folderBtn.addEventListener("click", (e) => { 574 - e.stopPropagation(); 575 - const uri = item.id || item.uri; 576 - openCollectionSelector(uri); 577 - }); 578 - actions.appendChild(folderBtn); 579 - } 580 - 581 - header.appendChild(actions); 582 - 583 - if (quote) { 584 - const quoteEl = document.createElement("div"); 585 - quoteEl.className = "annotation-item-quote"; 586 - quoteEl.textContent = '"' + quote + '"'; 587 - el.appendChild(quoteEl); 588 - } 589 - 590 - if (text) { 591 - const textEl = document.createElement("div"); 592 - textEl.className = "annotation-item-text"; 593 - textEl.textContent = text; 594 - el.appendChild(textEl); 595 - } 596 - 597 - els.annotationsList.appendChild(el); 598 - }); 599 - } 600 - 601 - function renderBookmarks(items) { 602 - els.bookmarksList.innerHTML = ""; 603 - 604 - if (!items || items.length === 0) { 605 - els.bookmarksEmpty.style.display = "flex"; 606 - return; 607 - } 608 - 609 - els.bookmarksEmpty.style.display = "none"; 610 - items.forEach((item) => { 611 - const el = document.createElement("a"); 612 - el.className = "bookmark-item"; 613 - el.href = item.source; 614 - el.target = "_blank"; 615 - 616 - const row = document.createElement("div"); 617 - row.style.display = "flex"; 618 - row.style.justifyContent = "space-between"; 619 - row.style.alignItems = "center"; 620 - 621 - const content = document.createElement("div"); 622 - content.style.flex = "1"; 623 - content.style.overflow = "hidden"; 624 - 625 - const titleEl = document.createElement("div"); 626 - titleEl.className = "bookmark-title"; 627 - titleEl.textContent = item.title || item.source; 628 - content.appendChild(titleEl); 629 - 630 - const urlEl = document.createElement("div"); 631 - urlEl.className = "bookmark-url"; 632 - urlEl.textContent = new URL(item.source).hostname; 633 - content.appendChild(urlEl); 634 - 635 - row.appendChild(content); 636 - 637 - if (currentUserDid) { 638 - const folderBtn = document.createElement("button"); 639 - folderBtn.className = "btn-icon"; 640 - folderBtn.innerHTML = 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 - folderBtn.title = "Add to Collection"; 643 - folderBtn.addEventListener("click", (e) => { 644 - e.preventDefault(); 645 - e.stopPropagation(); 646 - const uri = item.id || item.uri; 647 - openCollectionSelector(uri); 648 - }); 649 - row.appendChild(folderBtn); 650 - } 651 - 652 - el.appendChild(row); 653 - els.bookmarksList.appendChild(el); 654 - }); 655 - } 656 - 657 - function renderHighlights(items) { 658 - els.highlightsList.innerHTML = ""; 659 - 660 - if (!items || items.length === 0) { 661 - els.highlightsEmpty.style.display = "flex"; 662 - return; 663 - } 664 - 665 - els.highlightsEmpty.style.display = "none"; 666 - items.forEach((item) => { 667 - const el = document.createElement("div"); 668 - el.className = "annotation-item"; 669 - 670 - const target = item.target || {}; 671 - const selector = target.selector || {}; 672 - const quote = selector.exact || ""; 673 - const url = target.source || ""; 674 - 675 - const header = document.createElement("div"); 676 - header.className = "annotation-item-header"; 677 - 678 - const meta = document.createElement("div"); 679 - meta.className = "annotation-item-meta"; 680 - 681 - const authorEl = document.createElement("div"); 682 - authorEl.className = "annotation-item-author"; 683 - authorEl.textContent = new URL(url).hostname; 684 - meta.appendChild(authorEl); 685 - 686 - const timeEl = document.createElement("div"); 687 - timeEl.className = "annotation-item-time"; 688 - timeEl.textContent = formatDate(item.created); 689 - meta.appendChild(timeEl); 690 - 691 - header.appendChild(meta); 692 - 693 - const actions = document.createElement("div"); 694 - actions.className = "annotation-item-actions"; 695 - 696 - const folderBtn = document.createElement("button"); 697 - folderBtn.className = "btn-icon"; 698 - folderBtn.innerHTML = 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 - folderBtn.title = "Add to Collection"; 701 - folderBtn.addEventListener("click", (e) => { 702 - e.stopPropagation(); 703 - const uri = item.id || item.uri; 704 - openCollectionSelector(uri); 705 - }); 706 - actions.appendChild(folderBtn); 707 - 708 - header.appendChild(actions); 709 - el.appendChild(header); 710 - 711 - if (quote) { 712 - const quoteEl = document.createElement("div"); 713 - quoteEl.className = "annotation-item-quote"; 714 - quoteEl.style.marginLeft = "0"; 715 - quoteEl.style.borderColor = item.color || "#fcd34d"; 716 - quoteEl.textContent = '"' + quote + '"'; 717 - el.appendChild(quoteEl); 718 - } 719 - 720 - el.style.cursor = "pointer"; 721 - el.addEventListener("click", () => { 722 - const textFragment = createTextFragment(url, selector); 723 - browserAPI.tabs.create({ url: textFragment }); 724 - }); 725 - 726 - els.highlightsList.appendChild(el); 727 - }); 728 - } 729 - 730 - function createTextFragment(url, selector) { 731 - if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact) 732 - return url; 733 - 734 - let fragment = ":~:text="; 735 - if (selector.prefix) fragment += encodeURIComponent(selector.prefix) + "-,"; 736 - fragment += encodeURIComponent(selector.exact); 737 - if (selector.suffix) fragment += ",-" + encodeURIComponent(selector.suffix); 738 - 739 - return url + "#" + fragment; 740 - } 741 - 742 - function showQuotePreview(selector) { 743 - if (!selector?.exact) return; 744 - 745 - let preview = document.getElementById("quote-preview"); 746 - if (!preview) { 747 - preview = document.createElement("div"); 748 - preview.id = "quote-preview"; 749 - preview.className = "quote-preview"; 750 - const form = document.getElementById("create-form"); 751 - if (form) { 752 - form.insertBefore(preview, els.textInput); 753 - } 754 - } 755 - 756 - const header = document.createElement("div"); 757 - header.className = "quote-preview-header"; 758 - 759 - const label = document.createElement("span"); 760 - label.textContent = "Annotating:"; 761 - header.appendChild(label); 762 - 763 - const clearBtn = document.createElement("button"); 764 - clearBtn.className = "quote-preview-clear"; 765 - clearBtn.title = "Clear"; 766 - clearBtn.textContent = "×"; 767 - clearBtn.addEventListener("click", () => { 768 - pendingSelector = null; 769 - hideQuotePreview(); 770 - }); 771 - header.appendChild(clearBtn); 772 - 773 - preview.appendChild(header); 774 - 775 - const text = document.createElement("div"); 776 - text.className = "quote-preview-text"; 777 - text.textContent = '"' + selector.exact + '"'; 778 - preview.appendChild(text); 779 - 780 - els.textInput?.focus(); 781 - } 782 - 783 - function hideQuotePreview() { 784 - const preview = document.getElementById("quote-preview"); 785 - if (preview) preview.remove(); 786 - } 787 - 788 - function formatDate(dateString) { 789 - if (!dateString) return ""; 790 - try { 791 - return new Date(dateString).toLocaleDateString(); 792 - } catch { 793 - return dateString; 794 - } 795 - } 796 - 797 - function showView(viewName) { 798 - Object.keys(views).forEach((key) => { 799 - if (views[key]) views[key].style.display = "none"; 800 - }); 801 - if (views[viewName]) { 802 - views[viewName].style.display = 803 - viewName === "loading" || viewName === "settings" ? "flex" : "block"; 804 - } 805 - } 806 - 807 - function sendMessage(message) { 808 - return new Promise((resolve, reject) => { 809 - browserAPI.runtime.sendMessage(message, (response) => { 810 - if (browserAPI.runtime.lastError) { 811 - reject(browserAPI.runtime.lastError); 812 - } else { 813 - resolve(response); 814 - } 815 - }); 816 - }); 817 - } 818 - }); 819 - 820 - function applyTheme(theme) { 821 - document.body.classList.remove("light", "dark"); 822 - if (theme === "system") return; 823 - document.body.classList.add(theme); 824 - } 825 - 826 - function updateThemeUI(theme) { 827 - const btns = document.querySelectorAll(".theme-btn"); 828 - btns.forEach((btn) => { 829 - if (btn.getAttribute("data-theme") === theme) { 830 - btn.classList.add("active"); 831 - } else { 832 - btn.classList.remove("active"); 833 - } 834 - }); 835 - }
+6
extension/postcss.config.js
··· 1 + export default { 2 + plugins: { 3 + tailwindcss: {}, 4 + autoprefixer: {}, 5 + }, 6 + };
+1
extension/public/icons/semble-logo.svg
··· 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 - @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap"); 2 - 3 - :root { 4 - --bg-primary: #0a0a0d; 5 - --bg-secondary: #121216; 6 - --bg-tertiary: #1a1a1f; 7 - --bg-card: #0f0f13; 8 - --bg-elevated: #18181d; 9 - --bg-hover: #1e1e24; 10 - 11 - --text-primary: #eaeaee; 12 - --text-secondary: #b7b6c5; 13 - --text-tertiary: #6e6d7a; 14 - 15 - --border: rgba(183, 182, 197, 0.12); 16 - --border-hover: rgba(183, 182, 197, 0.2); 17 - 18 - --accent: #957a86; 19 - --accent-hover: #a98d98; 20 - --accent-subtle: rgba(149, 122, 134, 0.15); 21 - --accent-text: #c4a8b2; 22 - 23 - --success: #7fb069; 24 - --error: #d97766; 25 - --warning: #e8a54b; 26 - 27 - --radius-sm: 6px; 28 - --radius-md: 8px; 29 - --radius-lg: 12px; 30 - --radius-full: 9999px; 31 - 32 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 33 - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 34 - } 35 - 36 - @media (prefers-color-scheme: light) { 37 - :root { 38 - --bg-primary: #f8f8fa; 39 - --bg-secondary: #ffffff; 40 - --bg-tertiary: #f0f0f4; 41 - --bg-card: #ffffff; 42 - --bg-elevated: #ffffff; 43 - --bg-hover: #eeeef2; 44 - 45 - --text-primary: #18171c; 46 - --text-secondary: #5c495a; 47 - --text-tertiary: #8a8494; 48 - 49 - --border: rgba(92, 73, 90, 0.12); 50 - --border-hover: rgba(92, 73, 90, 0.22); 51 - 52 - --accent: #7a5f6d; 53 - --accent-hover: #664e5b; 54 - --accent-subtle: rgba(149, 122, 134, 0.12); 55 - --accent-text: #5c495a; 56 - 57 - --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06); 58 - --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08); 59 - } 60 - } 61 - 62 - body.light { 63 - --bg-primary: #f8f8fa; 64 - --bg-secondary: #ffffff; 65 - --bg-tertiary: #f0f0f4; 66 - --bg-card: #ffffff; 67 - --bg-elevated: #ffffff; 68 - --bg-hover: #eeeef2; 69 - 70 - --text-primary: #18171c; 71 - --text-secondary: #5c495a; 72 - --text-tertiary: #8a8494; 73 - 74 - --border: rgba(92, 73, 90, 0.12); 75 - --border-hover: rgba(92, 73, 90, 0.22); 76 - 77 - --accent: #7a5f6d; 78 - --accent-hover: #664e5b; 79 - --accent-subtle: rgba(149, 122, 134, 0.12); 80 - --accent-text: #5c495a; 81 - 82 - --shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06); 83 - --shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08); 84 - } 85 - 86 - body.dark { 87 - --bg-primary: #0a0a0d; 88 - --bg-secondary: #121216; 89 - --bg-tertiary: #1a1a1f; 90 - --bg-card: #0f0f13; 91 - --bg-elevated: #18181d; 92 - --bg-hover: #1e1e24; 93 - 94 - --text-primary: #eaeaee; 95 - --text-secondary: #b7b6c5; 96 - --text-tertiary: #6e6d7a; 97 - 98 - --border: rgba(183, 182, 197, 0.12); 99 - --border-hover: rgba(183, 182, 197, 0.2); 100 - 101 - --accent: #957a86; 102 - --accent-hover: #a98d98; 103 - --accent-subtle: rgba(149, 122, 134, 0.15); 104 - --accent-text: #c4a8b2; 105 - 106 - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); 107 - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); 108 - } 109 - 110 - * { 111 - margin: 0; 112 - padding: 0; 113 - box-sizing: border-box; 114 - } 115 - 116 - body { 117 - font-family: 118 - "IBM Plex Sans", 119 - -apple-system, 120 - BlinkMacSystemFont, 121 - sans-serif; 122 - background: var(--bg-primary); 123 - color: var(--text-primary); 124 - min-height: 100vh; 125 - -webkit-font-smoothing: antialiased; 126 - } 127 - 128 - .sidebar { 129 - display: flex; 130 - flex-direction: column; 131 - height: 100vh; 132 - background: var(--bg-primary); 133 - } 134 - 135 - .sidebar-header { 136 - display: flex; 137 - align-items: center; 138 - justify-content: space-between; 139 - padding: 14px 16px; 140 - border-bottom: 1px solid var(--border); 141 - background: var(--bg-primary); 142 - } 143 - 144 - .sidebar-brand { 145 - display: flex; 146 - align-items: center; 147 - gap: 10px; 148 - } 149 - 150 - .sidebar-logo { 151 - color: var(--accent); 152 - } 153 - 154 - .sidebar-title { 155 - font-weight: 600; 156 - font-size: 15px; 157 - color: var(--text-primary); 158 - letter-spacing: -0.02em; 159 - } 160 - 161 - .user-info { 162 - display: flex; 163 - align-items: center; 164 - gap: 8px; 165 - } 166 - 167 - .user-handle { 168 - font-size: 12px; 169 - color: var(--text-secondary); 170 - background: var(--bg-tertiary); 171 - padding: 4px 10px; 172 - border-radius: var(--radius-full); 173 - } 174 - 175 - .current-page-info { 176 - display: flex; 177 - align-items: center; 178 - gap: 8px; 179 - padding: 10px 16px; 180 - background: var(--bg-primary); 181 - border-bottom: 1px solid var(--border); 182 - } 183 - 184 - .page-url { 185 - font-size: 12px; 186 - color: var(--text-tertiary); 187 - white-space: nowrap; 188 - overflow: hidden; 189 - text-overflow: ellipsis; 190 - } 191 - 192 - .sidebar-content { 193 - flex: 1; 194 - overflow-y: auto; 195 - display: flex; 196 - flex-direction: column; 197 - } 198 - 199 - .loading { 200 - display: flex; 201 - flex-direction: column; 202 - align-items: center; 203 - justify-content: center; 204 - height: 100%; 205 - color: var(--text-tertiary); 206 - gap: 12px; 207 - } 208 - 209 - .spinner { 210 - width: 20px; 211 - height: 20px; 212 - border: 2px solid var(--border); 213 - border-top-color: var(--accent); 214 - border-radius: 50%; 215 - animation: spin 1s linear infinite; 216 - } 217 - 218 - @keyframes spin { 219 - to { 220 - transform: rotate(360deg); 221 - } 222 - } 223 - 224 - .login-prompt { 225 - display: flex; 226 - flex-direction: column; 227 - align-items: center; 228 - justify-content: center; 229 - height: 100%; 230 - padding: 32px; 231 - text-align: center; 232 - gap: 20px; 233 - } 234 - 235 - .login-at-logo { 236 - font-size: 3.5rem; 237 - font-weight: 700; 238 - color: var(--accent); 239 - line-height: 1; 240 - } 241 - 242 - .login-title { 243 - font-size: 1rem; 244 - font-weight: 600; 245 - color: var(--text-primary); 246 - } 247 - 248 - .login-text { 249 - font-size: 13px; 250 - color: var(--text-secondary); 251 - line-height: 1.5; 252 - } 253 - 254 - .tabs { 255 - display: flex; 256 - border-bottom: 1px solid var(--border); 257 - background: var(--bg-primary); 258 - padding: 4px 8px; 259 - gap: 4px; 260 - margin: 0; 261 - } 262 - 263 - .tab-btn { 264 - flex: 1; 265 - padding: 10px 8px; 266 - background: transparent; 267 - border: none; 268 - font-size: 12px; 269 - font-weight: 500; 270 - color: var(--text-tertiary); 271 - cursor: pointer; 272 - border-radius: var(--radius-sm); 273 - transition: all 0.15s; 274 - } 275 - 276 - .tab-btn:hover { 277 - color: var(--text-secondary); 278 - background: var(--bg-hover); 279 - } 280 - 281 - .tab-btn.active { 282 - color: var(--text-primary); 283 - background: var(--bg-tertiary); 284 - } 285 - 286 - .tab-content { 287 - display: none; 288 - flex: 1; 289 - flex-direction: column; 290 - overflow-y: auto; 291 - } 292 - 293 - .tab-content.active { 294 - display: flex; 295 - } 296 - 297 - .quick-actions { 298 - display: flex; 299 - gap: 8px; 300 - padding: 12px 16px; 301 - border-bottom: 1px solid var(--border); 302 - background: var(--bg-primary); 303 - } 304 - 305 - .btn { 306 - padding: 10px 18px; 307 - border-radius: var(--radius-md); 308 - border: none; 309 - font-weight: 600; 310 - cursor: pointer; 311 - font-size: 13px; 312 - transition: all 0.15s; 313 - display: inline-flex; 314 - align-items: center; 315 - justify-content: center; 316 - gap: 8px; 317 - } 318 - 319 - .btn-small { 320 - padding: 8px 14px; 321 - font-size: 12px; 322 - } 323 - 324 - .btn-primary { 325 - background: var(--accent); 326 - color: white; 327 - } 328 - 329 - .btn-primary:hover { 330 - background: var(--accent-hover); 331 - } 332 - 333 - .btn-primary:disabled { 334 - opacity: 0.5; 335 - cursor: not-allowed; 336 - } 337 - 338 - .btn-secondary { 339 - background: var(--bg-tertiary); 340 - border: 1px solid var(--border); 341 - color: var(--text-primary); 342 - flex: 1; 343 - } 344 - 345 - .btn-secondary:hover { 346 - background: var(--bg-hover); 347 - border-color: var(--border-hover); 348 - } 349 - 350 - .btn-icon-text { 351 - flex: 1; 352 - } 353 - 354 - .btn-icon { 355 - background: none; 356 - border: none; 357 - color: var(--text-tertiary); 358 - cursor: pointer; 359 - padding: 6px; 360 - border-radius: var(--radius-sm); 361 - } 362 - 363 - .btn-icon:hover { 364 - color: var(--text-primary); 365 - background: var(--bg-hover); 366 - } 367 - 368 - .create-form { 369 - padding: 16px; 370 - border-bottom: 1px solid var(--border); 371 - background: var(--bg-primary); 372 - } 373 - 374 - .form-header { 375 - display: flex; 376 - justify-content: space-between; 377 - align-items: center; 378 - margin-bottom: 10px; 379 - } 380 - 381 - .form-title { 382 - font-size: 12px; 383 - font-weight: 600; 384 - color: var(--text-primary); 385 - letter-spacing: -0.01em; 386 - } 387 - 388 - .annotation-input { 389 - width: 100%; 390 - padding: 12px; 391 - border: 1px solid var(--border); 392 - border-radius: var(--radius-md); 393 - font-family: inherit; 394 - font-size: 13px; 395 - resize: none; 396 - margin-bottom: 10px; 397 - background: var(--bg-elevated); 398 - color: var(--text-primary); 399 - transition: border-color 0.15s; 400 - } 401 - 402 - .annotation-input::placeholder { 403 - color: var(--text-tertiary); 404 - } 405 - 406 - .annotation-input:focus { 407 - outline: none; 408 - border-color: var(--accent); 409 - } 410 - 411 - .form-actions { 412 - display: flex; 413 - justify-content: flex-end; 414 - } 415 - 416 - .quote-preview { 417 - margin-bottom: 12px; 418 - padding: 12px; 419 - background: var(--accent-subtle); 420 - border-left: 2px solid var(--accent); 421 - border-radius: var(--radius-sm); 422 - } 423 - 424 - .quote-preview-header { 425 - display: flex; 426 - justify-content: space-between; 427 - align-items: center; 428 - margin-bottom: 8px; 429 - font-size: 10px; 430 - font-weight: 600; 431 - text-transform: uppercase; 432 - letter-spacing: 0.5px; 433 - color: var(--accent-text); 434 - } 435 - 436 - .quote-preview-clear { 437 - background: none; 438 - border: none; 439 - color: var(--text-tertiary); 440 - font-size: 16px; 441 - cursor: pointer; 442 - padding: 0 4px; 443 - line-height: 1; 444 - } 445 - 446 - .quote-preview-clear:hover { 447 - color: var(--text-primary); 448 - } 449 - 450 - .quote-preview-text { 451 - font-size: 12px; 452 - font-style: italic; 453 - color: var(--text-secondary); 454 - line-height: 1.5; 455 - } 456 - 457 - .annotations-section { 458 - flex: 1; 459 - } 460 - 461 - .section-header { 462 - display: flex; 463 - justify-content: space-between; 464 - align-items: center; 465 - padding: 14px 16px; 466 - background: var(--bg-primary); 467 - border-bottom: 1px solid var(--border); 468 - } 469 - 470 - .section-title { 471 - font-size: 11px; 472 - font-weight: 600; 473 - text-transform: uppercase; 474 - color: var(--text-tertiary); 475 - letter-spacing: 0.5px; 476 - } 477 - 478 - .annotation-count { 479 - font-size: 11px; 480 - background: var(--bg-tertiary); 481 - padding: 3px 8px; 482 - border-radius: var(--radius-full); 483 - color: var(--text-secondary); 484 - } 485 - 486 - .annotations-list { 487 - display: flex; 488 - flex-direction: column; 489 - gap: 1px; 490 - background: var(--border); 491 - } 492 - 493 - .annotation-item { 494 - padding: 14px 16px; 495 - background: var(--bg-primary); 496 - transition: background 0.15s; 497 - } 498 - 499 - .annotation-item:hover { 500 - background: var(--bg-hover); 501 - } 502 - 503 - .annotation-item-header { 504 - display: flex; 505 - align-items: center; 506 - margin-bottom: 8px; 507 - gap: 10px; 508 - } 509 - 510 - .annotation-item-avatar { 511 - width: 26px; 512 - height: 26px; 513 - border-radius: 50%; 514 - background: var(--accent); 515 - color: var(--bg-primary); 516 - display: flex; 517 - align-items: center; 518 - justify-content: center; 519 - font-size: 10px; 520 - font-weight: 600; 521 - } 522 - 523 - .annotation-item-meta { 524 - flex: 1; 525 - } 526 - 527 - .annotation-item-author { 528 - font-size: 12px; 529 - font-weight: 600; 530 - color: var(--text-primary); 531 - } 532 - 533 - .annotation-item-time { 534 - font-size: 11px; 535 - color: var(--text-tertiary); 536 - } 537 - 538 - .annotation-type-badge { 539 - font-size: 10px; 540 - padding: 3px 8px; 541 - border-radius: var(--radius-full); 542 - font-weight: 500; 543 - } 544 - 545 - .annotation-type-badge.highlight { 546 - background: var(--accent-subtle); 547 - color: var(--accent-text); 548 - } 549 - 550 - .annotation-item-quote { 551 - padding: 8px 12px; 552 - border-left: 2px solid var(--accent); 553 - margin-bottom: 8px; 554 - font-size: 12px; 555 - color: var(--text-secondary); 556 - font-style: italic; 557 - background: var(--accent-subtle); 558 - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; 559 - } 560 - 561 - .annotation-item-text { 562 - font-size: 13px; 563 - line-height: 1.5; 564 - color: var(--text-primary); 565 - } 566 - 567 - .bookmarks-list { 568 - display: flex; 569 - flex-direction: column; 570 - gap: 1px; 571 - background: var(--border); 572 - } 573 - 574 - .bookmark-item { 575 - padding: 14px 16px; 576 - background: var(--bg-primary); 577 - text-decoration: none; 578 - color: inherit; 579 - display: block; 580 - transition: background 0.15s; 581 - } 582 - 583 - .bookmark-item:hover { 584 - background: var(--bg-hover); 585 - } 586 - 587 - .bookmark-title { 588 - font-size: 13px; 589 - font-weight: 500; 590 - margin-bottom: 4px; 591 - white-space: nowrap; 592 - overflow: hidden; 593 - text-overflow: ellipsis; 594 - color: var(--text-primary); 595 - } 596 - 597 - .bookmark-url { 598 - font-size: 11px; 599 - color: var(--text-tertiary); 600 - white-space: nowrap; 601 - overflow: hidden; 602 - text-overflow: ellipsis; 603 - } 604 - 605 - .empty-state { 606 - display: flex; 607 - flex-direction: column; 608 - align-items: center; 609 - justify-content: center; 610 - padding: 40px 16px; 611 - text-align: center; 612 - color: var(--text-tertiary); 613 - } 614 - 615 - .empty-icon { 616 - margin-bottom: 12px; 617 - color: var(--text-tertiary); 618 - opacity: 0.4; 619 - } 620 - 621 - .empty-text { 622 - font-size: 13px; 623 - color: var(--text-secondary); 624 - margin-bottom: 4px; 625 - } 626 - 627 - .empty-hint { 628 - font-size: 12px; 629 - color: var(--text-tertiary); 630 - } 631 - 632 - .sidebar-footer { 633 - display: flex; 634 - align-items: center; 635 - justify-content: space-between; 636 - padding: 12px 16px; 637 - border-top: 1px solid var(--border); 638 - background: var(--bg-primary); 639 - } 640 - 641 - .sidebar-link { 642 - font-size: 12px; 643 - color: var(--text-tertiary); 644 - text-decoration: none; 645 - } 646 - 647 - .sidebar-link:hover { 648 - color: var(--accent-text); 649 - } 650 - 651 - .settings-view { 652 - position: absolute; 653 - top: 0; 654 - left: 0; 655 - width: 100%; 656 - height: 100%; 657 - background: var(--bg-primary); 658 - z-index: 20; 659 - display: flex; 660 - flex-direction: column; 661 - padding: 16px; 662 - } 663 - 664 - .settings-header { 665 - display: flex; 666 - justify-content: space-between; 667 - align-items: center; 668 - margin-bottom: 24px; 669 - color: var(--text-primary); 670 - } 671 - 672 - .settings-title { 673 - font-size: 18px; 674 - font-weight: 600; 675 - } 676 - 677 - .setting-item { 678 - margin-bottom: 20px; 679 - } 680 - 681 - .setting-item label { 682 - font-size: 13px; 683 - font-weight: 500; 684 - color: var(--text-primary); 685 - margin-bottom: 6px; 686 - display: block; 687 - } 688 - 689 - .settings-input { 690 - width: 100%; 691 - padding: 12px; 692 - border: 1px solid var(--border); 693 - border-radius: var(--radius-md); 694 - font-family: inherit; 695 - font-size: 13px; 696 - background: var(--bg-elevated); 697 - color: var(--text-primary); 698 - transition: border-color 0.15s; 699 - } 700 - 701 - .settings-input:focus { 702 - outline: none; 703 - border-color: var(--accent); 704 - } 705 - 706 - .setting-help { 707 - font-size: 11px; 708 - color: var(--text-tertiary); 709 - margin-top: 4px; 710 - } 711 - 712 - .scroll-to-btn { 713 - display: inline-flex; 714 - align-items: center; 715 - gap: 4px; 716 - padding: 6px 10px; 717 - font-size: 11px; 718 - color: var(--accent-text); 719 - background: var(--accent-subtle); 720 - border: none; 721 - border-radius: var(--radius-sm); 722 - cursor: pointer; 723 - margin-top: 8px; 724 - transition: all 0.15s; 725 - } 726 - 727 - .scroll-to-btn:hover { 728 - background: rgba(149, 122, 134, 0.25); 729 - } 730 - 731 - ::-webkit-scrollbar { 732 - width: 8px; 733 - } 734 - 735 - ::-webkit-scrollbar-track { 736 - background: transparent; 737 - } 738 - 739 - ::-webkit-scrollbar-thumb { 740 - background: var(--bg-hover); 741 - border-radius: var(--radius-full); 742 - } 743 - 744 - ::-webkit-scrollbar-thumb:hover { 745 - background: var(--text-tertiary); 746 - } 747 - 748 - .collection-selector { 749 - position: absolute; 750 - top: 0; 751 - left: 0; 752 - width: 100%; 753 - height: 100%; 754 - background: var(--bg-primary); 755 - z-index: 30; 756 - display: flex; 757 - flex-direction: column; 758 - padding: 16px; 759 - } 760 - 761 - .collection-list { 762 - display: flex; 763 - flex-direction: column; 764 - gap: 8px; 765 - overflow-y: auto; 766 - flex: 1; 767 - } 768 - 769 - .collection-select-btn { 770 - display: flex; 771 - align-items: center; 772 - gap: 12px; 773 - padding: 12px; 774 - background: var(--bg-primary); 775 - border: 1px solid var(--border); 776 - border-radius: var(--radius-md); 777 - color: var(--text-primary); 778 - font-size: 14px; 779 - cursor: pointer; 780 - text-align: left; 781 - transition: all 0.15s; 782 - } 783 - 784 - .collection-select-btn:hover { 785 - border-color: var(--accent); 786 - background: var(--bg-hover); 787 - } 788 - 789 - .collection-select-btn:disabled { 790 - opacity: 0.6; 791 - cursor: not-allowed; 792 - } 793 - 794 - .annotation-item-actions { 795 - display: flex; 796 - align-items: center; 797 - gap: 8px; 798 - margin-left: auto; 799 - } 800 - 801 - .toggle-switch { 802 - position: relative; 803 - display: inline-block; 804 - width: 40px; 805 - height: 22px; 806 - flex-shrink: 0; 807 - } 808 - 809 - .toggle-switch input { 810 - opacity: 0; 811 - width: 0; 812 - height: 0; 813 - } 814 - 815 - .toggle-slider { 816 - position: absolute; 817 - cursor: pointer; 818 - top: 0; 819 - left: 0; 820 - right: 0; 821 - bottom: 0; 822 - background-color: var(--bg-tertiary); 823 - transition: 0.2s; 824 - border-radius: 22px; 825 - } 826 - 827 - .toggle-slider:before { 828 - position: absolute; 829 - content: ""; 830 - height: 16px; 831 - width: 16px; 832 - left: 3px; 833 - bottom: 3px; 834 - background-color: var(--text-tertiary); 835 - transition: 0.2s; 836 - border-radius: 50%; 837 - } 838 - 839 - .toggle-switch input:checked + .toggle-slider { 840 - background-color: var(--accent); 841 - } 842 - 843 - .toggle-switch input:checked + .toggle-slider:before { 844 - transform: translateX(18px); 845 - background-color: white; 846 - } 847 - 848 - .theme-toggle-group { 849 - display: flex; 850 - background: var(--bg-tertiary); 851 - padding: 3px; 852 - border-radius: var(--radius-md); 853 - gap: 2px; 854 - margin-top: 8px; 855 - } 856 - 857 - .theme-btn { 858 - flex: 1; 859 - padding: 6px; 860 - border: none; 861 - background: transparent; 862 - color: var(--text-tertiary); 863 - font-size: 12px; 864 - font-weight: 500; 865 - border-radius: var(--radius-sm); 866 - cursor: pointer; 867 - transition: all 0.15s ease; 868 - } 869 - 870 - .theme-btn:hover { 871 - color: var(--text-secondary); 872 - } 873 - 874 - .theme-btn.active { 875 - background: var(--bg-primary); 876 - color: var(--text-primary); 877 - }
-347
extension/sidepanel/sidepanel.html
··· 1 - <!doctype html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>Margin</title> 7 - <link rel="preconnect" href="https://fonts.googleapis.com" /> 8 - <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 9 - <link 10 - href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" 11 - rel="stylesheet" 12 - /> 13 - <link rel="stylesheet" href="sidepanel.css" /> 14 - </head> 15 - 16 - <body> 17 - <div class="sidebar"> 18 - <header class="sidebar-header"> 19 - <div class="sidebar-brand"> 20 - <span class="sidebar-logo"> 21 - <img src="../icons/logo.svg" alt="Margin" width="20" height="20" /> 22 - </span> 23 - <span class="sidebar-title">Margin</span> 24 - </div> 25 - <div id="user-info" class="user-info" style="display: none"> 26 - <span id="user-handle" class="user-handle"></span> 27 - </div> 28 - </header> 29 - 30 - <div id="current-page-info" class="current-page-info"> 31 - <div class="page-favicon"></div> 32 - <span id="current-page-url" class="page-url">Loading...</span> 33 - </div> 34 - 35 - <div class="sidebar-content"> 36 - <div id="loading" class="loading"> 37 - <div class="spinner"></div> 38 - <span>Loading...</span> 39 - </div> 40 - 41 - <div id="login-prompt" class="login-prompt" style="display: none"> 42 - <span class="login-at-logo">@</span> 43 - <h2 class="login-title">Sign in with AT Protocol</h2> 44 - <p class="login-text"> 45 - Connect your Bluesky account to annotate, highlight, and bookmark 46 - the web. 47 - </p> 48 - <button id="sign-in" class="btn btn-primary">Sign In</button> 49 - </div> 50 - 51 - <div id="main-content" style="display: none"> 52 - <div class="tabs"> 53 - <button class="tab-btn active" data-tab="page">This Page</button> 54 - <button class="tab-btn" data-tab="highlights">Highlights</button> 55 - <button class="tab-btn" data-tab="bookmarks">Bookmarks</button> 56 - </div> 57 - 58 - <div id="tab-page" class="tab-content active"> 59 - <div class="quick-actions"> 60 - <button 61 - id="bookmark-page" 62 - class="btn btn-secondary btn-icon-text" 63 - title="Bookmark this page" 64 - > 65 - <svg 66 - width="14" 67 - height="14" 68 - viewBox="0 0 24 24" 69 - fill="none" 70 - stroke="currentColor" 71 - stroke-width="2" 72 - stroke-linecap="round" 73 - stroke-linejoin="round" 74 - > 75 - <path 76 - d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" 77 - ></path> 78 - </svg> 79 - Bookmark 80 - </button> 81 - <button 82 - id="refresh-annotations" 83 - class="btn btn-secondary btn-icon-text" 84 - title="Refresh" 85 - > 86 - <svg 87 - width="14" 88 - height="14" 89 - viewBox="0 0 24 24" 90 - fill="none" 91 - stroke="currentColor" 92 - stroke-width="2" 93 - stroke-linecap="round" 94 - stroke-linejoin="round" 95 - > 96 - <path 97 - d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" 98 - ></path> 99 - <path d="M3 3v5h5"></path> 100 - <path 101 - d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" 102 - ></path> 103 - <path d="M16 21h5v-5"></path> 104 - </svg> 105 - Refresh 106 - </button> 107 - </div> 108 - 109 - <div class="create-form"> 110 - <div class="form-header"> 111 - <span class="form-title">New Annotation</span> 112 - </div> 113 - <textarea 114 - id="annotation-text" 115 - class="annotation-input" 116 - placeholder="Write your annotation..." 117 - rows="3" 118 - ></textarea> 119 - <div class="form-actions"> 120 - <button 121 - id="submit-annotation" 122 - class="btn btn-primary btn-small" 123 - > 124 - Post 125 - </button> 126 - </div> 127 - </div> 128 - 129 - <div class="annotations-section"> 130 - <div class="section-header"> 131 - <span class="section-title">Annotations on this page</span> 132 - <span id="annotation-count" class="annotation-count">0</span> 133 - </div> 134 - <div id="annotations" class="annotations-list"></div> 135 - <div id="empty" class="empty-state" style="display: none"> 136 - <span class="empty-icon"> 137 - <svg 138 - width="32" 139 - height="32" 140 - viewBox="0 0 24 24" 141 - fill="none" 142 - stroke="currentColor" 143 - stroke-width="2" 144 - stroke-linecap="round" 145 - stroke-linejoin="round" 146 - > 147 - <path d="M22 12h-6l-2 3h-4l-2-3H2"></path> 148 - <path 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 - ></path> 151 - </svg> 152 - </span> 153 - <p class="empty-text">No annotations yet</p> 154 - <p class="empty-hint"> 155 - Select text and right-click to annotate or highlight 156 - </p> 157 - </div> 158 - </div> 159 - </div> 160 - 161 - <div id="tab-highlights" class="tab-content"> 162 - <div class="section-header"> 163 - <span class="section-title">My Highlights</span> 164 - </div> 165 - <div id="highlights-list" class="annotations-list"></div> 166 - <div 167 - id="highlights-empty" 168 - class="empty-state" 169 - style="display: none" 170 - > 171 - <span class="empty-icon"> 172 - <svg 173 - width="32" 174 - height="32" 175 - viewBox="0 0 24 24" 176 - fill="none" 177 - stroke="currentColor" 178 - stroke-width="2" 179 - stroke-linecap="round" 180 - stroke-linejoin="round" 181 - > 182 - <path d="m9 11-6 6v3h9l3-3"></path> 183 - <path 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 - ></path> 186 - </svg> 187 - </span> 188 - <p class="empty-text">No highlights yet</p> 189 - <p class="empty-hint"> 190 - Select text on any page and right-click → Highlight 191 - </p> 192 - </div> 193 - </div> 194 - 195 - <div id="tab-bookmarks" class="tab-content"> 196 - <div class="section-header"> 197 - <span class="section-title">My Bookmarks</span> 198 - </div> 199 - <div id="bookmarks-list" class="bookmarks-list"></div> 200 - <div id="bookmarks-empty" class="empty-state" style="display: none"> 201 - <span class="empty-icon"> 202 - <svg 203 - width="32" 204 - height="32" 205 - viewBox="0 0 24 24" 206 - fill="none" 207 - stroke="currentColor" 208 - stroke-width="2" 209 - stroke-linecap="round" 210 - stroke-linejoin="round" 211 - > 212 - <path 213 - d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" 214 - ></path> 215 - </svg> 216 - </span> 217 - <p class="empty-text">No bookmarks yet</p> 218 - <p class="empty-hint"> 219 - Right-click on any page → Bookmark this page 220 - </p> 221 - </div> 222 - </div> 223 - </div> 224 - </div> 225 - 226 - <footer class="sidebar-footer"> 227 - <a href="#" id="open-web" class="sidebar-link">Open Margin Web</a> 228 - <button id="toggle-settings" class="btn-icon" title="Settings"> 229 - <svg 230 - width="16" 231 - height="16" 232 - viewBox="0 0 24 24" 233 - fill="none" 234 - stroke="currentColor" 235 - stroke-width="2" 236 - stroke-linecap="round" 237 - stroke-linejoin="round" 238 - > 239 - <circle cx="12" cy="12" r="3"></circle> 240 - <path 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 - ></path> 243 - </svg> 244 - </button> 245 - </footer> 246 - 247 - <div id="settings-view" class="settings-view" style="display: none"> 248 - <div class="settings-header"> 249 - <h3 class="settings-title">Settings</h3> 250 - <button id="close-settings" class="btn-icon">×</button> 251 - </div> 252 - <div class="setting-item"> 253 - <div 254 - style=" 255 - display: flex; 256 - justify-content: space-between; 257 - align-items: center; 258 - " 259 - > 260 - <div> 261 - <label>Show page overlays</label> 262 - <p class="setting-help" style="margin-top: 2px"> 263 - Display highlights, badges, and tooltips on pages 264 - </p> 265 - </div> 266 - <label class="toggle-switch"> 267 - <input type="checkbox" id="overlay-toggle" checked /> 268 - <span class="toggle-slider"></span> 269 - </label> 270 - </div> 271 - </div> 272 - <div class="setting-item"> 273 - <label for="api-url">API URL</label> 274 - <input 275 - type="url" 276 - id="api-url" 277 - placeholder="http://localhost:8080" 278 - class="settings-input" 279 - /> 280 - <p class="setting-help">Enter your Margin backend URL</p> 281 - </div> 282 - <div class="setting-item"> 283 - <label for="theme-select">Theme</label> 284 - <div class="theme-toggle-group"> 285 - <button class="theme-btn active" data-theme="system">Auto</button> 286 - <button class="theme-btn" data-theme="light">Light</button> 287 - <button class="theme-btn" data-theme="dark">Dark</button> 288 - </div> 289 - </div> 290 - <button id="save-settings" class="btn btn-primary" style="width: 100%"> 291 - Save 292 - </button> 293 - <button 294 - id="sign-out" 295 - class="btn btn-secondary" 296 - style="width: 100%; margin-top: 8px" 297 - > 298 - Sign Out 299 - </button> 300 - </div> 301 - 302 - <div 303 - id="collection-selector" 304 - class="collection-selector" 305 - style="display: none" 306 - > 307 - <div 308 - class="selector-header" 309 - style=" 310 - display: flex; 311 - justify-content: space-between; 312 - align-items: center; 313 - margin-bottom: 12px; 314 - padding-bottom: 8px; 315 - border-bottom: 1px solid var(--border); 316 - " 317 - > 318 - <h3 class="selector-title" style="margin: 0; font-size: 14px"> 319 - Add to Collection 320 - </h3> 321 - <button id="close-collection-selector" class="btn-icon">×</button> 322 - </div> 323 - <div 324 - id="collection-loading" 325 - class="spinner" 326 - style="display: none; margin: 20px auto" 327 - ></div> 328 - <div 329 - id="collection-list" 330 - class="collection-list" 331 - style=" 332 - display: flex; 333 - flex-direction: column; 334 - gap: 4px; 335 - max-height: 200px; 336 - overflow-y: auto; 337 - " 338 - ></div> 339 - <div id="collections-empty" class="empty-state" style="display: none"> 340 - <p class="empty-text">No collections found</p> 341 - </div> 342 - </div> 343 - </div> 344 - 345 - <script src="sidepanel.js"></script> 346 - </body> 347 - </html>
-973
extension/sidepanel/sidepanel.js
··· 1 - document.addEventListener("DOMContentLoaded", async () => { 2 - const views = { 3 - loading: document.getElementById("loading"), 4 - login: document.getElementById("login-prompt"), 5 - main: document.getElementById("main-content"), 6 - settings: document.getElementById("settings-view"), 7 - collectionSelector: document.getElementById("collection-selector"), 8 - }; 9 - 10 - const els = { 11 - userHandle: document.getElementById("user-handle"), 12 - userInfo: document.getElementById("user-info"), 13 - signInBtn: document.getElementById("sign-in"), 14 - openWebBtn: document.getElementById("open-web"), 15 - submitBtn: document.getElementById("submit-annotation"), 16 - textInput: document.getElementById("annotation-text"), 17 - currentPageUrl: document.getElementById("current-page-url"), 18 - annotationsList: document.getElementById("annotations"), 19 - annotationCount: document.getElementById("annotation-count"), 20 - emptyState: document.getElementById("empty"), 21 - toggleSettings: document.getElementById("toggle-settings"), 22 - closeSettings: document.getElementById("close-settings"), 23 - saveSettings: document.getElementById("save-settings"), 24 - signOutBtn: document.getElementById("sign-out"), 25 - apiUrlInput: document.getElementById("api-url"), 26 - bookmarkBtn: document.getElementById("bookmark-page"), 27 - refreshBtn: document.getElementById("refresh-annotations"), 28 - tabs: document.querySelectorAll(".tab-btn"), 29 - tabContents: document.querySelectorAll(".tab-content"), 30 - bookmarksList: document.getElementById("bookmarks-list"), 31 - bookmarksEmpty: document.getElementById("bookmarks-empty"), 32 - highlightsList: document.getElementById("highlights-list"), 33 - highlightsEmpty: document.getElementById("highlights-empty"), 34 - closeCollectionSelector: document.getElementById( 35 - "close-collection-selector", 36 - ), 37 - collectionList: document.getElementById("collection-list"), 38 - collectionLoading: document.getElementById("collection-loading"), 39 - collectionsEmpty: document.getElementById("collections-empty"), 40 - overlayToggle: document.getElementById("overlay-toggle"), 41 - }; 42 - 43 - let currentTab = null; 44 - let apiUrl = ""; 45 - let currentUserDid = null; 46 - let pendingSelector = null; 47 - 48 - const storage = await chrome.storage.local.get(["apiUrl"]); 49 - if (storage.apiUrl) { 50 - apiUrl = storage.apiUrl; 51 - } 52 - 53 - els.apiUrlInput.value = apiUrl; 54 - 55 - const overlayStorage = await chrome.storage.local.get(["showOverlay"]); 56 - if (els.overlayToggle) { 57 - els.overlayToggle.checked = overlayStorage.showOverlay !== false; 58 - } 59 - 60 - chrome.storage.onChanged.addListener((changes, area) => { 61 - if (area === "local") { 62 - if (changes.apiUrl) { 63 - apiUrl = changes.apiUrl.newValue || ""; 64 - els.apiUrlInput.value = apiUrl; 65 - checkSession(); 66 - } 67 - if (changes.theme) { 68 - const newTheme = changes.theme.newValue || "system"; 69 - applyTheme(newTheme); 70 - updateThemeUI(newTheme); 71 - } 72 - } 73 - }); 74 - 75 - chrome.storage.local.get(["theme"], (result) => { 76 - const currentTheme = result.theme || "system"; 77 - applyTheme(currentTheme); 78 - updateThemeUI(currentTheme); 79 - }); 80 - 81 - const themeBtns = document.querySelectorAll(".theme-btn"); 82 - themeBtns.forEach((btn) => { 83 - btn.addEventListener("click", () => { 84 - const theme = btn.getAttribute("data-theme"); 85 - chrome.storage.local.set({ theme }); 86 - applyTheme(theme); 87 - updateThemeUI(theme); 88 - }); 89 - }); 90 - 91 - try { 92 - const [tab] = await chrome.tabs.query({ 93 - active: true, 94 - currentWindow: true, 95 - }); 96 - currentTab = tab; 97 - if (els.currentPageUrl) { 98 - try { 99 - els.currentPageUrl.textContent = new URL(tab.url).hostname; 100 - } catch { 101 - els.currentPageUrl.textContent = tab.url; 102 - } 103 - } 104 - 105 - let pendingData = null; 106 - if (chrome.storage.session) { 107 - const sessionData = await chrome.storage.session.get([ 108 - "pendingAnnotation", 109 - ]); 110 - if (sessionData.pendingAnnotation) { 111 - pendingData = sessionData.pendingAnnotation; 112 - await chrome.storage.session.remove(["pendingAnnotation"]); 113 - } 114 - } 115 - 116 - if (!pendingData) { 117 - const localData = await chrome.storage.local.get([ 118 - "pendingAnnotation", 119 - "pendingAnnotationExpiry", 120 - ]); 121 - if ( 122 - localData.pendingAnnotation && 123 - localData.pendingAnnotationExpiry > Date.now() 124 - ) { 125 - pendingData = localData.pendingAnnotation; 126 - } 127 - await chrome.storage.local.remove([ 128 - "pendingAnnotation", 129 - "pendingAnnotationExpiry", 130 - ]); 131 - } 132 - 133 - if (pendingData?.selector) { 134 - pendingSelector = pendingData.selector; 135 - showQuotePreview(pendingSelector); 136 - } 137 - 138 - checkSession(); 139 - } catch (err) { 140 - console.error("Init error:", err); 141 - showView("login"); 142 - } 143 - 144 - chrome.tabs.onActivated.addListener(async (activeInfo) => { 145 - try { 146 - const tab = await chrome.tabs.get(activeInfo.tabId); 147 - currentTab = tab; 148 - if (els.currentPageUrl) { 149 - try { 150 - els.currentPageUrl.textContent = new URL(tab.url).hostname; 151 - } catch { 152 - els.currentPageUrl.textContent = tab.url; 153 - } 154 - } 155 - loadAnnotations(); 156 - } catch (err) { 157 - console.error("Tab change error:", err); 158 - } 159 - }); 160 - 161 - chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 162 - if (currentTab && tabId === currentTab.id && changeInfo.url) { 163 - currentTab = tab; 164 - if (els.currentPageUrl) { 165 - try { 166 - els.currentPageUrl.textContent = new URL(tab.url).hostname; 167 - } catch { 168 - els.currentPageUrl.textContent = tab.url; 169 - } 170 - } 171 - loadAnnotations(); 172 - } 173 - }); 174 - 175 - els.signInBtn?.addEventListener("click", () => { 176 - chrome.runtime.sendMessage({ type: "OPEN_LOGIN" }); 177 - }); 178 - 179 - els.openWebBtn?.addEventListener("click", (e) => { 180 - e.preventDefault(); 181 - chrome.runtime.sendMessage({ type: "OPEN_WEB" }); 182 - }); 183 - 184 - els.tabs.forEach((btn) => { 185 - btn.addEventListener("click", () => { 186 - els.tabs.forEach((t) => t.classList.remove("active")); 187 - els.tabContents.forEach((c) => c.classList.remove("active")); 188 - const tabId = btn.getAttribute("data-tab"); 189 - btn.classList.add("active"); 190 - document.getElementById(`tab-${tabId}`).classList.add("active"); 191 - if (tabId === "bookmarks") loadBookmarks(); 192 - if (tabId === "highlights") loadHighlights(); 193 - }); 194 - }); 195 - 196 - els.submitBtn?.addEventListener("click", async () => { 197 - const text = els.textInput.value.trim(); 198 - if (!text) return; 199 - 200 - els.submitBtn.disabled = true; 201 - els.submitBtn.textContent = "Posting..."; 202 - 203 - try { 204 - const annotationData = { 205 - url: currentTab.url, 206 - text: text, 207 - title: currentTab.title, 208 - }; 209 - 210 - if (pendingSelector) { 211 - annotationData.selector = pendingSelector; 212 - } 213 - 214 - const res = await sendMessage({ 215 - type: "CREATE_ANNOTATION", 216 - data: annotationData, 217 - }); 218 - 219 - if (res.success) { 220 - els.textInput.value = ""; 221 - pendingSelector = null; 222 - hideQuotePreview(); 223 - loadAnnotations(); 224 - } else { 225 - alert("Failed to post annotation: " + (res.error || "Unknown error")); 226 - } 227 - } catch (err) { 228 - console.error("Post error:", err); 229 - alert("Error posting annotation"); 230 - } finally { 231 - els.submitBtn.disabled = false; 232 - els.submitBtn.textContent = "Post"; 233 - } 234 - }); 235 - 236 - els.bookmarkBtn?.addEventListener("click", async () => { 237 - els.bookmarkBtn.disabled = true; 238 - const originalText = els.bookmarkBtn.textContent; 239 - els.bookmarkBtn.textContent = "Saving..."; 240 - 241 - try { 242 - const res = await sendMessage({ 243 - type: "CREATE_BOOKMARK", 244 - data: { 245 - url: currentTab.url, 246 - title: currentTab.title, 247 - }, 248 - }); 249 - 250 - if (res.success) { 251 - els.bookmarkBtn.textContent = "✓ Bookmarked"; 252 - setTimeout(() => { 253 - els.bookmarkBtn.textContent = originalText; 254 - els.bookmarkBtn.disabled = false; 255 - }, 2000); 256 - } else { 257 - alert("Failed to bookmark page: " + (res.error || "Unknown error")); 258 - els.bookmarkBtn.textContent = originalText; 259 - els.bookmarkBtn.disabled = false; 260 - } 261 - } catch (err) { 262 - console.error("Bookmark error:", err); 263 - alert("Error bookmarking page"); 264 - els.bookmarkBtn.textContent = originalText; 265 - els.bookmarkBtn.disabled = false; 266 - } 267 - }); 268 - 269 - els.refreshBtn?.addEventListener("click", () => { 270 - loadAnnotations(); 271 - }); 272 - 273 - els.toggleSettings?.addEventListener("click", () => { 274 - views.settings.style.display = "flex"; 275 - }); 276 - 277 - els.closeSettings?.addEventListener("click", () => { 278 - views.settings.style.display = "none"; 279 - }); 280 - 281 - els.closeCollectionSelector?.addEventListener("click", () => { 282 - views.collectionSelector.style.display = "none"; 283 - }); 284 - 285 - els.saveSettings?.addEventListener("click", async () => { 286 - const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); 287 - const showOverlay = els.overlayToggle?.checked ?? true; 288 - 289 - await chrome.storage.local.set({ 290 - apiUrl: newUrl, 291 - showOverlay, 292 - }); 293 - if (newUrl) { 294 - apiUrl = newUrl; 295 - } 296 - await sendMessage({ type: "UPDATE_SETTINGS" }); 297 - 298 - const tabs = await chrome.tabs.query({}); 299 - for (const tab of tabs) { 300 - if (tab.id) { 301 - try { 302 - await chrome.tabs.sendMessage(tab.id, { 303 - type: "UPDATE_OVERLAY_VISIBILITY", 304 - show: showOverlay, 305 - }); 306 - } catch { 307 - /* ignore */ 308 - } 309 - } 310 - } 311 - 312 - views.settings.style.display = "none"; 313 - checkSession(); 314 - }); 315 - 316 - els.signOutBtn?.addEventListener("click", async () => { 317 - if (apiUrl) { 318 - await chrome.cookies.remove({ 319 - url: apiUrl, 320 - name: "margin_session", 321 - }); 322 - } 323 - views.settings.style.display = "none"; 324 - showView("login"); 325 - els.userInfo.style.display = "none"; 326 - }); 327 - 328 - async function checkSession() { 329 - showView("loading"); 330 - try { 331 - const res = await sendMessage({ type: "CHECK_SESSION" }); 332 - 333 - if (res.success && res.data?.authenticated) { 334 - if (els.userHandle) els.userHandle.textContent = "@" + res.data.handle; 335 - els.userInfo.style.display = "flex"; 336 - currentUserDid = res.data.did; 337 - showView("main"); 338 - loadAnnotations(); 339 - } else { 340 - els.userInfo.style.display = "none"; 341 - showView("login"); 342 - } 343 - } catch (err) { 344 - console.error("Session check error:", err); 345 - els.userInfo.style.display = "none"; 346 - showView("login"); 347 - } 348 - } 349 - 350 - async function loadAnnotations() { 351 - if (!currentTab?.url) return; 352 - 353 - try { 354 - const res = await sendMessage({ 355 - type: "GET_ANNOTATIONS", 356 - data: { url: currentTab.url }, 357 - }); 358 - 359 - if (res.success) { 360 - if (currentUserDid) { 361 - const isBookmarked = res.data.some( 362 - (item) => 363 - item.type === "Bookmark" && item.creator.did === currentUserDid, 364 - ); 365 - if (els.bookmarkBtn) { 366 - if (isBookmarked) { 367 - els.bookmarkBtn.textContent = "✓ Bookmarked"; 368 - els.bookmarkBtn.disabled = true; 369 - } else { 370 - els.bookmarkBtn.textContent = "Bookmark Page"; 371 - els.bookmarkBtn.disabled = false; 372 - } 373 - } 374 - } 375 - 376 - const listItems = res.data.filter((item) => item.type !== "Bookmark"); 377 - renderAnnotations(listItems); 378 - } 379 - } catch (err) { 380 - console.error("Load annotations error:", err); 381 - } 382 - } 383 - 384 - async function loadBookmarks() { 385 - if (!currentUserDid) return; 386 - els.bookmarksList.innerHTML = 387 - '<div class="loading"><div class="spinner"></div></div>'; 388 - els.bookmarksEmpty.style.display = "none"; 389 - 390 - try { 391 - const res = await sendMessage({ 392 - type: "GET_USER_BOOKMARKS", 393 - data: { did: currentUserDid }, 394 - }); 395 - 396 - if (res.success) { 397 - renderBookmarks(res.data); 398 - } 399 - } catch (err) { 400 - console.error("Load bookmarks error:", err); 401 - els.bookmarksList.innerHTML = 402 - '<p style="color: #ef4444; text-align: center;">Failed to load bookmarks</p>'; 403 - } 404 - } 405 - 406 - async function loadHighlights() { 407 - if (!currentUserDid) return; 408 - els.highlightsList.innerHTML = 409 - '<div class="loading"><div class="spinner"></div></div>'; 410 - els.highlightsEmpty.style.display = "none"; 411 - 412 - try { 413 - const res = await sendMessage({ 414 - type: "GET_USER_HIGHLIGHTS", 415 - data: { did: currentUserDid }, 416 - }); 417 - 418 - if (res.success) { 419 - renderHighlights(res.data); 420 - } 421 - } catch (err) { 422 - console.error("Load highlights error:", err); 423 - els.highlightsList.innerHTML = 424 - '<p style="color: #ef4444; text-align: center;">Failed to load highlights</p>'; 425 - } 426 - } 427 - 428 - async function openCollectionSelector(annotationUri) { 429 - if (!currentUserDid) { 430 - console.error("No currentUserDid, returning early"); 431 - return; 432 - } 433 - views.collectionSelector.style.display = "flex"; 434 - els.collectionList.innerHTML = ""; 435 - els.collectionLoading.style.display = "block"; 436 - els.collectionsEmpty.style.display = "none"; 437 - 438 - try { 439 - const [collectionsRes, containingRes] = await Promise.all([ 440 - sendMessage({ 441 - type: "GET_USER_COLLECTIONS", 442 - data: { did: currentUserDid }, 443 - }), 444 - sendMessage({ 445 - type: "GET_CONTAINING_COLLECTIONS", 446 - data: { uri: annotationUri }, 447 - }), 448 - ]); 449 - 450 - if (collectionsRes.success) { 451 - const containingUris = containingRes.success 452 - ? new Set(containingRes.data) 453 - : new Set(); 454 - renderCollectionList( 455 - collectionsRes.data, 456 - annotationUri, 457 - containingUris, 458 - ); 459 - } 460 - } catch (err) { 461 - console.error("Load collections error:", err); 462 - els.collectionList.innerHTML = 463 - '<p class="error">Failed to load collections</p>'; 464 - } finally { 465 - els.collectionLoading.style.display = "none"; 466 - } 467 - } 468 - 469 - function renderCollectionList( 470 - items, 471 - annotationUri, 472 - containingUris = new Set(), 473 - ) { 474 - els.collectionList.innerHTML = ""; 475 - els.collectionList.dataset.annotationUri = annotationUri; 476 - 477 - if (!items || items.length === 0) { 478 - els.collectionsEmpty.style.display = "block"; 479 - return; 480 - } 481 - 482 - items.forEach((collection) => { 483 - const btn = document.createElement("button"); 484 - btn.className = "collection-select-btn"; 485 - const isAdded = containingUris.has(collection.uri); 486 - 487 - const icon = document.createElement("span"); 488 - if (isAdded) { 489 - icon.textContent = "✓"; 490 - } else { 491 - icon.innerHTML = 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 - } 494 - btn.appendChild(icon); 495 - 496 - const name = document.createElement("span"); 497 - name.textContent = collection.name; 498 - btn.appendChild(name); 499 - 500 - if (isAdded) { 501 - btn.classList.add("added"); 502 - btn.disabled = true; 503 - } 504 - 505 - btn.addEventListener("click", async () => { 506 - if (btn.disabled) return; 507 - const annUri = els.collectionList.dataset.annotationUri; 508 - await handleAddToCollection(collection.uri, btn, annUri); 509 - }); 510 - 511 - els.collectionList.appendChild(btn); 512 - }); 513 - } 514 - 515 - async function handleAddToCollection( 516 - collectionUri, 517 - btnElement, 518 - annotationUri, 519 - ) { 520 - if (!annotationUri) { 521 - console.error("No annotationUri provided!"); 522 - alert("Error: No item selected to add"); 523 - return; 524 - } 525 - 526 - const originalText = btnElement.textContent; 527 - btnElement.disabled = true; 528 - btnElement.textContent = "Adding..."; 529 - 530 - try { 531 - const res = await sendMessage({ 532 - type: "ADD_TO_COLLECTION", 533 - data: { 534 - collectionUri: collectionUri, 535 - annotationUri: annotationUri, 536 - }, 537 - }); 538 - 539 - if (res && res.success) { 540 - btnElement.textContent = "✓ Added"; 541 - setTimeout(() => { 542 - btnElement.textContent = originalText; 543 - btnElement.disabled = false; 544 - }, 2000); 545 - } else { 546 - alert( 547 - "Failed to add to collection: " + (res?.error || "Unknown error"), 548 - ); 549 - btnElement.textContent = originalText; 550 - btnElement.disabled = false; 551 - } 552 - } catch (err) { 553 - console.error("Add to collection error:", err); 554 - alert("Error adding to collection: " + err.message); 555 - btnElement.textContent = originalText; 556 - btnElement.disabled = false; 557 - } 558 - } 559 - 560 - function renderAnnotations(items) { 561 - els.annotationsList.innerHTML = ""; 562 - els.annotationCount.textContent = items?.length || 0; 563 - 564 - if (!items || items.length === 0) { 565 - els.emptyState.style.display = "flex"; 566 - return; 567 - } 568 - 569 - els.emptyState.style.display = "none"; 570 - items.forEach((item) => { 571 - const el = document.createElement("div"); 572 - el.className = "annotation-item"; 573 - 574 - const author = item.creator || item.author || {}; 575 - const authorName = author.handle || author.displayName || "Unknown"; 576 - const authorInitial = authorName[0]?.toUpperCase() || "?"; 577 - const createdAt = item.created || item.createdAt; 578 - const text = item.body?.value || item.text || ""; 579 - const selector = item.target?.selector; 580 - const isHighlight = item.type === "Highlight"; 581 - const quote = selector?.exact || ""; 582 - 583 - const header = document.createElement("div"); 584 - header.className = "annotation-item-header"; 585 - 586 - const avatar = document.createElement("div"); 587 - avatar.className = "annotation-item-avatar"; 588 - 589 - if (author.avatar) { 590 - const img = document.createElement("img"); 591 - img.src = author.avatar; 592 - img.alt = authorName; 593 - img.style.width = "100%"; 594 - img.style.height = "100%"; 595 - img.style.borderRadius = "50%"; 596 - img.style.objectFit = "cover"; 597 - avatar.appendChild(img); 598 - avatar.style.background = "none"; 599 - } else { 600 - avatar.textContent = authorInitial; 601 - } 602 - header.appendChild(avatar); 603 - 604 - const meta = document.createElement("div"); 605 - meta.className = "annotation-item-meta"; 606 - 607 - const authorEl = document.createElement("div"); 608 - authorEl.className = "annotation-item-author"; 609 - authorEl.textContent = "@" + authorName; 610 - meta.appendChild(authorEl); 611 - 612 - const timeEl = document.createElement("div"); 613 - timeEl.className = "annotation-item-time"; 614 - timeEl.textContent = formatDate(createdAt); 615 - meta.appendChild(timeEl); 616 - 617 - header.appendChild(meta); 618 - 619 - if (isHighlight) { 620 - const badge = document.createElement("span"); 621 - badge.className = "annotation-type-badge highlight"; 622 - badge.textContent = "Highlight"; 623 - header.appendChild(badge); 624 - } 625 - 626 - if (currentUserDid) { 627 - const actions = document.createElement("div"); 628 - actions.className = "annotation-item-actions"; 629 - 630 - const folderBtn = document.createElement("button"); 631 - folderBtn.className = "btn-icon"; 632 - folderBtn.innerHTML = 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 - folderBtn.title = "Add to Collection"; 635 - folderBtn.addEventListener("click", (e) => { 636 - e.stopPropagation(); 637 - openCollectionSelector(item.uri); 638 - }); 639 - actions.appendChild(folderBtn); 640 - header.appendChild(actions); 641 - } 642 - 643 - el.appendChild(header); 644 - 645 - if (quote) { 646 - const quoteEl = document.createElement("div"); 647 - quoteEl.className = "annotation-item-quote"; 648 - quoteEl.textContent = '"' + quote + '"'; 649 - el.appendChild(quoteEl); 650 - } 651 - 652 - if (text) { 653 - const textEl = document.createElement("div"); 654 - textEl.className = "annotation-item-text"; 655 - textEl.textContent = text; 656 - el.appendChild(textEl); 657 - } 658 - 659 - if (selector) { 660 - const jumpBtn = document.createElement("button"); 661 - jumpBtn.className = "scroll-to-btn"; 662 - jumpBtn.textContent = "Jump to text →"; 663 - el.appendChild(jumpBtn); 664 - } 665 - 666 - if (selector) { 667 - el.querySelector(".scroll-to-btn")?.addEventListener("click", (e) => { 668 - e.stopPropagation(); 669 - scrollToText(selector); 670 - }); 671 - } 672 - 673 - els.annotationsList.appendChild(el); 674 - }); 675 - } 676 - 677 - function renderBookmarks(items) { 678 - els.bookmarksList.innerHTML = ""; 679 - 680 - if (!items || items.length === 0) { 681 - els.bookmarksEmpty.style.display = "flex"; 682 - return; 683 - } 684 - 685 - els.bookmarksEmpty.style.display = "none"; 686 - items.forEach((item) => { 687 - const el = document.createElement("div"); 688 - el.className = "bookmark-item"; 689 - el.style.cursor = "pointer"; 690 - el.addEventListener("click", () => { 691 - window.open(item.source, "_blank"); 692 - }); 693 - 694 - let hostname = item.source; 695 - try { 696 - hostname = new URL(item.source).hostname; 697 - } catch { 698 - /* ignore */ 699 - } 700 - 701 - const row = document.createElement("div"); 702 - row.style.display = "flex"; 703 - row.style.justifyContent = "space-between"; 704 - row.style.alignItems = "center"; 705 - 706 - const content = document.createElement("div"); 707 - content.style.flex = "1"; 708 - content.style.overflow = "hidden"; 709 - 710 - const titleEl = document.createElement("div"); 711 - titleEl.className = "bookmark-title"; 712 - titleEl.textContent = item.title || item.source; 713 - content.appendChild(titleEl); 714 - 715 - const urlEl = document.createElement("div"); 716 - urlEl.className = "bookmark-url"; 717 - urlEl.textContent = hostname; 718 - content.appendChild(urlEl); 719 - 720 - row.appendChild(content); 721 - 722 - if (currentUserDid) { 723 - const folderBtn = document.createElement("button"); 724 - folderBtn.className = "btn-icon"; 725 - folderBtn.innerHTML = 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 - folderBtn.title = "Add to Collection"; 728 - folderBtn.addEventListener("click", (e) => { 729 - e.preventDefault(); 730 - e.stopPropagation(); 731 - openCollectionSelector(item.uri); 732 - }); 733 - row.appendChild(folderBtn); 734 - } 735 - 736 - el.appendChild(row); 737 - els.bookmarksList.appendChild(el); 738 - }); 739 - } 740 - 741 - function renderHighlights(items) { 742 - els.highlightsList.innerHTML = ""; 743 - 744 - if (!items || items.length === 0) { 745 - els.highlightsEmpty.style.display = "flex"; 746 - return; 747 - } 748 - 749 - els.highlightsEmpty.style.display = "none"; 750 - items.forEach((item) => { 751 - const el = document.createElement("div"); 752 - el.className = "annotation-item"; 753 - 754 - const target = item.target || {}; 755 - const selector = target.selector || {}; 756 - const quote = selector.exact || ""; 757 - const url = target.source || ""; 758 - 759 - let hostname = url; 760 - try { 761 - hostname = new URL(url).hostname; 762 - } catch { 763 - /* ignore */ 764 - } 765 - 766 - const header = document.createElement("div"); 767 - header.className = "annotation-item-header"; 768 - 769 - const meta = document.createElement("div"); 770 - meta.className = "annotation-item-meta"; 771 - 772 - const authorEl = document.createElement("div"); 773 - authorEl.className = "annotation-item-author"; 774 - authorEl.textContent = hostname; 775 - meta.appendChild(authorEl); 776 - 777 - const timeEl = document.createElement("div"); 778 - timeEl.className = "annotation-item-time"; 779 - timeEl.textContent = formatDate(item.created); 780 - meta.appendChild(timeEl); 781 - 782 - header.appendChild(meta); 783 - 784 - if (currentUserDid) { 785 - const actions = document.createElement("div"); 786 - actions.className = "annotation-item-actions"; 787 - 788 - const folderBtn = document.createElement("button"); 789 - folderBtn.className = "btn-icon"; 790 - folderBtn.innerHTML = 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 - folderBtn.title = "Add to Collection"; 793 - folderBtn.addEventListener("click", (e) => { 794 - e.stopPropagation(); 795 - openCollectionSelector(item.uri); 796 - }); 797 - actions.appendChild(folderBtn); 798 - header.appendChild(actions); 799 - } 800 - 801 - el.appendChild(header); 802 - 803 - if (quote) { 804 - const quoteEl = document.createElement("div"); 805 - quoteEl.className = "annotation-item-quote"; 806 - quoteEl.style.borderColor = item.color || "#fcd34d"; 807 - quoteEl.textContent = '"' + quote + '"'; 808 - el.appendChild(quoteEl); 809 - } 810 - 811 - const openBtn = document.createElement("button"); 812 - openBtn.className = "scroll-to-btn"; 813 - openBtn.textContent = "Open page →"; 814 - el.appendChild(openBtn); 815 - 816 - el.querySelector(".scroll-to-btn")?.addEventListener("click", (e) => { 817 - e.stopPropagation(); 818 - const textFragment = createTextFragment(url, selector); 819 - chrome.tabs.create({ url: textFragment }); 820 - }); 821 - 822 - els.highlightsList.appendChild(el); 823 - }); 824 - } 825 - 826 - async function scrollToText(selector) { 827 - let tabId = currentTab?.id; 828 - if (!tabId) { 829 - try { 830 - const [tab] = await chrome.tabs.query({ 831 - active: true, 832 - currentWindow: true, 833 - }); 834 - tabId = tab?.id; 835 - } catch (e) { 836 - console.error("Could not get active tab:", e); 837 - } 838 - } 839 - 840 - if (!tabId) { 841 - console.error("No tab ID available for scroll"); 842 - return; 843 - } 844 - 845 - try { 846 - await chrome.tabs.sendMessage(tabId, { 847 - type: "SCROLL_TO_TEXT", 848 - selector: selector, 849 - }); 850 - } catch (err) { 851 - console.error("Error sending SCROLL_TO_TEXT:", err); 852 - } 853 - } 854 - 855 - function createTextFragment(url, selector) { 856 - if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact) 857 - return url; 858 - 859 - let fragment = ":~:text="; 860 - if (selector.prefix) fragment += encodeURIComponent(selector.prefix) + "-,"; 861 - fragment += encodeURIComponent(selector.exact); 862 - if (selector.suffix) fragment += ",-" + encodeURIComponent(selector.suffix); 863 - 864 - return url + "#" + fragment; 865 - } 866 - 867 - function formatDate(dateString) { 868 - if (!dateString) return ""; 869 - try { 870 - const date = new Date(dateString); 871 - const now = new Date(); 872 - const diffMs = now - date; 873 - const diffMins = Math.floor(diffMs / 60000); 874 - const diffHours = Math.floor(diffMs / 3600000); 875 - const diffDays = Math.floor(diffMs / 86400000); 876 - 877 - if (diffMins < 1) return "Just now"; 878 - if (diffMins < 60) return `${diffMins}m ago`; 879 - if (diffHours < 24) return `${diffHours}h ago`; 880 - if (diffDays < 7) return `${diffDays}d ago`; 881 - return date.toLocaleDateString(); 882 - } catch { 883 - return dateString; 884 - } 885 - } 886 - 887 - function showQuotePreview(selector) { 888 - if (!selector?.exact) return; 889 - 890 - let preview = document.getElementById("quote-preview"); 891 - if (!preview) { 892 - preview = document.createElement("div"); 893 - preview.id = "quote-preview"; 894 - preview.className = "quote-preview"; 895 - const form = document.querySelector(".create-form"); 896 - if (form) { 897 - form.insertBefore(preview, form.querySelector(".annotation-input")); 898 - } 899 - } 900 - 901 - const header = document.createElement("div"); 902 - header.className = "quote-preview-header"; 903 - 904 - const label = document.createElement("span"); 905 - label.textContent = "Annotating selection:"; 906 - header.appendChild(label); 907 - 908 - const clearBtn = document.createElement("button"); 909 - clearBtn.className = "quote-preview-clear"; 910 - clearBtn.title = "Clear selection"; 911 - clearBtn.textContent = "×"; 912 - clearBtn.addEventListener("click", () => { 913 - pendingSelector = null; 914 - hideQuotePreview(); 915 - }); 916 - header.appendChild(clearBtn); 917 - 918 - preview.appendChild(header); 919 - 920 - const text = document.createElement("div"); 921 - text.className = "quote-preview-text"; 922 - text.textContent = '"' + selector.exact + '"'; 923 - preview.appendChild(text); 924 - 925 - els.textInput?.focus(); 926 - } 927 - 928 - function hideQuotePreview() { 929 - const preview = document.getElementById("quote-preview"); 930 - if (preview) { 931 - preview.remove(); 932 - } 933 - } 934 - 935 - function showView(viewName) { 936 - Object.keys(views).forEach((key) => { 937 - if (views[key]) views[key].style.display = "none"; 938 - }); 939 - if (views[viewName]) { 940 - views[viewName].style.display = 941 - viewName === "loading" || viewName === "settings" ? "flex" : "block"; 942 - } 943 - } 944 - 945 - function sendMessage(message) { 946 - return new Promise((resolve, reject) => { 947 - chrome.runtime.sendMessage(message, (response) => { 948 - if (chrome.runtime.lastError) { 949 - reject(chrome.runtime.lastError); 950 - } else { 951 - resolve(response); 952 - } 953 - }); 954 - }); 955 - } 956 - 957 - function applyTheme(theme) { 958 - document.body.classList.remove("light", "dark"); 959 - if (theme === "system") return; 960 - document.body.classList.add(theme); 961 - } 962 - 963 - function updateThemeUI(theme) { 964 - const btns = document.querySelectorAll(".theme-btn"); 965 - btns.forEach((btn) => { 966 - if (btn.getAttribute("data-theme") === theme) { 967 - btn.classList.add("active"); 968 - } else { 969 - btn.classList.remove("active"); 970 - } 971 - }); 972 - } 973 - });
+161
extension/src/assets/styles.css
··· 1 + @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap'); 2 + 3 + @tailwind base; 4 + @tailwind components; 5 + @tailwind utilities; 6 + 7 + :root { 8 + --bg-primary: #0a0a0d; 9 + --bg-secondary: #121216; 10 + --bg-tertiary: #1a1a1f; 11 + --bg-card: #0f0f13; 12 + --bg-elevated: #18181d; 13 + --bg-hover: #1e1e24; 14 + --text-primary: #eaeaee; 15 + --text-secondary: #b7b6c5; 16 + --text-tertiary: #6e6d7a; 17 + --border: rgba(183, 182, 197, 0.12); 18 + --border-strong: rgba(183, 182, 197, 0.2); 19 + --accent: #957a86; 20 + --accent-hover: #a98d98; 21 + --accent-subtle: rgba(149, 122, 134, 0.15); 22 + --success: #34d399; 23 + --warning: #fbbf24; 24 + } 25 + 26 + .light { 27 + --bg-primary: #f8f8fa; 28 + --bg-secondary: #ffffff; 29 + --bg-tertiary: #f0f0f4; 30 + --bg-card: #ffffff; 31 + --bg-elevated: #ffffff; 32 + --bg-hover: #eeeef2; 33 + --text-primary: #18171c; 34 + --text-secondary: #5c495a; 35 + --text-tertiary: #8a8494; 36 + --border: rgba(92, 73, 90, 0.12); 37 + --border-strong: rgba(92, 73, 90, 0.2); 38 + --accent: #7a5f6d; 39 + --accent-hover: #664e5b; 40 + --accent-subtle: rgba(149, 122, 134, 0.12); 41 + } 42 + 43 + body { 44 + background: var(--bg-primary); 45 + color: var(--text-primary); 46 + font-family: 47 + 'IBM Plex Sans', 48 + -apple-system, 49 + BlinkMacSystemFont, 50 + sans-serif; 51 + margin: 0; 52 + padding: 0; 53 + min-width: 320px; 54 + max-width: 100%; 55 + min-height: 100vh; 56 + overflow: hidden; 57 + -webkit-font-smoothing: antialiased; 58 + -moz-osx-font-smoothing: grayscale; 59 + } 60 + 61 + html.popup body { 62 + width: 380px; 63 + height: 520px; 64 + min-height: 520px; 65 + } 66 + 67 + * { 68 + box-sizing: border-box; 69 + } 70 + 71 + ::-webkit-scrollbar { 72 + width: 6px; 73 + } 74 + 75 + ::-webkit-scrollbar-track { 76 + background: transparent; 77 + } 78 + 79 + ::-webkit-scrollbar-thumb { 80 + background: var(--bg-tertiary); 81 + border-radius: 3px; 82 + } 83 + 84 + ::-webkit-scrollbar-thumb:hover { 85 + background: var(--text-tertiary); 86 + } 87 + 88 + button, 89 + a, 90 + input, 91 + textarea { 92 + transition: all 0.15s ease; 93 + } 94 + 95 + input:focus, 96 + textarea:focus, 97 + button:focus-visible { 98 + outline: none; 99 + box-shadow: 100 + 0 0 0 2px var(--accent-subtle), 101 + 0 0 0 4px var(--accent); 102 + } 103 + 104 + @keyframes fadeIn { 105 + from { 106 + opacity: 0; 107 + transform: translateY(-4px); 108 + } 109 + to { 110 + opacity: 1; 111 + transform: translateY(0); 112 + } 113 + } 114 + 115 + @keyframes slideUp { 116 + from { 117 + opacity: 0; 118 + transform: translateY(8px); 119 + } 120 + to { 121 + opacity: 1; 122 + transform: translateY(0); 123 + } 124 + } 125 + 126 + @keyframes slideDown { 127 + from { 128 + opacity: 0; 129 + transform: translateY(-8px); 130 + } 131 + to { 132 + opacity: 1; 133 + transform: translateY(0); 134 + } 135 + } 136 + 137 + @keyframes pulse { 138 + 0%, 139 + 100% { 140 + opacity: 1; 141 + } 142 + 50% { 143 + opacity: 0.5; 144 + } 145 + } 146 + 147 + .animate-fadeIn { 148 + animation: fadeIn 0.2s ease forwards; 149 + } 150 + 151 + .animate-slideUp { 152 + animation: slideUp 0.25s ease forwards; 153 + } 154 + 155 + .animate-slideDown { 156 + animation: slideDown 0.2s ease forwards; 157 + } 158 + 159 + .animate-pulse { 160 + animation: pulse 1.5s ease-in-out infinite; 161 + }
+124
extension/src/components/CollectionIcon.tsx
··· 1 + import { 2 + Folder, 3 + Star, 4 + Heart, 5 + Bookmark, 6 + Lightbulb, 7 + Zap, 8 + Coffee, 9 + Music, 10 + Camera, 11 + Code, 12 + Globe, 13 + Flag, 14 + Tag, 15 + Box, 16 + Archive, 17 + FileText, 18 + Image, 19 + Video, 20 + Mail, 21 + MapPin, 22 + Calendar, 23 + Clock, 24 + Search, 25 + Settings, 26 + User, 27 + Users, 28 + Home, 29 + Briefcase, 30 + Gift, 31 + Award, 32 + Target, 33 + TrendingUp, 34 + Activity, 35 + Cpu, 36 + Database, 37 + Cloud, 38 + Sun, 39 + Moon, 40 + Flame, 41 + Leaf, 42 + type LucideIcon, 43 + } from 'lucide-react'; 44 + 45 + const ICON_MAP: Record<string, LucideIcon> = { 46 + folder: Folder, 47 + star: Star, 48 + heart: Heart, 49 + bookmark: Bookmark, 50 + lightbulb: Lightbulb, 51 + zap: Zap, 52 + coffee: Coffee, 53 + music: Music, 54 + camera: Camera, 55 + code: Code, 56 + globe: Globe, 57 + flag: Flag, 58 + tag: Tag, 59 + box: Box, 60 + archive: Archive, 61 + file: FileText, 62 + image: Image, 63 + video: Video, 64 + mail: Mail, 65 + pin: MapPin, 66 + calendar: Calendar, 67 + clock: Clock, 68 + search: Search, 69 + settings: Settings, 70 + user: User, 71 + users: Users, 72 + home: Home, 73 + briefcase: Briefcase, 74 + gift: Gift, 75 + award: Award, 76 + target: Target, 77 + trending: TrendingUp, 78 + activity: Activity, 79 + cpu: Cpu, 80 + database: Database, 81 + cloud: Cloud, 82 + sun: Sun, 83 + moon: Moon, 84 + flame: Flame, 85 + leaf: Leaf, 86 + }; 87 + 88 + interface CollectionIconProps { 89 + icon?: string; 90 + size?: number; 91 + className?: string; 92 + } 93 + 94 + export default function CollectionIcon({ icon, size = 18, className = '' }: CollectionIconProps) { 95 + if (!icon) { 96 + return <Folder size={size} className={className} />; 97 + } 98 + 99 + if (icon === 'icon:semble') { 100 + return ( 101 + <img 102 + src="/icons/semble-logo.svg" 103 + alt="Semble" 104 + style={{ width: size, height: size, objectFit: 'contain' }} 105 + className={className} 106 + /> 107 + ); 108 + } 109 + 110 + if (icon.startsWith('icon:')) { 111 + const iconName = icon.replace('icon:', ''); 112 + const IconComponent = ICON_MAP[iconName]; 113 + if (IconComponent) { 114 + return <IconComponent size={size} className={className} />; 115 + } 116 + return <Folder size={size} className={className} />; 117 + } 118 + 119 + return ( 120 + <span style={{ fontSize: `${size * 0.065}rem`, lineHeight: 1 }} className={className}> 121 + {icon} 122 + </span> 123 + ); 124 + }
+986
extension/src/components/popup/App.tsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import { sendMessage } from '@/utils/messaging'; 3 + import { themeItem, apiUrlItem, overlayEnabledItem } from '@/utils/storage'; 4 + import type { MarginSession, Annotation, Bookmark, Highlight, Collection } from '@/utils/types'; 5 + import CollectionIcon from '@/components/CollectionIcon'; 6 + import { 7 + Settings, 8 + ExternalLink, 9 + Bookmark as BookmarkIcon, 10 + Highlighter, 11 + MessageSquare, 12 + X, 13 + Sun, 14 + Moon, 15 + Monitor, 16 + Check, 17 + Globe, 18 + ChevronRight, 19 + Sparkles, 20 + FolderPlus, 21 + Folder, 22 + } from 'lucide-react'; 23 + 24 + type Tab = 'page' | 'bookmarks' | 'highlights' | 'collections'; 25 + 26 + export function App() { 27 + const [session, setSession] = useState<MarginSession | null>(null); 28 + const [loading, setLoading] = useState(true); 29 + const [activeTab, setActiveTab] = useState<Tab>('page'); 30 + const [annotations, setAnnotations] = useState<Annotation[]>([]); 31 + const [bookmarks, setBookmarks] = useState<Bookmark[]>([]); 32 + const [highlights, setHighlights] = useState<Highlight[]>([]); 33 + const [collections, setCollections] = useState<Collection[]>([]); 34 + const [loadingAnnotations, setLoadingAnnotations] = useState(false); 35 + const [loadingBookmarks, setLoadingBookmarks] = useState(false); 36 + const [loadingHighlights, setLoadingHighlights] = useState(false); 37 + const [loadingCollections, setLoadingCollections] = useState(false); 38 + const [collectionModalItem, setCollectionModalItem] = useState<string | null>(null); 39 + const [addingToCollection, setAddingToCollection] = useState<string | null>(null); 40 + const [containingCollections, setContainingCollections] = useState<Set<string>>(new Set()); 41 + const [currentUrl, setCurrentUrl] = useState(''); 42 + const [currentTitle, setCurrentTitle] = useState(''); 43 + const [text, setText] = useState(''); 44 + const [posting, setPosting] = useState(false); 45 + const [bookmarking, setBookmarking] = useState(false); 46 + const [bookmarked, setBookmarked] = useState(false); 47 + const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system'); 48 + const [showSettings, setShowSettings] = useState(false); 49 + const [apiUrl, setApiUrl] = useState('https://margin.at'); 50 + const [overlayEnabled, setOverlayEnabled] = useState(true); 51 + 52 + useEffect(() => { 53 + checkSession(); 54 + loadCurrentTab(); 55 + loadTheme(); 56 + loadSettings(); 57 + }, []); 58 + 59 + useEffect(() => { 60 + if (session?.authenticated && currentUrl) { 61 + if (activeTab === 'page') loadAnnotations(); 62 + else if (activeTab === 'bookmarks') loadBookmarks(); 63 + else if (activeTab === 'highlights') loadHighlights(); 64 + else if (activeTab === 'collections') loadCollections(); 65 + } 66 + }, [activeTab, session, currentUrl]); 67 + 68 + async function loadSettings() { 69 + const url = await apiUrlItem.getValue(); 70 + const overlay = await overlayEnabledItem.getValue(); 71 + setApiUrl(url); 72 + setOverlayEnabled(overlay); 73 + } 74 + 75 + async function saveSettings() { 76 + const cleanUrl = apiUrl.replace(/\/$/, ''); 77 + await apiUrlItem.setValue(cleanUrl); 78 + await overlayEnabledItem.setValue(overlayEnabled); 79 + 80 + const tabs = await browser.tabs.query({}); 81 + for (const tab of tabs) { 82 + if (tab.id) { 83 + try { 84 + await browser.tabs.sendMessage(tab.id, { 85 + type: 'UPDATE_OVERLAY_VISIBILITY', 86 + show: overlayEnabled, 87 + }); 88 + } catch { 89 + /* ignore */ 90 + } 91 + } 92 + } 93 + 94 + setShowSettings(false); 95 + checkSession(); 96 + } 97 + 98 + async function loadTheme() { 99 + const t = await themeItem.getValue(); 100 + setTheme(t); 101 + applyTheme(t); 102 + 103 + themeItem.watch((newTheme) => { 104 + setTheme(newTheme); 105 + applyTheme(newTheme); 106 + }); 107 + } 108 + 109 + function applyTheme(t: string) { 110 + document.body.classList.remove('light', 'dark'); 111 + if (t === 'system') { 112 + if (window.matchMedia('(prefers-color-scheme: light)').matches) { 113 + document.body.classList.add('light'); 114 + } 115 + } else { 116 + document.body.classList.add(t); 117 + } 118 + } 119 + 120 + async function handleThemeChange(newTheme: 'light' | 'dark' | 'system') { 121 + await themeItem.setValue(newTheme); 122 + setTheme(newTheme); 123 + applyTheme(newTheme); 124 + } 125 + 126 + async function checkSession() { 127 + try { 128 + const result = await sendMessage('checkSession', undefined); 129 + setSession(result); 130 + } catch (error) { 131 + console.error('Session check error:', error); 132 + setSession({ authenticated: false }); 133 + } finally { 134 + setLoading(false); 135 + } 136 + } 137 + 138 + async function loadCurrentTab() { 139 + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); 140 + if (tab?.url) { 141 + setCurrentUrl(tab.url); 142 + setCurrentTitle(tab.title || ''); 143 + } 144 + } 145 + 146 + async function loadAnnotations() { 147 + if (!currentUrl) return; 148 + setLoadingAnnotations(true); 149 + try { 150 + let result = await sendMessage('getCachedAnnotations', { url: currentUrl }); 151 + 152 + if (!result) { 153 + result = await sendMessage('getAnnotations', { url: currentUrl }); 154 + } 155 + 156 + const filtered = (result || []).filter((item: any) => item.type !== 'Bookmark'); 157 + setAnnotations(filtered); 158 + 159 + const isBookmarked = (result || []).some( 160 + (item: any) => item.type === 'Bookmark' && item.creator?.did === session?.did 161 + ); 162 + setBookmarked(isBookmarked); 163 + } catch (error) { 164 + console.error('Load annotations error:', error); 165 + } finally { 166 + setLoadingAnnotations(false); 167 + } 168 + } 169 + 170 + async function loadBookmarks() { 171 + if (!session?.did) return; 172 + setLoadingBookmarks(true); 173 + try { 174 + const result = await sendMessage('getUserBookmarks', { did: session.did }); 175 + setBookmarks(result || []); 176 + } catch (error) { 177 + console.error('Load bookmarks error:', error); 178 + } finally { 179 + setLoadingBookmarks(false); 180 + } 181 + } 182 + 183 + async function loadHighlights() { 184 + if (!session?.did) return; 185 + setLoadingHighlights(true); 186 + try { 187 + const result = await sendMessage('getUserHighlights', { did: session.did }); 188 + setHighlights(result || []); 189 + } catch (error) { 190 + console.error('Load highlights error:', error); 191 + } finally { 192 + setLoadingHighlights(false); 193 + } 194 + } 195 + 196 + async function loadCollections() { 197 + if (!session?.did) return; 198 + setLoadingCollections(true); 199 + try { 200 + const result = await sendMessage('getUserCollections', { did: session.did }); 201 + setCollections(result || []); 202 + } catch (error) { 203 + console.error('Load collections error:', error); 204 + } finally { 205 + setLoadingCollections(false); 206 + } 207 + } 208 + 209 + async function openCollectionModal(itemUri: string) { 210 + setCollectionModalItem(itemUri); 211 + setContainingCollections(new Set()); 212 + 213 + if (collections.length === 0) { 214 + await loadCollections(); 215 + } 216 + 217 + try { 218 + const itemCollectionUris = await sendMessage('getItemCollections', { 219 + annotationUri: itemUri, 220 + }); 221 + setContainingCollections(new Set(itemCollectionUris)); 222 + } catch (error) { 223 + console.error('Failed to get item collections:', error); 224 + } 225 + } 226 + 227 + async function handleAddToCollection(collectionUri: string) { 228 + if (!collectionModalItem) return; 229 + 230 + if (containingCollections.has(collectionUri)) { 231 + setCollectionModalItem(null); 232 + return; 233 + } 234 + 235 + setAddingToCollection(collectionUri); 236 + try { 237 + const result = await sendMessage('addToCollection', { 238 + collectionUri, 239 + annotationUri: collectionModalItem, 240 + }); 241 + if (result.success) { 242 + setContainingCollections((prev) => new Set([...prev, collectionUri])); 243 + } else { 244 + alert('Failed to add to collection'); 245 + } 246 + } catch (error) { 247 + console.error('Add to collection error:', error); 248 + alert('Error adding to collection'); 249 + } finally { 250 + setAddingToCollection(null); 251 + } 252 + } 253 + 254 + async function handlePost() { 255 + if (!text.trim()) return; 256 + setPosting(true); 257 + try { 258 + const result = await sendMessage('createAnnotation', { 259 + url: currentUrl, 260 + text: text.trim(), 261 + title: currentTitle, 262 + }); 263 + if (result.success) { 264 + setText(''); 265 + loadAnnotations(); 266 + } else { 267 + alert('Failed to post annotation'); 268 + } 269 + } catch (error) { 270 + console.error('Post error:', error); 271 + alert('Error posting annotation'); 272 + } finally { 273 + setPosting(false); 274 + } 275 + } 276 + 277 + async function handleBookmark() { 278 + setBookmarking(true); 279 + try { 280 + const result = await sendMessage('createBookmark', { 281 + url: currentUrl, 282 + title: currentTitle, 283 + }); 284 + if (result.success) { 285 + setBookmarked(true); 286 + } else { 287 + alert('Failed to bookmark page'); 288 + } 289 + } catch (error) { 290 + console.error('Bookmark error:', error); 291 + alert('Error bookmarking page'); 292 + } finally { 293 + setBookmarking(false); 294 + } 295 + } 296 + 297 + function formatDate(dateString?: string) { 298 + if (!dateString) return ''; 299 + try { 300 + return new Date(dateString).toLocaleDateString(); 301 + } catch { 302 + return dateString; 303 + } 304 + } 305 + 306 + if (loading) { 307 + return ( 308 + <div className="flex items-center justify-center h-screen"> 309 + <div className="animate-spin rounded-full h-8 w-8 border-2 border-[var(--accent)] border-t-transparent" /> 310 + </div> 311 + ); 312 + } 313 + 314 + if (!session?.authenticated) { 315 + return ( 316 + <div className="flex flex-col h-screen"> 317 + {showSettings && ( 318 + <div className="absolute inset-0 bg-[var(--bg-primary)] z-10 flex flex-col"> 319 + <header className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]"> 320 + <span className="font-medium">Settings</span> 321 + <button 322 + onClick={() => setShowSettings(false)} 323 + className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)]" 324 + > 325 + <X size={18} /> 326 + </button> 327 + </header> 328 + 329 + <div className="flex-1 overflow-y-auto p-4 space-y-6"> 330 + <div> 331 + <label className="block text-sm font-medium mb-2">API URL</label> 332 + <input 333 + type="text" 334 + value={apiUrl} 335 + onChange={(e) => setApiUrl(e.target.value)} 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 + placeholder="https://margin.at" 338 + /> 339 + </div> 340 + 341 + <div> 342 + <label className="block text-sm font-medium mb-2">Theme</label> 343 + <div className="flex gap-2"> 344 + {(['light', 'dark', 'system'] as const).map((t) => ( 345 + <button 346 + key={t} 347 + onClick={() => handleThemeChange(t)} 348 + className={`flex-1 py-2 px-3 text-xs rounded-lg border transition-colors flex items-center justify-center gap-1 ${ 349 + theme === t 350 + ? 'bg-[var(--accent)] text-white border-[var(--accent)]' 351 + : 'bg-[var(--bg-card)] border-[var(--border)] hover:bg-[var(--bg-hover)]' 352 + }`} 353 + > 354 + {t === 'light' ? ( 355 + <Sun size={12} /> 356 + ) : t === 'dark' ? ( 357 + <Moon size={12} /> 358 + ) : ( 359 + <Monitor size={12} /> 360 + )} 361 + {t.charAt(0).toUpperCase() + t.slice(1)} 362 + </button> 363 + ))} 364 + </div> 365 + </div> 366 + </div> 367 + 368 + <div className="p-4 border-t border-[var(--border)]"> 369 + <button 370 + onClick={saveSettings} 371 + className="w-full py-2.5 bg-[var(--accent)] text-white rounded-lg font-medium hover:bg-[var(--accent-hover)] transition-colors" 372 + > 373 + Save Settings 374 + </button> 375 + </div> 376 + </div> 377 + )} 378 + 379 + <div className="flex flex-col items-center justify-center flex-1 p-6 text-center"> 380 + <img src="/icons/logo.svg" alt="Margin" className="w-12 h-12 mb-4" /> 381 + <h2 className="text-lg font-semibold mb-2">Sign in with AT Protocol</h2> 382 + <p className="text-[var(--text-secondary)] text-sm mb-6"> 383 + Connect your Bluesky account to annotate, highlight, and bookmark the web. 384 + </p> 385 + <button 386 + onClick={() => browser.tabs.create({ url: `${apiUrl}/login` })} 387 + className="px-6 py-2.5 bg-[var(--accent)] text-white rounded-lg font-medium hover:bg-[var(--accent-hover)] transition-colors" 388 + > 389 + Continue 390 + </button> 391 + <button 392 + onClick={() => setShowSettings(true)} 393 + className="mt-4 text-xs text-[var(--text-tertiary)] hover:text-[var(--text-primary)] flex items-center gap-1" 394 + > 395 + <Settings size={12} /> Settings 396 + </button> 397 + </div> 398 + </div> 399 + ); 400 + } 401 + 402 + return ( 403 + <div className="flex flex-col h-screen"> 404 + {showSettings && ( 405 + <div className="absolute inset-0 bg-[var(--bg-primary)] z-10 flex flex-col"> 406 + <header className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]"> 407 + <span className="font-medium">Settings</span> 408 + <button 409 + onClick={() => setShowSettings(false)} 410 + className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)]" 411 + > 412 + <X size={18} /> 413 + </button> 414 + </header> 415 + 416 + <div className="flex-1 overflow-y-auto p-4 space-y-6"> 417 + <div> 418 + <label className="block text-sm font-medium mb-2">API URL</label> 419 + <input 420 + type="text" 421 + value={apiUrl} 422 + onChange={(e) => setApiUrl(e.target.value)} 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 + placeholder="https://margin.at" 425 + /> 426 + <p className="text-xs text-[var(--text-tertiary)] mt-1"> 427 + Change this for development or self-hosted instances 428 + </p> 429 + </div> 430 + 431 + <div className="flex items-center justify-between"> 432 + <div> 433 + <label className="text-sm font-medium">Show page overlays</label> 434 + <p className="text-xs text-[var(--text-tertiary)]"> 435 + Highlights, badges, and tooltips on pages 436 + </p> 437 + </div> 438 + <input 439 + type="checkbox" 440 + checked={overlayEnabled} 441 + onChange={(e) => setOverlayEnabled(e.target.checked)} 442 + className="w-5 h-5 rounded accent-[var(--accent)]" 443 + /> 444 + </div> 445 + 446 + <div> 447 + <label className="block text-sm font-medium mb-2">Theme</label> 448 + <div className="flex gap-2"> 449 + {(['light', 'dark', 'system'] as const).map((t) => ( 450 + <button 451 + key={t} 452 + onClick={() => handleThemeChange(t)} 453 + className={`flex-1 py-2 px-3 text-xs rounded-lg border transition-colors flex items-center justify-center gap-1 ${ 454 + theme === t 455 + ? 'bg-[var(--accent)] text-white border-[var(--accent)]' 456 + : 'bg-[var(--bg-card)] border-[var(--border)] hover:bg-[var(--bg-hover)]' 457 + }`} 458 + > 459 + {t === 'light' ? ( 460 + <Sun size={12} /> 461 + ) : t === 'dark' ? ( 462 + <Moon size={12} /> 463 + ) : ( 464 + <Monitor size={12} /> 465 + )} 466 + {t.charAt(0).toUpperCase() + t.slice(1)} 467 + </button> 468 + ))} 469 + </div> 470 + </div> 471 + </div> 472 + 473 + <div className="p-4 border-t border-[var(--border)]"> 474 + <button 475 + onClick={saveSettings} 476 + className="w-full py-2.5 bg-[var(--accent)] text-white rounded-lg font-medium hover:bg-[var(--accent-hover)] transition-colors" 477 + > 478 + Save 479 + </button> 480 + </div> 481 + </div> 482 + )} 483 + 484 + <header className="flex items-center justify-between px-4 py-2.5 border-b border-[var(--border)] bg-[var(--bg-secondary)]"> 485 + <div className="flex items-center gap-2.5"> 486 + <img src="/icons/logo.svg" alt="Margin" className="w-6 h-6" /> 487 + <span className="font-bold text-sm tracking-tight">Margin</span> 488 + </div> 489 + <div className="flex items-center gap-2"> 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 + @{session.handle} 492 + </div> 493 + </div> 494 + </header> 495 + 496 + <div className="flex border-b border-[var(--border)] px-2 gap-0.5 bg-[var(--bg-secondary)]"> 497 + {(['page', 'bookmarks', 'highlights', 'collections'] as Tab[]).map((tab) => { 498 + const icons: Record<Tab, JSX.Element> = { 499 + page: <Globe size={13} />, 500 + bookmarks: <BookmarkIcon size={13} />, 501 + highlights: <Highlighter size={13} />, 502 + collections: <Folder size={13} />, 503 + }; 504 + const labels: Record<Tab, string> = { 505 + page: 'Page', 506 + bookmarks: 'Bookmarks', 507 + highlights: 'Highlights', 508 + collections: 'Collections', 509 + }; 510 + return ( 511 + <button 512 + key={tab} 513 + onClick={() => setActiveTab(tab)} 514 + className={`flex-1 py-2.5 text-[11px] font-medium flex items-center justify-center gap-1 border-b-2 transition-all ${ 515 + activeTab === tab 516 + ? 'border-[var(--accent)] text-[var(--accent)]' 517 + : 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]' 518 + }`} 519 + > 520 + {icons[tab]} 521 + {labels[tab]} 522 + </button> 523 + ); 524 + })} 525 + </div> 526 + 527 + <div className="flex-1 overflow-y-auto"> 528 + {activeTab === 'page' && ( 529 + <div> 530 + <div className="p-4 border-b border-[var(--border)]"> 531 + <div className="flex items-start gap-3 p-3 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl"> 532 + <div className="w-10 h-10 rounded-lg bg-[var(--bg-hover)] flex items-center justify-center flex-shrink-0 overflow-hidden"> 533 + {currentUrl ? ( 534 + <img 535 + src={`https://www.google.com/s2/favicons?domain=${new URL(currentUrl).hostname}&sz=64`} 536 + alt="" 537 + className="w-6 h-6" 538 + onError={(e) => { 539 + (e.target as HTMLImageElement).style.display = 'none'; 540 + (e.target as HTMLImageElement).nextElementSibling?.classList.remove( 541 + 'hidden' 542 + ); 543 + }} 544 + /> 545 + ) : null} 546 + <Globe 547 + size={18} 548 + className={`text-[var(--text-tertiary)] ${currentUrl ? 'hidden' : ''}`} 549 + /> 550 + </div> 551 + <div className="flex-1 min-w-0"> 552 + <div className="text-sm font-semibold truncate mb-0.5"> 553 + {currentTitle || 'Untitled'} 554 + </div> 555 + <div className="text-xs text-[var(--text-tertiary)] truncate"> 556 + {currentUrl ? new URL(currentUrl).hostname : ''} 557 + </div> 558 + </div> 559 + <button 560 + onClick={handleBookmark} 561 + disabled={bookmarking || bookmarked} 562 + className={`p-2 rounded-lg transition-all flex-shrink-0 ${ 563 + bookmarked 564 + ? 'bg-[var(--success)]/15 text-[var(--success)]' 565 + : 'bg-[var(--bg-hover)] hover:bg-[var(--accent-subtle)] text-[var(--text-secondary)] hover:text-[var(--accent)]' 566 + }`} 567 + title={bookmarked ? 'Bookmarked' : 'Bookmark page'} 568 + > 569 + {bookmarked ? <Check size={16} /> : <BookmarkIcon size={16} />} 570 + </button> 571 + </div> 572 + </div> 573 + 574 + <div className="p-4 border-b border-[var(--border)]"> 575 + <div className="relative"> 576 + <textarea 577 + value={text} 578 + onChange={(e) => setText(e.target.value)} 579 + placeholder="Share your thoughts on this page..." 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 + /> 582 + <div className="absolute bottom-3 right-3"> 583 + <button 584 + onClick={handlePost} 585 + disabled={posting || !text.trim()} 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 + > 588 + {posting ? 'Posting...' : 'Post'} 589 + </button> 590 + </div> 591 + </div> 592 + </div> 593 + 594 + <div> 595 + <div className="flex justify-between items-center px-4 py-3"> 596 + <div className="flex items-center gap-2"> 597 + <MessageSquare size={14} className="text-[var(--text-tertiary)]" /> 598 + <span className="text-xs font-semibold text-[var(--text-secondary)]"> 599 + Annotations 600 + </span> 601 + </div> 602 + <span className="text-xs font-semibold bg-[var(--accent-subtle)] text-[var(--accent)] px-2.5 py-1 rounded-full"> 603 + {annotations.length} 604 + </span> 605 + </div> 606 + 607 + {loadingAnnotations ? ( 608 + <div className="flex items-center justify-center py-12"> 609 + <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 610 + </div> 611 + ) : annotations.length === 0 ? ( 612 + <div className="flex flex-col items-center justify-center py-12 text-[var(--text-tertiary)]"> 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 + <Sparkles size={24} className="opacity-40" /> 615 + </div> 616 + <p className="text-sm font-medium mb-1">No annotations yet</p> 617 + <p className="text-xs text-[var(--text-tertiary)]"> 618 + Be the first to annotate this page 619 + </p> 620 + </div> 621 + ) : ( 622 + <div className="divide-y divide-[var(--border)]"> 623 + {annotations.map((item) => ( 624 + <AnnotationCard 625 + key={item.uri || item.id} 626 + item={item} 627 + formatDate={formatDate} 628 + onAddToCollection={() => openCollectionModal(item.uri || item.id || '')} 629 + /> 630 + ))} 631 + </div> 632 + )} 633 + </div> 634 + </div> 635 + )} 636 + 637 + {activeTab === 'bookmarks' && ( 638 + <div className="p-4"> 639 + {loadingBookmarks ? ( 640 + <div className="flex items-center justify-center py-16"> 641 + <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 642 + </div> 643 + ) : bookmarks.length === 0 ? ( 644 + <div className="flex flex-col items-center justify-center py-16 text-[var(--text-tertiary)]"> 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 + <BookmarkIcon size={24} className="opacity-40" /> 647 + </div> 648 + <p className="text-sm font-medium mb-1">No bookmarks yet</p> 649 + <p className="text-xs text-[var(--text-tertiary)]">Save pages to read later</p> 650 + </div> 651 + ) : ( 652 + <div className="space-y-2"> 653 + {bookmarks.map((item) => ( 654 + <div 655 + key={item.uri || item.id} 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 + > 658 + <div className="w-9 h-9 rounded-lg bg-[var(--accent-subtle)] flex items-center justify-center flex-shrink-0"> 659 + <BookmarkIcon size={16} className="text-[var(--accent)]" /> 660 + </div> 661 + <a 662 + href={item.source} 663 + target="_blank" 664 + rel="noopener noreferrer" 665 + className="flex-1 min-w-0" 666 + > 667 + <div className="text-sm font-medium truncate group-hover:text-[var(--accent)] transition-colors"> 668 + {item.title || 'Untitled'} 669 + </div> 670 + <div className="text-xs text-[var(--text-tertiary)] truncate"> 671 + {item.source ? new URL(item.source).hostname : ''} 672 + </div> 673 + </a> 674 + <button 675 + onClick={() => openCollectionModal(item.uri || item.id || '')} 676 + className="p-1.5 text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent-subtle)] rounded-lg transition-all" 677 + title="Add to collection" 678 + > 679 + <FolderPlus size={14} /> 680 + </button> 681 + </div> 682 + ))} 683 + </div> 684 + )} 685 + </div> 686 + )} 687 + 688 + {activeTab === 'highlights' && ( 689 + <div className="p-4"> 690 + {loadingHighlights ? ( 691 + <div className="flex items-center justify-center py-16"> 692 + <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 693 + </div> 694 + ) : highlights.length === 0 ? ( 695 + <div className="flex flex-col items-center justify-center py-16 text-[var(--text-tertiary)]"> 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 + <Highlighter size={24} className="opacity-40" /> 698 + </div> 699 + <p className="text-sm font-medium mb-1">No highlights yet</p> 700 + <p className="text-xs text-[var(--text-tertiary)]"> 701 + Select text on any page to highlight 702 + </p> 703 + </div> 704 + ) : ( 705 + <div className="space-y-3"> 706 + {highlights.map((item) => ( 707 + <div 708 + key={item.uri || item.id} 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 + > 711 + {item.target?.selector?.exact && ( 712 + <div 713 + className="text-sm leading-relaxed border-l-3 pl-3 mb-3 py-1" 714 + style={{ 715 + borderColor: item.color || '#fbbf24', 716 + background: `linear-gradient(90deg, ${item.color || '#fbbf24'}15, transparent)`, 717 + }} 718 + > 719 + " 720 + {item.target.selector.exact.length > 120 721 + ? item.target.selector.exact.slice(0, 120) + '...' 722 + : item.target.selector.exact} 723 + " 724 + </div> 725 + )} 726 + <div className="flex items-center justify-between"> 727 + <div 728 + className="flex items-center gap-2 text-xs text-[var(--text-tertiary)] flex-1 cursor-pointer hover:text-[var(--accent)]" 729 + onClick={() => { 730 + if (item.target?.source) { 731 + browser.tabs.create({ url: item.target.source }); 732 + } 733 + }} 734 + > 735 + <Globe size={12} /> 736 + {item.target?.source ? new URL(item.target.source).hostname : ''} 737 + <ChevronRight 738 + size={14} 739 + className="ml-auto text-[var(--text-tertiary)] group-hover:text-[var(--accent)] transition-colors" 740 + /> 741 + </div> 742 + <button 743 + onClick={(e) => { 744 + e.stopPropagation(); 745 + openCollectionModal(item.uri || item.id || ''); 746 + }} 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 + title="Add to collection" 749 + > 750 + <FolderPlus size={14} /> 751 + </button> 752 + </div> 753 + </div> 754 + ))} 755 + </div> 756 + )} 757 + </div> 758 + )} 759 + 760 + {activeTab === 'collections' && ( 761 + <div className="p-4"> 762 + {loadingCollections ? ( 763 + <div className="flex items-center justify-center py-16"> 764 + <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 765 + </div> 766 + ) : collections.length === 0 ? ( 767 + <div className="flex flex-col items-center justify-center py-16 text-[var(--text-tertiary)]"> 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 + <Folder size={24} className="opacity-40" /> 770 + </div> 771 + <p className="text-sm font-medium mb-1">No collections yet</p> 772 + <p className="text-xs text-[var(--text-tertiary)]"> 773 + Organize your annotations into collections 774 + </p> 775 + </div> 776 + ) : ( 777 + <div className="space-y-2"> 778 + {collections.map((item) => ( 779 + <button 780 + key={item.uri || item.id} 781 + onClick={() => 782 + browser.tabs.create({ 783 + url: `${apiUrl}/collection/${encodeURIComponent(item.uri || item.id || '')}`, 784 + }) 785 + } 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 + > 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 + <CollectionIcon icon={item.icon} size={18} /> 790 + </div> 791 + <div className="flex-1 min-w-0"> 792 + <div className="text-sm font-medium group-hover:text-[var(--accent)] transition-colors"> 793 + {item.name} 794 + </div> 795 + {item.description && ( 796 + <div className="text-xs text-[var(--text-tertiary)] truncate"> 797 + {item.description} 798 + </div> 799 + )} 800 + </div> 801 + <ChevronRight 802 + size={16} 803 + className="text-[var(--text-tertiary)] group-hover:text-[var(--accent)] transition-colors" 804 + /> 805 + </button> 806 + ))} 807 + </div> 808 + )} 809 + </div> 810 + )} 811 + </div> 812 + 813 + {collectionModalItem && ( 814 + <div 815 + className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 animate-fadeIn" 816 + onClick={() => setCollectionModalItem(null)} 817 + > 818 + <div 819 + className="bg-[var(--bg-primary)] rounded-2xl w-[90%] max-w-[340px] max-h-[80vh] overflow-hidden shadow-2xl animate-scaleIn" 820 + onClick={(e) => e.stopPropagation()} 821 + > 822 + <div className="flex items-center justify-between p-4 border-b border-[var(--border)]"> 823 + <h3 className="text-sm font-bold">Add to Collection</h3> 824 + <button 825 + onClick={() => setCollectionModalItem(null)} 826 + className="p-1.5 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] rounded-lg transition-all" 827 + > 828 + <X size={16} /> 829 + </button> 830 + </div> 831 + <div className="p-4 max-h-[300px] overflow-y-auto"> 832 + {loadingCollections ? ( 833 + <div className="flex items-center justify-center py-8"> 834 + <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 835 + </div> 836 + ) : collections.length === 0 ? ( 837 + <div className="text-center py-8 text-[var(--text-tertiary)]"> 838 + <Folder size={32} className="mx-auto mb-2 opacity-40" /> 839 + <p className="text-sm">No collections yet</p> 840 + <p className="text-xs mt-1">Create collections on margin.at</p> 841 + </div> 842 + ) : ( 843 + <div className="space-y-2"> 844 + {collections.map((col) => { 845 + const colUri = col.uri || col.id || ''; 846 + const isInCollection = containingCollections.has(colUri); 847 + const isAdding = addingToCollection === colUri; 848 + return ( 849 + <button 850 + key={colUri} 851 + onClick={() => !isInCollection && handleAddToCollection(colUri)} 852 + disabled={isAdding || isInCollection} 853 + className={`w-full text-left p-3 border rounded-xl transition-all flex items-center gap-3 ${ 854 + isInCollection 855 + ? 'bg-emerald-400/10 border-emerald-400/30 cursor-default' 856 + : 'bg-[var(--bg-card)] border-[var(--border)] hover:bg-[var(--bg-hover)] hover:border-[var(--accent)]' 857 + }`} 858 + > 859 + <div 860 + className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 text-base ${ 861 + isInCollection 862 + ? 'bg-emerald-400/15 text-emerald-400' 863 + : 'bg-[var(--accent)]/15 text-[var(--accent)]' 864 + }`} 865 + > 866 + <CollectionIcon icon={col.icon} size={16} /> 867 + </div> 868 + <div className="flex-1 min-w-0"> 869 + <div className="text-sm font-medium">{col.name}</div> 870 + </div> 871 + {isAdding ? ( 872 + <div className="animate-spin rounded-full h-4 w-4 border-2 border-[var(--accent)] border-t-transparent" /> 873 + ) : isInCollection ? ( 874 + <Check size={16} className="text-emerald-400" /> 875 + ) : ( 876 + <FolderPlus size={16} className="text-[var(--text-tertiary)]" /> 877 + )} 878 + </button> 879 + ); 880 + })} 881 + </div> 882 + )} 883 + </div> 884 + </div> 885 + </div> 886 + )} 887 + 888 + <footer className="flex items-center justify-between px-4 py-2.5 border-t border-[var(--border)] bg-[var(--bg-secondary)]"> 889 + <button 890 + onClick={() => browser.tabs.create({ url: apiUrl })} 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 + > 893 + Open Margin <ExternalLink size={12} /> 894 + </button> 895 + <button 896 + onClick={() => setShowSettings(true)} 897 + className="p-2 text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent-subtle)] rounded-lg transition-all" 898 + title="Settings" 899 + > 900 + <Settings size={16} /> 901 + </button> 902 + </footer> 903 + </div> 904 + ); 905 + } 906 + 907 + function AnnotationCard({ 908 + item, 909 + formatDate, 910 + onAddToCollection, 911 + }: { 912 + item: Annotation; 913 + formatDate: (d?: string) => string; 914 + onAddToCollection?: () => void; 915 + }) { 916 + const author = item.author || item.creator || {}; 917 + const handle = author.handle || 'User'; 918 + const text = item.body?.value || item.text || ''; 919 + const selector = item.target?.selector; 920 + const quote = selector?.exact || ''; 921 + const isHighlight = (item as any).type === 'Highlight'; 922 + 923 + return ( 924 + <div className="px-4 py-4 hover:bg-[var(--bg-hover)] transition-colors"> 925 + <div className="flex items-start gap-3"> 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 + {author.avatar ? ( 928 + <img src={author.avatar} alt={handle} className="w-full h-full object-cover" /> 929 + ) : ( 930 + handle[0]?.toUpperCase() || 'U' 931 + )} 932 + </div> 933 + <div className="flex-1 min-w-0"> 934 + <div className="flex items-center gap-2 mb-1.5"> 935 + <span className="text-sm font-semibold hover:text-[var(--accent)] cursor-pointer transition-colors"> 936 + @{handle} 937 + </span> 938 + <span className="text-[11px] text-[var(--text-tertiary)]"> 939 + {formatDate(item.created || item.createdAt)} 940 + </span> 941 + {isHighlight && ( 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 + <Highlighter size={10} /> Highlight 944 + </span> 945 + )} 946 + {onAddToCollection && ( 947 + <button 948 + onClick={(e) => { 949 + e.stopPropagation(); 950 + onAddToCollection(); 951 + }} 952 + className="ml-auto p-1 text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent-subtle)] rounded transition-all" 953 + title="Add to collection" 954 + > 955 + <FolderPlus size={14} /> 956 + </button> 957 + )} 958 + </div> 959 + 960 + {quote && ( 961 + <div 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 + onClick={async (e) => { 964 + e.stopPropagation(); 965 + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); 966 + if (tab?.id) { 967 + browser.tabs.sendMessage(tab.id, { type: 'SCROLL_TO_TEXT', text: quote }); 968 + window.close(); 969 + } 970 + }} 971 + title="Jump to text on page" 972 + > 973 + "{quote.length > 100 ? quote.slice(0, 100) + '...' : quote}" 974 + </div> 975 + )} 976 + 977 + {text && ( 978 + <div className="text-[13px] leading-relaxed text-[var(--text-primary)]">{text}</div> 979 + )} 980 + </div> 981 + </div> 982 + </div> 983 + ); 984 + } 985 + 986 + export default App;
+913
extension/src/components/sidepanel/App.tsx
··· 1 + import { useState, useEffect } from 'react'; 2 + import { sendMessage } from '@/utils/messaging'; 3 + import { themeItem, overlayEnabledItem } from '@/utils/storage'; 4 + import type { MarginSession, Annotation, Bookmark, Highlight, Collection } from '@/utils/types'; 5 + import { APP_URL } from '@/utils/types'; 6 + import CollectionIcon from '@/components/CollectionIcon'; 7 + 8 + type Tab = 'page' | 'bookmarks' | 'highlights' | 'collections'; 9 + 10 + const Icons = { 11 + settings: ( 12 + <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 13 + <path d="M12 15a3 3 0 100-6 3 3 0 000 6z" /> 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 + </svg> 16 + ), 17 + globe: ( 18 + <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 19 + <circle cx="12" cy="12" r="10" /> 20 + <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" /> 21 + <path d="M2 12h20" /> 22 + </svg> 23 + ), 24 + bookmark: ( 25 + <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 26 + <path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" /> 27 + </svg> 28 + ), 29 + highlighter: ( 30 + <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 31 + <path d="m9 11-6 6v3h9l3-3" /> 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 + </svg> 34 + ), 35 + folder: ( 36 + <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 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 + </svg> 39 + ), 40 + folderPlus: ( 41 + <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 42 + <path d="M12 10v6" /> 43 + <path d="M9 13h6" /> 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 + </svg> 46 + ), 47 + check: ( 48 + <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 49 + <polyline points="20 6 9 17 4 12" /> 50 + </svg> 51 + ), 52 + chevronRight: ( 53 + <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 54 + <polyline points="9 18 15 12 9 6" /> 55 + </svg> 56 + ), 57 + sparkles: ( 58 + <svg className="w-6 h-6" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 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 + </svg> 61 + ), 62 + externalLink: ( 63 + <svg className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 64 + <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /> 65 + <polyline points="15 3 21 3 21 9" /> 66 + <line x1="10" x2="21" y1="14" y2="3" /> 67 + </svg> 68 + ), 69 + x: ( 70 + <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> 71 + <path d="M18 6 6 18" /> 72 + <path d="m6 6 12 12" /> 73 + </svg> 74 + ), 75 + }; 76 + 77 + export function App() { 78 + const [session, setSession] = useState<MarginSession | null>(null); 79 + const [loading, setLoading] = useState(true); 80 + const [activeTab, setActiveTab] = useState<Tab>('page'); 81 + const [annotations, setAnnotations] = useState<Annotation[]>([]); 82 + const [bookmarks, setBookmarks] = useState<Bookmark[]>([]); 83 + const [highlights, setHighlights] = useState<Highlight[]>([]); 84 + const [collections, setCollections] = useState<Collection[]>([]); 85 + const [loadingAnnotations, setLoadingAnnotations] = useState(false); 86 + const [loadingBookmarks, setLoadingBookmarks] = useState(false); 87 + const [loadingHighlights, setLoadingHighlights] = useState(false); 88 + const [loadingCollections, setLoadingCollections] = useState(false); 89 + const [currentUrl, setCurrentUrl] = useState(''); 90 + const [currentTitle, setCurrentTitle] = useState(''); 91 + const [text, setText] = useState(''); 92 + const [posting, setPosting] = useState(false); 93 + const [bookmarking, setBookmarking] = useState(false); 94 + const [bookmarked, setBookmarked] = useState(false); 95 + const [theme, setTheme] = useState<'light' | 'dark' | 'system'>('system'); 96 + const [overlayEnabled, setOverlayEnabled] = useState(true); 97 + const [showSettings, setShowSettings] = useState(false); 98 + const [collectionModalItem, setCollectionModalItem] = useState<string | null>(null); 99 + const [addingToCollection, setAddingToCollection] = useState<string | null>(null); 100 + const [containingCollections, setContainingCollections] = useState<Set<string>>(new Set()); 101 + 102 + useEffect(() => { 103 + checkSession(); 104 + loadCurrentTab(); 105 + loadSettings(); 106 + 107 + browser.tabs.onActivated.addListener(loadCurrentTab); 108 + browser.tabs.onUpdated.addListener((_, info) => { 109 + if (info.url) loadCurrentTab(); 110 + }); 111 + 112 + return () => { 113 + browser.tabs.onActivated.removeListener(loadCurrentTab); 114 + }; 115 + }, []); 116 + 117 + useEffect(() => { 118 + if (session?.authenticated && currentUrl) { 119 + if (activeTab === 'page') loadAnnotations(); 120 + else if (activeTab === 'bookmarks') loadBookmarks(); 121 + else if (activeTab === 'highlights') loadHighlights(); 122 + else if (activeTab === 'collections') loadCollections(); 123 + } 124 + }, [activeTab, session, currentUrl]); 125 + 126 + async function loadSettings() { 127 + const t = await themeItem.getValue(); 128 + setTheme(t); 129 + applyTheme(t); 130 + 131 + const overlay = await overlayEnabledItem.getValue(); 132 + setOverlayEnabled(overlay); 133 + 134 + themeItem.watch((newTheme) => { 135 + setTheme(newTheme); 136 + applyTheme(newTheme); 137 + }); 138 + 139 + overlayEnabledItem.watch((enabled) => { 140 + setOverlayEnabled(enabled); 141 + }); 142 + } 143 + 144 + function applyTheme(t: string) { 145 + document.body.classList.remove('light', 'dark'); 146 + if (t === 'system') { 147 + if (window.matchMedia('(prefers-color-scheme: light)').matches) { 148 + document.body.classList.add('light'); 149 + } 150 + } else { 151 + document.body.classList.add(t); 152 + } 153 + } 154 + 155 + async function checkSession() { 156 + try { 157 + const result = await sendMessage('checkSession', undefined); 158 + setSession(result); 159 + } catch (error) { 160 + console.error('Session check error:', error); 161 + setSession({ authenticated: false }); 162 + } finally { 163 + setLoading(false); 164 + } 165 + } 166 + 167 + async function loadCurrentTab() { 168 + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); 169 + if (tab?.url) { 170 + setCurrentUrl(tab.url); 171 + setCurrentTitle(tab.title || ''); 172 + } 173 + } 174 + 175 + async function loadAnnotations() { 176 + if (!currentUrl) return; 177 + setLoadingAnnotations(true); 178 + try { 179 + let result = await sendMessage('getCachedAnnotations', { url: currentUrl }); 180 + 181 + if (!result) { 182 + result = await sendMessage('getAnnotations', { url: currentUrl }); 183 + } 184 + 185 + const filtered = (result || []).filter((item: any) => item.type !== 'Bookmark'); 186 + setAnnotations(filtered); 187 + 188 + const isBookmarked = (result || []).some( 189 + (item: any) => item.type === 'Bookmark' && item.creator?.did === session?.did 190 + ); 191 + setBookmarked(isBookmarked); 192 + } catch (error) { 193 + console.error('Load annotations error:', error); 194 + } finally { 195 + setLoadingAnnotations(false); 196 + } 197 + } 198 + 199 + async function loadBookmarks() { 200 + if (!session?.did) return; 201 + setLoadingBookmarks(true); 202 + try { 203 + const result = await sendMessage('getUserBookmarks', { did: session.did }); 204 + setBookmarks(result || []); 205 + } catch (error) { 206 + console.error('Load bookmarks error:', error); 207 + } finally { 208 + setLoadingBookmarks(false); 209 + } 210 + } 211 + 212 + async function loadHighlights() { 213 + if (!session?.did) return; 214 + setLoadingHighlights(true); 215 + try { 216 + const result = await sendMessage('getUserHighlights', { did: session.did }); 217 + setHighlights(result || []); 218 + } catch (error) { 219 + console.error('Load highlights error:', error); 220 + } finally { 221 + setLoadingHighlights(false); 222 + } 223 + } 224 + 225 + async function loadCollections() { 226 + if (!session?.did) return; 227 + setLoadingCollections(true); 228 + try { 229 + const result = await sendMessage('getUserCollections', { did: session.did }); 230 + setCollections(result || []); 231 + } catch (error) { 232 + console.error('Load collections error:', error); 233 + } finally { 234 + setLoadingCollections(false); 235 + } 236 + } 237 + 238 + async function handlePost() { 239 + if (!text.trim()) return; 240 + setPosting(true); 241 + try { 242 + const result = await sendMessage('createAnnotation', { 243 + url: currentUrl, 244 + text: text.trim(), 245 + title: currentTitle, 246 + }); 247 + if (result.success) { 248 + setText(''); 249 + loadAnnotations(); 250 + } else { 251 + alert('Failed to post annotation'); 252 + } 253 + } catch (error) { 254 + console.error('Post error:', error); 255 + alert('Error posting annotation'); 256 + } finally { 257 + setPosting(false); 258 + } 259 + } 260 + 261 + async function handleBookmark() { 262 + setBookmarking(true); 263 + try { 264 + const result = await sendMessage('createBookmark', { 265 + url: currentUrl, 266 + title: currentTitle, 267 + }); 268 + if (result.success) { 269 + setBookmarked(true); 270 + } else { 271 + alert('Failed to bookmark page'); 272 + } 273 + } catch (error) { 274 + console.error('Bookmark error:', error); 275 + alert('Error bookmarking page'); 276 + } finally { 277 + setBookmarking(false); 278 + } 279 + } 280 + 281 + async function handleThemeChange(newTheme: 'light' | 'dark' | 'system') { 282 + await themeItem.setValue(newTheme); 283 + } 284 + 285 + async function handleOverlayToggle() { 286 + await overlayEnabledItem.setValue(!overlayEnabled); 287 + } 288 + 289 + async function openCollectionModal(itemUri: string) { 290 + setCollectionModalItem(itemUri); 291 + setContainingCollections(new Set()); 292 + 293 + if (collections.length === 0) { 294 + await loadCollections(); 295 + } 296 + 297 + try { 298 + const itemCollectionUris = await sendMessage('getItemCollections', { 299 + annotationUri: itemUri, 300 + }); 301 + setContainingCollections(new Set(itemCollectionUris)); 302 + } catch (error) { 303 + console.error('Failed to get item collections:', error); 304 + } 305 + } 306 + 307 + async function handleAddToCollection(collectionUri: string) { 308 + if (!collectionModalItem) return; 309 + 310 + if (containingCollections.has(collectionUri)) { 311 + setCollectionModalItem(null); 312 + return; 313 + } 314 + 315 + setAddingToCollection(collectionUri); 316 + try { 317 + const result = await sendMessage('addToCollection', { 318 + collectionUri, 319 + annotationUri: collectionModalItem, 320 + }); 321 + if (result.success) { 322 + setContainingCollections((prev) => new Set([...prev, collectionUri])); 323 + } else { 324 + alert('Failed to add to collection'); 325 + } 326 + } catch (error) { 327 + console.error('Add to collection error:', error); 328 + alert('Error adding to collection'); 329 + } finally { 330 + setAddingToCollection(null); 331 + } 332 + } 333 + 334 + function formatDate(dateString?: string) { 335 + if (!dateString) return ''; 336 + try { 337 + return new Date(dateString).toLocaleDateString(); 338 + } catch { 339 + return dateString; 340 + } 341 + } 342 + 343 + if (loading) { 344 + return ( 345 + <div className="flex items-center justify-center h-screen"> 346 + <div className="animate-spin rounded-full h-8 w-8 border-2 border-[var(--accent)] border-t-transparent" /> 347 + </div> 348 + ); 349 + } 350 + 351 + if (!session?.authenticated) { 352 + return ( 353 + <div className="flex flex-col items-center justify-center h-screen p-8 text-center"> 354 + <img src="/icons/logo.svg" alt="Margin" className="w-16 h-16 mb-6" /> 355 + <h2 className="text-xl font-bold mb-3">Welcome to Margin</h2> 356 + <p className="text-[var(--text-secondary)] mb-8 max-w-xs leading-relaxed"> 357 + Sign in to annotate, bookmark, and highlight web pages using the AT Protocol. 358 + </p> 359 + <button 360 + onClick={() => browser.tabs.create({ url: `${APP_URL}/login` })} 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 + > 363 + Sign In 364 + </button> 365 + </div> 366 + ); 367 + } 368 + 369 + return ( 370 + <div className="flex flex-col h-screen"> 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 + <div className="flex items-center gap-2 flex-shrink-0"> 373 + <img src="/icons/logo.svg" alt="Margin" className="w-5 h-5" /> 374 + <span className="font-bold text-sm hidden sm:inline">Margin</span> 375 + </div> 376 + <div className="flex items-center gap-2 min-w-0"> 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 + @{session.handle} 379 + </div> 380 + <button 381 + onClick={() => setShowSettings(!showSettings)} 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 + title="Settings" 384 + > 385 + {Icons.settings} 386 + </button> 387 + </div> 388 + </header> 389 + 390 + {showSettings && ( 391 + <div className="p-4 border-b border-[var(--border)] bg-[var(--bg-card)] animate-slideDown"> 392 + <h3 className="text-sm font-bold mb-4 flex items-center gap-2"> 393 + {Icons.settings} 394 + Settings 395 + </h3> 396 + 397 + <div className="mb-5"> 398 + <label className="text-xs font-medium text-[var(--text-secondary)] mb-2 block"> 399 + Theme 400 + </label> 401 + <div className="flex gap-2"> 402 + {(['system', 'light', 'dark'] as const).map((t) => ( 403 + <button 404 + key={t} 405 + onClick={() => handleThemeChange(t)} 406 + className={`flex-1 px-3 py-2 text-xs font-medium rounded-lg transition-all ${ 407 + theme === t 408 + ? 'bg-[var(--accent)] text-white' 409 + : 'bg-[var(--bg-elevated)] border border-[var(--border)] hover:bg-[var(--bg-hover)]' 410 + }`} 411 + > 412 + {t.charAt(0).toUpperCase() + t.slice(1)} 413 + </button> 414 + ))} 415 + </div> 416 + </div> 417 + 418 + <div className="flex items-center justify-between py-2"> 419 + <div> 420 + <span className="text-sm font-medium">Page Overlay</span> 421 + <p className="text-xs text-[var(--text-tertiary)]">Show highlights on pages</p> 422 + </div> 423 + <button 424 + onClick={handleOverlayToggle} 425 + className={`w-11 h-6 rounded-full transition-colors relative ${ 426 + overlayEnabled 427 + ? 'bg-[var(--accent)]' 428 + : 'bg-[var(--bg-hover)] border border-[var(--border)]' 429 + }`} 430 + > 431 + <div 432 + className={`absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform ${ 433 + overlayEnabled ? 'left-6' : 'left-1' 434 + }`} 435 + /> 436 + </button> 437 + </div> 438 + 439 + <div className="mt-4 pt-4 border-t border-[var(--border)]"> 440 + <button 441 + onClick={() => browser.tabs.create({ url: APP_URL })} 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 + > 444 + Open Margin App {Icons.externalLink} 445 + </button> 446 + </div> 447 + </div> 448 + )} 449 + 450 + <div className="flex border-b border-[var(--border)] bg-[var(--bg-secondary)]"> 451 + {(['page', 'bookmarks', 'highlights', 'collections'] as Tab[]).map((tab) => { 452 + const icons = { 453 + page: Icons.globe, 454 + bookmarks: Icons.bookmark, 455 + highlights: Icons.highlighter, 456 + collections: Icons.folder, 457 + }; 458 + const labels = { 459 + page: 'Page', 460 + bookmarks: 'Bookmarks', 461 + highlights: 'Highlights', 462 + collections: 'Collections', 463 + }; 464 + return ( 465 + <button 466 + key={tab} 467 + onClick={() => setActiveTab(tab)} 468 + className={`flex-1 py-2.5 text-[11px] font-medium flex items-center justify-center gap-1 border-b-2 transition-all ${ 469 + activeTab === tab 470 + ? 'border-[var(--accent)] text-[var(--accent)]' 471 + : 'border-transparent text-[var(--text-tertiary)] hover:text-[var(--text-secondary)]' 472 + }`} 473 + > 474 + {icons[tab]} 475 + <span className="hidden min-[340px]:inline">{labels[tab]}</span> 476 + </button> 477 + ); 478 + })} 479 + </div> 480 + 481 + <div className="flex-1 overflow-y-auto"> 482 + {activeTab === 'page' && ( 483 + <div className="p-4"> 484 + <div className="mb-4 p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl"> 485 + <div className="flex items-start gap-3"> 486 + <div className="w-10 h-10 rounded-lg bg-[var(--bg-hover)] flex items-center justify-center flex-shrink-0 overflow-hidden"> 487 + {currentUrl ? ( 488 + <img 489 + src={`https://www.google.com/s2/favicons?domain=${new URL(currentUrl).hostname}&sz=64`} 490 + alt="" 491 + className="w-6 h-6" 492 + onError={(e) => { 493 + (e.target as HTMLImageElement).style.display = 'none'; 494 + (e.target as HTMLImageElement).nextElementSibling?.classList.remove( 495 + 'hidden' 496 + ); 497 + }} 498 + /> 499 + ) : null} 500 + <span className={`text-[var(--text-tertiary)] ${currentUrl ? 'hidden' : ''}`}> 501 + {Icons.globe} 502 + </span> 503 + </div> 504 + <div className="flex-1 min-w-0"> 505 + <div className="text-sm font-semibold truncate">{currentTitle || 'Untitled'}</div> 506 + <div className="text-xs text-[var(--text-tertiary)] truncate"> 507 + {currentUrl ? new URL(currentUrl).hostname : ''} 508 + </div> 509 + </div> 510 + <button 511 + onClick={handleBookmark} 512 + disabled={bookmarking || bookmarked} 513 + className={`p-2 rounded-lg transition-all ${ 514 + bookmarked 515 + ? 'bg-emerald-400/15 text-emerald-400' 516 + : 'bg-[var(--bg-hover)] hover:bg-[var(--accent)]/15 text-[var(--text-secondary)] hover:text-[var(--accent)]' 517 + }`} 518 + > 519 + {bookmarked ? Icons.check : Icons.bookmark} 520 + </button> 521 + </div> 522 + </div> 523 + 524 + <div className="mb-6"> 525 + <textarea 526 + value={text} 527 + onChange={(e) => setText(e.target.value)} 528 + placeholder="Share your thoughts on this page..." 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 + /> 531 + <div className="flex gap-2 mt-3"> 532 + <button 533 + onClick={handlePost} 534 + disabled={posting || !text.trim()} 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 + > 537 + {posting ? 'Posting...' : 'Post Annotation'} 538 + </button> 539 + </div> 540 + </div> 541 + 542 + <div className="flex items-center justify-between text-xs text-[var(--text-tertiary)] mb-3"> 543 + <span className="font-semibold"> 544 + {annotations.length} annotation{annotations.length !== 1 ? 's' : ''} 545 + </span> 546 + </div> 547 + 548 + {loadingAnnotations ? ( 549 + <div className="flex items-center justify-center py-16"> 550 + <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 551 + </div> 552 + ) : annotations.length === 0 ? ( 553 + <div className="text-center py-16 text-[var(--text-tertiary)]"> 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 + {Icons.sparkles} 556 + </div> 557 + <p className="font-medium mb-1">No annotations yet</p> 558 + <p className="text-xs">Be the first to annotate</p> 559 + </div> 560 + ) : ( 561 + <div className="space-y-3"> 562 + {annotations.map((item) => ( 563 + <AnnotationCard 564 + key={item.uri || item.id} 565 + item={item} 566 + formatDate={formatDate} 567 + onAddToCollection={() => openCollectionModal(item.uri || item.id || '')} 568 + /> 569 + ))} 570 + </div> 571 + )} 572 + </div> 573 + )} 574 + 575 + {activeTab === 'bookmarks' && ( 576 + <div className="p-4"> 577 + {loadingBookmarks ? ( 578 + <div className="flex items-center justify-center py-16"> 579 + <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 580 + </div> 581 + ) : bookmarks.length === 0 ? ( 582 + <div className="text-center py-16 text-[var(--text-tertiary)]"> 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 + {Icons.bookmark} 585 + </div> 586 + <p className="font-medium mb-1">No bookmarks yet</p> 587 + <p className="text-xs">Save pages to read later</p> 588 + </div> 589 + ) : ( 590 + <div className="space-y-2"> 591 + {bookmarks.map((item) => ( 592 + <div 593 + key={item.uri || item.id} 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 + > 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 + {Icons.bookmark} 598 + </div> 599 + <a 600 + href={item.source} 601 + target="_blank" 602 + rel="noopener noreferrer" 603 + className="flex-1 min-w-0" 604 + > 605 + <div className="text-sm font-medium truncate group-hover:text-[var(--accent)] transition-colors"> 606 + {item.title || item.source} 607 + </div> 608 + <div className="text-xs text-[var(--text-tertiary)] truncate"> 609 + {item.source ? new URL(item.source).hostname : ''} 610 + </div> 611 + </a> 612 + <button 613 + onClick={(e) => { 614 + e.stopPropagation(); 615 + if (item.uri) openCollectionModal(item.uri); 616 + }} 617 + className="p-1.5 rounded-lg text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors" 618 + title="Add to collection" 619 + > 620 + {Icons.folderPlus} 621 + </button> 622 + <a 623 + href={item.source} 624 + target="_blank" 625 + rel="noopener noreferrer" 626 + className="text-[var(--text-tertiary)] group-hover:text-[var(--accent)]" 627 + > 628 + {Icons.chevronRight} 629 + </a> 630 + </div> 631 + ))} 632 + </div> 633 + )} 634 + </div> 635 + )} 636 + 637 + {activeTab === 'highlights' && ( 638 + <div className="p-4"> 639 + {loadingHighlights ? ( 640 + <div className="flex items-center justify-center py-16"> 641 + <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 642 + </div> 643 + ) : highlights.length === 0 ? ( 644 + <div className="text-center py-16 text-[var(--text-tertiary)]"> 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 + {Icons.highlighter} 647 + </div> 648 + <p className="font-medium mb-1">No highlights yet</p> 649 + <p className="text-xs">Select text on any page to highlight</p> 650 + </div> 651 + ) : ( 652 + <div className="space-y-3"> 653 + {highlights.map((item) => ( 654 + <div 655 + key={item.uri || item.id} 656 + className="p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all group" 657 + > 658 + {item.target?.selector?.exact && ( 659 + <div 660 + className="text-sm leading-relaxed border-l-3 pl-3 mb-3 cursor-pointer" 661 + style={{ 662 + borderColor: item.color || '#fbbf24', 663 + background: `linear-gradient(90deg, ${item.color || '#fbbf24'}10, transparent)`, 664 + }} 665 + onClick={() => { 666 + if (item.target?.source) { 667 + browser.tabs.create({ url: item.target.source }); 668 + } 669 + }} 670 + > 671 + " 672 + {item.target.selector.exact.length > 150 673 + ? item.target.selector.exact.slice(0, 150) + '...' 674 + : item.target.selector.exact} 675 + " 676 + </div> 677 + )} 678 + <div className="flex items-center justify-between text-xs text-[var(--text-tertiary)]"> 679 + <span className="flex items-center gap-1.5"> 680 + {Icons.globe}{' '} 681 + {item.target?.source ? new URL(item.target.source).hostname : ''} 682 + </span> 683 + <div className="flex items-center gap-2"> 684 + <button 685 + onClick={(e) => { 686 + e.stopPropagation(); 687 + if (item.uri) openCollectionModal(item.uri); 688 + }} 689 + className="p-1 rounded text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors" 690 + title="Add to collection" 691 + > 692 + {Icons.folderPlus} 693 + </button> 694 + <button 695 + onClick={() => { 696 + if (item.target?.source) { 697 + browser.tabs.create({ url: item.target.source }); 698 + } 699 + }} 700 + className="group-hover:text-[var(--accent)]" 701 + > 702 + {Icons.chevronRight} 703 + </button> 704 + </div> 705 + </div> 706 + </div> 707 + ))} 708 + </div> 709 + )} 710 + </div> 711 + )} 712 + 713 + {activeTab === 'collections' && ( 714 + <div className="p-4"> 715 + {loadingCollections ? ( 716 + <div className="flex items-center justify-center py-16"> 717 + <div className="animate-spin rounded-full h-6 w-6 border-2 border-[var(--accent)] border-t-transparent" /> 718 + </div> 719 + ) : collections.length === 0 ? ( 720 + <div className="text-center py-16 text-[var(--text-tertiary)]"> 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 + {Icons.folder} 723 + </div> 724 + <p className="font-medium mb-1">No collections yet</p> 725 + <p className="text-xs">Organize your bookmarks into collections</p> 726 + </div> 727 + ) : ( 728 + <div className="space-y-2"> 729 + {collections.map((item) => ( 730 + <button 731 + key={item.uri || item.id} 732 + onClick={() => 733 + browser.tabs.create({ 734 + url: `${APP_URL}/collection/${encodeURIComponent(item.uri || item.id || '')}`, 735 + }) 736 + } 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 + > 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 + <CollectionIcon icon={item.icon} size={18} /> 741 + </div> 742 + <div className="flex-1 min-w-0"> 743 + <div className="text-sm font-medium group-hover:text-[var(--accent)] transition-colors"> 744 + {item.name} 745 + </div> 746 + {item.description && ( 747 + <div className="text-xs text-[var(--text-tertiary)] truncate"> 748 + {item.description} 749 + </div> 750 + )} 751 + </div> 752 + <div className="text-[var(--text-tertiary)] group-hover:text-[var(--accent)]"> 753 + {Icons.chevronRight} 754 + </div> 755 + </button> 756 + ))} 757 + </div> 758 + )} 759 + </div> 760 + )} 761 + </div> 762 + 763 + {collectionModalItem && ( 764 + <div 765 + className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4" 766 + onClick={() => setCollectionModalItem(null)} 767 + > 768 + <div 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 + onClick={(e) => e.stopPropagation()} 771 + > 772 + <div className="flex items-center justify-between p-4 border-b border-[var(--border)]"> 773 + <h3 className="font-semibold">Add to Collection</h3> 774 + <button 775 + onClick={() => setCollectionModalItem(null)} 776 + className="p-1 rounded-lg hover:bg-[var(--bg-hover)] text-[var(--text-tertiary)]" 777 + > 778 + {Icons.x} 779 + </button> 780 + </div> 781 + <div className="p-2 max-h-[50vh] overflow-y-auto"> 782 + {collections.length === 0 ? ( 783 + <div className="text-center py-8 text-[var(--text-tertiary)]"> 784 + <p className="text-sm">No collections yet</p> 785 + <p className="text-xs mt-1">Create a collection on the web app first</p> 786 + </div> 787 + ) : ( 788 + collections.map((col) => { 789 + const colUri = col.uri || col.id || ''; 790 + const isAdding = addingToCollection === colUri; 791 + const isInCollection = containingCollections.has(colUri); 792 + return ( 793 + <button 794 + key={colUri} 795 + onClick={() => !isInCollection && handleAddToCollection(colUri)} 796 + disabled={isAdding || isInCollection} 797 + className={`w-full text-left p-3 rounded-xl flex items-center gap-3 transition-all ${ 798 + isInCollection 799 + ? 'bg-emerald-400/10 cursor-default' 800 + : 'hover:bg-[var(--bg-hover)]' 801 + }`} 802 + > 803 + <div 804 + className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 text-base ${ 805 + isInCollection 806 + ? 'bg-emerald-400/15 text-emerald-400' 807 + : 'bg-[var(--accent)]/15 text-[var(--accent)]' 808 + }`} 809 + > 810 + <CollectionIcon icon={col.icon} size={16} /> 811 + </div> 812 + <div className="flex-1 min-w-0"> 813 + <div className="text-sm font-medium truncate">{col.name}</div> 814 + {col.description && ( 815 + <div className="text-xs text-[var(--text-tertiary)] truncate"> 816 + {col.description} 817 + </div> 818 + )} 819 + </div> 820 + {isAdding ? ( 821 + <div className="animate-spin rounded-full h-4 w-4 border-2 border-[var(--accent)] border-t-transparent" /> 822 + ) : isInCollection ? ( 823 + <span className="text-emerald-400">{Icons.check}</span> 824 + ) : ( 825 + Icons.folderPlus 826 + )} 827 + </button> 828 + ); 829 + }) 830 + )} 831 + </div> 832 + </div> 833 + </div> 834 + )} 835 + </div> 836 + ); 837 + } 838 + 839 + function AnnotationCard({ 840 + item, 841 + formatDate, 842 + onAddToCollection, 843 + }: { 844 + item: Annotation; 845 + formatDate: (d?: string) => string; 846 + onAddToCollection?: () => void; 847 + }) { 848 + const author = item.author || item.creator || {}; 849 + const handle = author.handle || 'User'; 850 + const text = item.body?.value || item.text || ''; 851 + const selector = item.target?.selector; 852 + const quote = selector?.exact || ''; 853 + const isHighlight = (item as any).type === 'Highlight'; 854 + 855 + return ( 856 + <div className="p-4 bg-[var(--bg-card)] border border-[var(--border)] rounded-xl hover:bg-[var(--bg-hover)] transition-all"> 857 + <div className="flex items-start gap-3"> 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 + {author.avatar ? ( 860 + <img src={author.avatar} alt={handle} className="w-full h-full object-cover" /> 861 + ) : ( 862 + handle[0]?.toUpperCase() || 'U' 863 + )} 864 + </div> 865 + <div className="flex-1 min-w-0"> 866 + <div className="flex items-center gap-2 mb-2"> 867 + <span className="text-sm font-semibold">@{handle}</span> 868 + <span className="text-xs text-[var(--text-tertiary)]"> 869 + {formatDate(item.created || item.createdAt)} 870 + </span> 871 + {isHighlight && ( 872 + <span className="text-[10px] font-semibold bg-amber-400/15 text-amber-400 px-2 py-0.5 rounded-full"> 873 + Highlight 874 + </span> 875 + )} 876 + {onAddToCollection && ( 877 + <button 878 + onClick={(e) => { 879 + e.stopPropagation(); 880 + onAddToCollection(); 881 + }} 882 + className="ml-auto p-1 rounded text-[var(--text-tertiary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 transition-colors" 883 + title="Add to collection" 884 + > 885 + {Icons.folderPlus} 886 + </button> 887 + )} 888 + </div> 889 + 890 + {quote && ( 891 + <div 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 + onClick={async (e) => { 894 + e.stopPropagation(); 895 + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); 896 + if (tab?.id) { 897 + browser.tabs.sendMessage(tab.id, { type: 'SCROLL_TO_TEXT', text: quote }); 898 + } 899 + }} 900 + title="Jump to text on page" 901 + > 902 + "{quote.length > 150 ? quote.slice(0, 150) + '...' : quote}" 903 + </div> 904 + )} 905 + 906 + {text && <div className="text-sm leading-relaxed">{text}</div>} 907 + </div> 908 + </div> 909 + </div> 910 + ); 911 + } 912 + 913 + export default App;
+366
extension/src/entrypoints/background.ts
··· 1 + import { onMessage } from '@/utils/messaging'; 2 + import type { Annotation } from '@/utils/types'; 3 + import { 4 + checkSession, 5 + getAnnotations, 6 + createAnnotation, 7 + createBookmark, 8 + createHighlight, 9 + getUserBookmarks, 10 + getUserHighlights, 11 + getUserCollections, 12 + addToCollection, 13 + getItemCollections, 14 + getReplies, 15 + createReply, 16 + } from '@/utils/api'; 17 + import { overlayEnabledItem, apiUrlItem } from '@/utils/storage'; 18 + 19 + export default defineBackground(() => { 20 + console.log('Margin extension loaded'); 21 + 22 + const annotationCache = new Map<string, { annotations: Annotation[]; timestamp: number }>(); 23 + const CACHE_TTL = 60000; 24 + 25 + onMessage('checkSession', async () => { 26 + return await checkSession(); 27 + }); 28 + 29 + onMessage('getAnnotations', async ({ data }) => { 30 + return await getAnnotations(data.url); 31 + }); 32 + 33 + onMessage('createAnnotation', async ({ data }) => { 34 + return await createAnnotation(data); 35 + }); 36 + 37 + onMessage('createBookmark', async ({ data }) => { 38 + return await createBookmark(data); 39 + }); 40 + 41 + onMessage('createHighlight', async ({ data }) => { 42 + return await createHighlight(data); 43 + }); 44 + 45 + onMessage('getUserBookmarks', async ({ data }) => { 46 + return await getUserBookmarks(data.did); 47 + }); 48 + 49 + onMessage('getUserHighlights', async ({ data }) => { 50 + return await getUserHighlights(data.did); 51 + }); 52 + 53 + onMessage('getUserCollections', async ({ data }) => { 54 + return await getUserCollections(data.did); 55 + }); 56 + 57 + onMessage('addToCollection', async ({ data }) => { 58 + return await addToCollection(data.collectionUri, data.annotationUri); 59 + }); 60 + 61 + onMessage('getItemCollections', async ({ data }) => { 62 + return await getItemCollections(data.annotationUri); 63 + }); 64 + 65 + onMessage('getReplies', async ({ data }) => { 66 + return await getReplies(data.uri); 67 + }); 68 + 69 + onMessage('createReply', async ({ data }) => { 70 + return await createReply(data); 71 + }); 72 + 73 + onMessage('getOverlayEnabled', async () => { 74 + return await overlayEnabledItem.getValue(); 75 + }); 76 + 77 + onMessage('openAppUrl', async ({ data }) => { 78 + const apiUrl = await apiUrlItem.getValue(); 79 + await browser.tabs.create({ url: `${apiUrl}${data.path}` }); 80 + }); 81 + 82 + onMessage('updateBadge', async ({ data }) => { 83 + const { count, tabId } = data; 84 + const text = count > 0 ? String(count > 99 ? '99+' : count) : ''; 85 + 86 + if (tabId) { 87 + await browser.action.setBadgeText({ text, tabId }); 88 + await browser.action.setBadgeBackgroundColor({ color: '#6366f1', tabId }); 89 + } else { 90 + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); 91 + if (tab?.id) { 92 + await browser.action.setBadgeText({ text, tabId: tab.id }); 93 + await browser.action.setBadgeBackgroundColor({ color: '#6366f1', tabId: tab.id }); 94 + } 95 + } 96 + }); 97 + 98 + browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => { 99 + if (changeInfo.status === 'loading' && changeInfo.url) { 100 + await browser.action.setBadgeText({ text: '', tabId }); 101 + } 102 + }); 103 + 104 + onMessage('cacheAnnotations', async ({ data }) => { 105 + const { url, annotations } = data; 106 + const normalizedUrl = normalizeUrl(url); 107 + annotationCache.set(normalizedUrl, { annotations, timestamp: Date.now() }); 108 + }); 109 + 110 + onMessage('getCachedAnnotations', async ({ data }) => { 111 + const normalizedUrl = normalizeUrl(data.url); 112 + const cached = annotationCache.get(normalizedUrl); 113 + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { 114 + return cached.annotations; 115 + } 116 + return null; 117 + }); 118 + 119 + function normalizeUrl(url: string): string { 120 + try { 121 + const u = new URL(url); 122 + u.hash = ''; 123 + const path = u.pathname.replace(/\/$/, '') || '/'; 124 + return `${u.origin}${path}${u.search}`; 125 + } catch { 126 + return url; 127 + } 128 + } 129 + 130 + async function ensureContextMenus() { 131 + await browser.contextMenus.removeAll(); 132 + 133 + browser.contextMenus.create({ 134 + id: 'margin-annotate', 135 + title: 'Annotate "%s"', 136 + contexts: ['selection'], 137 + }); 138 + 139 + browser.contextMenus.create({ 140 + id: 'margin-highlight', 141 + title: 'Highlight "%s"', 142 + contexts: ['selection'], 143 + }); 144 + 145 + browser.contextMenus.create({ 146 + id: 'margin-bookmark', 147 + title: 'Bookmark this page', 148 + contexts: ['page'], 149 + }); 150 + 151 + browser.contextMenus.create({ 152 + id: 'margin-open-sidebar', 153 + title: 'Open Margin Sidebar', 154 + contexts: ['page', 'selection', 'link'], 155 + }); 156 + } 157 + 158 + browser.runtime.onInstalled.addListener(async () => { 159 + await ensureContextMenus(); 160 + }); 161 + 162 + browser.runtime.onStartup.addListener(async () => { 163 + await ensureContextMenus(); 164 + }); 165 + 166 + browser.contextMenus.onClicked.addListener((info, tab) => { 167 + if (info.menuItemId === 'margin-open-sidebar') { 168 + const browserAny = browser as any; 169 + if (browserAny.sidePanel && tab?.windowId) { 170 + browserAny.sidePanel.open({ windowId: tab.windowId }).catch((err: Error) => { 171 + console.error('Could not open side panel:', err); 172 + }); 173 + } else if (browserAny.sidebarAction) { 174 + browserAny.sidebarAction.open().catch((err: Error) => { 175 + console.warn('Could not open Firefox sidebar:', err); 176 + }); 177 + } 178 + return; 179 + } 180 + 181 + handleContextMenuAction(info, tab); 182 + }); 183 + 184 + async function handleContextMenuAction(info: any, tab?: any) { 185 + const apiUrl = await apiUrlItem.getValue(); 186 + 187 + if (info.menuItemId === 'margin-bookmark' && tab?.url) { 188 + const session = await checkSession(); 189 + if (!session.authenticated) { 190 + await browser.tabs.create({ url: `${apiUrl}/login` }); 191 + return; 192 + } 193 + 194 + const result = await createBookmark({ 195 + url: tab.url, 196 + title: tab.title, 197 + }); 198 + 199 + if (result.success) { 200 + showNotification('Margin', 'Page bookmarked!'); 201 + } 202 + return; 203 + } 204 + 205 + if (info.menuItemId === 'margin-annotate' && tab?.url && info.selectionText) { 206 + const session = await checkSession(); 207 + if (!session.authenticated) { 208 + await browser.tabs.create({ url: `${apiUrl}/login` }); 209 + return; 210 + } 211 + 212 + try { 213 + await browser.tabs.sendMessage(tab.id!, { 214 + type: 'SHOW_INLINE_ANNOTATE', 215 + data: { 216 + url: tab.url, 217 + title: tab.title, 218 + selector: { 219 + type: 'TextQuoteSelector', 220 + exact: info.selectionText, 221 + }, 222 + }, 223 + }); 224 + } catch { 225 + let composeUrl = `${apiUrl}/new?url=${encodeURIComponent(tab.url)}`; 226 + composeUrl += `&selector=${encodeURIComponent( 227 + JSON.stringify({ 228 + type: 'TextQuoteSelector', 229 + exact: info.selectionText, 230 + }) 231 + )}`; 232 + await browser.tabs.create({ url: composeUrl }); 233 + } 234 + return; 235 + } 236 + 237 + if (info.menuItemId === 'margin-highlight' && tab?.url && info.selectionText) { 238 + const session = await checkSession(); 239 + if (!session.authenticated) { 240 + await browser.tabs.create({ url: `${apiUrl}/login` }); 241 + return; 242 + } 243 + 244 + const result = await createHighlight({ 245 + url: tab.url, 246 + title: tab.title, 247 + selector: { 248 + type: 'TextQuoteSelector', 249 + exact: info.selectionText, 250 + }, 251 + }); 252 + 253 + if (result.success) { 254 + showNotification('Margin', 'Text highlighted!'); 255 + try { 256 + await browser.tabs.sendMessage(tab.id!, { type: 'REFRESH_ANNOTATIONS' }); 257 + } catch { 258 + /* ignore */ 259 + } 260 + } 261 + return; 262 + } 263 + } 264 + 265 + function showNotification(title: string, message: string) { 266 + const browserAny = browser as any; 267 + if (browserAny.notifications) { 268 + browserAny.notifications.create({ 269 + type: 'basic', 270 + iconUrl: '/icons/icon-128.png', 271 + title, 272 + message, 273 + }); 274 + } 275 + } 276 + 277 + browser.commands?.onCommand.addListener((command) => { 278 + if (command === 'open-sidebar') { 279 + const browserAny = browser as any; 280 + if (browserAny.sidePanel) { 281 + chrome.windows.getCurrent((win) => { 282 + if (win?.id) { 283 + browserAny.sidePanel.open({ windowId: win.id }).catch((err: Error) => { 284 + console.error('Could not open side panel:', err); 285 + }); 286 + } 287 + }); 288 + } else if (browserAny.sidebarAction) { 289 + browserAny.sidebarAction.open().catch((err: Error) => { 290 + console.warn('Could not open Firefox sidebar:', err); 291 + }); 292 + } 293 + return; 294 + } 295 + 296 + handleCommandAsync(command); 297 + }); 298 + 299 + async function handleCommandAsync(command: string) { 300 + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); 301 + 302 + if (command === 'toggle-overlay') { 303 + const current = await overlayEnabledItem.getValue(); 304 + await overlayEnabledItem.setValue(!current); 305 + return; 306 + } 307 + 308 + if (command === 'bookmark-page' && tab?.url) { 309 + const session = await checkSession(); 310 + if (!session.authenticated) { 311 + const apiUrl = await apiUrlItem.getValue(); 312 + await browser.tabs.create({ url: `${apiUrl}/login` }); 313 + return; 314 + } 315 + 316 + const result = await createBookmark({ 317 + url: tab.url, 318 + title: tab.title, 319 + }); 320 + 321 + if (result.success) { 322 + showNotification('Margin', 'Page bookmarked!'); 323 + } 324 + return; 325 + } 326 + 327 + if ((command === 'annotate-selection' || command === 'highlight-selection') && tab?.id) { 328 + try { 329 + const selection = (await browser.tabs.sendMessage(tab.id, { type: 'GET_SELECTION' })) as 330 + | { text?: string } 331 + | undefined; 332 + if (!selection?.text) return; 333 + 334 + const session = await checkSession(); 335 + if (!session.authenticated) { 336 + const apiUrl = await apiUrlItem.getValue(); 337 + await browser.tabs.create({ url: `${apiUrl}/login` }); 338 + return; 339 + } 340 + 341 + if (command === 'annotate-selection') { 342 + await browser.tabs.sendMessage(tab.id, { 343 + type: 'SHOW_INLINE_ANNOTATE', 344 + data: { selector: { exact: selection.text } }, 345 + }); 346 + } else if (command === 'highlight-selection') { 347 + const result = await createHighlight({ 348 + url: tab.url!, 349 + title: tab.title, 350 + selector: { 351 + type: 'TextQuoteSelector', 352 + exact: selection.text, 353 + }, 354 + }); 355 + 356 + if (result.success) { 357 + showNotification('Margin', 'Text highlighted!'); 358 + await browser.tabs.sendMessage(tab.id, { type: 'REFRESH_ANNOTATIONS' }); 359 + } 360 + } 361 + } catch (err) { 362 + console.error('Error handling keyboard shortcut:', err); 363 + } 364 + } 365 + } 366 + });
+754
extension/src/entrypoints/content.ts
··· 1 + import { sendMessage } from '@/utils/messaging'; 2 + import { overlayEnabledItem, themeItem } from '@/utils/storage'; 3 + import { overlayStyles } from '@/utils/overlay-styles'; 4 + import { DOMTextMatcher } from '@/utils/text-matcher'; 5 + import type { Annotation } from '@/utils/types'; 6 + import { APP_URL } from '@/utils/types'; 7 + 8 + const Icons = { 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + }; 18 + 19 + export default defineContentScript({ 20 + matches: ['<all_urls>'], 21 + runAt: 'document_idle', 22 + cssInjectionMode: 'ui', 23 + 24 + async main(ctx) { 25 + let overlayHost: HTMLElement | null = null; 26 + let shadowRoot: ShadowRoot | null = null; 27 + let popoverEl: HTMLElement | null = null; 28 + let hoverIndicator: HTMLElement | null = null; 29 + let composeModal: HTMLElement | null = null; 30 + let activeItems: Array<{ range: Range; item: Annotation }> = []; 31 + let cachedMatcher: DOMTextMatcher | null = null; 32 + const injectedStyles = new Set<string>(); 33 + let overlayEnabled = true; 34 + 35 + function initOverlay() { 36 + overlayHost = document.createElement('div'); 37 + overlayHost.id = 'margin-overlay-host'; 38 + overlayHost.style.cssText = ` 39 + position: absolute; top: 0; left: 0; width: 100%; 40 + height: 0; overflow: visible; 41 + pointer-events: none; z-index: 2147483647; 42 + `; 43 + if (document.body) { 44 + document.body.appendChild(overlayHost); 45 + } else { 46 + document.documentElement.appendChild(overlayHost); 47 + } 48 + 49 + shadowRoot = overlayHost.attachShadow({ mode: 'open' }); 50 + 51 + const styleEl = document.createElement('style'); 52 + styleEl.textContent = overlayStyles; 53 + shadowRoot.appendChild(styleEl); 54 + const overlayContainer = document.createElement('div'); 55 + overlayContainer.className = 'margin-overlay'; 56 + overlayContainer.id = 'margin-overlay-container'; 57 + shadowRoot.appendChild(overlayContainer); 58 + 59 + document.addEventListener('mousemove', handleMouseMove); 60 + document.addEventListener('click', handleDocumentClick, true); 61 + document.addEventListener('keydown', handleKeyDown); 62 + } 63 + if (document.body) { 64 + initOverlay(); 65 + } else { 66 + document.addEventListener('DOMContentLoaded', initOverlay); 67 + } 68 + 69 + overlayEnabledItem.getValue().then((enabled) => { 70 + overlayEnabled = enabled; 71 + if (!enabled && overlayHost) { 72 + overlayHost.style.display = 'none'; 73 + sendMessage('updateBadge', { count: 0 }); 74 + } else { 75 + applyTheme(); 76 + if ('requestIdleCallback' in window) { 77 + requestIdleCallback(() => fetchAnnotations(), { timeout: 2000 }); 78 + } else { 79 + setTimeout(() => fetchAnnotations(), 100); 80 + } 81 + } 82 + }); 83 + 84 + ctx.onInvalidated(() => { 85 + document.removeEventListener('mousemove', handleMouseMove); 86 + document.removeEventListener('click', handleDocumentClick, true); 87 + document.removeEventListener('keydown', handleKeyDown); 88 + overlayHost?.remove(); 89 + }); 90 + 91 + async function applyTheme() { 92 + if (!overlayHost) return; 93 + const theme = await themeItem.getValue(); 94 + overlayHost.classList.remove('light', 'dark'); 95 + if (theme === 'system' || !theme) { 96 + if (window.matchMedia('(prefers-color-scheme: light)').matches) { 97 + overlayHost.classList.add('light'); 98 + } 99 + } else { 100 + overlayHost.classList.add(theme); 101 + } 102 + } 103 + 104 + themeItem.watch((newTheme) => { 105 + if (overlayHost) { 106 + overlayHost.classList.remove('light', 'dark'); 107 + if (newTheme === 'system') { 108 + if (window.matchMedia('(prefers-color-scheme: light)').matches) { 109 + overlayHost.classList.add('light'); 110 + } 111 + } else { 112 + overlayHost.classList.add(newTheme); 113 + } 114 + } 115 + }); 116 + 117 + overlayEnabledItem.watch((enabled) => { 118 + overlayEnabled = enabled; 119 + if (overlayHost) { 120 + overlayHost.style.display = enabled ? '' : 'none'; 121 + if (enabled) { 122 + fetchAnnotations(); 123 + } else { 124 + activeItems = []; 125 + if (typeof CSS !== 'undefined' && CSS.highlights) { 126 + CSS.highlights.clear(); 127 + } 128 + sendMessage('updateBadge', { count: 0 }); 129 + } 130 + } 131 + }); 132 + 133 + function handleKeyDown(e: KeyboardEvent) { 134 + if (e.key === 'Escape') { 135 + if (composeModal) { 136 + composeModal.remove(); 137 + composeModal = null; 138 + } 139 + if (popoverEl) { 140 + popoverEl.remove(); 141 + popoverEl = null; 142 + } 143 + } 144 + } 145 + 146 + function showComposeModal(quoteText: string) { 147 + if (!shadowRoot) return; 148 + 149 + const container = shadowRoot.getElementById('margin-overlay-container'); 150 + if (!container) return; 151 + 152 + if (composeModal) composeModal.remove(); 153 + 154 + composeModal = document.createElement('div'); 155 + composeModal.className = 'inline-compose-modal'; 156 + 157 + const left = Math.max(20, (window.innerWidth - 380) / 2); 158 + const top = Math.max(60, window.innerHeight * 0.2); 159 + 160 + composeModal.style.left = `${left}px`; 161 + composeModal.style.top = `${top}px`; 162 + 163 + const truncatedQuote = quoteText.length > 150 ? quoteText.slice(0, 150) + '...' : quoteText; 164 + 165 + composeModal.innerHTML = ` 166 + <div class="compose-header"> 167 + <span class="compose-title">New Annotation</span> 168 + <button class="compose-close">${Icons.close}</button> 169 + </div> 170 + <div class="compose-body"> 171 + <div class="inline-compose-quote">"${escapeHtml(truncatedQuote)}"</div> 172 + <textarea class="inline-compose-textarea" placeholder="Write your annotation..."></textarea> 173 + </div> 174 + <div class="compose-footer"> 175 + <button class="btn-cancel">Cancel</button> 176 + <button class="btn-submit">Post</button> 177 + </div> 178 + `; 179 + 180 + composeModal.querySelector('.compose-close')?.addEventListener('click', () => { 181 + composeModal?.remove(); 182 + composeModal = null; 183 + }); 184 + 185 + composeModal.querySelector('.btn-cancel')?.addEventListener('click', () => { 186 + composeModal?.remove(); 187 + composeModal = null; 188 + }); 189 + 190 + const textarea = composeModal.querySelector( 191 + '.inline-compose-textarea' 192 + ) as HTMLTextAreaElement; 193 + const submitBtn = composeModal.querySelector('.btn-submit') as HTMLButtonElement; 194 + 195 + submitBtn.addEventListener('click', async () => { 196 + const text = textarea?.value.trim(); 197 + if (!text) return; 198 + 199 + submitBtn.disabled = true; 200 + submitBtn.textContent = 'Posting...'; 201 + 202 + try { 203 + await sendMessage('createAnnotation', { 204 + url: window.location.href, 205 + title: document.title, 206 + text, 207 + selector: { type: 'TextQuoteSelector', exact: quoteText }, 208 + }); 209 + 210 + showToast('Annotation created!', 'success'); 211 + composeModal?.remove(); 212 + composeModal = null; 213 + 214 + setTimeout(() => fetchAnnotations(), 500); 215 + } catch (error) { 216 + console.error('Failed to create annotation:', error); 217 + showToast('Failed to create annotation', 'error'); 218 + submitBtn.disabled = false; 219 + submitBtn.textContent = 'Post'; 220 + } 221 + }); 222 + 223 + container.appendChild(composeModal); 224 + setTimeout(() => textarea?.focus(), 100); 225 + } 226 + browser.runtime.onMessage.addListener((message: any) => { 227 + if (message.type === 'SHOW_INLINE_ANNOTATE' && message.data?.selector?.exact) { 228 + showComposeModal(message.data.selector.exact); 229 + } 230 + if (message.type === 'REFRESH_ANNOTATIONS') { 231 + fetchAnnotations(); 232 + } 233 + if (message.type === 'SCROLL_TO_TEXT' && message.text) { 234 + scrollToText(message.text); 235 + } 236 + if (message.type === 'GET_SELECTION') { 237 + const selection = window.getSelection(); 238 + const text = selection?.toString().trim() || ''; 239 + return Promise.resolve({ text }); 240 + } 241 + }); 242 + 243 + function scrollToText(text: string) { 244 + if (!text || text.length < 10) return; 245 + 246 + const searchText = text.slice(0, 150); 247 + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null); 248 + 249 + let node: Text | null; 250 + while ((node = walker.nextNode() as Text | null)) { 251 + const content = node.textContent || ''; 252 + const index = content.indexOf(searchText.slice(0, 50)); 253 + if (index !== -1) { 254 + const range = document.createRange(); 255 + range.setStart(node, index); 256 + range.setEnd(node, Math.min(index + searchText.length, content.length)); 257 + 258 + const rect = range.getBoundingClientRect(); 259 + const scrollY = window.scrollY + rect.top - window.innerHeight / 3; 260 + 261 + window.scrollTo({ top: scrollY, behavior: 'smooth' }); 262 + 263 + const highlight = document.createElement('mark'); 264 + highlight.style.cssText = 265 + 'background: #6366f1; color: white; padding: 2px 0; border-radius: 2px; transition: background 0.5s;'; 266 + range.surroundContents(highlight); 267 + 268 + setTimeout(() => { 269 + highlight.style.background = 'transparent'; 270 + highlight.style.color = 'inherit'; 271 + setTimeout(() => { 272 + const parent = highlight.parentNode; 273 + if (parent) { 274 + parent.replaceChild( 275 + document.createTextNode(highlight.textContent || ''), 276 + highlight 277 + ); 278 + parent.normalize(); 279 + } 280 + }, 500); 281 + }, 1500); 282 + 283 + return; 284 + } 285 + } 286 + } 287 + 288 + function showToast(message: string, type: 'success' | 'error' = 'success') { 289 + if (!shadowRoot) return; 290 + 291 + const container = shadowRoot.getElementById('margin-overlay-container'); 292 + if (!container) return; 293 + 294 + container.querySelectorAll('.margin-toast').forEach((el) => el.remove()); 295 + 296 + const toast = document.createElement('div'); 297 + toast.className = `margin-toast ${type === 'success' ? 'toast-success' : ''}`; 298 + toast.innerHTML = ` 299 + <span class="toast-icon">${type === 'success' ? Icons.check : Icons.close}</span> 300 + <span>${message}</span> 301 + `; 302 + 303 + container.appendChild(toast); 304 + 305 + setTimeout(() => { 306 + toast.classList.add('toast-out'); 307 + setTimeout(() => toast.remove(), 200); 308 + }, 2500); 309 + } 310 + 311 + async function fetchAnnotations(retryCount = 0) { 312 + if (!overlayEnabled) { 313 + sendMessage('updateBadge', { count: 0 }); 314 + return; 315 + } 316 + 317 + try { 318 + const _citedUrls = Array.from(document.querySelectorAll('[cite]')) 319 + .map((el) => el.getAttribute('cite')) 320 + .filter((url): url is string => !!url && url.startsWith('http')); 321 + 322 + const annotations = await sendMessage('getAnnotations', { url: window.location.href }); 323 + 324 + sendMessage('updateBadge', { count: annotations?.length || 0 }); 325 + 326 + if (annotations) { 327 + sendMessage('cacheAnnotations', { url: window.location.href, annotations }); 328 + } 329 + 330 + if (annotations && annotations.length > 0) { 331 + renderBadges(annotations); 332 + } else if (retryCount < 3) { 333 + setTimeout(() => fetchAnnotations(retryCount + 1), 1000 * (retryCount + 1)); 334 + } 335 + } catch (error) { 336 + console.error('Failed to fetch annotations:', error); 337 + if (retryCount < 3) { 338 + setTimeout(() => fetchAnnotations(retryCount + 1), 1000 * (retryCount + 1)); 339 + } 340 + } 341 + } 342 + 343 + function renderBadges(annotations: Annotation[]) { 344 + if (!shadowRoot) return; 345 + 346 + activeItems = []; 347 + const rangesByColor: Record<string, Range[]> = {}; 348 + 349 + if (!cachedMatcher) { 350 + cachedMatcher = new DOMTextMatcher(); 351 + } 352 + const matcher = cachedMatcher; 353 + 354 + annotations.forEach((item) => { 355 + const selector = item.target?.selector || item.selector; 356 + if (!selector?.exact) return; 357 + 358 + const range = matcher.findRange(selector.exact); 359 + if (range) { 360 + activeItems.push({ range, item }); 361 + 362 + const isHighlight = (item as any).type === 'Highlight'; 363 + const defaultColor = isHighlight ? '#f59e0b' : '#6366f1'; 364 + const color = item.color || defaultColor; 365 + if (!rangesByColor[color]) rangesByColor[color] = []; 366 + rangesByColor[color].push(range); 367 + } 368 + }); 369 + 370 + if (typeof CSS !== 'undefined' && CSS.highlights) { 371 + CSS.highlights.clear(); 372 + for (const [color, ranges] of Object.entries(rangesByColor)) { 373 + const highlight = new Highlight(...ranges); 374 + const safeColor = color.replace(/[^a-zA-Z0-9]/g, ''); 375 + const name = `margin-hl-${safeColor}`; 376 + CSS.highlights.set(name, highlight); 377 + injectHighlightStyle(name, color); 378 + } 379 + } 380 + } 381 + 382 + function injectHighlightStyle(name: string, color: string) { 383 + if (injectedStyles.has(name)) return; 384 + const style = document.createElement('style'); 385 + style.textContent = ` 386 + ::highlight(${name}) { 387 + text-decoration: underline; 388 + text-decoration-color: ${color}; 389 + text-decoration-thickness: 2px; 390 + text-underline-offset: 2px; 391 + cursor: pointer; 392 + } 393 + `; 394 + document.head.appendChild(style); 395 + injectedStyles.add(name); 396 + } 397 + 398 + function handleMouseMove(e: MouseEvent) { 399 + if (!overlayEnabled || !overlayHost) return; 400 + 401 + const x = e.clientX; 402 + const y = e.clientY; 403 + 404 + const foundItems: Array<{ range: Range; item: Annotation; rect: DOMRect }> = []; 405 + let firstRange: Range | null = null; 406 + 407 + for (const { range, item } of activeItems) { 408 + const rects = range.getClientRects(); 409 + for (const rect of rects) { 410 + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { 411 + let container: Node | null = range.commonAncestorContainer; 412 + if (container.nodeType === Node.TEXT_NODE) { 413 + container = container.parentNode; 414 + } 415 + 416 + if ( 417 + container && 418 + ((e.target as Node).contains(container) || container.contains(e.target as Node)) 419 + ) { 420 + if (!firstRange) firstRange = range; 421 + if (!foundItems.some((f) => f.item === item)) { 422 + foundItems.push({ range, item, rect }); 423 + } 424 + } 425 + break; 426 + } 427 + } 428 + } 429 + 430 + if (foundItems.length > 0 && shadowRoot) { 431 + document.body.style.cursor = 'pointer'; 432 + 433 + if (!hoverIndicator) { 434 + const container = shadowRoot.getElementById('margin-overlay-container'); 435 + if (container) { 436 + hoverIndicator = document.createElement('div'); 437 + hoverIndicator.className = 'margin-hover-indicator'; 438 + container.appendChild(hoverIndicator); 439 + } 440 + } 441 + 442 + if (hoverIndicator && firstRange) { 443 + const authorsMap = new Map<string, any>(); 444 + foundItems.forEach(({ item }) => { 445 + const author = item.author || item.creator || {}; 446 + const id = author.did || author.handle || 'unknown'; 447 + if (!authorsMap.has(id)) { 448 + authorsMap.set(id, author); 449 + } 450 + }); 451 + 452 + const uniqueAuthors = Array.from(authorsMap.values()); 453 + const maxShow = 3; 454 + const displayAuthors = uniqueAuthors.slice(0, maxShow); 455 + const overflow = uniqueAuthors.length - maxShow; 456 + 457 + let html = displayAuthors 458 + .map((author, i) => { 459 + const avatar = author.avatar; 460 + const handle = author.handle || 'U'; 461 + const marginLeft = i === 0 ? '0' : '-8px'; 462 + 463 + if (avatar) { 464 + return `<img src="${avatar}" style="width: 24px; height: 24px; border-radius: 50%; object-fit: cover; border: 2px solid #09090b; margin-left: ${marginLeft};">`; 465 + } else { 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 + } 468 + }) 469 + .join(''); 470 + 471 + if (overflow > 0) { 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 + } 474 + 475 + hoverIndicator.innerHTML = html; 476 + 477 + const firstRect = firstRange.getClientRects()[0]; 478 + const totalWidth = 479 + Math.min(uniqueAuthors.length, maxShow + (overflow > 0 ? 1 : 0)) * 18 + 8; 480 + const leftPos = firstRect.left - totalWidth; 481 + const topPos = firstRect.top + firstRect.height / 2 - 12; 482 + 483 + hoverIndicator.style.left = `${leftPos}px`; 484 + hoverIndicator.style.top = `${topPos}px`; 485 + hoverIndicator.classList.add('visible'); 486 + } 487 + } else { 488 + document.body.style.cursor = ''; 489 + if (hoverIndicator) { 490 + hoverIndicator.classList.remove('visible'); 491 + } 492 + } 493 + } 494 + 495 + function handleDocumentClick(e: MouseEvent) { 496 + if (!overlayEnabled || !overlayHost) return; 497 + 498 + const x = e.clientX; 499 + const y = e.clientY; 500 + 501 + if (popoverEl) { 502 + const rect = popoverEl.getBoundingClientRect(); 503 + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { 504 + return; 505 + } 506 + } 507 + 508 + if (composeModal) { 509 + const rect = composeModal.getBoundingClientRect(); 510 + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { 511 + return; 512 + } 513 + composeModal.remove(); 514 + composeModal = null; 515 + } 516 + 517 + const clickedItems: Annotation[] = []; 518 + for (const { range, item } of activeItems) { 519 + const rects = range.getClientRects(); 520 + for (const rect of rects) { 521 + if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) { 522 + let container: Node | null = range.commonAncestorContainer; 523 + if (container.nodeType === Node.TEXT_NODE) { 524 + container = container.parentNode; 525 + } 526 + 527 + if ( 528 + container && 529 + ((e.target as Node).contains(container) || container.contains(e.target as Node)) 530 + ) { 531 + if (!clickedItems.includes(item)) { 532 + clickedItems.push(item); 533 + } 534 + } 535 + break; 536 + } 537 + } 538 + } 539 + 540 + if (clickedItems.length > 0) { 541 + e.preventDefault(); 542 + e.stopPropagation(); 543 + 544 + if (popoverEl) { 545 + const currentIds = popoverEl.dataset.itemIds; 546 + const newIds = clickedItems 547 + .map((i) => i.uri || i.id) 548 + .sort() 549 + .join(','); 550 + if (currentIds === newIds) { 551 + popoverEl.remove(); 552 + popoverEl = null; 553 + return; 554 + } 555 + } 556 + 557 + const firstItem = clickedItems[0]; 558 + const match = activeItems.find((x) => x.item === firstItem); 559 + if (match) { 560 + const rects = match.range.getClientRects(); 561 + if (rects.length > 0) { 562 + const rect = rects[0]; 563 + const top = rect.top + window.scrollY; 564 + const left = rect.left + window.scrollX; 565 + showPopover(clickedItems, top, left); 566 + } 567 + } 568 + } else { 569 + if (popoverEl) { 570 + popoverEl.remove(); 571 + popoverEl = null; 572 + } 573 + } 574 + } 575 + 576 + function showPopover(items: Annotation[], top: number, left: number) { 577 + if (!shadowRoot) return; 578 + if (popoverEl) popoverEl.remove(); 579 + 580 + const container = shadowRoot.getElementById('margin-overlay-container'); 581 + if (!container) return; 582 + 583 + popoverEl = document.createElement('div'); 584 + popoverEl.className = 'margin-popover'; 585 + 586 + const ids = items 587 + .map((i) => i.uri || i.id) 588 + .sort() 589 + .join(','); 590 + popoverEl.dataset.itemIds = ids; 591 + 592 + const popWidth = 320; 593 + const screenWidth = window.innerWidth; 594 + let finalLeft = left; 595 + if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20; 596 + if (finalLeft < 10) finalLeft = 10; 597 + 598 + popoverEl.style.top = `${top + 24}px`; 599 + popoverEl.style.left = `${finalLeft}px`; 600 + 601 + const count = items.length; 602 + const title = count === 1 ? 'Annotation' : `Annotations`; 603 + 604 + const contentHtml = items 605 + .map((item) => { 606 + const author = item.author || item.creator || {}; 607 + const handle = author.handle || 'User'; 608 + const avatar = author.avatar; 609 + const text = item.body?.value || item.text || ''; 610 + const id = item.id || item.uri; 611 + const isHighlight = (item as any).type === 'Highlight'; 612 + const createdAt = item.createdAt ? formatRelativeTime(item.createdAt) : ''; 613 + 614 + let avatarHtml = `<div class="comment-avatar">${handle[0]?.toUpperCase() || 'U'}</div>`; 615 + if (avatar) { 616 + avatarHtml = `<img src="${avatar}" class="comment-avatar" style="object-fit: cover;">`; 617 + } 618 + 619 + let bodyHtml = ''; 620 + if (isHighlight && !text) { 621 + bodyHtml = `<div class="highlight-badge">${Icons.highlightMarker} Highlighted</div>`; 622 + } else { 623 + bodyHtml = `<div class="comment-text">${escapeHtml(text)}</div>`; 624 + } 625 + 626 + return ` 627 + <div class="comment-item"> 628 + <div class="comment-header"> 629 + ${avatarHtml} 630 + <div class="comment-meta"> 631 + <span class="comment-handle">@${handle}</span> 632 + ${createdAt ? `<span class="comment-time">${createdAt}</span>` : ''} 633 + </div> 634 + </div> 635 + ${bodyHtml} 636 + <div class="comment-actions"> 637 + ${!isHighlight ? `<button class="comment-action-btn btn-reply" data-id="${id}">${Icons.reply} Reply</button>` : ''} 638 + <button class="comment-action-btn btn-share" data-id="${id}" data-text="${escapeHtml(text)}">${Icons.share} Share</button> 639 + </div> 640 + </div> 641 + `; 642 + }) 643 + .join(''); 644 + 645 + popoverEl.innerHTML = ` 646 + <div class="popover-header"> 647 + <span class="popover-title">${title} <span class="popover-count">${count}</span></span> 648 + <button class="popover-close">${Icons.close}</button> 649 + </div> 650 + <div class="popover-scroll-area"> 651 + ${contentHtml} 652 + </div> 653 + `; 654 + 655 + popoverEl.querySelector('.popover-close')?.addEventListener('click', (e) => { 656 + e.stopPropagation(); 657 + popoverEl?.remove(); 658 + popoverEl = null; 659 + }); 660 + 661 + popoverEl.querySelectorAll('.btn-reply').forEach((btn) => { 662 + btn.addEventListener('click', (e) => { 663 + e.stopPropagation(); 664 + const id = (btn as HTMLElement).getAttribute('data-id'); 665 + if (id) { 666 + window.open(`${APP_URL}/annotation/${encodeURIComponent(id)}`, '_blank'); 667 + } 668 + }); 669 + }); 670 + 671 + popoverEl.querySelectorAll('.btn-share').forEach((btn) => { 672 + btn.addEventListener('click', async (e) => { 673 + e.stopPropagation(); 674 + const id = (btn as HTMLElement).getAttribute('data-id'); 675 + const text = (btn as HTMLElement).getAttribute('data-text'); 676 + const url = `${APP_URL}/annotation/${encodeURIComponent(id || '')}`; 677 + const shareText = text ? `${text}\n${url}` : url; 678 + 679 + try { 680 + await navigator.clipboard.writeText(shareText); 681 + const originalHtml = btn.innerHTML; 682 + btn.innerHTML = `${Icons.check} Copied!`; 683 + setTimeout(() => (btn.innerHTML = originalHtml), 2000); 684 + } catch (err) { 685 + console.error('Failed to copy', err); 686 + } 687 + }); 688 + }); 689 + 690 + container.appendChild(popoverEl); 691 + } 692 + 693 + function formatRelativeTime(dateStr: string): string { 694 + const date = new Date(dateStr); 695 + const now = new Date(); 696 + const diffMs = now.getTime() - date.getTime(); 697 + const diffMins = Math.floor(diffMs / 60000); 698 + const diffHours = Math.floor(diffMs / 3600000); 699 + const diffDays = Math.floor(diffMs / 86400000); 700 + 701 + if (diffMins < 1) return 'just now'; 702 + if (diffMins < 60) return `${diffMins}m`; 703 + if (diffHours < 24) return `${diffHours}h`; 704 + if (diffDays < 7) return `${diffDays}d`; 705 + return date.toLocaleDateString(); 706 + } 707 + 708 + function escapeHtml(text: string): string { 709 + const div = document.createElement('div'); 710 + div.textContent = text; 711 + return div.innerHTML; 712 + } 713 + 714 + let lastUrl = window.location.href; 715 + function checkUrlChange() { 716 + if (window.location.href !== lastUrl) { 717 + lastUrl = window.location.href; 718 + onUrlChange(); 719 + } 720 + } 721 + 722 + function onUrlChange() { 723 + if (typeof CSS !== 'undefined' && CSS.highlights) { 724 + CSS.highlights.clear(); 725 + } 726 + activeItems = []; 727 + sendMessage('updateBadge', { count: 0 }); 728 + if (overlayEnabled) { 729 + fetchAnnotations(); 730 + } 731 + } 732 + 733 + window.addEventListener('popstate', onUrlChange); 734 + 735 + const originalPushState = history.pushState; 736 + const originalReplaceState = history.replaceState; 737 + 738 + history.pushState = function (...args) { 739 + originalPushState.apply(this, args); 740 + checkUrlChange(); 741 + }; 742 + 743 + history.replaceState = function (...args) { 744 + originalReplaceState.apply(this, args); 745 + checkUrlChange(); 746 + }; 747 + 748 + setInterval(checkUrlChange, 1000); 749 + 750 + window.addEventListener('load', () => { 751 + setTimeout(() => fetchAnnotations(), 500); 752 + }); 753 + }, 754 + });
+12
extension/src/entrypoints/popup/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en" class="popup"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Margin</title> 7 + </head> 8 + <body> 9 + <div id="root"></div> 10 + <script type="module" src="./main.tsx"></script> 11 + </body> 12 + </html>
+10
extension/src/entrypoints/popup/main.tsx
··· 1 + import React from 'react'; 2 + import ReactDOM from 'react-dom/client'; 3 + import App from '@/components/popup/App'; 4 + import '@/assets/styles.css'; 5 + 6 + ReactDOM.createRoot(document.getElementById('root')!).render( 7 + <React.StrictMode> 8 + <App /> 9 + </React.StrictMode> 10 + );
+13
extension/src/entrypoints/sidepanel/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Margin</title> 7 + <meta name="manifest.default_icon" content="{ '16': '/icons/icon-16.png', '32': '/icons/icon-32.png', '48': '/icons/icon-48.png' }" /> 8 + </head> 9 + <body> 10 + <div id="root"></div> 11 + <script type="module" src="./main.tsx"></script> 12 + </body> 13 + </html>
+10
extension/src/entrypoints/sidepanel/main.tsx
··· 1 + import React from 'react'; 2 + import ReactDOM from 'react-dom/client'; 3 + import App from '@/components/sidepanel/App'; 4 + import '@/assets/styles.css'; 5 + 6 + ReactDOM.createRoot(document.getElementById('root')!).render( 7 + <React.StrictMode> 8 + <App /> 9 + </React.StrictMode> 10 + );
+304
extension/src/utils/api.ts
··· 1 + import type { MarginSession, TextSelector } from './types'; 2 + import { apiUrlItem } from './storage'; 3 + 4 + async function getApiUrl(): Promise<string> { 5 + return await apiUrlItem.getValue(); 6 + } 7 + 8 + async function getSessionCookie(): Promise<string | null> { 9 + try { 10 + const apiUrl = await getApiUrl(); 11 + const cookie = await browser.cookies.get({ 12 + url: apiUrl, 13 + name: 'margin_session', 14 + }); 15 + return cookie?.value || null; 16 + } catch (error) { 17 + console.error('Get cookie error:', error); 18 + return null; 19 + } 20 + } 21 + 22 + export async function checkSession(): Promise<MarginSession> { 23 + try { 24 + const apiUrl = await getApiUrl(); 25 + const cookie = await getSessionCookie(); 26 + 27 + if (!cookie) { 28 + return { authenticated: false }; 29 + } 30 + 31 + const res = await fetch(`${apiUrl}/auth/session`, { 32 + headers: { 33 + 'X-Session-Token': cookie, 34 + }, 35 + }); 36 + 37 + if (!res.ok) { 38 + return { authenticated: false }; 39 + } 40 + 41 + const sessionData = await res.json(); 42 + 43 + if (!sessionData.did || !sessionData.handle) { 44 + return { authenticated: false }; 45 + } 46 + 47 + return { 48 + authenticated: true, 49 + did: sessionData.did, 50 + handle: sessionData.handle, 51 + accessJwt: sessionData.accessJwt, 52 + refreshJwt: sessionData.refreshJwt, 53 + }; 54 + } catch (error) { 55 + console.error('Session check error:', error); 56 + return { authenticated: false }; 57 + } 58 + } 59 + 60 + async function apiRequest(path: string, options: RequestInit = {}): Promise<Response> { 61 + const apiUrl = await getApiUrl(); 62 + const cookie = await getSessionCookie(); 63 + 64 + const headers: Record<string, string> = { 65 + 'Content-Type': 'application/json', 66 + ...(options.headers as Record<string, string>), 67 + }; 68 + 69 + if (cookie) { 70 + headers['X-Session-Token'] = cookie; 71 + } 72 + 73 + const apiPath = path.startsWith('/api') ? path : `/api${path}`; 74 + 75 + const response = await fetch(`${apiUrl}${apiPath}`, { 76 + ...options, 77 + headers, 78 + credentials: 'include', 79 + }); 80 + 81 + return response; 82 + } 83 + 84 + export async function getAnnotations(url: string, citedUrls: string[] = []) { 85 + try { 86 + const apiUrl = await getApiUrl(); 87 + const uniqueUrls = [...new Set([url, ...citedUrls])]; 88 + 89 + const fetchPromises = uniqueUrls.map(async (u) => { 90 + try { 91 + const res = await fetch(`${apiUrl}/api/targets?source=${encodeURIComponent(u)}`); 92 + if (!res.ok) return { annotations: [], highlights: [], bookmarks: [] }; 93 + return await res.json(); 94 + } catch { 95 + return { annotations: [], highlights: [], bookmarks: [] }; 96 + } 97 + }); 98 + 99 + const results = await Promise.all(fetchPromises); 100 + const allItems: any[] = []; 101 + const seenIds = new Set<string>(); 102 + 103 + results.forEach((data) => { 104 + const items = [ 105 + ...(data.annotations || []), 106 + ...(data.highlights || []), 107 + ...(data.bookmarks || []), 108 + ]; 109 + items.forEach((item: any) => { 110 + const id = item.uri || item.id; 111 + if (id && !seenIds.has(id)) { 112 + seenIds.add(id); 113 + allItems.push(item); 114 + } 115 + }); 116 + }); 117 + 118 + return allItems; 119 + } catch (error) { 120 + console.error('Get annotations error:', error); 121 + return []; 122 + } 123 + } 124 + 125 + export async function createAnnotation(data: { 126 + url: string; 127 + text: string; 128 + title?: string; 129 + selector?: TextSelector; 130 + }) { 131 + try { 132 + const res = await apiRequest('/annotations', { 133 + method: 'POST', 134 + body: JSON.stringify({ 135 + target: { 136 + source: data.url, 137 + selector: data.selector, 138 + }, 139 + body: { type: 'TextualBody', value: data.text, format: 'text/plain' }, 140 + motivation: 'commenting', 141 + title: data.title, 142 + }), 143 + }); 144 + 145 + if (!res.ok) { 146 + const error = await res.text(); 147 + return { success: false, error }; 148 + } 149 + 150 + return { success: true, data: await res.json() }; 151 + } catch (error) { 152 + return { success: false, error: String(error) }; 153 + } 154 + } 155 + 156 + export async function createBookmark(data: { url: string; title?: string }) { 157 + try { 158 + const res = await apiRequest('/bookmarks', { 159 + method: 'POST', 160 + body: JSON.stringify({ url: data.url, title: data.title }), 161 + }); 162 + 163 + if (!res.ok) { 164 + const error = await res.text(); 165 + return { success: false, error }; 166 + } 167 + 168 + return { success: true, data: await res.json() }; 169 + } catch (error) { 170 + return { success: false, error: String(error) }; 171 + } 172 + } 173 + 174 + export async function createHighlight(data: { 175 + url: string; 176 + title?: string; 177 + selector: TextSelector; 178 + color?: string; 179 + }) { 180 + try { 181 + const res = await apiRequest('/highlights', { 182 + method: 'POST', 183 + body: JSON.stringify({ 184 + url: data.url, 185 + title: data.title, 186 + selector: data.selector, 187 + color: data.color, 188 + }), 189 + }); 190 + 191 + if (!res.ok) { 192 + const error = await res.text(); 193 + return { success: false, error }; 194 + } 195 + 196 + return { success: true, data: await res.json() }; 197 + } catch (error) { 198 + return { success: false, error: String(error) }; 199 + } 200 + } 201 + 202 + export async function getUserBookmarks(did: string) { 203 + try { 204 + const res = await apiRequest(`/users/${did}/bookmarks`); 205 + if (!res.ok) return []; 206 + const data = await res.json(); 207 + return data.items || data || []; 208 + } catch (error) { 209 + console.error('Get bookmarks error:', error); 210 + return []; 211 + } 212 + } 213 + 214 + export async function getUserHighlights(did: string) { 215 + try { 216 + const res = await apiRequest(`/users/${did}/highlights`); 217 + if (!res.ok) return []; 218 + const data = await res.json(); 219 + return data.items || data || []; 220 + } catch (error) { 221 + console.error('Get highlights error:', error); 222 + return []; 223 + } 224 + } 225 + 226 + export async function getUserCollections(did: string) { 227 + try { 228 + const res = await apiRequest(`/collections?author=${encodeURIComponent(did)}`); 229 + if (!res.ok) return []; 230 + const data = await res.json(); 231 + return data.items || data || []; 232 + } catch (error) { 233 + console.error('Get collections error:', error); 234 + return []; 235 + } 236 + } 237 + 238 + export async function addToCollection(collectionUri: string, annotationUri: string) { 239 + try { 240 + const res = await apiRequest(`/collections/${encodeURIComponent(collectionUri)}/items`, { 241 + method: 'POST', 242 + body: JSON.stringify({ annotationUri, position: 0 }), 243 + }); 244 + 245 + if (!res.ok) { 246 + const error = await res.text(); 247 + return { success: false, error }; 248 + } 249 + 250 + return { success: true }; 251 + } catch (error) { 252 + return { success: false, error: String(error) }; 253 + } 254 + } 255 + 256 + export async function getItemCollections(annotationUri: string): Promise<string[]> { 257 + try { 258 + const res = await apiRequest( 259 + `/collections/containing?uri=${encodeURIComponent(annotationUri)}` 260 + ); 261 + if (!res.ok) return []; 262 + const data = await res.json(); 263 + return Array.isArray(data) ? data : []; 264 + } catch (error) { 265 + console.error('Get item collections error:', error); 266 + return []; 267 + } 268 + } 269 + 270 + export async function getReplies(uri: string) { 271 + try { 272 + const res = await apiRequest(`/annotations/${encodeURIComponent(uri)}/replies`); 273 + if (!res.ok) return []; 274 + const data = await res.json(); 275 + return data.items || data || []; 276 + } catch (error) { 277 + console.error('Get replies error:', error); 278 + return []; 279 + } 280 + } 281 + 282 + export async function createReply(data: { 283 + parentUri: string; 284 + parentCid: string; 285 + rootUri: string; 286 + rootCid: string; 287 + text: string; 288 + }) { 289 + try { 290 + const res = await apiRequest('/replies', { 291 + method: 'POST', 292 + body: JSON.stringify(data), 293 + }); 294 + 295 + if (!res.ok) { 296 + const error = await res.text(); 297 + return { success: false, error }; 298 + } 299 + 300 + return { success: true }; 301 + } catch (error) { 302 + return { success: false, error: String(error) }; 303 + } 304 + }
+61
extension/src/utils/messaging.ts
··· 1 + import { defineExtensionMessaging } from '@webext-core/messaging'; 2 + import type { 3 + MarginSession, 4 + Annotation, 5 + Bookmark, 6 + Highlight, 7 + Collection, 8 + TextSelector, 9 + } from './types'; 10 + 11 + interface ProtocolMap { 12 + checkSession(): MarginSession; 13 + 14 + getAnnotations(data: { url: string }): Annotation[]; 15 + createAnnotation(data: { url: string; text: string; title?: string; selector?: TextSelector }): { 16 + success: boolean; 17 + data?: Annotation; 18 + error?: string; 19 + }; 20 + 21 + createBookmark(data: { url: string; title?: string }): { 22 + success: boolean; 23 + data?: Bookmark; 24 + error?: string; 25 + }; 26 + getUserBookmarks(data: { did: string }): Bookmark[]; 27 + 28 + createHighlight(data: { url: string; title?: string; selector: TextSelector; color?: string }): { 29 + success: boolean; 30 + data?: Highlight; 31 + error?: string; 32 + }; 33 + getUserHighlights(data: { did: string }): Highlight[]; 34 + 35 + getUserCollections(data: { did: string }): Collection[]; 36 + addToCollection(data: { collectionUri: string; annotationUri: string }): { 37 + success: boolean; 38 + error?: string; 39 + }; 40 + getItemCollections(data: { annotationUri: string }): string[]; 41 + 42 + getReplies(data: { uri: string }): Annotation[]; 43 + createReply(data: { 44 + parentUri: string; 45 + parentCid: string; 46 + rootUri: string; 47 + rootCid: string; 48 + text: string; 49 + }): { success: boolean; error?: string }; 50 + 51 + getOverlayEnabled(): boolean; 52 + 53 + openAppUrl(data: { path: string }): void; 54 + 55 + updateBadge(data: { count: number; tabId?: number }): void; 56 + 57 + cacheAnnotations(data: { url: string; annotations: Annotation[] }): void; 58 + getCachedAnnotations(data: { url: string }): Annotation[] | null; 59 + } 60 + 61 + export const { sendMessage, onMessage } = defineExtensionMessaging<ProtocolMap>();
+576
extension/src/utils/overlay-styles.ts
··· 1 + export const overlayStyles = /* css */ ` 2 + :host { 3 + all: initial; 4 + --bg-primary: #0a0a0d; 5 + --bg-secondary: #121216; 6 + --bg-tertiary: #1a1a1f; 7 + --bg-card: #0f0f13; 8 + --bg-elevated: #18181d; 9 + --bg-hover: #1e1e24; 10 + 11 + --text-primary: #eaeaee; 12 + --text-secondary: #b7b6c5; 13 + --text-tertiary: #6e6d7a; 14 + --border: rgba(183, 182, 197, 0.12); 15 + 16 + --accent: #957a86; 17 + --accent-hover: #a98d98; 18 + --accent-subtle: rgba(149, 122, 134, 0.15); 19 + 20 + --highlight-yellow: #fbbf24; 21 + --highlight-green: #34d399; 22 + --highlight-blue: #60a5fa; 23 + --highlight-pink: #f472b6; 24 + --highlight-purple: #a78bfa; 25 + } 26 + 27 + :host(.light) { 28 + --bg-primary: #f8f8fa; 29 + --bg-secondary: #ffffff; 30 + --bg-tertiary: #f0f0f4; 31 + --bg-card: #ffffff; 32 + --bg-elevated: #ffffff; 33 + --bg-hover: #eeeef2; 34 + 35 + --text-primary: #18171c; 36 + --text-secondary: #5c495a; 37 + --text-tertiary: #8a8494; 38 + --border: rgba(92, 73, 90, 0.12); 39 + 40 + --accent: #7a5f6d; 41 + --accent-hover: #664e5b; 42 + --accent-subtle: rgba(149, 122, 134, 0.12); 43 + } 44 + 45 + .margin-overlay { 46 + position: absolute; 47 + top: 0; 48 + left: 0; 49 + width: 100%; 50 + height: 100%; 51 + pointer-events: none; 52 + } 53 + 54 + .margin-selection-toolbar { 55 + position: fixed; 56 + display: flex; 57 + align-items: center; 58 + gap: 2px; 59 + padding: 4px; 60 + background: var(--bg-card); 61 + border: 1px solid var(--border); 62 + border-radius: 10px; 63 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255,255,255,0.05); 64 + z-index: 2147483647; 65 + pointer-events: auto; 66 + font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 67 + opacity: 0; 68 + transform: translateY(8px) scale(0.95); 69 + animation: toolbar-in 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; 70 + } 71 + 72 + @keyframes toolbar-in { 73 + to { opacity: 1; transform: translateY(0) scale(1); } 74 + } 75 + 76 + .toolbar-btn { 77 + display: flex; 78 + align-items: center; 79 + justify-content: center; 80 + gap: 6px; 81 + padding: 8px 12px; 82 + background: transparent; 83 + border: none; 84 + border-radius: 6px; 85 + color: var(--text-primary); 86 + font-size: 12px; 87 + font-weight: 500; 88 + cursor: pointer; 89 + transition: all 0.15s ease; 90 + white-space: nowrap; 91 + } 92 + 93 + .toolbar-btn:hover { 94 + background: var(--bg-hover); 95 + } 96 + 97 + .toolbar-btn:active { 98 + transform: scale(0.96); 99 + } 100 + 101 + .toolbar-btn svg { 102 + width: 16px; 103 + height: 16px; 104 + flex-shrink: 0; 105 + } 106 + 107 + .toolbar-btn.highlight-btn { 108 + color: var(--highlight-yellow); 109 + } 110 + 111 + .toolbar-btn.highlight-btn:hover { 112 + background: rgba(251, 191, 36, 0.15); 113 + } 114 + 115 + .toolbar-divider { 116 + width: 1px; 117 + height: 20px; 118 + background: var(--border); 119 + margin: 0 2px; 120 + } 121 + 122 + .color-picker { 123 + position: absolute; 124 + top: 100%; 125 + left: 50%; 126 + transform: translateX(-50%); 127 + margin-top: 6px; 128 + display: flex; 129 + gap: 6px; 130 + padding: 8px; 131 + background: var(--bg-card); 132 + border: 1px solid var(--border); 133 + border-radius: 10px; 134 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25); 135 + animation: toolbar-in 0.15s ease forwards; 136 + } 137 + 138 + .color-dot { 139 + width: 24px; 140 + height: 24px; 141 + border-radius: 50%; 142 + border: 2px solid transparent; 143 + cursor: pointer; 144 + transition: all 0.15s ease; 145 + } 146 + 147 + .color-dot:hover { 148 + transform: scale(1.15); 149 + border-color: var(--text-primary); 150 + } 151 + 152 + .margin-popover { 153 + position: absolute; 154 + width: 320px; 155 + background: var(--bg-card); 156 + border: 1px solid var(--border); 157 + border-radius: 14px; 158 + padding: 0; 159 + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255,255,255,0.05); 160 + display: flex; 161 + flex-direction: column; 162 + pointer-events: auto; 163 + z-index: 2147483647; 164 + font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 165 + color: var(--text-primary); 166 + opacity: 0; 167 + transform: translateY(-8px) scale(0.96); 168 + animation: popover-in 0.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; 169 + max-height: 450px; 170 + overflow: hidden; 171 + } 172 + 173 + @keyframes popover-in { 174 + to { opacity: 1; transform: translateY(0) scale(1); } 175 + } 176 + 177 + .popover-header { 178 + padding: 12px 16px; 179 + border-bottom: 1px solid var(--border); 180 + display: flex; 181 + justify-content: space-between; 182 + align-items: center; 183 + background: var(--bg-primary); 184 + border-radius: 14px 14px 0 0; 185 + } 186 + 187 + .popover-title { 188 + font-weight: 600; 189 + font-size: 13px; 190 + color: var(--text-primary); 191 + display: flex; 192 + align-items: center; 193 + gap: 6px; 194 + } 195 + 196 + .popover-count { 197 + font-size: 11px; 198 + color: var(--text-tertiary); 199 + background: var(--bg-tertiary); 200 + padding: 2px 8px; 201 + border-radius: 10px; 202 + } 203 + 204 + .popover-close { 205 + background: none; 206 + border: none; 207 + color: var(--text-tertiary); 208 + cursor: pointer; 209 + padding: 4px; 210 + border-radius: 6px; 211 + display: flex; 212 + align-items: center; 213 + justify-content: center; 214 + transition: all 0.15s; 215 + } 216 + 217 + .popover-close:hover { 218 + background: var(--bg-hover); 219 + color: var(--text-primary); 220 + } 221 + 222 + .popover-close svg { 223 + width: 16px; 224 + height: 16px; 225 + } 226 + 227 + .popover-scroll-area { 228 + overflow-y: auto; 229 + max-height: 350px; 230 + overscroll-behavior: contain; 231 + scrollbar-width: thin; 232 + scrollbar-color: var(--bg-tertiary) transparent; 233 + } 234 + 235 + .popover-scroll-area::-webkit-scrollbar { 236 + width: 6px; 237 + } 238 + 239 + .popover-scroll-area::-webkit-scrollbar-track { 240 + background: transparent; 241 + margin: 4px 0; 242 + } 243 + 244 + .popover-scroll-area::-webkit-scrollbar-thumb { 245 + background: var(--bg-tertiary); 246 + border-radius: 3px; 247 + } 248 + 249 + .popover-scroll-area::-webkit-scrollbar-thumb:hover { 250 + background: var(--text-tertiary); 251 + } 252 + 253 + .comment-item { 254 + padding: 14px 16px; 255 + border-bottom: 1px solid var(--border); 256 + transition: background 0.15s; 257 + } 258 + 259 + .comment-item:hover { 260 + background: var(--bg-hover); 261 + } 262 + 263 + .comment-item:last-child { 264 + border-bottom: none; 265 + } 266 + 267 + .comment-header { 268 + display: flex; 269 + align-items: center; 270 + gap: 10px; 271 + margin-bottom: 8px; 272 + } 273 + 274 + .comment-avatar { 275 + width: 28px; 276 + height: 28px; 277 + border-radius: 50%; 278 + background: linear-gradient(135deg, var(--accent), var(--accent-hover)); 279 + display: flex; 280 + align-items: center; 281 + justify-content: center; 282 + font-size: 11px; 283 + font-weight: 600; 284 + color: white; 285 + flex-shrink: 0; 286 + } 287 + 288 + .comment-meta { 289 + flex: 1; 290 + min-width: 0; 291 + } 292 + 293 + .comment-handle { 294 + font-size: 13px; 295 + font-weight: 600; 296 + color: var(--text-primary); 297 + } 298 + 299 + .comment-time { 300 + font-size: 11px; 301 + color: var(--text-tertiary); 302 + } 303 + 304 + .comment-text { 305 + font-size: 13px; 306 + line-height: 1.55; 307 + color: var(--text-primary); 308 + } 309 + 310 + .highlight-badge { 311 + display: inline-flex; 312 + align-items: center; 313 + gap: 5px; 314 + font-size: 11px; 315 + color: var(--text-tertiary); 316 + background: var(--bg-tertiary); 317 + padding: 4px 10px; 318 + border-radius: 12px; 319 + font-weight: 500; 320 + } 321 + 322 + .highlight-badge svg { 323 + width: 12px; 324 + height: 12px; 325 + } 326 + 327 + .comment-actions { 328 + display: flex; 329 + gap: 4px; 330 + margin-top: 10px; 331 + padding-top: 10px; 332 + border-top: 1px solid var(--border); 333 + } 334 + 335 + .comment-action-btn { 336 + background: none; 337 + border: none; 338 + padding: 6px 10px; 339 + color: var(--text-tertiary); 340 + font-size: 12px; 341 + font-weight: 500; 342 + cursor: pointer; 343 + border-radius: 6px; 344 + transition: all 0.15s; 345 + display: flex; 346 + align-items: center; 347 + gap: 5px; 348 + } 349 + 350 + .comment-action-btn svg { 351 + width: 14px; 352 + height: 14px; 353 + } 354 + 355 + .comment-action-btn:hover { 356 + background: var(--bg-tertiary); 357 + color: var(--text-primary); 358 + } 359 + 360 + .inline-compose-modal { 361 + position: fixed; 362 + width: 380px; 363 + max-width: calc(100vw - 32px); 364 + background: var(--bg-card); 365 + border: 1px solid var(--border); 366 + border-radius: 16px; 367 + padding: 0; 368 + box-sizing: border-box; 369 + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255,255,255,0.05); 370 + z-index: 2147483647; 371 + pointer-events: auto; 372 + font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 373 + color: var(--text-primary); 374 + animation: modal-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; 375 + overflow: hidden; 376 + } 377 + 378 + @keyframes modal-in { 379 + from { opacity: 0; transform: scale(0.95) translateY(10px); } 380 + to { opacity: 1; transform: scale(1) translateY(0); } 381 + } 382 + 383 + .inline-compose-modal * { 384 + box-sizing: border-box; 385 + } 386 + 387 + .compose-header { 388 + padding: 14px 16px; 389 + border-bottom: 1px solid var(--border); 390 + display: flex; 391 + justify-content: space-between; 392 + align-items: center; 393 + background: var(--bg-primary); 394 + } 395 + 396 + .compose-title { 397 + font-weight: 600; 398 + font-size: 14px; 399 + color: var(--text-primary); 400 + } 401 + 402 + .compose-close { 403 + background: none; 404 + border: none; 405 + color: var(--text-tertiary); 406 + cursor: pointer; 407 + padding: 4px; 408 + border-radius: 6px; 409 + display: flex; 410 + transition: all 0.15s; 411 + } 412 + 413 + .compose-close:hover { 414 + background: var(--bg-hover); 415 + color: var(--text-primary); 416 + } 417 + 418 + .compose-body { 419 + padding: 16px; 420 + } 421 + 422 + .inline-compose-quote { 423 + padding: 12px 14px; 424 + background: var(--accent-subtle); 425 + border-left: 3px solid var(--accent); 426 + border-radius: 0 8px 8px 0; 427 + font-size: 13px; 428 + color: var(--text-secondary); 429 + font-style: italic; 430 + margin-bottom: 14px; 431 + max-height: 80px; 432 + overflow: hidden; 433 + word-break: break-word; 434 + line-height: 1.5; 435 + } 436 + 437 + .inline-compose-textarea { 438 + width: 100%; 439 + min-height: 100px; 440 + padding: 12px 14px; 441 + background: var(--bg-elevated); 442 + border: 1px solid var(--border); 443 + border-radius: 10px; 444 + color: var(--text-primary); 445 + font-family: inherit; 446 + font-size: 14px; 447 + line-height: 1.5; 448 + resize: none; 449 + box-sizing: border-box; 450 + transition: border-color 0.15s; 451 + } 452 + 453 + .inline-compose-textarea::placeholder { 454 + color: var(--text-tertiary); 455 + } 456 + 457 + .inline-compose-textarea:focus { 458 + outline: none; 459 + border-color: var(--accent); 460 + } 461 + 462 + .compose-footer { 463 + padding: 12px 16px; 464 + border-top: 1px solid var(--border); 465 + display: flex; 466 + justify-content: flex-end; 467 + gap: 8px; 468 + background: var(--bg-primary); 469 + } 470 + 471 + .btn-cancel { 472 + padding: 9px 18px; 473 + background: transparent; 474 + border: 1px solid var(--border); 475 + border-radius: 8px; 476 + color: var(--text-secondary); 477 + font-size: 13px; 478 + font-weight: 500; 479 + cursor: pointer; 480 + transition: all 0.15s; 481 + } 482 + 483 + .btn-cancel:hover { 484 + background: var(--bg-hover); 485 + color: var(--text-primary); 486 + border-color: var(--border); 487 + } 488 + 489 + .btn-submit { 490 + padding: 9px 20px; 491 + background: var(--accent); 492 + border: none; 493 + border-radius: 8px; 494 + color: white; 495 + font-size: 13px; 496 + font-weight: 600; 497 + cursor: pointer; 498 + transition: all 0.15s; 499 + } 500 + 501 + .btn-submit:hover { 502 + background: var(--accent-hover); 503 + transform: translateY(-1px); 504 + } 505 + 506 + .btn-submit:active { 507 + transform: translateY(0); 508 + } 509 + 510 + .btn-submit:disabled { 511 + opacity: 0.5; 512 + cursor: not-allowed; 513 + transform: none; 514 + } 515 + 516 + .margin-hover-indicator { 517 + position: fixed; 518 + display: flex; 519 + align-items: center; 520 + pointer-events: none; 521 + z-index: 2147483647; 522 + opacity: 0; 523 + transition: opacity 0.2s ease, transform 0.2s ease; 524 + transform: scale(0.8) translateX(4px); 525 + } 526 + 527 + .margin-hover-indicator.visible { 528 + opacity: 1; 529 + transform: scale(1) translateX(0); 530 + } 531 + 532 + .margin-toast { 533 + position: fixed; 534 + bottom: 24px; 535 + left: 50%; 536 + transform: translateX(-50%) translateY(20px); 537 + padding: 12px 20px; 538 + background: var(--bg-card); 539 + border: 1px solid var(--border); 540 + border-radius: 10px; 541 + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); 542 + font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif; 543 + font-size: 13px; 544 + font-weight: 500; 545 + color: var(--text-primary); 546 + z-index: 2147483647; 547 + pointer-events: auto; 548 + display: flex; 549 + align-items: center; 550 + gap: 10px; 551 + opacity: 0; 552 + animation: toast-in 0.3s ease forwards; 553 + } 554 + 555 + @keyframes toast-in { 556 + to { opacity: 1; transform: translateX(-50%) translateY(0); } 557 + } 558 + 559 + .margin-toast.toast-out { 560 + animation: toast-out 0.2s ease forwards; 561 + } 562 + 563 + @keyframes toast-out { 564 + to { opacity: 0; transform: translateX(-50%) translateY(10px); } 565 + } 566 + 567 + .toast-icon { 568 + width: 18px; 569 + height: 18px; 570 + color: var(--accent); 571 + } 572 + 573 + .toast-success .toast-icon { 574 + color: #34d399; 575 + } 576 + `;
+11
extension/src/utils/storage.ts
··· 1 + export const apiUrlItem = storage.defineItem<string>('local:apiUrl', { 2 + fallback: 'https://margin.at', 3 + }); 4 + 5 + export const overlayEnabledItem = storage.defineItem<boolean>('local:overlayEnabled', { 6 + fallback: true, 7 + }); 8 + 9 + export const themeItem = storage.defineItem<'light' | 'dark' | 'system'>('local:theme', { 10 + fallback: 'system', 11 + });
+183
extension/src/utils/text-matcher.ts
··· 1 + interface TextNodeIndex { 2 + start: number; 3 + node: Text; 4 + length: number; 5 + } 6 + 7 + interface TextPoint { 8 + node: Text; 9 + offset: number; 10 + } 11 + 12 + export class DOMTextMatcher { 13 + private textNodes: Text[] = []; 14 + private corpus = ''; 15 + private indices: TextNodeIndex[] = []; 16 + private built = false; 17 + 18 + constructor() {} 19 + 20 + private ensureBuilt(): void { 21 + if (!this.built) { 22 + this.buildMap(); 23 + this.built = true; 24 + } 25 + } 26 + 27 + private buildMap(): void { 28 + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, { 29 + acceptNode: (node: Text) => { 30 + if (!node.parentNode) return NodeFilter.FILTER_REJECT; 31 + const parent = node.parentNode as Element; 32 + const tag = parent.tagName; 33 + if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT'].includes(tag)) { 34 + return NodeFilter.FILTER_REJECT; 35 + } 36 + if (!node.textContent?.trim()) return NodeFilter.FILTER_SKIP; 37 + const htmlParent = parent as HTMLElement; 38 + if ( 39 + htmlParent.style && 40 + (htmlParent.style.display === 'none' || htmlParent.style.visibility === 'hidden') 41 + ) { 42 + return NodeFilter.FILTER_REJECT; 43 + } 44 + return NodeFilter.FILTER_ACCEPT; 45 + }, 46 + }); 47 + 48 + let currentNode: Text | null; 49 + let index = 0; 50 + const parts: string[] = []; 51 + 52 + while ((currentNode = walker.nextNode() as Text | null)) { 53 + const text = currentNode.textContent || ''; 54 + this.textNodes.push(currentNode); 55 + parts.push(text); 56 + this.indices.push({ 57 + start: index, 58 + node: currentNode, 59 + length: text.length, 60 + }); 61 + index += text.length; 62 + } 63 + 64 + this.corpus = parts.join(''); 65 + } 66 + 67 + findRange(searchText: string): Range | null { 68 + if (!searchText) return null; 69 + 70 + this.ensureBuilt(); 71 + 72 + let matchIndex = this.corpus.indexOf(searchText); 73 + 74 + if (matchIndex === -1) { 75 + const normalizedSearch = searchText.replace(/\s+/g, ' ').trim(); 76 + matchIndex = this.corpus.indexOf(normalizedSearch); 77 + 78 + if (matchIndex === -1) { 79 + const fuzzyMatch = this.fuzzyFindInCorpus(searchText); 80 + if (fuzzyMatch) { 81 + const start = this.mapIndexToPoint(fuzzyMatch.start); 82 + const end = this.mapIndexToPoint(fuzzyMatch.end); 83 + if (start && end) { 84 + const range = document.createRange(); 85 + range.setStart(start.node, start.offset); 86 + range.setEnd(end.node, end.offset); 87 + return range; 88 + } 89 + } 90 + return null; 91 + } 92 + } 93 + 94 + const start = this.mapIndexToPoint(matchIndex); 95 + const end = this.mapIndexToPoint(matchIndex + searchText.length); 96 + 97 + if (start && end) { 98 + const range = document.createRange(); 99 + range.setStart(start.node, start.offset); 100 + range.setEnd(end.node, end.offset); 101 + return range; 102 + } 103 + return null; 104 + } 105 + 106 + private fuzzyFindInCorpus(searchText: string): { start: number; end: number } | null { 107 + const searchWords = searchText 108 + .trim() 109 + .split(/\s+/) 110 + .filter((w) => w.length > 0); 111 + if (searchWords.length === 0) return null; 112 + 113 + const corpusLower = this.corpus.toLowerCase(); 114 + const firstWord = searchWords[0].toLowerCase(); 115 + let searchStart = 0; 116 + 117 + while (searchStart < corpusLower.length) { 118 + const wordStart = corpusLower.indexOf(firstWord, searchStart); 119 + if (wordStart === -1) break; 120 + 121 + let corpusPos = wordStart; 122 + let matched = true; 123 + let lastMatchEnd = wordStart; 124 + 125 + for (const word of searchWords) { 126 + const wordLower = word.toLowerCase(); 127 + while (corpusPos < corpusLower.length && /\s/.test(this.corpus[corpusPos])) { 128 + corpusPos++; 129 + } 130 + const corpusSlice = corpusLower.slice(corpusPos, corpusPos + wordLower.length); 131 + if (corpusSlice !== wordLower) { 132 + matched = false; 133 + break; 134 + } 135 + corpusPos += wordLower.length; 136 + lastMatchEnd = corpusPos; 137 + } 138 + 139 + if (matched) { 140 + return { start: wordStart, end: lastMatchEnd }; 141 + } 142 + searchStart = wordStart + 1; 143 + } 144 + 145 + return null; 146 + } 147 + 148 + private mapIndexToPoint(corpusIndex: number): TextPoint | null { 149 + for (const info of this.indices) { 150 + if (corpusIndex >= info.start && corpusIndex < info.start + info.length) { 151 + return { node: info.node, offset: corpusIndex - info.start }; 152 + } 153 + } 154 + if (this.indices.length > 0) { 155 + const last = this.indices[this.indices.length - 1]; 156 + if (corpusIndex === last.start + last.length) { 157 + return { node: last.node, offset: last.length }; 158 + } 159 + } 160 + return null; 161 + } 162 + } 163 + 164 + export function findCanonicalUrl(range: Range): string | null { 165 + let node: Node | null = range.commonAncestorContainer; 166 + if (node.nodeType === Node.TEXT_NODE) { 167 + node = node.parentNode; 168 + } 169 + 170 + while (node && node !== document.body) { 171 + const element = node as Element; 172 + if ( 173 + (element.tagName === 'BLOCKQUOTE' || element.tagName === 'Q') && 174 + element.hasAttribute('cite') 175 + ) { 176 + if (element.contains(range.commonAncestorContainer)) { 177 + return element.getAttribute('cite'); 178 + } 179 + } 180 + node = node.parentNode; 181 + } 182 + return null; 183 + }
+76
extension/src/utils/types.ts
··· 1 + export interface MarginSession { 2 + authenticated: boolean; 3 + did?: string; 4 + handle?: string; 5 + accessJwt?: string; 6 + refreshJwt?: string; 7 + } 8 + 9 + export interface TextSelector { 10 + type?: string; 11 + exact: string; 12 + prefix?: string; 13 + suffix?: string; 14 + } 15 + 16 + export interface Annotation { 17 + uri?: string; 18 + id?: string; 19 + cid?: string; 20 + type?: 'Annotation' | 'Bookmark' | 'Highlight'; 21 + body?: { value: string }; 22 + text?: string; 23 + target?: { 24 + source?: string; 25 + selector?: TextSelector; 26 + }; 27 + selector?: TextSelector; 28 + color?: string; 29 + created?: string; 30 + createdAt?: string; 31 + creator?: Author; 32 + author?: Author; 33 + } 34 + 35 + export interface Author { 36 + did?: string; 37 + handle?: string; 38 + displayName?: string; 39 + avatar?: string; 40 + } 41 + 42 + export interface Bookmark { 43 + uri?: string; 44 + id?: string; 45 + source?: string; 46 + url?: string; 47 + title?: string; 48 + description?: string; 49 + image?: string; 50 + createdAt?: string; 51 + } 52 + 53 + export interface Highlight { 54 + uri?: string; 55 + id?: string; 56 + target?: { 57 + source?: string; 58 + selector?: TextSelector; 59 + }; 60 + color?: string; 61 + title?: string; 62 + createdAt?: string; 63 + } 64 + 65 + export interface Collection { 66 + uri?: string; 67 + id?: string; 68 + name: string; 69 + description?: string; 70 + icon?: string; 71 + createdAt?: string; 72 + itemCount?: number; 73 + } 74 + 75 + export const DEFAULT_API_URL = 'https://margin.at'; 76 + export const APP_URL = 'https://margin.at';
+12
extension/tailwind.config.js
··· 1 + /** @type {import('tailwindcss').Config} */ 2 + export default { 3 + content: ['./src/**/*.{html,js,ts,jsx,tsx}'], 4 + theme: { 5 + extend: { 6 + fontFamily: { 7 + sans: ['IBM Plex Sans', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'], 8 + }, 9 + }, 10 + }, 11 + plugins: [], 12 + };
+16
extension/tsconfig.json
··· 1 + { 2 + "extends": "./.wxt/tsconfig.json", 3 + "compilerOptions": { 4 + "jsx": "react-jsx", 5 + "jsxImportSource": "react", 6 + "moduleResolution": "bundler", 7 + "strict": true, 8 + "skipLibCheck": true, 9 + "esModuleInterop": true, 10 + "allowSyntheticDefaultImports": true, 11 + "paths": { 12 + "@/*": ["./src/*"] 13 + } 14 + }, 15 + "include": ["src/**/*", ".wxt/wxt.d.ts"] 16 + }
+87
extension/wxt.config.ts
··· 1 + import { defineConfig } from 'wxt'; 2 + import { cp } from 'fs/promises'; 3 + import { existsSync } from 'fs'; 4 + import { resolve } from 'path'; 5 + 6 + export default defineConfig({ 7 + srcDir: 'src', 8 + modules: ['@wxt-dev/module-react'], 9 + manifestVersion: 3, 10 + hooks: { 11 + 'build:done': async (wxt) => { 12 + const publicDir = resolve(__dirname, 'public'); 13 + const outDir = wxt.config.outDir; 14 + 15 + if (existsSync(publicDir)) { 16 + await cp(publicDir, outDir, { recursive: true }); 17 + } 18 + }, 19 + }, 20 + manifest: ({ browser }) => { 21 + const basePermissions = ['storage', 'activeTab', 'tabs', 'cookies', 'contextMenus']; 22 + const chromePermissions = [...basePermissions, 'sidePanel']; 23 + 24 + return { 25 + name: 'Margin', 26 + description: 'Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.', 27 + permissions: browser === 'firefox' ? basePermissions : chromePermissions, 28 + host_permissions: ['https://margin.at/*', '*://*/*'], 29 + icons: { 30 + 16: '/icons/icon-16.png', 31 + 32: '/icons/icon-32.png', 32 + 48: '/icons/icon-48.png', 33 + 128: '/icons/icon-128.png', 34 + }, 35 + commands: { 36 + 'open-sidebar': { 37 + suggested_key: { 38 + default: 'Alt+M', 39 + mac: 'Alt+M', 40 + }, 41 + description: 'Open Margin sidebar', 42 + }, 43 + 'annotate-selection': { 44 + suggested_key: { 45 + default: 'Alt+A', 46 + mac: 'Alt+A', 47 + }, 48 + description: 'Annotate selected text', 49 + }, 50 + 'highlight-selection': { 51 + suggested_key: { 52 + default: 'Alt+H', 53 + mac: 'Alt+H', 54 + }, 55 + description: 'Highlight selected text', 56 + }, 57 + 'bookmark-page': { 58 + suggested_key: { 59 + default: 'Alt+B', 60 + mac: 'Alt+B', 61 + }, 62 + description: 'Bookmark current page', 63 + }, 64 + }, 65 + action: { 66 + default_title: 'Margin', 67 + default_popup: 'popup.html', 68 + default_icon: { 69 + 16: '/icons/icon-16.png', 70 + 32: '/icons/icon-32.png', 71 + 48: '/icons/icon-48.png', 72 + 128: '/icons/icon-128.png', 73 + }, 74 + }, 75 + ...(browser === 'chrome' ? { 76 + side_panel: { 77 + default_path: 'sidepanel.html', 78 + }, 79 + } : { 80 + sidebar_action: { 81 + default_title: 'Margin', 82 + default_panel: 'sidepanel.html', 83 + }, 84 + }), 85 + }; 86 + }, 87 + });