diff --git a/.gitignore b/.gitignore index 28e34ae..7db14fe 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ coverage/ .turbo/ test-results/ playwright-report/ +.sid/ +.claude/ diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..159f5dc --- /dev/null +++ b/assets/README.md @@ -0,0 +1,15 @@ +# assets + +Source artwork for the project. Not shipped as-is — processed into per-shell +icon PNGs by `scripts/gen-icons.mjs`. + +- `gitmarks.svg` — the master extension icon. A square viewBox (e.g. + `0 0 512 512`) renders cleanest. `scripts/gen-icons.mjs` rasterizes it to + 16/32/48/128 px PNGs and writes them into each extension shell's `icons/` + directory (which is git-ignored and regenerated on build). + +Regenerate manually with: + +```bash +node scripts/gen-icons.mjs +``` diff --git a/assets/gitmarks.svg b/assets/gitmarks.svg new file mode 100644 index 0000000..9fcd10e --- /dev/null +++ b/assets/gitmarks.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/package.json b/package.json index 208d47d..6b09ca5 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "clean": "pnpm -r exec rm -rf dist" }, "devDependencies": { + "@resvg/resvg-js": "^2.6.2", "typescript": "^5.4.0" }, "engines": { diff --git a/packages/extension-chrome/.gitignore b/packages/extension-chrome/.gitignore index aecafef..1c8e360 100644 --- a/packages/extension-chrome/.gitignore +++ b/packages/extension-chrome/.gitignore @@ -1,2 +1,3 @@ src/popup.html src/options.html +icons/ diff --git a/packages/extension-chrome/README.md b/packages/extension-chrome/README.md index 1642ecd..4095286 100644 --- a/packages/extension-chrome/README.md +++ b/packages/extension-chrome/README.md @@ -20,8 +20,17 @@ Then in Chrome: 3. Click **Load unpacked**. 4. Select `packages/extension-chrome/dist/`. -The extension's toolbar icon appears as a default puzzle piece (icons -are deferred). Pin it for easy access. +Pin the toolbar icon for easy access. + +**Other Chromium browsers (Brave, Edge, Opera, Vivaldi):** this same +`dist/` loads unchanged — no separate build. Use the equivalent +extensions page (`brave://extensions`, `edge://extensions`, …), enable +Developer mode, and **Load unpacked** the same folder. Brave Shields +don't affect the extension's own requests to `api.github.com` (those are +extension-origin, not page content). + +The toolbar/extensions-page icon is generated from `assets/gitmarks.svg` +at build time (see "Icons" below). ## First-run setup @@ -243,5 +252,13 @@ user action. Filed and closed as #2. - Tracking-param URL stripping (`utm_*`) — tracked as #6 - Folder-rename batching for thousands of bookmarks — closed as #7 (blocked on onMoved push handling) - Tags UI (tags live in the JSON but no UI here yet — web UI scope, #24/#25) -- Icons (Chrome shows the default puzzle piece) - Conflict resolution beyond core's automatic 409/422 retry + +## Icons + +The toolbar button and extensions-page tile use icons generated from a +single source: `assets/gitmarks.svg` at the repo root. The `prebuild` +hook runs `scripts/gen-icons.mjs`, which rasterizes that SVG to +16/32/48/128 px PNGs into `icons/` (git-ignored) and `@crxjs/vite-plugin` +emits them into `dist/icons/`. To change the icon, edit (or replace) +`assets/gitmarks.svg` and rebuild — no manifest changes needed. diff --git a/packages/extension-chrome/manifest.config.ts b/packages/extension-chrome/manifest.config.ts index 274119f..1cc23fe 100644 --- a/packages/extension-chrome/manifest.config.ts +++ b/packages/extension-chrome/manifest.config.ts @@ -7,9 +7,21 @@ export default defineManifest({ description: "Save bookmarks to your own GitHub repo.", permissions: ["storage", "activeTab", "bookmarks", "alarms"], host_permissions: ["https://api.github.com/*"], + icons: { + 16: "icons/icon-16.png", + 32: "icons/icon-32.png", + 48: "icons/icon-48.png", + 128: "icons/icon-128.png", + }, action: { default_popup: "src/popup.html", default_title: "gitmarks", + default_icon: { + 16: "icons/icon-16.png", + 32: "icons/icon-32.png", + 48: "icons/icon-48.png", + 128: "icons/icon-128.png", + }, }, options_page: "src/options.html", background: { diff --git a/packages/extension-chrome/package.json b/packages/extension-chrome/package.json index 3e02b2a..deb55cf 100644 --- a/packages/extension-chrome/package.json +++ b/packages/extension-chrome/package.json @@ -4,15 +4,15 @@ "private": true, "type": "module", "scripts": { - "predev": "node ./scripts/copy-html.mjs", + "predev": "node ../../scripts/gen-icons.mjs && node ./scripts/copy-html.mjs", "dev": "vite build --watch --mode development", - "prebuild": "node ./scripts/copy-html.mjs", + "prebuild": "node ../../scripts/gen-icons.mjs && node ./scripts/copy-html.mjs", "build": "vite build", "pretypecheck": "node ./scripts/copy-html.mjs", "typecheck": "tsc -p tsconfig.json --noEmit", - "pree2e": "node ./scripts/copy-html.mjs && vite build", + "pree2e": "node ../../scripts/gen-icons.mjs && node ./scripts/copy-html.mjs && vite build", "e2e": "playwright test", - "pree2e:headed": "node ./scripts/copy-html.mjs && vite build", + "pree2e:headed": "node ../../scripts/gen-icons.mjs && node ./scripts/copy-html.mjs && vite build", "e2e:headed": "playwright test --headed" }, "dependencies": { diff --git a/packages/extension-firefox/.gitignore b/packages/extension-firefox/.gitignore index aecafef..1c8e360 100644 --- a/packages/extension-firefox/.gitignore +++ b/packages/extension-firefox/.gitignore @@ -1,2 +1,3 @@ src/popup.html src/options.html +icons/ diff --git a/packages/extension-firefox/README.md b/packages/extension-firefox/README.md index c73ca97..c2d6626 100644 --- a/packages/extension-firefox/README.md +++ b/packages/extension-firefox/README.md @@ -47,8 +47,9 @@ version Chrome uses) need a manual check: **Load + popup:** - [ ] Build, load via `about:debugging` → "Load Temporary Add-on", select - `dist/manifest.json`. The gitmarks toolbar icon appears as a default - puzzle piece (pin it for easy access). + `dist/manifest.json`. The gitmarks toolbar icon (generated from + `assets/gitmarks.svg` at build time) appears; pin it for easy + access. - [ ] Click the icon before configuring → popup shows "Set up gitmarks". **Setup flow:** diff --git a/packages/extension-firefox/manifest.json b/packages/extension-firefox/manifest.json index e06a92b..eca56fc 100644 --- a/packages/extension-firefox/manifest.json +++ b/packages/extension-firefox/manifest.json @@ -5,9 +5,21 @@ "description": "Save bookmarks to your own GitHub repo.", "permissions": ["storage", "activeTab", "bookmarks", "alarms"], "host_permissions": ["https://api.github.com/*"], + "icons": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, "action": { "default_popup": "popup.html", - "default_title": "gitmarks" + "default_title": "gitmarks", + "default_icon": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } }, "options_ui": { "page": "options.html", diff --git a/packages/extension-firefox/package.json b/packages/extension-firefox/package.json index 0efeb0b..5b47c20 100644 --- a/packages/extension-firefox/package.json +++ b/packages/extension-firefox/package.json @@ -4,9 +4,9 @@ "private": true, "type": "module", "scripts": { - "predev": "node ./scripts/copy-html.mjs", + "predev": "node ../../scripts/gen-icons.mjs && node ./scripts/copy-html.mjs", "dev": "vite", - "prebuild": "node ./scripts/copy-html.mjs", + "prebuild": "node ../../scripts/gen-icons.mjs && node ./scripts/copy-html.mjs", "build": "vite build && node ./scripts/copy-manifest.mjs", "pretypecheck": "node ./scripts/copy-html.mjs", "typecheck": "tsc -p tsconfig.json --noEmit" diff --git a/packages/extension-firefox/scripts/copy-manifest.mjs b/packages/extension-firefox/scripts/copy-manifest.mjs index 5107e9d..40a4182 100644 --- a/packages/extension-firefox/scripts/copy-manifest.mjs +++ b/packages/extension-firefox/scripts/copy-manifest.mjs @@ -1,4 +1,4 @@ -import { copyFileSync, mkdirSync } from "node:fs"; +import { copyFileSync, mkdirSync, existsSync } from "node:fs"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; @@ -10,3 +10,21 @@ copyFileSync( resolve(root, "dist/manifest.json"), ); console.log("[firefox] copied manifest.json to dist/"); + +// Plain Vite (root: "src") doesn't see the generated icons/ dir, so copy the +// PNGs referenced by manifest.json into dist/icons/ ourselves. +const iconsSrc = resolve(root, "icons"); +const iconsDest = resolve(root, "dist/icons"); +if (!existsSync(iconsSrc)) { + throw new Error( + `icons not found: ${iconsSrc} — run scripts/gen-icons.mjs first (prebuild does this)`, + ); +} +mkdirSync(iconsDest, { recursive: true }); +for (const size of [16, 32, 48, 128]) { + copyFileSync( + resolve(iconsSrc, `icon-${size}.png`), + resolve(iconsDest, `icon-${size}.png`), + ); +} +console.log("[firefox] copied icons/ to dist/"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 439948e..d3e4bc1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@resvg/resvg-js': + specifier: ^2.6.2 + version: 2.6.2 typescript: specifier: ^5.4.0 version: 5.9.3 @@ -450,6 +453,82 @@ packages: resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} engines: {node: '>=14.0.0'} + '@resvg/resvg-js-android-arm-eabi@2.6.2': + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.6.2': + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.6.2': + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.6.2': + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.6.2': + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} + engines: {node: '>= 10'} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1874,6 +1953,57 @@ snapshots: '@remix-run/router@1.23.2': {} + '@resvg/resvg-js-android-arm-eabi@2.6.2': + optional: true + + '@resvg/resvg-js-android-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-x64@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js@2.6.2': + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@4.2.1': diff --git a/scripts/gen-icons.mjs b/scripts/gen-icons.mjs new file mode 100644 index 0000000..7a25576 --- /dev/null +++ b/scripts/gen-icons.mjs @@ -0,0 +1,46 @@ +// Rasterize assets/gitmarks.svg into per-shell extension icon PNGs. +// +// Single source of truth: assets/gitmarks.svg. The generated PNGs are +// git-ignored and recreated on each build (the shells' `prebuild` runs this). +// Uses @resvg/resvg-js (prebuilt binary, no system ImageMagick/librsvg needed). +import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { Resvg } from "@resvg/resvg-js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = resolve(here, ".."); +const source = resolve(root, "assets/gitmarks.svg"); + +const SIZES = [16, 32, 48, 128]; +const SHELL_ICON_DIRS = [ + resolve(root, "packages/extension-chrome/icons"), + resolve(root, "packages/extension-firefox/icons"), +]; + +if (!existsSync(source)) { + throw new Error( + `icon source not found: ${source}\n` + + `Drop a square SVG at assets/gitmarks.svg (see assets/README.md), then re-run.`, + ); +} + +const svg = readFileSync(source); + +for (const dir of SHELL_ICON_DIRS) { + mkdirSync(dir, { recursive: true }); + for (const size of SIZES) { + // Render at the target raster width so each size is crisp, not downscaled + // from one bitmap. + const png = new Resvg(svg, { + fitTo: { mode: "width", value: size }, + }) + .render() + .asPng(); + writeFileSync(resolve(dir, `icon-${size}.png`), png); + } +} + +console.log( + `[icons] generated ${SIZES.join("/")} px PNGs into ${SHELL_ICON_DIRS.length} shells`, +);