From eae530984310f7d8c9cd81193be801ab551155c2 Mon Sep 17 00:00:00 2001 From: Avinal Kumar Date: Mon, 20 Apr 2026 17:14:35 +0530 Subject: [PATCH] feat: project infrastructure and MV3 extension setup TypeScript + esbuild build system, Manifest V3 with background script, content script for iframe overlay injection, and typed message contracts between extension contexts. Assisted-by: Claude Code Signed-off-by: Avinal Kumar --- build.mjs | 25 +++ manifest.json | 44 +++++ package-lock.json | 486 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 14 ++ src/background.ts | 141 ++++++++++++++ src/chrome.d.ts | 78 ++++++++ src/content.ts | 101 ++++++++++ src/types.ts | 56 ++++++ tsconfig.json | 17 ++ 9 files changed, 962 insertions(+) create mode 100644 build.mjs create mode 100644 manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/background.ts create mode 100644 src/chrome.d.ts create mode 100644 src/content.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json diff --git a/build.mjs b/build.mjs new file mode 100644 index 0000000..1d8d343 --- /dev/null +++ b/build.mjs @@ -0,0 +1,25 @@ +import * as esbuild from "esbuild"; + +const watch = process.argv.includes("--watch"); + +const options = { + entryPoints: [ + "src/background.ts", + "src/content.ts", + "src/sciezka.ts", + ], + bundle: true, + outdir: "dist", + format: "iife", + target: "es2020", + sourcemap: watch, +}; + +if (watch) { + const ctx = await esbuild.context(options); + await ctx.watch(); + console.log("Watching for changes..."); +} else { + await esbuild.build(options); + console.log("Build complete."); +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..c043f26 --- /dev/null +++ b/manifest.json @@ -0,0 +1,44 @@ +{ + "manifest_version": 3, + "name": "Sciezka", + "version": "0.1.0", + "description": "A browser extension to fuzzy search tabs, history and bookmarks.", + "icons": { + "48": "icons/border-48.png" + }, + "permissions": [ + "tabs", + "history", + "bookmarks", + "sessions", + "storage" + ], + "background": { + "scripts": ["dist/background.js"] + }, + "content_scripts": [ + { + "matches": [""], + "js": ["dist/content.js"], + "run_at": "document_idle" + } + ], + "commands": { + "toggle-sciezka": { + "suggested_key": { + "default": "Alt+K" + }, + "description": "Toggle Sciezka search" + } + }, + "options_ui": { + "page": "options/options.html", + "open_in_tab": true + }, + "web_accessible_resources": [ + { + "resources": ["sciezka.html", "sciezka.css", "dist/sciezka.js"], + "matches": [""] + } + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6c536ec --- /dev/null +++ b/package-lock.json @@ -0,0 +1,486 @@ +{ + "name": "sciezka", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sciezka", + "version": "0.1.0", + "devDependencies": { + "esbuild": "^0.25.0", + "typescript": "^5.8.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..dbc6558 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "sciezka", + "version": "0.1.0", + "private": true, + "description": "A browser extension for fuzzy searching tabs, history and bookmarks.", + "scripts": { + "build": "node build.mjs", + "watch": "node build.mjs --watch" + }, + "devDependencies": { + "esbuild": "^0.25.0", + "typescript": "^5.8.0" + } +} diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 0000000..b083f52 --- /dev/null +++ b/src/background.ts @@ -0,0 +1,141 @@ +import type { Message, SearchRequest, ActionRequest, SearchItem } from "./types"; + +async function getOpenTabs(): Promise { + const tabs = await chrome.tabs.query({}); + return tabs.map((tab) => ({ + id: `tab-${tab.id}`, + title: tab.title ?? "", + url: tab.url ?? "", + type: "tabs" as const, + favIconUrl: tab.favIconUrl, + })); +} + +async function getHistory(query: string): Promise { + const results = await chrome.history.search({ + text: query, + maxResults: 50, + startTime: 0, + }); + return results.map((item) => ({ + id: `history-${item.id}`, + title: item.title ?? "", + url: item.url ?? "", + type: "history" as const, + })); +} + +async function getBookmarks(query: string): Promise { + const results = await chrome.bookmarks.search(query || " "); + return results + .filter((b) => b.url) + .map((item) => ({ + id: `bookmark-${item.id}`, + title: item.title ?? "", + url: item.url!, + type: "bookmarks" as const, + })); +} + +async function getRecentlyClosed(): Promise { + const sessions = await chrome.sessions.getRecentlyClosed({ maxResults: 25 }); + return sessions + .filter((s) => s.tab) + .map((session) => ({ + id: `closed-${session.tab!.sessionId}`, + title: session.tab!.title ?? "", + url: session.tab!.url ?? "", + type: "closed" as const, + favIconUrl: session.tab!.favIconUrl, + })); +} + +async function handleSearch(request: SearchRequest): Promise { + const { query, mode } = request; + switch (mode) { + case "tabs": + return getOpenTabs(); + case "history": + return getHistory(query); + case "bookmarks": + return getBookmarks(query); + case "closed": + return getRecentlyClosed(); + case "all": { + const [tabs, history, bookmarks, closed] = await Promise.all([ + getOpenTabs(), + getHistory(query), + getBookmarks(query), + getRecentlyClosed(), + ]); + return [...tabs, ...history, ...bookmarks, ...closed]; + } + } +} + +async function handleAction(request: ActionRequest): Promise { + const rawId = request.id.replace(/^(tab|history|bookmark|closed)-/, ""); + + switch (request.action) { + case "switch": { + const tabId = parseInt(rawId, 10); + await chrome.tabs.update(tabId, { active: true }); + const tab = await chrome.tabs.get(tabId); + if (tab.windowId != null) { + await chrome.windows.update(tab.windowId, { focused: true }); + } + break; + } + case "open": { + if (request.newTab) { + await chrome.tabs.create({ url: request.id }); + } else { + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + if (activeTab?.id != null) { + await chrome.tabs.update(activeTab.id, { url: request.id }); + } + } + break; + } + case "close": { + const tabId = parseInt(rawId, 10); + await chrome.tabs.remove(tabId); + break; + } + case "restore": { + await chrome.sessions.restore(rawId); + break; + } + } +} + +chrome.commands.onCommand.addListener(async (command) => { + if (command === "toggle-sciezka") { + const [tab] = await chrome.tabs.query({ + active: true, + currentWindow: true, + }); + if (tab?.id != null) { + chrome.tabs.sendMessage(tab.id, { type: "toggle" } satisfies Message); + } + } +}); + +chrome.runtime.onMessage.addListener( + (msg: unknown, _sender, sendResponse) => { + const message = msg as Message; + if (message.type === "search") { + handleSearch(message).then((items) => { + sendResponse({ type: "searchResults", results: items }); + }); + return true; + } + if (message.type === "action") { + handleAction(message).then(() => sendResponse({ ok: true })); + return true; + } + } +); diff --git a/src/chrome.d.ts b/src/chrome.d.ts new file mode 100644 index 0000000..1076b63 --- /dev/null +++ b/src/chrome.d.ts @@ -0,0 +1,78 @@ +declare namespace chrome { + namespace tabs { + interface Tab { + id?: number; + windowId?: number; + title?: string; + url?: string; + favIconUrl?: string; + active?: boolean; + } + function query(queryInfo: Record): Promise; + function get(tabId: number): Promise; + function create(createProperties: { url?: string; active?: boolean }): Promise; + function update(tabId: number, updateProperties: { active?: boolean; url?: string }): Promise; + function remove(tabId: number | number[]): Promise; + function sendMessage(tabId: number, message: unknown): void; + } + + namespace windows { + function update(windowId: number, updateInfo: { focused?: boolean }): Promise; + } + + namespace history { + interface HistoryItem { + id: string; + title?: string; + url?: string; + lastVisitTime?: number; + } + function search(query: { text: string; maxResults?: number; startTime?: number }): Promise; + } + + namespace bookmarks { + interface BookmarkTreeNode { + id: string; + title: string; + url?: string; + children?: BookmarkTreeNode[]; + } + function search(query: string): Promise; + } + + namespace sessions { + interface Session { + tab?: tabs.Tab & { sessionId?: string }; + window?: { sessionId?: string }; + } + function getRecentlyClosed(filter?: { maxResults?: number }): Promise; + function restore(sessionId: string): Promise; + } + + namespace commands { + const onCommand: { + addListener(callback: (command: string) => void): void; + }; + } + + namespace runtime { + function getURL(path: string): string; + const onMessage: { + addListener( + callback: ( + message: unknown, + sender: { tab?: tabs.Tab; id?: string }, + sendResponse: (response?: unknown) => void + ) => boolean | void + ): void; + }; + function sendMessage(message: unknown, responseCallback?: (response: unknown) => void): void; + } + + namespace storage { + const sync: { + get(keys: string | string[]): Promise>; + set(items: Record): Promise; + }; + } +} diff --git a/src/content.ts b/src/content.ts new file mode 100644 index 0000000..70d4170 --- /dev/null +++ b/src/content.ts @@ -0,0 +1,101 @@ +import type { Message } from "./types"; + +let overlay: HTMLDivElement | null = null; +let iframe: HTMLIFrameElement | null = null; + +function createOverlay(): void { + overlay = document.createElement("div"); + overlay.id = "sciezka-overlay"; + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0); + z-index: 2147483647; + display: flex; + justify-content: center; + align-items: flex-start; + padding-top: 12vh; + transition: background 0.15s ease; + `; + + iframe = document.createElement("iframe"); + iframe.src = chrome.runtime.getURL("sciezka.html"); + iframe.style.cssText = ` + width: 620px; + height: 60px; + border: none; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3); + background: transparent; + opacity: 0; + transform: translateY(-8px); + transition: opacity 0.15s ease, transform 0.15s ease, height 0.1s ease; + `; + + overlay.appendChild(iframe); + document.body.appendChild(overlay); + + requestAnimationFrame(() => { + overlay!.style.background = "rgba(0, 0, 0, 0.4)"; + iframe!.style.opacity = "1"; + iframe!.style.transform = "translateY(0)"; + }); + + overlay.addEventListener("click", (e) => { + if (e.target === overlay) { + removeOverlay(); + } + }); +} + +function removeOverlay(): void { + if (overlay) { + overlay.remove(); + overlay = null; + iframe = null; + } +} + +function toggle(): void { + if (overlay) { + removeOverlay(); + } else { + createOverlay(); + } +} + +chrome.runtime.onMessage.addListener((msg: unknown) => { + const message = msg as Message; + if (message.type === "toggle") { + toggle(); + } +}); + +document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && overlay) { + removeOverlay(); + } +}); + +window.addEventListener("message", (event) => { + if (event.source !== iframe?.contentWindow) return; + + const data = event.data as Message; + if (data.type === "closeSaka") { + removeOverlay(); + return; + } + if (data.type === "resize") { + if (iframe) { + iframe.style.height = `${(data as { height: number }).height}px`; + } + return; + } + if (data.type === "search" || data.type === "action") { + chrome.runtime.sendMessage(data, (response) => { + iframe?.contentWindow?.postMessage(response, "*"); + }); + } +}); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..1dde0bd --- /dev/null +++ b/src/types.ts @@ -0,0 +1,56 @@ +export type SearchMode = "tabs" | "history" | "bookmarks" | "closed" | "all"; +export type SearchMethod = "fuzzy" | "fulltext" | "prefix"; + +export interface SearchItem { + id: string; + title: string; + url: string; + type: SearchMode; + favIconUrl?: string; +} + +export interface SearchResult { + item: SearchItem; + score: number; + positions: number[]; +} + +export interface SearchRequest { + type: "search"; + query: string; + mode: SearchMode; + method: SearchMethod; +} + +export interface SearchResponse { + type: "searchResults"; + results: SearchResult[]; +} + +export interface ActionRequest { + type: "action"; + action: "switch" | "open" | "close" | "restore"; + id: string; + newTab?: boolean; +} + +export interface ToggleMessage { + type: "toggle"; +} + +export interface CloseMessage { + type: "closeSaka"; +} + +export interface ResizeMessage { + type: "resize"; + height: number; +} + +export type Message = + | SearchRequest + | SearchResponse + | ActionRequest + | ToggleMessage + | CloseMessage + | ResizeMessage; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2ee05d2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src", + "lib": ["ES2020", "DOM", "DOM.Iterable"] + }, + "include": ["src/**/*.ts"] +}