diff --git a/CHANGELOG.md b/CHANGELOG.md index 2539cdf..4ec4812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,39 @@ Notable user-facing changes are recorded here. Cookie Loom versions are independent of the historical Cookie Quick Manager version numbers. +## 0.1.1-preview.1 — 2026-10-12 + +The second unsigned GitHub preview. Browser manifests use `0.1.1`. + +### Changed + +- Preact replaces the React DOM renderer; advanced tools load from local chunks + on demand. Browser ZIPs fall from about 138 KB to 84 KB. +- The popup reads its captured site's store and host scope. Cookie edits verify + matching identities without reading the whole profile. +- Large domain lists show 50 entries initially, with complete search and Show + more; the selected domain stays visible. The narrow-layout domain filter + survives resizing, and hidden desktop options are no longer mounted. +- Shared domain/identity indexes, a reused date formatter, and fewer store + enumerations reduce repeated work. Refresh has an explicit busy state. +- Dirty-draft and keyboard guards run synchronously, and nested dialogs restore + focus after closing. Existing stale-write and cleanup safeguards are retained. +- Reproducible-source archives omit documentation PNGs while preserving build + inputs, runtime icons, licenses, and attribution. The imagery remains in Git. + +### Added + +- A native-cookie benchmark with committed before/after samples and methodology. + On the 2,000-cookie Chromium fixture, workbench startup took 59% less time; + results are specific to the measured environment. +- Raw installed/startup JavaScript and ZIP size budgets in CI. +- Scoped-read and large-domain regressions: 153 unit tests, 58 browser workflows, + four native Chrome workflows, and 14 native Firefox checks. Minimum versions + remain Chrome 132 and Firefox 140. + +See [the performance record](docs/extension-performance.md) and +[review](docs/review-2026-10-12-optimization.md). Store publication remains pending. + ## 0.1.0-preview.1 — 2026-10-12 The first public GitHub preview. Browser manifests use `0.1.0`; packages are diff --git a/README.md b/README.md index ea4e910..0810ddd 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,15 @@ and developer work, with a quick site popup and a full cookie workbench. Cookie Loom is an independent, substantial rewrite of [Cookie Quick Manager](https://github.com/ysard/cookie-quick-manager), with a -new TypeScript / React codebase and shared Chromium and Firefox builds. It +new TypeScript / Preact codebase and shared Chromium and Firefox builds. It continues under GPL-3.0-or-later and retains the original project's history and attribution. See [NOTICE](NOTICE) and [the migration guide](docs/migration.md). -**Status: 0.1.0 public preview.** Download the unsigned browser ZIPs, reproducible -source, and checksums from [the preview release](https://github.com/q1/cookie-quick-manager/releases/tag/v0.1.0-preview.1). +**Status: 0.1.1 public preview.** Download the unsigned browser ZIPs, reproducible +source, and checksums from [the preview release](https://github.com/q1/cookie-quick-manager/releases/tag/v0.1.1-preview.1). + +The [0.1.1 performance comparison](docs/extension-performance.md) records smaller +packages, faster large-profile workflows, and the reproducible benchmark. Chromium can load the extracted build unpacked; Firefox supports a temporary development installation. No Cookie Loom browser-store release is claimed here. Targets are desktop Chromium 132+ and Firefox 140+; browser-specific behavior @@ -111,7 +114,7 @@ bun run test:firefox The Bun workspace contains: ```text -apps/extension/ WXT extension, React interface, browser adapter, local demo +apps/extension/ WXT extension, Preact interface, browser adapter, local demo packages/core/ Cookie identity, validation, filters, import/export rules docs/ Architecture, development, migration, verification, releases legacy/ Original extension and historical documentation/assets diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 3a556a4..bf5203a 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -4,70 +4,14 @@ These notices cover code bundled in the Cookie Loom extension. Development tools retain their licenses in their installed packages. Update this file with `bun run prepare:assets` after dependency changes. -## react 19.3.0 +## preact 11.0.1 License: MIT. ```text -MIT License - -Copyright (c) Meta Platforms, Inc. and affiliates. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -## react-dom 19.3.0 - -License: MIT. - -```text -MIT License - -Copyright (c) Meta Platforms, Inc. and affiliates. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -## scheduler 0.28.0 - -License: MIT. - -```text -MIT License +The MIT License (MIT) -Copyright (c) Meta Platforms, Inc. and affiliates. +Copyright (c) 2015-present Jason Miller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/apps/extension/package.json b/apps/extension/package.json index 1c0d092..c951421 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -1,6 +1,6 @@ { "name": "@cookie-loom/extension", - "version": "0.1.0", + "version": "0.1.1", "private": true, "type": "module", "license": "GPL-3.0-or-later", @@ -16,14 +16,13 @@ "dependencies": { "@cookie-loom/core": "*", "lucide-react": "0.468.0", - "react": "19.3.0", - "react-dom": "19.3.0" + "preact": "11.0.1", + "react": "19.3.0" }, "devDependencies": { + "@preact/preset-vite": "2.10.6", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", - "@vitejs/plugin-react": "5.2.0", - "@wxt-dev/module-react": "1.2.2", "vite": "8.3.4", "web-ext": "10.7.0", "wxt": "0.21.4" diff --git a/apps/extension/public/THIRD_PARTY_NOTICES.md b/apps/extension/public/THIRD_PARTY_NOTICES.md index 3a556a4..bf5203a 100644 --- a/apps/extension/public/THIRD_PARTY_NOTICES.md +++ b/apps/extension/public/THIRD_PARTY_NOTICES.md @@ -4,70 +4,14 @@ These notices cover code bundled in the Cookie Loom extension. Development tools retain their licenses in their installed packages. Update this file with `bun run prepare:assets` after dependency changes. -## react 19.3.0 +## preact 11.0.1 License: MIT. ```text -MIT License - -Copyright (c) Meta Platforms, Inc. and affiliates. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -## react-dom 19.3.0 - -License: MIT. - -```text -MIT License - -Copyright (c) Meta Platforms, Inc. and affiliates. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -## scheduler 0.28.0 - -License: MIT. - -```text -MIT License +The MIT License (MIT) -Copyright (c) Meta Platforms, Inc. and affiliates. +Copyright (c) 2015-present Jason Miller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/apps/extension/src/lib/demo-gateway.ts b/apps/extension/src/lib/demo-gateway.ts index 2d1ad2f..8f8d5cd 100644 --- a/apps/extension/src/lib/demo-gateway.ts +++ b/apps/extension/src/lib/demo-gateway.ts @@ -1,4 +1,10 @@ -import { cookieKey, validateCookie, type CookieRecord, type CookieStore } from '@cookie-loom/core'; +import { + cookieKey, + domainMatches, + validateCookie, + type CookieRecord, + type CookieStore, +} from '@cookie-loom/core'; import { DEFAULT_PREFERENCES, type Gateway, type Preferences } from './types'; const DEMO_STORES: CookieStore[] = [ @@ -94,8 +100,14 @@ export function createDemoGateway(): Gateway { async listStores() { return clone(DEMO_STORES); }, - async listCookies() { - return clone(cookies); + async listCookies(options = {}) { + return clone( + cookies.filter( + (cookie) => + (!options.storeIds || options.storeIds.includes(cookie.storeId)) && + (!options.host || domainMatches(cookie, options.host)), + ), + ); }, async saveCookie(cookie, original) { const errors = validateCookie(cookie).filter((issue) => issue.severity === 'error'); diff --git a/apps/extension/src/lib/gateway.test.ts b/apps/extension/src/lib/gateway.test.ts index 4da4caf..4e972d3 100644 --- a/apps/extension/src/lib/gateway.test.ts +++ b/apps/extension/src/lib/gateway.test.ts @@ -73,7 +73,12 @@ function fixture(initial: CookieRecord[] = [], firefox = false) { const matches = (item: CookieRecord, details: Record) => { if (details.storeId !== undefined && item.storeId !== details.storeId) return false; if (details.name !== undefined && item.name !== details.name) return false; - if (details.domain !== undefined && cookieDomain(item.domain) !== details.domain) return false; + if ( + typeof details.domain === 'string' && + cookieDomain(item.domain) !== details.domain && + !cookieDomain(item.domain).endsWith(`.${details.domain}`) + ) + return false; if (details.path !== undefined && item.path !== details.path) return false; if ( details.firstPartyDomain !== undefined && @@ -268,6 +273,80 @@ describe('browser gateway', () => { expect(f.api.cookies.getAll).not.toHaveBeenCalled(); }); + for (const firefox of [false, true]) { + it(`scopes site reads without losing parent domains, paths, or partitions (${firefox ? 'Firefox' : 'Chromium'})`, async () => { + const storeId = firefox ? 'firefox-default' : '0'; + const parent = cookie({ domain: '.example.com', hostOnly: false, storeId }); + const path = cookie({ domain: 'app.example.com', path: '/account', storeId }); + const partition = cookie({ + domain: 'app.example.com', + storeId, + partitionKey: { topLevelSite: 'https://first.test', hasCrossSiteAncestor: true }, + }); + const fpi = cookie({ + domain: 'app.example.com', + name: 'fpi', + storeId, + ...(firefox ? { firstPartyDomain: 'first.test' } : {}), + }); + const f = fixture( + [ + parent, + path, + partition, + fpi, + cookie({ domain: 'example.com', hostOnly: true, storeId }), + cookie({ domain: 'other.example.com', storeId }), + cookie({ domain: 'app.example.com', storeId: 'another-store' }), + ], + firefox, + ); + const result = await f.gateway.listCookies({ storeIds: [storeId], host: 'app.example.com' }); + expect(result.map((item) => item.name)).toHaveLength(4); + expect(result.some((item) => item.path === '/account')).toBe(true); + expect(result.some((item) => item.partitionKey?.hasCrossSiteAncestor)).toBe(true); + expect(result.every((item) => item.storeId === storeId)).toBe(true); + expect(result.some((item) => item.domain === 'example.com')).toBe(false); + expect(result.some((item) => item.domain === 'other.example.com')).toBe(false); + expect(f.api.cookies.getAllCookieStores).not.toHaveBeenCalled(); + f.denyHostAccess(); + await expect( + f.gateway.listCookies({ storeIds: [storeId], host: 'app.example.com' }), + ).rejects.toThrow('Allow website access'); + }); + } + + for (const host of ['127.0.0.1', '[::1]', 'localhost']) { + it(`does not query parent fragments of ${host}`, async () => { + const f = fixture([cookie({ domain: host })]); + expect(await f.gateway.listCookies({ storeIds: ['0'], host })).toHaveLength(1); + expect(f.api.cookies.getAll).toHaveBeenCalledTimes(1); + expect(f.api.cookies.getAll).toHaveBeenCalledWith({ + storeId: '0', + domain: host, + partitionKey: {}, + }); + }); + } + + it('checks an edited identity without enumerating the entire browser profile', async () => { + const original = cookie(); + const f = fixture([original, cookie({ name: 'unrelated' })]); + await f.gateway.saveCookie({ ...original, value: 'changed' }, original); + expect(f.api.cookies.getAllCookieStores).not.toHaveBeenCalled(); + expect( + mocked(f.api.cookies.getAll).mock.calls.every(([details]) => details.name === original.name), + ).toBe(true); + }); + + it('retains parent localhost domain cookies for a development subdomain', async () => { + const parent = cookie({ domain: '.localhost', hostOnly: false, path: '/account' }); + const f = fixture([parent, cookie({ domain: 'localhost', hostOnly: true })]); + expect(await f.gateway.listCookies({ storeIds: ['0'], host: 'app.localhost' })).toEqual([ + parent, + ]); + }); + it('blocks mutations when host access has been revoked after loading cookies', async () => { const original = cookie(); const f = fixture([original]); diff --git a/apps/extension/src/lib/gateway.ts b/apps/extension/src/lib/gateway.ts index fb4e1fa..0d6032b 100644 --- a/apps/extension/src/lib/gateway.ts +++ b/apps/extension/src/lib/gateway.ts @@ -2,6 +2,7 @@ import { cookieDomain, cookieKey, cookieUrl, + domainMatches, validateCookie, type CookieRecord, type CookieStore, @@ -278,26 +279,45 @@ export function createGateway( } return [...result.values()]; }, - async listCookies() { + async listCookies(options = {}) { await requireAccess(); - const stores = await gateway.listStores(); + const storeIds = options.storeIds ?? (await gateway.listStores()).map((store) => store.id); + const host = options.host?.toLowerCase().replace(/\.$/, ''); + // getAll({url}) misses cookies on other paths. Domain queries preserve those, + // with explicit parent queries for domain cookies and a final applicability filter. + const domains: (string | undefined)[] = host + ? /^\d+(\.\d+){3}$/.test(host) || host.includes(':') + ? [host] + : host + .split('.') + .map((_, index, parts) => parts.slice(index).join('.')) + .filter( + (domain, index) => index === 0 || domain.includes('.') || domain === 'localhost', + ) + : [undefined]; // Empty partitionKey enumerates both partitioned and unpartitioned cookies. // Firefox additionally needs firstPartyDomain:null to include FPI cookies. const groups = await Promise.all( - stores.map((store) => - api.cookies.getAll({ - storeId: store.id, - partitionKey: {}, - ...(firefox ? { firstPartyDomain: null } : {}), - }), + storeIds.flatMap((storeId) => + domains.map((domain) => + api.cookies.getAll({ + storeId, + ...(domain ? { domain } : {}), + partitionKey: {}, + ...(firefox ? { firstPartyDomain: null } : {}), + }), + ), ), ); return [ ...new Map( - groups.flat().map((cookie) => { - const normalized = forBrowser(cookie); - return [cookieKey(normalized), normalized] as const; - }), + groups + .flat() + .filter((cookie) => !host || domainMatches(cookie, host)) + .map((cookie) => { + const normalized = forBrowser(cookie); + return [cookieKey(normalized), normalized] as const; + }), ).values(), ]; }, @@ -324,7 +344,10 @@ export function createGateway( throw failure('A cookie already exists at this name, domain, path, store and partition.'); } if (original) { - const cookies = await gateway.listCookies(); + const cookies = + baseIdentity(forBrowser(original)) === baseIdentity(candidate) + ? before + : await candidateScope(forBrowser(original)); const current = cookies.find((item) => cookieKey(item) === originalKey); if (!current || !equivalent(current, forBrowser(original))) throw failure('This cookie changed in the browser. Refresh before saving.'); diff --git a/apps/extension/src/lib/types.ts b/apps/extension/src/lib/types.ts index a283789..c62f362 100644 --- a/apps/extension/src/lib/types.ts +++ b/apps/extension/src/lib/types.ts @@ -24,12 +24,19 @@ export interface DeleteResult { failed: { cookie: CookieRecord; error: string }[]; } +export interface CookieReadOptions { + /** Known stores from this refresh; omitted by background callers. */ + storeIds?: string[]; + /** Include every path and parent-domain cookie that applies to this host. */ + host?: string; +} + export interface Gateway { readonly kind: 'browser' | 'demo'; getStatus(): Promise; requestHostAccess(): Promise; listStores(): Promise; - listCookies(): Promise; + listCookies(options?: CookieReadOptions): Promise; saveCookie(cookie: CookieRecord, original?: CookieRecord): Promise; deleteCookies(cookies: CookieRecord[]): Promise; getPreferences(): Promise; diff --git a/apps/extension/src/popup.tsx b/apps/extension/src/popup.tsx index f6ed7c2..94a6f3d 100644 --- a/apps/extension/src/popup.tsx +++ b/apps/extension/src/popup.tsx @@ -5,7 +5,7 @@ import { Popup } from './ui/Popup'; import './ui/styles.css'; createRoot(document.getElementById('root')!).render( - + , ); diff --git a/apps/extension/src/ui/CookieTable.tsx b/apps/extension/src/ui/CookieTable.tsx index 038226a..31bbc52 100644 --- a/apps/extension/src/ui/CookieTable.tsx +++ b/apps/extension/src/ui/CookieTable.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { LockKeyhole, ShieldCheck, KeyRound, Layers, ArrowUp, ArrowDown, Copy } from 'lucide-react'; import { cookieKey, type CookieRecord } from '@cookie-loom/core'; import { expiryLabel } from './shared'; @@ -30,6 +30,14 @@ export function CookieTable({ onCopy: (cookie: CookieRecord) => void; }) { const selectAll = useRef(null); + const nameCounts = useMemo(() => { + const counts = new Map(); + for (const cookie of cookies) { + const key = JSON.stringify([cookie.domain, cookie.name]); + counts.set(key, (counts.get(key) ?? 0) + 1); + } + return counts; + }, [cookies]); const selectedCount = cookies.filter((cookie) => selected.has(cookieKey(cookie))).length; useEffect(() => { if (selectAll.current) @@ -98,10 +106,7 @@ export function CookieTable({ ] .filter(Boolean) .join(' · '); - const ambiguous = cookies.some( - (other) => - other !== cookie && other.name === cookie.name && other.domain === cookie.domain, - ); + const ambiguous = nameCounts.get(JSON.stringify([cookie.domain, cookie.name]))! > 1; return ( diff --git a/apps/extension/src/ui/Editor.tsx b/apps/extension/src/ui/Editor.tsx index 84f2529..69fdb14 100644 --- a/apps/extension/src/ui/Editor.tsx +++ b/apps/extension/src/ui/Editor.tsx @@ -1,4 +1,4 @@ -import { useEffect, useId, useRef, useState, type FormEvent } from 'react'; +import { useEffect, useLayoutEffect, useId, useRef, useState, type FormEvent } from 'react'; import { cookieKey, validateCookie, type CookieRecord } from '@cookie-loom/core'; import { X, Eye, EyeOff, Copy, Check, Trash2 } from 'lucide-react'; import { useWorkspace } from './context'; @@ -88,25 +88,27 @@ export function CookieEditor({ }); } - useEffect(() => { + useLayoutEffect(() => { onDirtyChange?.(dirty); return () => onDirtyChange?.(false); }, [dirty, onDirtyChange]); - useEffect(() => () => onSavingChange?.(false), [onSavingChange]); + useLayoutEffect(() => () => onSavingChange?.(false), [onSavingChange]); useEffect(() => { const media = window.matchMedia('(max-width: 1180px)'); const change = () => setCompact(media.matches); media.addEventListener('change', change); return () => media.removeEventListener('change', change); }, []); - useEffect(() => { + useLayoutEffect(() => { if (!compact) return; const modal = dialog.current; const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null; modal?.showModal(); return () => { modal?.close(); - opener?.focus(); + queueMicrotask(() => { + if (opener?.isConnected) opener.focus(); + }); }; }, [compact]); useEffect(() => { @@ -114,7 +116,7 @@ export function CookieEditor({ const timer = setTimeout(() => setCopied(false), 1800); return () => clearTimeout(timer); }, [copied]); - useEffect(() => { + useLayoutEffect(() => { if (!dirty) return; const preventUnload = (event: BeforeUnloadEvent) => { event.preventDefault(); @@ -129,7 +131,7 @@ export function CookieEditor({ if (dirty) setDiscardAction(() => action); else action(); } - useEffect(() => { + useLayoutEffect(() => { const escape = (event: KeyboardEvent) => { if (event.key !== 'Escape' || event.defaultPrevented) return; const open = [...document.querySelectorAll('dialog[open]')]; diff --git a/apps/extension/src/ui/Sidebar.tsx b/apps/extension/src/ui/Sidebar.tsx index 734ae43..0957125 100644 --- a/apps/extension/src/ui/Sidebar.tsx +++ b/apps/extension/src/ui/Sidebar.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { Cookie, Shield, Settings, Globe2, LockKeyhole } from 'lucide-react'; -import { cookieKey, groupCookiesByDomain } from '@cookie-loom/core'; +import { cookieKey } from '@cookie-loom/core'; import { Brand } from './shared'; import { useWorkspace } from './context'; @@ -14,11 +14,20 @@ export function Sidebar({ domain: string; onNavigate: (view: View, domain?: string) => void; }) { - const { cookies, gateway, preferences } = useWorkspace(); + const { cookies, gateway, preferences, domains } = useWorkspace(); const [query, setQuery] = useState(''); - const groups = useMemo(() => groupCookiesByDomain(cookies), [cookies]); - const keys = new Set(preferences.protectedKeys); - const protectedCount = cookies.filter((cookie) => keys.has(cookieKey(cookie))).length; + const [limit, setLimit] = useState(50); + const groups = useMemo( + () => domains.filter((group) => group.domain.toLowerCase().includes(query.toLowerCase())), + [domains, query], + ); + const visibleGroups = groups.slice(0, limit); + const active = groups.find((group) => group.domain === domain); + if (active && !visibleGroups.includes(active)) visibleGroups.unshift(active); + const protectedCount = useMemo(() => { + const keys = new Set(preferences.protectedKeys); + return cookies.filter((cookie) => keys.has(cookieKey(cookie))).length; + }, [cookies, preferences.protectedKeys]); return (