diff --git a/components/ClearableList.tsx b/components/ClearableList.tsx new file mode 100644 index 0000000..1d1dbc5 --- /dev/null +++ b/components/ClearableList.tsx @@ -0,0 +1,41 @@ +import type { ReactNode } from 'react'; + +interface ClearableListProps { + items: T[]; + keyOf: (item: T) => string | number | undefined; + emptyLabel: string; + renderRow: (item: T) => ReactNode; + buttonLabel: (item: T) => string; + buttonDisabled: (item: T) => boolean; + onClear: (item: T) => void; +} + +export function ClearableList({ + items, + keyOf, + emptyLabel, + renderRow, + buttonLabel, + buttonDisabled, + onClear, +}: ClearableListProps) { + return ( +
    + {items.length === 0 && ( +
  • {emptyLabel}
  • + )} + {items.map((item) => ( +
  • + {renderRow(item)} + +
  • + ))} +
+ ); +} diff --git a/components/FilterRow.tsx b/components/FilterRow.tsx new file mode 100644 index 0000000..a2a0309 --- /dev/null +++ b/components/FilterRow.tsx @@ -0,0 +1,23 @@ +import type { ReactNode } from 'react'; + +interface FilterRowProps { + value: string; + onChange: (value: string) => void; + placeholder: string; + children: ReactNode; +} + +export function FilterRow({ value, onChange, placeholder, children }: FilterRowProps) { + return ( +
+ onChange(e.target.value)} + placeholder={placeholder} + className="flex-1 rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500" + /> + {children} +
+ ); +} diff --git a/components/HistoryList.tsx b/components/HistoryList.tsx index 3f7e652..f644360 100644 --- a/components/HistoryList.tsx +++ b/components/HistoryList.tsx @@ -1,5 +1,6 @@ import type { Browser } from 'wxt/browser'; import { useTranslation } from '../hooks/useTranslation'; +import { ClearableList } from './ClearableList'; interface HistoryListProps { items: Browser.history.HistoryItem[]; @@ -10,21 +11,14 @@ export function HistoryList({ items, onDelete }: HistoryListProps) { const t = useTranslation(); return ( -
    - {items.length === 0 && ( -
  • {t('noMatchingHistory')}
  • - )} - {items.map((item) => ( -
  • - {item.title || item.url} - -
  • - ))} -
+ item.id} + emptyLabel={t('noMatchingHistory')} + renderRow={(item) => {item.title || item.url}} + buttonLabel={() => t('remove')} + buttonDisabled={() => false} + onClear={(item) => item.url && onDelete(item.url)} + /> ); } diff --git a/components/SectionHeading.tsx b/components/SectionHeading.tsx new file mode 100644 index 0000000..9018feb --- /dev/null +++ b/components/SectionHeading.tsx @@ -0,0 +1,14 @@ +interface SectionHeadingProps { + as?: 'h2' | 'h3'; + title: string; + description?: string; +} + +export function SectionHeading({ as: Tag = 'h2', title, description }: SectionHeadingProps) { + return ( + <> + {title} + {description &&

{description}

} + + ); +} diff --git a/components/TabClearList.tsx b/components/TabClearList.tsx index 48ca719..d7a9331 100644 --- a/components/TabClearList.tsx +++ b/components/TabClearList.tsx @@ -1,6 +1,7 @@ import type { Browser } from 'wxt/browser'; import { useTranslation } from '../hooks/useTranslation'; -import { tabDomain } from '../utils/clearing'; +import { tabHostname } from '../utils/clearing'; +import { ClearableList } from './ClearableList'; interface TabClearListProps { tabs: Browser.tabs.Tab[]; @@ -10,47 +11,32 @@ interface TabClearListProps { onClear: (tab: Browser.tabs.Tab) => void; } -export function TabClearList({ - tabs, - busyTabIds, - failedTabIds, - reloadingTabIds, - onClear, -}: TabClearListProps) { +export function TabClearList({ tabs, busyTabIds, failedTabIds, reloadingTabIds, onClear }: TabClearListProps) { const t = useTranslation(); return ( -
    - {tabs.length === 0 && ( -
  • {t('noMatchingTabs')}
  • + tab.id} + emptyLabel={t('noMatchingTabs')} + renderRow={(tab) => ( + + {tabHostname(tab) && ( + [{tabHostname(tab)}] + )} + {tab.title ?? tab.url} + )} - {tabs.map((tab) => { - const busy = tab.id != null && busyTabIds.has(tab.id); - const reloading = tab.id != null && reloadingTabIds?.has(tab.id); - return ( -
  • - - {tabDomain(tab) && ( - [{tabDomain(tab)}] - )} - {tab.title ?? tab.url} - - -
  • - ); - })} -
+ buttonLabel={(tab) => { + if (tab.id != null && busyTabIds.has(tab.id)) return t('clearing'); + if (tab.id != null && reloadingTabIds?.has(tab.id)) return t('reloading'); + if (tab.id != null && failedTabIds.has(tab.id)) return t('failed'); + return t('clear'); + }} + buttonDisabled={(tab) => + (tab.id != null && busyTabIds.has(tab.id)) || (tab.id != null && (reloadingTabIds?.has(tab.id) ?? false)) + } + onClear={onClear} + /> ); } diff --git a/components/VisitedSiteList.tsx b/components/VisitedSiteList.tsx index b27ff9c..4525110 100644 --- a/components/VisitedSiteList.tsx +++ b/components/VisitedSiteList.tsx @@ -1,5 +1,6 @@ import { useTranslation } from '../hooks/useTranslation'; import type { VisitedSite } from '../utils/clearing'; +import { ClearableList } from './ClearableList'; interface VisitedSiteListProps { sites: VisitedSite[]; @@ -12,25 +13,18 @@ export function VisitedSiteList({ sites, busyHostnames, failedHostnames, onClear const t = useTranslation(); return ( -
    - {sites.length === 0 && ( -
  • {t('noMatchingSites')}
  • - )} - {sites.map((site) => { - const busy = busyHostnames.has(site.hostname); - return ( -
  • - {site.hostname} - -
  • - ); - })} -
+ site.hostname} + emptyLabel={t('noMatchingSites')} + renderRow={(site) => {site.hostname}} + buttonLabel={(site) => { + if (busyHostnames.has(site.hostname)) return t('clearing'); + if (failedHostnames.has(site.hostname)) return t('failed'); + return t('clear'); + }} + buttonDisabled={(site) => busyHostnames.has(site.hostname)} + onClear={onClear} + /> ); } diff --git a/components/options/GlobalSection.tsx b/components/options/GlobalSection.tsx index cd08373..d0732f0 100644 --- a/components/options/GlobalSection.tsx +++ b/components/options/GlobalSection.tsx @@ -4,6 +4,7 @@ import { useTranslation } from '../../hooks/useTranslation'; import { DATA_TYPES, type DataTypeId } from '../../utils/data-types'; import { DangerConfirmButton } from '../DangerConfirmButton'; import { DataTypeGrid } from '../DataTypeGrid'; +import { SectionHeading } from '../SectionHeading'; import { type ClearStatus } from '../StatusButton'; import { HistorySection } from './HistorySection'; @@ -41,10 +42,7 @@ export function GlobalSection({ return (
-

- {t('scopeGlobal')} -

-

{t('scopeGlobalDescription')}

+

{t('storageKeyShareNote')}

diff --git a/components/options/HistorySection.tsx b/components/options/HistorySection.tsx index 87569c0..8d1383a 100644 --- a/components/options/HistorySection.tsx +++ b/components/options/HistorySection.tsx @@ -1,8 +1,10 @@ import type { Browser } from 'wxt/browser'; import { useTranslation } from '../../hooks/useTranslation'; +import { clearAllLabel } from '../../utils/clear-status'; +import { FilterRow } from '../FilterRow'; import { HistoryList } from '../HistoryList'; +import { SectionHeading } from '../SectionHeading'; import { type ClearStatus } from '../StatusButton'; -import { clearAllLabel } from '../../utils/clear-status'; interface HistorySectionProps { items: Browser.history.HistoryItem[]; @@ -18,17 +20,8 @@ export function HistorySection({ items, filter, onFilterChange, status, onClearA return (
-

- {t('recentHistory')} -

-
- onFilterChange(e.target.value)} - placeholder={t('filterHistoryPlaceholder')} - className="flex-1 rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500" - /> + + -
+
); diff --git a/components/options/PerSiteSection.tsx b/components/options/PerSiteSection.tsx deleted file mode 100644 index f22d586..0000000 --- a/components/options/PerSiteSection.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import type { Browser } from 'wxt/browser'; -import { useTranslation } from '../../hooks/useTranslation'; -import { isGeckoBased } from '../../utils/browser-info'; -import type { DataTypeDef, DataTypeId } from '../../utils/data-types'; -import type { VisitedSite } from '../../utils/clearing'; -import { DataTypeGrid } from '../DataTypeGrid'; -import { OriginMappingsEditor } from '../OriginMappingsEditor'; -import { type ClearStatus } from '../StatusButton'; -import { ProtectedSitesSection } from './ProtectedSitesSection'; -import { SiteTabsSection } from './SiteTabsSection'; -import { VisitedSitesSection } from './VisitedSitesSection'; - -interface PerSiteSectionProps { - types: DataTypeDef[]; - selectedTypes: DataTypeId[]; - onToggleType: (id: DataTypeId) => void; - autoReloadAfterClear: boolean; - onAutoReloadChange: (value: boolean) => void; - useOriginMappings: boolean; - onUseOriginMappingsChange: (value: boolean) => void; - linkedOrigins: string; - onLinkedOriginsChange: (value: string) => void; - protectedSites: string; - onProtectedSitesChange: (value: string) => void; - siteFilter: string; - onSiteFilterChange: (value: string) => void; - bulkStatus: ClearStatus; - onClearAllTabs: () => void; - tabs: Browser.tabs.Tab[]; - busyTabIds: Set; - failedTabIds: Set; - reloadingTabIds: Set; - onClearTab: (tab: Browser.tabs.Tab) => void; - visitedSiteFilter: string; - onVisitedSiteFilterChange: (value: string) => void; - visitedSites: VisitedSite[]; - visitedBulkStatus: ClearStatus; - onClearAllVisitedSites: () => void; - busyVisitedHostnames: Set; - failedVisitedHostnames: Set; - onClearVisitedSite: (site: VisitedSite) => void; -} - -export function PerSiteSection({ - types, - selectedTypes, - onToggleType, - autoReloadAfterClear, - onAutoReloadChange, - useOriginMappings, - onUseOriginMappingsChange, - linkedOrigins, - onLinkedOriginsChange, - protectedSites, - onProtectedSitesChange, - siteFilter, - onSiteFilterChange, - bulkStatus, - onClearAllTabs, - tabs, - busyTabIds, - failedTabIds, - reloadingTabIds, - onClearTab, - visitedSiteFilter, - onVisitedSiteFilterChange, - visitedSites, - visitedBulkStatus, - onClearAllVisitedSites, - busyVisitedHostnames, - failedVisitedHostnames, - onClearVisitedSite, -}: PerSiteSectionProps) { - const t = useTranslation(); - - return ( -
-

- {t('scopePerSite')} -

-

{t('scopePerSiteDescription')}

- - -
- -
- - - -
- -
- -
- - - -
- - {!isGeckoBased() && ( - <> - -
- - )} - - -
- ); -} diff --git a/components/options/SiteTabsSection.tsx b/components/options/SiteTabsSection.tsx index cd420ac..c2f7576 100644 --- a/components/options/SiteTabsSection.tsx +++ b/components/options/SiteTabsSection.tsx @@ -1,8 +1,9 @@ import type { Browser } from 'wxt/browser'; import { useTranslation } from '../../hooks/useTranslation'; +import { clearAllLabel } from '../../utils/clear-status'; +import { FilterRow } from '../FilterRow'; import { type ClearStatus } from '../StatusButton'; import { TabClearList } from '../TabClearList'; -import { clearAllLabel } from '../../utils/clear-status'; interface SiteTabsSectionProps { filter: string; @@ -33,14 +34,7 @@ export function SiteTabsSection({

{t('openTabsOnly')}

-
- onFilterChange(e.target.value)} - placeholder={t('filterTabsPlaceholder')} - className="flex-1 rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500" - /> + -
+ -

- {t('visitedSites')} -

-

{t('visitedSitesDescription')}

+ {browserName && (

{t('visitedSitesContainerCaveat', { browser: browserName })}

)} -
- onFilterChange(e.target.value)} - placeholder={t('filterVisitedSitesPlaceholder')} - className="flex-1 rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500" - /> +
-
+ { - const raw = (await readPersistedSettingsRaw(SETTINGS_KEY)) as PersistedShape | null; - return { - linkedOrigins: raw?.state?.linkedOrigins ?? '', - useOriginMappings: raw?.state?.useOriginMappings ?? false, - }; +type MappingSettings = { linkedOrigins: string; useOriginMappings: boolean }; + +let cachedSettings: MappingSettings | null = null; + +async function getMappingSettings(): Promise { + if (!cachedSettings) { + const raw = (await readPersistedSettingsRaw(SETTINGS_KEY)) as PersistedShape | null; + cachedSettings = { + linkedOrigins: raw?.state?.linkedOrigins ?? '', + useOriginMappings: raw?.state?.useOriginMappings ?? false, + }; + } + return cachedSettings; } async function updateBadge(tabId: number, url: string | undefined) { @@ -25,7 +32,7 @@ async function updateBadge(tabId: number, url: string | undefined) { return; } - const { linkedOrigins, useOriginMappings } = await readMappingSettings(); + const { linkedOrigins, useOriginMappings } = await getMappingSettings(); const hasMapping = useOriginMappings && linkedOriginsFor(linkedOrigins, origin).length > 0; await browser.action.setBadgeText({ tabId, text: hasMapping ? '●' : '' }); @@ -43,10 +50,10 @@ export default defineBackground(() => { if (info.status === 'complete') updateBadge(tabId, tab.url); }); - // Editing the mapping list or toggling "Use origin mappings" should update the - // badge immediately, not just on the next tab switch/navigation. browser.storage.onChanged.addListener((changes, areaName) => { - if (areaName !== 'sync' || !Object.keys(changes).some((key) => key.startsWith(SETTINGS_KEY))) return; + if (areaName !== 'sync' || !Object.keys(changes).some((key) => key.startsWith(SETTINGS_KEY))) + return; + cachedSettings = null; browser.tabs.query({ active: true }).then((tabs) => { for (const tab of tabs) { if (tab.id != null) updateBadge(tab.id, tab.url); diff --git a/entrypoints/options/App.tsx b/entrypoints/options/App.tsx index 140afe7..27bef4e 100644 --- a/entrypoints/options/App.tsx +++ b/entrypoints/options/App.tsx @@ -1,13 +1,18 @@ import { useEffect, useMemo, useState } from 'react'; import { browser, type Browser } from 'wxt/browser'; +import { DataTypeGrid } from '../../components/DataTypeGrid'; import { HelpPanel } from '../../components/HelpPanel'; +import { OriginMappingsEditor } from '../../components/OriginMappingsEditor'; import { BackupSection } from '../../components/options/BackupSection'; import { ClearLogSection } from '../../components/options/ClearLogSection'; import { GlobalSection } from '../../components/options/GlobalSection'; import { LanguageSection } from '../../components/options/LanguageSection'; -import { PerSiteSection } from '../../components/options/PerSiteSection'; +import { ProtectedSitesSection } from '../../components/options/ProtectedSitesSection'; import { SettingsHeader, type OptionsTab } from '../../components/options/SettingsHeader'; +import { SiteTabsSection } from '../../components/options/SiteTabsSection'; import { ThemeSection } from '../../components/options/ThemeSection'; +import { VisitedSitesSection } from '../../components/options/VisitedSitesSection'; +import { SectionHeading } from '../../components/SectionHeading'; import { type ClearStatus } from '../../components/StatusButton'; import { useReloadGuard } from '../../hooks/useReloadGuard'; import { useTheme } from '../../hooks/useTheme'; @@ -15,6 +20,7 @@ import { useTranslation } from '../../hooks/useTranslation'; import { recordClear } from '../../store/clearLog'; import { useSettingsStore } from '../../store/settings'; import { isGeckoBased } from '../../utils/browser-info'; +import { runClear } from '../../utils/clear-status'; import { clearGlobal, clearLinkedOrigin, @@ -27,12 +33,13 @@ import { requestOriginPermission, siteScopedIds, tabCookieStoreId, - tabDomain, + tabHostname, tabOrigin, type VisitedSite, } from '../../utils/clearing'; import { mapWithConcurrency } from '../../utils/concurrency'; import { siteScopedDataTypes } from '../../utils/data-types'; +import { withItem, withoutItem } from '../../utils/set-helpers'; type Tab = Browser.tabs.Tab; @@ -99,9 +106,9 @@ export default function App() { const filteredTabs = useMemo(() => { const q = siteFilter.trim().toLowerCase(); return tabs.filter((tab) => { - const domain = tabDomain(tab) ?? ''; - if (isProtectedSite(protectedSites, domain)) return false; - return !q || domain.toLowerCase().includes(q); + const hostname = tabHostname(tab) ?? ''; + if (isProtectedSite(protectedSites, hostname)) return false; + return !q || hostname.toLowerCase().includes(q); }); }, [tabs, siteFilter, protectedSites]); @@ -131,37 +138,27 @@ export default function App() { }, [visitedSites, visitedSiteFilter]); async function handleGlobalClear() { - setGlobalStatus('clearing'); - try { - // Only Chrome can exclude specific origins from an unscoped clear at all — this - // only excludes protected sites we actually know the real origin of (i.e. ones - // in your history); Firefox has no such option and clears everything regardless. - const excludeOrigins = allVisitedSites - .filter((site) => isProtectedSite(protectedSites, site.hostname)) - .map((site) => site.origin); + // Only Chrome can exclude specific origins from an unscoped clear at all, and only + // for sites we actually know the real origin of (i.e. ones in your history). + const excludeOrigins = allVisitedSites + .filter((site) => isProtectedSite(protectedSites, site.hostname)) + .map((site) => site.origin); + await runClear(setGlobalStatus, async () => { await clearGlobal(selectedTypesGlobal, excludeOrigins); recordClear('(global)', selectedTypesGlobal); - setGlobalStatus('done'); - } catch (err) { - console.error('Bleep: global clear failed', err); - setGlobalStatus('failed'); - } - setTimeout(() => setGlobalStatus('idle'), 1500); + }); } async function handleSiteClear(tab: Tab) { if (tab.id == null || isReloading(tab.id)) return; - if (isProtectedSite(protectedSites, tabDomain(tab) ?? '')) return; - setBusyTabIds((s) => new Set(s).add(tab.id!)); - setFailedTabIds((s) => { - const next = new Set(s); - next.delete(tab.id!); - return next; - }); + if (isProtectedSite(protectedSites, tabHostname(tab) ?? '')) return; + const tabId = tab.id; + setBusyTabIds((s) => withItem(s, tabId)); + setFailedTabIds((s) => withoutItem(s, tabId)); try { const ids = siteScopedIds(selectedTypesSite); const ok = await clearTab(tab, ids); - if (!ok) setFailedTabIds((s) => new Set(s).add(tab.id!)); + if (!ok) setFailedTabIds((s) => withItem(s, tabId)); else { const origin = tabOrigin(tab); let linkedTargets: string[] = []; @@ -171,21 +168,17 @@ export default function App() { linkedTargets.map((target) => clearLinkedOrigin(target, ids, tabCookieStoreId(tab))), ); } - recordClear(tabDomain(tab) ?? origin ?? tab.url ?? '?', ids, linkedTargets); + recordClear(tabHostname(tab) ?? origin ?? tab.url ?? '?', ids, linkedTargets); if (autoReloadAfterClear) { - markReloading(tab.id); - browser.tabs.reload(tab.id, { bypassCache: true }); + markReloading(tabId); + browser.tabs.reload(tabId, { bypassCache: true }); } } } catch (err) { console.error('Bleep: site clear failed', err); - setFailedTabIds((s) => new Set(s).add(tab.id!)); + setFailedTabIds((s) => withItem(s, tabId)); } - setBusyTabIds((s) => { - const next = new Set(s); - next.delete(tab.id!); - return next; - }); + setBusyTabIds((s) => withoutItem(s, tabId)); } async function handleClearAllTabs() { @@ -251,11 +244,9 @@ export default function App() { async function clearVisitedSite(site: VisitedSite): Promise { const ids = siteScopedIds(selectedTypesSite); - // Firefox has no native per-site clear for cache/localStorage/sessionStorage, so - // clearLinkedOrigin falls back to a throwaway background tab for those. If the - // site is already open in a real tab, clear through that instead — no extra tab, - // and it picks up the tab's actual cookieStoreId (Firefox container) for free. - const openTab = (await browser.tabs.query({})).find((tab) => tabDomain(tab) === site.hostname); + // Prefer an already-open tab over clearLinkedOrigin's throwaway background tab — + // picks up the tab's real cookieStoreId (Firefox container) too. + const openTab = (await browser.tabs.query({})).find((tab) => tabHostname(tab) === site.hostname); const cookieStoreId = openTab ? tabCookieStoreId(openTab) : undefined; if (openTab?.id != null) { @@ -274,27 +265,20 @@ export default function App() { async function handleClearVisitedSite(site: VisitedSite) { if (isProtectedSite(protectedSites, site.hostname)) return; - setBusyVisitedHostnames((s) => new Set(s).add(site.hostname)); - setFailedVisitedHostnames((s) => { - const next = new Set(s); - next.delete(site.hostname); - return next; - }); + const { hostname } = site; + setBusyVisitedHostnames((s) => withItem(s, hostname)); + setFailedVisitedHostnames((s) => withoutItem(s, hostname)); try { const granted = await requestOriginPermission(); if (!granted) throw new Error('permission denied'); const linkedTargets = await clearVisitedSite(site); - recordClear(site.hostname, siteScopedIds(selectedTypesSite), linkedTargets); - setClearedHostnames((s) => new Set(s).add(site.hostname)); + recordClear(hostname, siteScopedIds(selectedTypesSite), linkedTargets); + setClearedHostnames((s) => withItem(s, hostname)); } catch (err) { console.error('Bleep: visited site clear failed', err); - setFailedVisitedHostnames((s) => new Set(s).add(site.hostname)); + setFailedVisitedHostnames((s) => withItem(s, hostname)); } - setBusyVisitedHostnames((s) => { - const next = new Set(s); - next.delete(site.hostname); - return next; - }); + setBusyVisitedHostnames((s) => withoutItem(s, hostname)); } async function handleClearAllVisitedSites() { @@ -410,36 +394,70 @@ export default function App() { )} {activeTab === 'perSite' && ( - +
+ + + +
+ +
+ + + +
+ +
+ +
+ + + +
+ + {!isGeckoBased() && ( + <> + +
+ + )} + + +
)} {activeTab === 'help' && } diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index 4265d2c..ddd7a8d 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -9,6 +9,7 @@ import { useTheme } from '../../hooks/useTheme'; import { useTranslation } from '../../hooks/useTranslation'; import { recordClear } from '../../store/clearLog'; import { useSettingsStore } from '../../store/settings'; +import { runClear } from '../../utils/clear-status'; import { clearGlobal, clearLinkedOrigin, @@ -20,7 +21,7 @@ import { requestOriginPermission, siteScopedIds, tabCookieStoreId, - tabDomain, + tabHostname, tabOrigin, } from '../../utils/clearing'; import { siteScopedDataTypes } from '../../utils/data-types'; @@ -42,23 +43,20 @@ export default function App() { const [status, setStatus] = useState('idle'); const [tabStatus, setTabStatus] = useState('idle'); const [activeTabId, setActiveTabId] = useState(null); - const [activeTabDomain, setActiveTabDomain] = useState(null); + const [activeTabHostname, setActiveTabHostname] = useState(null); const { markReloading, isReloading } = useReloadGuard(); useEffect(() => { browser.tabs.query({ active: true, currentWindow: true }).then(([tab]) => { setActiveTabId(tab?.id ?? null); - setActiveTabDomain(tab ? tabDomain(tab) : null); + setActiveTabHostname(tab ? tabHostname(tab) : null); }); }, []); - const isActiveTabProtected = activeTabDomain != null && isProtectedSite(protectedSites, activeTabDomain); + const isActiveTabProtected = activeTabHostname != null && isProtectedSite(protectedSites, activeTabHostname); async function handleClear() { - setStatus('clearing'); - try { - // Only fetch history (needed to resolve protected hostnames to real origins) if - // there's actually something to exclude — most popup opens won't need this. + await runClear(setStatus, async () => { let excludeOrigins: string[] = []; if (protectedSites.trim()) { const historyItems = await browser.history.search({ text: '', maxResults: 1000 }); @@ -68,12 +66,7 @@ export default function App() { } await clearGlobal(selectedTypesGlobal, excludeOrigins); recordClear('(global)', selectedTypesGlobal); - setStatus('done'); - } catch (err) { - console.error('Bleep: global clear failed', err); - setStatus('failed'); - } - setTimeout(() => setStatus('idle'), 1500); + }); } async function handleClearActiveTab() { @@ -99,7 +92,7 @@ export default function App() { ); } } - recordClear(tabDomain(activeTab) ?? activeTab.url ?? '?', ids, linkedTargets); + recordClear(tabHostname(activeTab) ?? activeTab.url ?? '?', ids, linkedTargets); } setTabStatus(ok ? 'done' : 'failed'); if (ok && autoReloadAfterClear && activeTab?.id != null) { diff --git a/package.json b/package.json index f162977..0a1d2fe 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "react": "^19.2.4", "react-dom": "^19.2.4", - "zustand": "^5.0.14" + "zustand": "^5.0.15" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -33,14 +33,14 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", - "globals": "^17.9.0", + "globals": "^17.11.0", "prettier": "^3.9.6", "tailwindcss": "^4.3.3", "typescript": "^5.9.3", "typescript-eslint": "^8.67.0", - "vite": "^8.2.1", - "vitest": "^4.1.10", + "vite": "^8.2.2", + "vitest": "^4.1.11", "web-ext": "^10.5.0", - "wxt": "^0.21.3" + "wxt": "^0.21.4" } } diff --git a/utils/clear-status.test.ts b/utils/clear-status.test.ts index fb3d0b5..b0d3788 100644 --- a/utils/clear-status.test.ts +++ b/utils/clear-status.test.ts @@ -1,6 +1,7 @@ -import { describe, expect, it } from 'vitest'; -import { clearAllLabel } from './clear-status'; +import { describe, expect, it, vi } from 'vitest'; +import { clearAllLabel, runClear } from './clear-status'; import type { TranslationKey } from './i18n'; +import type { ClearStatus } from '../components/StatusButton'; const t = (key: TranslationKey, vars?: Record) => vars ? `${key}:${JSON.stringify(vars)}` : key; @@ -22,3 +23,37 @@ describe('clearAllLabel', () => { expect(clearAllLabel(t, 'idle', 3)).toBe('clearAllCount:{"count":3}'); }); }); + +describe('runClear', () => { + it('transitions clearing -> done -> idle on success', async () => { + vi.useFakeTimers(); + const statuses: ClearStatus[] = []; + const setStatus = (s: ClearStatus) => statuses.push(s); + + const promise = runClear(setStatus, async () => {}); + expect(statuses).toEqual(['clearing']); + await promise; + expect(statuses).toEqual(['clearing', 'done']); + + vi.advanceTimersByTime(1500); + expect(statuses).toEqual(['clearing', 'done', 'idle']); + vi.useRealTimers(); + }); + + it('transitions clearing -> failed -> idle when fn throws', async () => { + vi.useFakeTimers(); + vi.spyOn(console, 'error').mockImplementation(() => {}); + const statuses: ClearStatus[] = []; + const setStatus = (s: ClearStatus) => statuses.push(s); + + await runClear(setStatus, async () => { + throw new Error('boom'); + }); + expect(statuses).toEqual(['clearing', 'failed']); + + vi.advanceTimersByTime(1500); + expect(statuses).toEqual(['clearing', 'failed', 'idle']); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); +}); diff --git a/utils/clear-status.ts b/utils/clear-status.ts index e7aa54a..2010b1c 100644 --- a/utils/clear-status.ts +++ b/utils/clear-status.ts @@ -11,3 +11,17 @@ export function clearAllLabel( if (status === 'failed') return t('someFailed'); return t('clearAllCount', { count }); } + +/** clearing → run fn → done/failed → back to idle after a short flash, used by every + * all-or-nothing clear action (Global clear in both popup and options). */ +export async function runClear(setStatus: (status: ClearStatus) => void, fn: () => Promise): Promise { + setStatus('clearing'); + try { + await fn(); + setStatus('done'); + } catch (err) { + console.error('Bleep: clear failed', err); + setStatus('failed'); + } + setTimeout(() => setStatus('idle'), 1500); +} diff --git a/utils/clearing.firefox.test.ts b/utils/clearing.firefox.test.ts index 6491f59..3c2cf4d 100644 --- a/utils/clearing.firefox.test.ts +++ b/utils/clearing.firefox.test.ts @@ -136,6 +136,25 @@ describe('clearLinkedOrigin (Firefox path)', () => { expect(browser.cookies.getAll).not.toHaveBeenCalled(); expect(browser.browsingData.remove).not.toHaveBeenCalled(); }); + + it('removes the tabs.onUpdated listener even when the tab never reaches "complete" (no leak on timeout)', async () => { + vi.useFakeTimers(); + // Override the default mock: never fire the "complete" event for this tab. + (browser.tabs.create as ReturnType).mockImplementation(async (props: { url: string }) => ({ + id: nextTabId++, + url: props.url, + })); + + const promise = clearLinkedOrigin('https://auth.domain.com', ['cacheStorage']); + await vi.advanceTimersByTimeAsync(0); + expect(onUpdatedListeners.length).toBe(1); + + await vi.advanceTimersByTimeAsync(10000); + await promise; + + expect(onUpdatedListeners.length).toBe(0); + vi.useRealTimers(); + }); }); describe('clearTabData (Firefox path)', () => { diff --git a/utils/clearing.test.ts b/utils/clearing.test.ts index 5162631..e1190c0 100644 --- a/utils/clearing.test.ts +++ b/utils/clearing.test.ts @@ -8,7 +8,7 @@ import { parseOriginMappings, siteScopedIds, tabCookieStoreId, - tabDomain, + tabHostname, tabOrigin, } from './clearing'; @@ -151,21 +151,21 @@ describe('dedupeSitesByHostname', () => { }); }); -describe('tabOrigin / tabDomain / tabCookieStoreId', () => { +describe('tabOrigin / tabHostname / tabCookieStoreId', () => { it('extracts origin and hostname from a tab url', () => { const tab = { url: 'https://sub.domain.com:8443/path?x=1' }; expect(tabOrigin(tab)).toBe('https://sub.domain.com:8443'); - expect(tabDomain(tab)).toBe('sub.domain.com'); + expect(tabHostname(tab)).toBe('sub.domain.com'); }); it('returns null for a missing url', () => { expect(tabOrigin({})).toBeNull(); - expect(tabDomain({})).toBeNull(); + expect(tabHostname({})).toBeNull(); }); it('returns null for an unparsable url', () => { expect(tabOrigin({ url: 'not a url' })).toBeNull(); - expect(tabDomain({ url: 'not a url' })).toBeNull(); + expect(tabHostname({ url: 'not a url' })).toBeNull(); }); it('reads the Firefox-only cookieStoreId field when present', () => { diff --git a/utils/clearing.ts b/utils/clearing.ts index 617486f..66a8357 100644 --- a/utils/clearing.ts +++ b/utils/clearing.ts @@ -197,14 +197,10 @@ async function clearCookiesForOrigin(origin: string, storeId?: string): Promise< ); } -/** Adds https:// if no scheme is present, so plain "domain.com" parses the same as - * "https://domain.com". */ function withScheme(s: string): string { return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(s) ? s : `https://${s}`; } -/** A target needs to be a real origin for the clearing calls; accepts scheme-less - * input and strips any path/query the user pastes along with it. */ function normalizeTargetOrigin(input: string): string { const trimmed = input.trim(); try { @@ -214,7 +210,6 @@ function normalizeTargetOrigin(input: string): string { } } -/** Ignores scheme and path — only the hostname matters for matching a mapping. */ function extractHostname(input: string): string { const trimmed = input.trim(); try { @@ -226,8 +221,8 @@ function extractHostname(input: string): string { const DOMAIN_RE = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i; -/** Loose check for "does this look like a domain" (e.g. rejects "asdasd"), used for - * inline validation hints only — matching itself (linkedOriginsFor) doesn't call this. */ +/** Loose check for "does this look like a domain" (rejects e.g. "asdasd") — used + * only for inline validation hints, not for matching. */ export function isValidHost(input: string): boolean { const host = extractHostname(input); if (!host) return false; @@ -289,26 +284,27 @@ export function isProtectedSite(raw: string, hostname: string): boolean { return parseHostList(raw).some((protectedHost) => host === protectedHost || host.endsWith(`.${protectedHost}`)); } -/** Drops any linked-origin target that's itself protected, so a mapping can't be - * used to route around the protected-sites list. */ export function filterProtectedTargets(targets: string[], protectedRaw: string): string[] { if (!protectedRaw.trim()) return targets; return targets.filter((target) => !isProtectedSite(protectedRaw, extractHostname(target))); } function waitForTabComplete(tabId: number, ms: number): Promise { - return withTimeout( - new Promise((resolve) => { - function onUpdated(id: number, info: { status?: string }) { - if (id === tabId && info.status === 'complete') { - browser.tabs.onUpdated.removeListener(onUpdated); - resolve(); - } + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + browser.tabs.onUpdated.removeListener(onUpdated); + reject(new Error(`Timed out after ${ms}ms`)); + }, ms); + + function onUpdated(id: number, info: { status?: string }) { + if (id === tabId && info.status === 'complete') { + clearTimeout(timer); + browser.tabs.onUpdated.removeListener(onUpdated); + resolve(); } - browser.tabs.onUpdated.addListener(onUpdated); - }), - ms, - ); + } + browser.tabs.onUpdated.addListener(onUpdated); + }); } export async function clearLinkedOrigin( @@ -371,7 +367,7 @@ export function tabOrigin(tab: { url?: string }): string | null { } } -export function tabDomain(tab: { url?: string }): string | null { +export function tabHostname(tab: { url?: string }): string | null { if (!tab.url) return null; try { return new URL(tab.url).hostname; @@ -398,7 +394,7 @@ export function dedupeSitesByHostname(urls: Array): VisitedS if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') continue; if (!seen.has(parsed.hostname)) seen.set(parsed.hostname, parsed.origin); } catch { - // not a parsable URL — skip + // skip } } return Array.from(seen, ([hostname, origin]) => ({ hostname, origin })); diff --git a/utils/set-helpers.test.ts b/utils/set-helpers.test.ts new file mode 100644 index 0000000..5979afa --- /dev/null +++ b/utils/set-helpers.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { withItem, withoutItem } from './set-helpers'; + +describe('withItem', () => { + it('returns a new set with the item added', () => { + const original = new Set([1, 2]); + const result = withItem(original, 3); + expect(result).toEqual(new Set([1, 2, 3])); + expect(original).toEqual(new Set([1, 2])); + }); + + it('is a no-op value-wise when the item is already present', () => { + expect(withItem(new Set([1]), 1)).toEqual(new Set([1])); + }); +}); + +describe('withoutItem', () => { + it('returns a new set with the item removed', () => { + const original = new Set([1, 2, 3]); + const result = withoutItem(original, 2); + expect(result).toEqual(new Set([1, 3])); + expect(original).toEqual(new Set([1, 2, 3])); + }); + + it('is a no-op when the item is absent', () => { + expect(withoutItem(new Set([1]), 2)).toEqual(new Set([1])); + }); +}); diff --git a/utils/set-helpers.ts b/utils/set-helpers.ts new file mode 100644 index 0000000..c8adc58 --- /dev/null +++ b/utils/set-helpers.ts @@ -0,0 +1,9 @@ +export function withItem(set: Set, item: T): Set { + return new Set(set).add(item); +} + +export function withoutItem(set: Set, item: T): Set { + const next = new Set(set); + next.delete(item); + return next; +} diff --git a/utils/storage-adapter.ts b/utils/storage-adapter.ts index 52080fa..47da5e6 100644 --- a/utils/storage-adapter.ts +++ b/utils/storage-adapter.ts @@ -47,8 +47,6 @@ async function removeChunked(area: Browser.storage.StorageArea, name: string): P await area.remove([lenKey, ...Array.from({ length: len }, (_, i) => `${name}__${i}`)]); } -/** Plain, unchunked local storage — used for state that's device-specific by nature - * (e.g. the clear-activity log) and shouldn't consume the sync quota. */ export const browserLocalStorage: StateStorage = { getItem: async (name) => { const result = await browser.storage.local.get(name); diff --git a/yarn.lock b/yarn.lock index 6b9b9e2..759b63e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -404,16 +404,16 @@ integrity sha512-Rutrrc3FYOc+um4/QC4WFETNk2fV8NKWoqQl3tQfcVTQ1WFaoJRbsJBasSF34ltitmnYYD6ZsKW1cOQtSj1jbQ== "@napi-rs/wasm-runtime@^1.1.4": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz#c70706532e5827c0932ca6bf43ee2c512f29c639" - integrity sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw== + version "1.2.3" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz#97e3d45d7424dc5da1d4e32f3bf3b292f6c1b44c" + integrity sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q== dependencies: "@tybys/wasm-util" "^0.10.3" -"@oxc-project/types@=0.143.0": - version "0.143.0" - resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.143.0.tgz#c3e4f3178b7b54e4dd194eac6d45258a60f0092b" - integrity sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA== +"@oxc-project/types@=0.146.0": + version "0.146.0" + resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.146.0.tgz#d57a2591abbf1f6e50981b07ee24ab269530d87a" + integrity sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA== "@pinojs/redact@^0.4.0": version "0.4.0" @@ -441,75 +441,80 @@ "@pnpm/network.ca-file" "^1.0.1" config-chain "^1.1.11" -"@rolldown/binding-android-arm64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz#001b8b0b01844701efda1bb6bed84b681c4a488b" - integrity sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw== +"@rolldown/binding-android-arm-eabi@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz#165b80910de7cd33f772d5b7b045b259acb7420c" + integrity sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA== -"@rolldown/binding-darwin-arm64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz#5e87c602ed634a6fef092e2162e24fbfb881c4ec" - integrity sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA== +"@rolldown/binding-android-arm64@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz#5ed74d4b8fa56c68eb1aeb81d0d207a85b6de05c" + integrity sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig== -"@rolldown/binding-darwin-x64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz#f32e0b286714bd03a421d693415d05d97d265b77" - integrity sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA== +"@rolldown/binding-darwin-arm64@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz#6f27c7060e58ca03061fa7d50f9dc409bc377fe1" + integrity sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww== -"@rolldown/binding-freebsd-x64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz#5f38ad5761b6b7b21b57a99566bb52634c60ab19" - integrity sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg== +"@rolldown/binding-darwin-x64@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz#74ab897d134ede4072fdc6108f00193e6167ee28" + integrity sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A== -"@rolldown/binding-linux-arm-gnueabihf@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz#ab4dcd07f1bd88e8d659ae0c3bb9d2f290adb897" - integrity sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw== +"@rolldown/binding-freebsd-x64@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz#7d337f9ae4b739674e1938747118ab0676c8ef8a" + integrity sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw== -"@rolldown/binding-linux-arm64-gnu@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz#d279b7016039a725fb66d82784b9841f42df83da" - integrity sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw== +"@rolldown/binding-linux-arm-gnueabihf@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz#a8195dc1091ade0f912b0613ef6500941e5615f4" + integrity sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg== -"@rolldown/binding-linux-arm64-musl@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz#d08bbc93d2742214548c5adf7df7788944e5a89a" - integrity sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q== +"@rolldown/binding-linux-arm64-gnu@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz#40fbbb97072b1acaa3e0e8fe9774fe524e860bba" + integrity sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA== -"@rolldown/binding-linux-ppc64-gnu@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz#6418e63745b3193f26ab3bb88744b3a4a1356d7c" - integrity sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w== +"@rolldown/binding-linux-arm64-musl@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz#3cfe8b0f7c13de29dace9a9fc6c03081164176ab" + integrity sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA== -"@rolldown/binding-linux-s390x-gnu@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz#77ec30d0704cf4eb1cb4a63f501c9852c6728cf4" - integrity sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg== +"@rolldown/binding-linux-ppc64-gnu@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz#3bd890d96ae29f93718aa142ed0982f2ee2978ad" + integrity sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA== -"@rolldown/binding-linux-x64-gnu@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz#3b9b6e0dd3e86c597f42858748ca25f1dfd58ed8" - integrity sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w== +"@rolldown/binding-linux-s390x-gnu@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz#0959a0d5af22741d5787918e27aef70dc8602804" + integrity sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ== -"@rolldown/binding-linux-x64-musl@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz#f78033c592c8bd2af48284a45f8e4baaa0befbf5" - integrity sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A== +"@rolldown/binding-linux-x64-gnu@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz#7baa94e826328ac6f457d9e1abacffa0353e8d22" + integrity sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ== -"@rolldown/binding-openharmony-arm64@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz#36e951f5a6fca922a5205e283d0a82b9f98199ca" - integrity sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug== +"@rolldown/binding-linux-x64-musl@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz#5f37597eaf0e22313d1e3d4be7d1be1fd332904e" + integrity sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA== -"@rolldown/binding-win32-arm64-msvc@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz#c1e494ac47e13bd857fca0b3ad59c33580241f7e" - integrity sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw== +"@rolldown/binding-openharmony-arm64@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz#d096567af3f738cbe6aa858ab0a01aae9f357ef4" + integrity sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw== -"@rolldown/binding-win32-x64-msvc@1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz#b0effffcd6872f8a021373eb437916b1b52283a4" - integrity sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg== +"@rolldown/binding-win32-arm64-msvc@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz#d53b911aa4e6b547b789c07747fabab2ac8c237d" + integrity sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw== + +"@rolldown/binding-win32-x64-msvc@1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz#7bbd08cfda6a98de9b756472c772a36ebb7229bc" + integrity sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw== "@rolldown/pluginutils@^1.0.0", "@rolldown/pluginutils@^1.0.1": version "1.0.1" @@ -806,69 +811,69 @@ eslint-visitor-keys "^5.0.0" "@vitejs/plugin-react@^4.4.1 || ^5.0.0 || ^6.0.0": - version "6.0.5" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz#48ee7951093ff9054952cff9d64e874355cde372" - integrity sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA== + version "6.1.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz#a3d7a3b998c618385d076e390b74398c3d8c65bc" + integrity sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw== dependencies: "@rolldown/pluginutils" "^1.0.1" -"@vitest/expect@4.1.10": - version "4.1.10" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.10.tgz#799c06fc44bb0cf7e2784137b627c5cc173285d4" - integrity sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA== +"@vitest/expect@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.11.tgz#5f580d1f9cdbba314dbf23b2d911f8eb23878f5f" + integrity sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw== dependencies: "@standard-schema/spec" "^1.1.0" "@types/chai" "^5.2.2" - "@vitest/spy" "4.1.10" - "@vitest/utils" "4.1.10" + "@vitest/spy" "4.1.11" + "@vitest/utils" "4.1.11" chai "^6.2.2" tinyrainbow "^3.1.0" -"@vitest/mocker@4.1.10": - version "4.1.10" - resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.10.tgz#2413987ab4cd7fa1c2b614b404c407bf6ad1ead1" - integrity sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow== +"@vitest/mocker@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.11.tgz#8e2906361bc5dfa271757a858ae80643118fcbb4" + integrity sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ== dependencies: - "@vitest/spy" "4.1.10" + "@vitest/spy" "4.1.11" estree-walker "^3.0.3" magic-string "^0.30.21" -"@vitest/pretty-format@4.1.10": - version "4.1.10" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.10.tgz#75542e7273a08cc10fd4d8dad4e3eb1f16cd958c" - integrity sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q== +"@vitest/pretty-format@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.11.tgz#8b28eb8240771d6ea970e33beaeb41384b51868e" + integrity sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw== dependencies: tinyrainbow "^3.1.0" -"@vitest/runner@4.1.10": - version "4.1.10" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.10.tgz#febf0a21a9168421422d1955370e606feab60355" - integrity sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg== +"@vitest/runner@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.11.tgz#bfbad98c8d6c3f1fb4df12056ad569821ff77f21" + integrity sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw== dependencies: - "@vitest/utils" "4.1.10" + "@vitest/utils" "4.1.11" pathe "^2.0.3" -"@vitest/snapshot@4.1.10": - version "4.1.10" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.10.tgz#7e3e9fec7d4d47232e493cfdcbd2170de4371c04" - integrity sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw== +"@vitest/snapshot@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.11.tgz#df461eb165924a3155986dde68e13360f53f3d4c" + integrity sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog== dependencies: - "@vitest/pretty-format" "4.1.10" - "@vitest/utils" "4.1.10" + "@vitest/pretty-format" "4.1.11" + "@vitest/utils" "4.1.11" magic-string "^0.30.21" pathe "^2.0.3" -"@vitest/spy@4.1.10": - version "4.1.10" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.10.tgz#5c0bfa97b56bba9e37403c976db776ff6ab56f65" - integrity sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw== +"@vitest/spy@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.11.tgz#0add45cae953afed9c88f98e2f6fc9164558c32a" + integrity sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA== -"@vitest/utils@4.1.10": - version "4.1.10" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.10.tgz#ffc71055f18bfccb1fd0586365ebc2824892e403" - integrity sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA== +"@vitest/utils@4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.11.tgz#9b27a4293b827942b223539bfab1bd9f7eada31b" + integrity sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ== dependencies: - "@vitest/pretty-format" "4.1.10" + "@vitest/pretty-format" "4.1.11" convert-source-map "^2.0.0" tinyrainbow "^3.1.0" @@ -892,10 +897,10 @@ resolved "https://registry.yarnpkg.com/@webext-core/match-patterns/-/match-patterns-2.0.0.tgz#3fc7c1a652ebe6f23315ca036f0236e7dcc18041" integrity sha512-EsyMMKfQuSE4rWCjP/TifwvUWlhC/OhiGn5OK3p9F0d57UmtLib0nzguzPKyxM1/kZV7SkE+jYXkE8gsdPRLmw== -"@wxt-dev/browser@*", "@wxt-dev/browser@>=0.1", "@wxt-dev/browser@^0.2.2": - version "0.2.5" - resolved "https://registry.yarnpkg.com/@wxt-dev/browser/-/browser-0.2.5.tgz#0307664dce5a0a0ecfd805c08a949fe0aeb4b281" - integrity sha512-676RhVsOFNSZnisfeM9U40DdYjZmx5whJuBHJ7v4R2sR6e/SYfuIuzmJf3owr2n82d2vCDwGNbvt4nLU14sGZg== +"@wxt-dev/browser@*", "@wxt-dev/browser@>=0.1": + version "0.2.6" + resolved "https://registry.yarnpkg.com/@wxt-dev/browser/-/browser-0.2.6.tgz#1d6f40cb774e80ed2df34f6b46e81e8df17763a2" + integrity sha512-rQMn4gr36988e3RYVxsr0Qvy6A/9bIcA0meQvGtfQcEPmRJ73SU3xkHh6eX2yjv/j1QkMLacdzES/jf9xqz5/A== dependencies: "@types/filesystem" "*" "@types/har-format" "*" @@ -1016,9 +1021,9 @@ ansi-regex@^5.0.1: integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.2.2: - version "6.2.2" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" - integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== + version "6.3.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.3.0.tgz#247c8e7b70a1a43b10ce14c0226fcbf58e8815d5" + integrity sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ== ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" @@ -1081,9 +1086,9 @@ balanced-match@^4.0.2: integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== baseline-browser-mapping@^2.11.12: - version "2.11.13" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz#660073103c1bee93e54df55f117b7528adf6af19" - integrity sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ== + version "2.11.16" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.16.tgz#0fa19a4ece2e34439ecaa3fdca8a59acfbd287fb" + integrity sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ== bluebird@~3.7: version "3.7.2" @@ -1470,9 +1475,9 @@ default-browser-id@^5.0.0: integrity sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q== default-browser@^5.4.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.5.0.tgz#2792e886f2422894545947cc80e1a444496c5976" - integrity sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw== + version "5.5.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.5.1.tgz#1790affc52680fbb11e17cab2752d69aa2a37d2c" + integrity sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw== dependencies: bundle-name "^4.1.0" default-browser-id "^5.0.0" @@ -1596,9 +1601,9 @@ ecdsa-sig-formatter@1.0.11: safe-buffer "^5.0.1" electron-to-chromium@^1.5.402: - version "1.5.403" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.403.tgz#8a6e422bc68c5d2fa4d8c5fa2ba9a583c3d4452c" - integrity sha512-MQsYmdaLzvaCX5j+ZZBr5Fm6uCCnPQcRtlvmvRlWqrXy+BH2O4ffXIAScF+JQznQWB9brWp4lSD9Z4yNmaf2BA== + version "1.5.411" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.411.tgz#d56c7e43f91e2b3abb53b4eecd646e20d0b18b34" + integrity sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg== emoji-regex@^10.3.0: version "10.6.0" @@ -1647,9 +1652,9 @@ entities@^8.0.0: integrity sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA== es-module-lexer@^2.0.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.3.1.tgz#5bf2df06999dbbe5f006a5f46a11fb9f5b7b391b" - integrity sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA== + version "2.3.2" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz#311fa4f40168c1975c505477c51b23234d41ad55" + integrity sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw== es6-error@4.1.1: version "4.1.1" @@ -1993,11 +1998,6 @@ get-east-asian-width@^1.0.0: resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz#216900f91df11a8b2c198c3e1d93d6c035a776b9" integrity sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA== -get-port-please@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.2.0.tgz#0ce3cee194c448ac640ec39dc357a500f5d7d2bb" - integrity sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A== - "giget@^1.2.3 || ^2.0.0 || ^3.0.0", giget@^3.2.0: version "3.3.1" resolved "https://registry.yarnpkg.com/giget/-/giget-3.3.1.tgz#4a4e610cd112e5dc478c6035986fdc19c76dad73" @@ -2022,10 +2022,10 @@ globals@^14.0.0: resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== -globals@^17.9.0: - version "17.9.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-17.9.0.tgz#e43f252d6bbe71508da43902a1709c8895a59f70" - integrity sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg== +globals@^17.11.0: + version "17.11.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-17.11.0.tgz#d643485bb30220d7751e511cf4f68c73d3870d87" + integrity sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw== graceful-fs@4.2.10: version "4.2.10" @@ -2226,7 +2226,7 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -is-wsl@^3.1.0, is-wsl@^3.1.1: +is-wsl@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.1.tgz#327897b26832a3eb117da6c27492d04ca132594f" integrity sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw== @@ -2636,9 +2636,9 @@ magic-string@^0.30.21: "@jridgewell/sourcemap-codec" "^1.5.5" magic-string@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-1.1.0.tgz#54a8470aabf2a04b970177f26c0308cabea74a3f" - integrity sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g== + version "1.2.2" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-1.2.2.tgz#7b84c3b4f594964a5f402a13fd7a3539c56709df" + integrity sha512-veT/+7iXrXzT39XnEN4lOxtNl72dMgJ8Lp+5Bd6YcMSWpb0n0MjBM8Uuooi6jgJr8dhUW2swQgBmoZVMni5SVg== dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" @@ -2715,23 +2715,11 @@ multimatch@6.0.0: array-union "^3.0.1" minimatch "^3.0.4" -nano-spawn@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/nano-spawn/-/nano-spawn-2.1.0.tgz#cddc7fe9379ea5c1a8e3bb2b9a9250a4aeb6be38" - integrity sha512-yTW+2okrElHiH4fsiz/+/zc0EDo9BDDoC3iKk8dpv1GeRc9nUWzUZHx6TofMWErchhUQR8hY9/Eu1Uja9x1nqA== - nanoid@^3.3.17: version "3.3.18" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913" integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w== -nanospinner@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/nanospinner/-/nanospinner-1.2.2.tgz#5a38f4410b5bf7a41585964bee74d32eab3e040b" - integrity sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA== - dependencies: - picocolors "^1.1.1" - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2747,11 +2735,6 @@ node-releases@^2.0.53: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.53.tgz#0fe5ad8a7935e075172f574e56f0c20f07fa41af" integrity sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ== -normalize-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - nth-check@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" @@ -2781,9 +2764,9 @@ obug@^2.1.1: integrity sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA== ohash@^2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.11.tgz#60b11e8cff62ca9dee88d13747a5baa145f5900b" - integrity sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ== + version "2.0.12" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-2.0.12.tgz#cb97c19888ff31e7b22214cd7924187fc97ef05c" + integrity sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw== on-exit-leak-free@^2.1.0: version "2.1.2" @@ -2977,7 +2960,7 @@ pkg-types@^2.3.0, pkg-types@^2.3.1: exsolve "^1.0.8" pathe "^2.0.3" -postcss@^8.5.25: +postcss@^8.5.26: version "8.5.26" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.26.tgz#6e75135780c7e10df3433bf2266c552d35c8c620" integrity sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ== @@ -3141,28 +3124,29 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -rolldown@~1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.3.tgz#103bdcbbd575d51265277b8b510f080827b6eb6f" - integrity sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A== +rolldown@~1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.5.tgz#1f504a7d05260a769e617d950410bbb051498c50" + integrity sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA== dependencies: - "@oxc-project/types" "=0.143.0" + "@oxc-project/types" "=0.146.0" "@rolldown/pluginutils" "^1.0.0" optionalDependencies: - "@rolldown/binding-android-arm64" "1.2.3" - "@rolldown/binding-darwin-arm64" "1.2.3" - "@rolldown/binding-darwin-x64" "1.2.3" - "@rolldown/binding-freebsd-x64" "1.2.3" - "@rolldown/binding-linux-arm-gnueabihf" "1.2.3" - "@rolldown/binding-linux-arm64-gnu" "1.2.3" - "@rolldown/binding-linux-arm64-musl" "1.2.3" - "@rolldown/binding-linux-ppc64-gnu" "1.2.3" - "@rolldown/binding-linux-s390x-gnu" "1.2.3" - "@rolldown/binding-linux-x64-gnu" "1.2.3" - "@rolldown/binding-linux-x64-musl" "1.2.3" - "@rolldown/binding-openharmony-arm64" "1.2.3" - "@rolldown/binding-win32-arm64-msvc" "1.2.3" - "@rolldown/binding-win32-x64-msvc" "1.2.3" + "@rolldown/binding-android-arm-eabi" "1.2.5" + "@rolldown/binding-android-arm64" "1.2.5" + "@rolldown/binding-darwin-arm64" "1.2.5" + "@rolldown/binding-darwin-x64" "1.2.5" + "@rolldown/binding-freebsd-x64" "1.2.5" + "@rolldown/binding-linux-arm-gnueabihf" "1.2.5" + "@rolldown/binding-linux-arm64-gnu" "1.2.5" + "@rolldown/binding-linux-arm64-musl" "1.2.5" + "@rolldown/binding-linux-ppc64-gnu" "1.2.5" + "@rolldown/binding-linux-s390x-gnu" "1.2.5" + "@rolldown/binding-linux-x64-gnu" "1.2.5" + "@rolldown/binding-linux-x64-musl" "1.2.5" + "@rolldown/binding-openharmony-arm64" "1.2.5" + "@rolldown/binding-win32-arm64-msvc" "1.2.5" + "@rolldown/binding-win32-x64-msvc" "1.2.5" run-applescript@^7.0.0: version "7.1.0" @@ -3603,31 +3587,31 @@ util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -"vite@^6.0.0 || ^7.0.0 || ^8.0.0", vite@^8.2.1: - version "8.2.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-8.2.1.tgz#6fc8d8bb843bd52353091fac978e194d4de5b31d" - integrity sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw== +"vite@^6.0.0 || ^7.0.0 || ^8.0.0", vite@^8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-8.2.2.tgz#399aefad3656145145be110d137a07ea5bb55014" + integrity sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q== dependencies: lightningcss "^1.33.0" picomatch "^4.0.5" - postcss "^8.5.25" - rolldown "~1.2.1" + postcss "^8.5.26" + rolldown "~1.2.4" tinyglobby "^0.2.17" optionalDependencies: fsevents "~2.3.3" -vitest@^4.1.10: - version "4.1.10" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.10.tgz#7e9285efe264b1167050b7a3a7ff34788e1b7afc" - integrity sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw== - dependencies: - "@vitest/expect" "4.1.10" - "@vitest/mocker" "4.1.10" - "@vitest/pretty-format" "4.1.10" - "@vitest/runner" "4.1.10" - "@vitest/snapshot" "4.1.10" - "@vitest/spy" "4.1.10" - "@vitest/utils" "4.1.10" +vitest@^4.1.11: + version "4.1.11" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.11.tgz#1653c1521ae917f960d9b21877797c47dfd8bf21" + integrity sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw== + dependencies: + "@vitest/expect" "4.1.11" + "@vitest/mocker" "4.1.11" + "@vitest/pretty-format" "4.1.11" + "@vitest/runner" "4.1.11" + "@vitest/snapshot" "4.1.11" + "@vitest/spy" "4.1.11" + "@vitest/utils" "4.1.11" es-module-lexer "^2.0.0" expect-type "^1.3.0" magic-string "^0.30.21" @@ -3775,10 +3759,10 @@ wsl-utils@^0.3.0: is-wsl "^3.1.0" powershell-utils "^0.1.0" -wxt@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/wxt/-/wxt-0.21.3.tgz#bdc9125b08407f2acb79fe53f60bd4c2224a7bb5" - integrity sha512-UXvzDmgYVdoZiqZyWuoJ5zpjaxCsBRz1fr3q9gVFfge4AeJr0SOA/ijzVRY3ot0L/INZviP4p1by5dCnnwt6bw== +wxt@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/wxt/-/wxt-0.21.4.tgz#56fefb36b65d394557eeb5efd84732f82693a93f" + integrity sha512-iyVMTXb37BQAbsPHCuPzrK3ZxnO9pZmvbKl7VJWIHe3RG40On2NyP2FOjArtOITO96plEYnh3o+iUlRp9qE90w== dependencies: "@1natsu/wait-element" "^4.1.2" "@aklinker1/rollup-plugin-visualizer" "5.12.0" @@ -3787,7 +3771,7 @@ wxt@^0.21.3: "@webext-core/fake-browser" "^2.0.1" "@webext-core/isolated-element" "^1.1.3 || ^2 || ^3" "@webext-core/match-patterns" "^2.0.0" - "@wxt-dev/browser" "^0.2.2" + "@wxt-dev/browser" ">=0.1" "@wxt-dev/storage" "^1.0.0" c12 "^3.3.4" cac "^6.7.14 || ^7.0.0" @@ -3796,23 +3780,17 @@ wxt@^0.21.3: defu "^6.1.4" dotenv-expand "^13.0.0" filesize "^11.0.17" - get-port-please "^3.2.0" giget "^1.2.3 || ^2.0.0 || ^3.0.0" hookable "^6.1.0" - is-wsl "^3.1.1" json5 "^2.2.3" linkedom "^0.18.12" magicast "^0.5.2" - nano-spawn "^2.0.0" - nanospinner "^1.2.2" - normalize-path "^3.0.0" nypm "^0.6.5" - ohash "^2.0.11" picomatch "^4.0.3" publish-browser-extension "^5.1.0 || ^6.0.0" - scule "^1.3.0" superlock "^1.3.2" tiny-open "^1.3.0" + tinyexec "^1.2.4" tinyglobby "^0.2.16" unimport "^3.13.1 || ^4.0.0 || ^5.0.0 || ^6.0.0" @@ -3905,7 +3883,7 @@ zip-dir@2.0.0: resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== -zustand@^5.0.14: - version "5.0.14" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.14.tgz#18216c24fcb980cf36898f9c57520e67b1f77855" - integrity sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g== +zustand@^5.0.15: + version "5.0.15" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.15.tgz#42bddf35647cb80a818a8943a69f6618c126fcfe" + integrity sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==