From 704e172da931828e7d76149f3ecaffb2e186bcdd Mon Sep 17 00:00:00 2001 From: David Date: Wed, 29 Apr 2026 10:27:25 -0400 Subject: [PATCH] Rename project to trailhead-path-bookmarker - npm name: trailhead-path-bookmarker (package.json + lockfile) - electron-builder appId: com.dbeuttel.trailhead-path-bookmarker - AppUserModelId in main.js matches the new appId - README title updated; userData path now %APPDATA%\Trailhead Path Bookmarker Icon generator now emits both sizes: - assets/tray-icon.png (32x32) for the system tray - assets/app-icon.png (256x256) for electron-builder's win.icon, which rejects PNGs smaller than 256 --- README.md | 6 +- assets/app-icon.png | Bin 0 -> 1515 bytes assets/tray-icon.png | Bin 157 -> 159 bytes electron/main.js | 2 +- package-lock.json | 4 +- package.json | 6 +- scripts/generate-icon.js | 138 +++++++++++++++++++++++---------------- 7 files changed, 91 insertions(+), 65 deletions(-) create mode 100644 assets/app-icon.png diff --git a/README.md b/README.md index 343c2c2..2130d58 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Folder Bookmark Tray +# Trailhead Path Bookmarker A Windows system-tray app for bookmarking folder paths with quick-action buttons (Explorer, Terminal, Claude, Visual Studio). @@ -41,7 +41,7 @@ npm run icon # regenerate assets/tray-icon.png Persisted to `app.getPath('userData')/config.json`. On Windows that's: ``` -%APPDATA%\Folder Bookmark Tray\config.json +%APPDATA%\Trailhead Path Bookmarker\config.json ``` Schema: @@ -78,7 +78,7 @@ assets/tray-icon.png orange folder pictogram, 32×32 ## Stack -Electron 33, React 18, Vite 5. NSIS installer via `electron-builder` (`appId: com.dbeuttel.folder-bookmark-tray`). No native deps. +Electron 33, React 18, Vite 5. NSIS installer via `electron-builder` (`appId: com.dbeuttel.trailhead-path-bookmarker`). No native deps. ## Non-goals diff --git a/assets/app-icon.png b/assets/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d57b4b05c6ff283903890bc45e0ab59c70196693 GIT binary patch literal 1515 zcmdT@ZAep57(RDfZraDTg4D=6%c3BP^hfJ!%|uNjHFHHlu`RQREXANZ*V?^`*r%=_ zEJza*!OUC~bVtp$K4g6dp$4hl=~|1$+EuxBo9^E3odibF?>c|Z;XTjuKJW9s=bYY} zyJ=?3ycht$%O2*5WAZ8%@LndKxzJ>Rz}fnfs}9rFj8qV}UAi0a zzDX{_nMei$MO)LS-i!=uPfg8Dc?cMSbQ)DDB;%W>T+h6k)FMV`JaZ~F$AT7$L|8uM zoR-S>kr<=#UsP)>Xu&Ul5RFfYn?^l^jF$;Njc_fdagQ}A5u9sN+~YllnzV+Aq6oJz z;!!=e%;JbxFq4;HR371ep5!v`J?L8AerRwy#?4y6bh+4l5weFTe;f|-?c2Nih8dKj z0PJjrODUNjwSrq|Dt8F#W|>gX=k-G+acCGRrlaf{m3#SPD|>Z0QE}0)5OJ2%(I%9IJ3GwEy6a+7`4bAcKM~mZ2t#Xwu7lO%@eh)g#0PwIU43DD zqwh*tb<=Bd&zgr>KKgF^f%;KXVQh*7S9ZO8$f!P_k=^bgh${&PVWP<L_t(|oMT`Z1w#ScED!%rDqb37@qg3-!`}hFo?Qpyk?8=K z<+vO%yd8jUIW|WOUkBi_9NiJa*8zj7AR2YRr~^hFFyIa#R-PsfAV&?=96+f$iX1>q iwIn-$R_aH=$OZtpu5ivZ@CvK|0000 { app.whenReady().then(() => { if (process.platform === 'win32') { - app.setAppUserModelId('com.dbeuttel.folder-bookmark-tray'); + app.setAppUserModelId('com.dbeuttel.trailhead-path-bookmarker'); } migrateConfigIfNeeded(); pinned = !!readConfig().pinned; diff --git a/package-lock.json b/package-lock.json index 1fa2985..437cd24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "folder-bookmark-tray", + "name": "trailhead-path-bookmarker", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "folder-bookmark-tray", + "name": "trailhead-path-bookmarker", "version": "0.1.0", "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index a9cb65b..2ff2baf 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "folder-bookmark-tray", + "name": "trailhead-path-bookmarker", "version": "0.1.0", "description": "Windows system tray app for bookmarking folders with quick-action buttons (Explorer, Terminal, Claude, Visual Studio).", "main": "electron/main.js", @@ -28,7 +28,7 @@ "wait-on": "^8.0.1" }, "build": { - "appId": "com.dbeuttel.folder-bookmark-tray", + "appId": "com.dbeuttel.trailhead-path-bookmarker", "productName": "Trailhead Path Bookmarker", "files": [ "electron/**/*", @@ -37,7 +37,7 @@ ], "win": { "target": "nsis", - "icon": "assets/tray-icon.png" + "icon": "assets/app-icon.png" }, "nsis": { "oneClick": false, diff --git a/scripts/generate-icon.js b/scripts/generate-icon.js index 27f8be8..e8e5f3b 100644 --- a/scripts/generate-icon.js +++ b/scripts/generate-icon.js @@ -1,66 +1,85 @@ -// Generates assets/tray-icon.png — Trailhead branding: a bookmark ribbon -// (vertical rectangle with a V-cut at the bottom — the universal "save" -// glyph) carrying a mountain peak motif inside it. The ribbon silhouette -// communicates the app's *function* (bookmarking); the inner peak ties to -// the Trailhead theme. -// Strokes are kept >=2 source pixels wide so the silhouette survives the -// downscale to 16-24px Windows tray sizes. +// Generates two PNGs from the same procedural Trailhead glyph: +// assets/tray-icon.png — 32x32 (Windows tray) +// assets/app-icon.png — 256x256 (electron-builder requires >=256 for the +// win.icon used in the installer + .exe resource) +// +// Glyph: a vertical bookmark ribbon (rectangle with a V-cut bottom — the +// universal "save" mark) carrying a cream mountain peak inside it. +// Geometry is expressed as fractions of the canvas so a single set of rules +// renders cleanly at both sizes. // No external image deps; raw PNG bytes via zlib. const fs = require('fs'); const path = require('path'); const zlib = require('zlib'); -const SIZE = 32; -const OUT = path.join(__dirname, '..', 'assets', 'tray-icon.png'); - const ORANGE = [217, 119, 87, 255]; // #d97757 — ribbon fill const ORANGE_DARK = [165, 82, 56, 255]; // shadow on right edge const CREAM = [250, 230, 215, 255]; // mountain motif inside the ribbon const TRANSPARENT = [0, 0, 0, 0]; -// Ribbon spans 14px wide (x=9..22) and 25px tall (y=3..27). The bottom 5 -// rows form a V-cut, narrowing the ribbon into two prongs. -function inRibbon(x, y) { - if (x < 9 || x > 22) return false; - if (y < 3 || y > 27) return false; - if (y >= 23) { - const depth = y - 23; // 0..4 - const cutLeft = 15 - depth; - const cutRight = 16 + depth; - if (x >= cutLeft && x <= cutRight) return false; - } - return true; +// Geometry, in 0..1 fractions of canvas. +const G = { + ribbonLeft: 9 / 32, + ribbonRight: 23 / 32, // exclusive + ribbonTop: 3 / 32, + ribbonBottom: 28 / 32, // exclusive + vCutTop: 23 / 32, // V notch starts here, points up into the ribbon + mountainTop: 7 / 32, + mountainBottom: 15 / 32, // exclusive + mountainBaseHalfW: 4.5 / 32, + shadowWidth: 1 / 32, +}; + +function makeColorAt(size) { + const rL = G.ribbonLeft * size; + const rR = G.ribbonRight * size; + const rT = G.ribbonTop * size; + const rB = G.ribbonBottom * size; + const vT = G.vCutTop * size; + const mT = G.mountainTop * size; + const mB = G.mountainBottom * size; + const sw = G.shadowWidth * size; + const cx = (rL + rR) / 2; + const baseHalf = G.mountainBaseHalfW * size; + + return function colorAt(x, y) { + // Sample at pixel center so geometry edges land cleanly. + const px = x + 0.5; + const py = y + 0.5; + + if (px < rL || px >= rR) return TRANSPARENT; + if (py < rT || py >= rB) return TRANSPARENT; + + // V-cut: triangular notch carved from the bottom of the ribbon. + if (py >= vT) { + const depth = (py - vT) / (rB - vT); // 0..1 + const cutHalfW = depth * ((rR - rL) / 2); + if (Math.abs(px - cx) < cutHalfW) return TRANSPARENT; + } + + // Mountain peak silhouette inside the upper half of the ribbon. + if (py >= mT && py < mB) { + const t = (py - mT) / (mB - mT); // 0..1 (top..base) + const halfW = t * baseHalf; + if (Math.abs(px - cx) <= halfW) return CREAM; + } + + // Right-edge shadow on the ribbon body, stopping before the V-cut so the + // notch edges read clean rather than half-shaded. + if (px >= rR - sw && py < vT) return ORANGE_DARK; + + return ORANGE; + }; } -// Mountain peak motif inside the upper half of the ribbon. Peak at y=7, -// base at y=14, max half-width 4 (so base spans 8 source pixels). -function inMountain(x, y) { - if (y < 7 || y > 14) return false; - const halfW = Math.floor((y - 7) / 2) + 1; - return x >= 15 - halfW + 1 && x <= 15 + halfW; -} - -function colorAt(x, y) { - if (!inRibbon(x, y)) return TRANSPARENT; - - // Inner mountain silhouette painted in cream so it pops against the - // orange ribbon at any size. - if (inMountain(x, y)) return CREAM; - - // Right-edge shadow on the ribbon body (skip the V-cut zone so the cut - // edges look clean rather than half-shaded). - if (x === 22 && y >= 3 && y < 23) return ORANGE_DARK; - - return ORANGE; -} - -function buildRawImage() { - const rowLen = SIZE * 4 + 1; - const raw = Buffer.alloc(rowLen * SIZE); - for (let y = 0; y < SIZE; y++) { +function buildRawImage(size) { + const colorAt = makeColorAt(size); + const rowLen = size * 4 + 1; + const raw = Buffer.alloc(rowLen * size); + for (let y = 0; y < size; y++) { raw[y * rowLen] = 0; - for (let x = 0; x < SIZE; x++) { + for (let x = 0; x < size; x++) { const [r, g, b, a] = colorAt(x, y); const off = y * rowLen + 1 + x * 4; raw[off] = r; @@ -97,17 +116,17 @@ function chunk(type, data) { return Buffer.concat([len, typeBuf, data, crcBuf]); } -function buildPng() { +function buildPng(size) { const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(SIZE, 0); - ihdr.writeUInt32BE(SIZE, 4); + ihdr.writeUInt32BE(size, 0); + ihdr.writeUInt32BE(size, 4); ihdr[8] = 8; // bit depth ihdr[9] = 6; // color type RGBA ihdr[10] = 0; // compression ihdr[11] = 0; // filter ihdr[12] = 0; // interlace - const idat = zlib.deflateSync(buildRawImage()); + const idat = zlib.deflateSync(buildRawImage(size)); return Buffer.concat([ sig, chunk('IHDR', ihdr), @@ -116,6 +135,13 @@ function buildPng() { ]); } -fs.mkdirSync(path.dirname(OUT), { recursive: true }); -fs.writeFileSync(OUT, buildPng()); -console.log(`Wrote ${OUT}`); +const targets = [ + { size: 32, file: path.join(__dirname, '..', 'assets', 'tray-icon.png') }, + { size: 256, file: path.join(__dirname, '..', 'assets', 'app-icon.png') }, +]; + +for (const t of targets) { + fs.mkdirSync(path.dirname(t.file), { recursive: true }); + fs.writeFileSync(t.file, buildPng(t.size)); + console.log(`Wrote ${t.file} (${t.size}x${t.size})`); +}