Skip to content

Commit f06ccc6

Browse files
committed
Redesign app logo and add custom NSIS installer branding
New minimal dark squircle icon with refined Sn monogram, inset depth, dual-corner border lighting, and letter shadows. Add branded installer sidebar, header, and custom NSIS include with welcome/finish page text.
1 parent 07a4d9d commit f06ccc6

8 files changed

Lines changed: 239 additions & 85 deletions

File tree

build/icon-512.png

16.8 KB
Loading

build/icon.ico

0 Bytes
Binary file not shown.

build/icon.png

32.6 KB
Loading

build/installer.nsh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
; SnCode — Custom NSIS installer configuration
2+
; Included by electron-builder via the "include" option in package.json
3+
4+
!macro customHeader
5+
; ── Welcome page ──
6+
!ifndef MUI_WELCOMEPAGE_TITLE
7+
!define MUI_WELCOMEPAGE_TITLE "Welcome to SnCode"
8+
!endif
9+
!ifndef MUI_WELCOMEPAGE_TEXT
10+
!define MUI_WELCOMEPAGE_TEXT "SnCode is your AI-powered desktop coding agent.$\r$\n$\r$\nThis wizard will guide you through the installation.$\r$\nClick Next to continue."
11+
!endif
12+
13+
; ── Finish page ──
14+
!ifndef MUI_FINISHPAGE_TITLE
15+
!define MUI_FINISHPAGE_TITLE "SnCode is Ready"
16+
!endif
17+
!ifndef MUI_FINISHPAGE_TEXT
18+
!define MUI_FINISHPAGE_TEXT "SnCode has been installed successfully.$\r$\n$\r$\nClick Finish to close the wizard and start coding."
19+
!endif
20+
21+
; ── Abort warning ──
22+
!ifndef MUI_ABORTWARNING
23+
!define MUI_ABORTWARNING
24+
!endif
25+
!ifndef MUI_ABORTWARNING_TEXT
26+
!define MUI_ABORTWARNING_TEXT "Are you sure you want to cancel the SnCode installation?"
27+
!endif
28+
!macroend

build/installerHeader.bmp

25.2 KB
Binary file not shown.

build/installerSidebar.bmp

151 KB
Binary file not shown.

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,13 @@
100100
"perMachine": false,
101101
"allowToChangeInstallationDirectory": true,
102102
"deleteAppDataOnUninstall": false,
103-
"shortcutName": "SnCode"
103+
"shortcutName": "SnCode",
104+
"installerIcon": "build/icon.ico",
105+
"uninstallerIcon": "build/icon.ico",
106+
"installerSidebar": "build/installerSidebar.bmp",
107+
"uninstallerSidebar": "build/installerSidebar.bmp",
108+
"installerHeader": "build/installerHeader.bmp",
109+
"include": "build/installer.nsh"
104110
},
105111
"mac": {
106112
"target": [

scripts/generate-icons.mjs

Lines changed: 204 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,237 @@
11
#!/usr/bin/env node
22
/**
3-
* Generate SnCode app icons for all platforms.
4-
* Creates build/icon.png (1024x1024), build/icon.ico, build/icon.icns
3+
* Generate SnCode app icons + Windows installer assets.
54
*
6-
* Design: Dark rounded-rectangle with a stylized "Sn" monogram + terminal cursor.
7-
* Matches the app's dark neutral palette (#141414 base).
5+
* Output (all in build/):
6+
* icon.png 1024x1024 All platforms
7+
* icon-512.png 512x512 Linux
8+
* icon.ico multi-res Windows
9+
* installerSidebar.bmp 164x314 NSIS sidebar
10+
* installerHeader.bmp 150x57 NSIS header
11+
*
12+
* Design: Inset dark squircle, "Sn" monogram with depth. Monochromatic.
813
*/
914

1015
import sharp from "sharp";
1116
import pngToIco from "png-to-ico";
12-
import { writeFileSync, mkdirSync } from "fs";
17+
import { writeFileSync, mkdirSync, unlinkSync } from "fs";
1318
import { join, dirname } from "path";
1419
import { fileURLToPath } from "url";
1520

1621
const __dirname = dirname(fileURLToPath(import.meta.url));
1722
const buildDir = join(__dirname, "..", "build");
1823
mkdirSync(buildDir, { recursive: true });
1924

25+
/* ─────────────────────────── SVGs ─────────────────────────── */
26+
2027
const SIZE = 1024;
2128

22-
// SVG icon: dark rounded square with "Sn" text and a blinking cursor accent
23-
const svg = `
29+
const logoSvg = `
2430
<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
2531
<defs>
26-
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
27-
<stop offset="0%" stop-color="#1e1e1e"/>
28-
<stop offset="100%" stop-color="#111111"/>
29-
</linearGradient>
30-
<linearGradient id="accent" x1="0" y1="0" x2="0" y2="1">
31-
<stop offset="0%" stop-color="#e0e0e0"/>
32-
<stop offset="100%" stop-color="#a0a0a0"/>
32+
<linearGradient id="bg" x1="0" y1="0" x2="0.8" y2="1">
33+
<stop offset="0%" stop-color="#1c1c20"/>
34+
<stop offset="100%" stop-color="#0e0e11"/>
3335
</linearGradient>
34-
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
35-
<feDropShadow dx="0" dy="4" stdDeviation="12" flood-color="#000" flood-opacity="0.5"/>
36+
37+
<!-- Border stroke glow at corners -->
38+
<radialGradient id="borderTR" cx="0.88" cy="0.12" r="0.4">
39+
<stop offset="0%" stop-color="#ffffff" stop-opacity="1"/>
40+
<stop offset="30%" stop-color="#ffffff" stop-opacity="0.15"/>
41+
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
42+
</radialGradient>
43+
<radialGradient id="borderBL" cx="0.12" cy="0.88" r="0.4">
44+
<stop offset="0%" stop-color="#ffffff" stop-opacity="1"/>
45+
<stop offset="30%" stop-color="#ffffff" stop-opacity="0.15"/>
46+
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
47+
</radialGradient>
48+
49+
<!-- Edge vignette for inset depth -->
50+
<radialGradient id="vignette" cx="0.5" cy="0.48" r="0.52">
51+
<stop offset="0%" stop-color="#000000" stop-opacity="0"/>
52+
<stop offset="75%" stop-color="#000000" stop-opacity="0"/>
53+
<stop offset="100%" stop-color="#000000" stop-opacity="0.3"/>
54+
</radialGradient>
55+
56+
<!-- Subtle top-center light for dimension -->
57+
<radialGradient id="topLight" cx="0.5" cy="0.25" r="0.45">
58+
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.03"/>
59+
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
60+
</radialGradient>
61+
62+
<!-- Text depth: shadow beneath letters -->
63+
<filter id="td" x="-5%" y="-5%" width="110%" height="120%">
64+
<feDropShadow dx="0" dy="5" stdDeviation="5" flood-color="#000" flood-opacity="0.7"/>
3665
</filter>
3766
</defs>
3867
39-
<!-- Background rounded square -->
40-
<rect x="64" y="64" width="896" height="896" rx="180" ry="180"
41-
fill="url(#bg)" filter="url(#shadow)" stroke="#2a2a2a" stroke-width="3"/>
42-
43-
<!-- Subtle inner border glow -->
44-
<rect x="80" y="80" width="864" height="864" rx="168" ry="168"
45-
fill="none" stroke="#ffffff08" stroke-width="2"/>
46-
47-
<!-- Terminal angle bracket ">" on the left -->
48-
<polyline points="220,340 360,512 220,684"
49-
fill="none" stroke="#555555" stroke-width="48"
50-
stroke-linecap="round" stroke-linejoin="round"/>
51-
52-
<!-- "Sn" text - the brand -->
53-
<text x="520" y="590" font-family="'SF Pro Display', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
54-
font-size="320" font-weight="700" letter-spacing="-8">
55-
<tspan fill="#ffffff">S</tspan><tspan fill="#555555">n</tspan>
56-
</text>
68+
<!-- Background squircle (inset — no outer shadow) -->
69+
<rect x="64" y="64" width="896" height="896" rx="212" ry="212" fill="url(#bg)"/>
70+
71+
<!-- Vignette darkening at edges -->
72+
<rect x="64" y="64" width="896" height="896" rx="212" ry="212" fill="url(#vignette)"/>
73+
74+
<!-- Subtle top light -->
75+
<rect x="64" y="64" width="896" height="896" rx="212" ry="212" fill="url(#topLight)"/>
76+
77+
<!-- Border: top-right corner glow -->
78+
<rect x="62" y="62" width="900" height="900" rx="214" ry="214"
79+
fill="none" stroke="url(#borderTR)" stroke-width="8"/>
80+
<!-- Border: bottom-left corner glow -->
81+
<rect x="62" y="62" width="900" height="900" rx="214" ry="214"
82+
fill="none" stroke="url(#borderBL)" stroke-width="8"/>
83+
84+
<!-- "S" — bold, white, with depth -->
85+
<text x="240" y="695"
86+
font-family="Bahnschrift,'SF Pro Display','Segoe UI',sans-serif"
87+
font-size="540" font-weight="700" fill="#ffffff"
88+
filter="url(#td)" letter-spacing="-10">S</text>
89+
90+
<!-- "n" — visible gray, with depth -->
91+
<text x="565" y="695"
92+
font-family="Bahnschrift,'SF Pro Display','Segoe UI',sans-serif"
93+
font-size="380" font-weight="600" fill="#b4b4bc"
94+
filter="url(#td)" letter-spacing="-4">n</text>
95+
</svg>`;
96+
97+
// NSIS sidebar: 164x314 — centered branding
98+
const sidebarSvg = `
99+
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="314" viewBox="0 0 164 314">
100+
<defs>
101+
<linearGradient id="sbg" x1="0" y1="0" x2="0" y2="1">
102+
<stop offset="0%" stop-color="#1a1a1e"/>
103+
<stop offset="100%" stop-color="#0e0e11"/>
104+
</linearGradient>
105+
</defs>
106+
<rect width="164" height="314" fill="url(#sbg)"/>
107+
108+
<!-- Right-edge separator -->
109+
<rect x="162" y="0" width="2" height="314" fill="rgba(255,255,255,0.08)"/>
110+
111+
<!-- Large "Sn" mark, centered -->
112+
<text x="82" y="125" text-anchor="middle"
113+
font-family="Bahnschrift,'Segoe UI',sans-serif"
114+
font-size="80" font-weight="700" fill="#ffffff">S<tspan font-size="55" font-weight="600" fill="#b4b4bc">n</tspan></text>
115+
116+
<!-- Separator -->
117+
<rect x="56" y="142" width="52" height="1" rx="0.5" fill="rgba(255,255,255,0.1)"/>
118+
119+
<!-- App name, centered -->
120+
<text x="82" y="170" text-anchor="middle"
121+
font-family="Bahnschrift,'Segoe UI',sans-serif"
122+
font-size="18" font-weight="600" fill="#d4d4d8" letter-spacing="2">SnCode</text>
123+
124+
<!-- Tagline, centered -->
125+
<text x="82" y="192" text-anchor="middle"
126+
font-family="Bahnschrift,'Segoe UI',sans-serif"
127+
font-size="11" fill="#71717a" letter-spacing="0.5">AI Coding Agent</text>
128+
129+
<!-- Version, bottom center -->
130+
<text x="82" y="296" text-anchor="middle"
131+
font-family="Bahnschrift,'Segoe UI',sans-serif"
132+
font-size="10" fill="#52525b">v0.2.0</text>
133+
</svg>`;
134+
135+
// NSIS header: 150x57 — clean centered bar
136+
const headerSvg = `
137+
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="57" viewBox="0 0 150 57">
138+
<defs>
139+
<linearGradient id="hbg" x1="0" y1="0" x2="1" y2="0">
140+
<stop offset="0%" stop-color="#1a1a1e"/>
141+
<stop offset="100%" stop-color="#131316"/>
142+
</linearGradient>
143+
</defs>
144+
<rect width="150" height="57" fill="url(#hbg)"/>
145+
146+
<!-- Bottom separator -->
147+
<rect x="0" y="55" width="150" height="2" fill="rgba(255,255,255,0.08)"/>
148+
149+
<!-- "SnCode" centered -->
150+
<text x="75" y="36" text-anchor="middle"
151+
font-family="Bahnschrift,'Segoe UI',sans-serif"
152+
font-size="22" font-weight="600" fill="#e4e4e7" letter-spacing="1.5">SnCode</text>
153+
</svg>`;
154+
155+
/* ─────────── BMP conversion (24-bit, no alpha) ─────────── */
156+
157+
function rawToBmp24(rgba, w, h, bgR = 0x0e, bgG = 0x0e, bgB = 0x11) {
158+
const rowBytes = w * 3;
159+
const pad = (4 - (rowBytes % 4)) % 4;
160+
const stride = rowBytes + pad;
161+
const dataSize = stride * h;
162+
const buf = Buffer.alloc(14 + 40 + dataSize);
163+
164+
buf.write("BM", 0);
165+
buf.writeUInt32LE(buf.length, 2);
166+
buf.writeUInt32LE(0, 6);
167+
buf.writeUInt32LE(54, 10);
168+
169+
buf.writeUInt32LE(40, 14);
170+
buf.writeInt32LE(w, 18);
171+
buf.writeInt32LE(h, 22);
172+
buf.writeUInt16LE(1, 26);
173+
buf.writeUInt16LE(24, 28);
174+
buf.writeUInt32LE(0, 30);
175+
buf.writeUInt32LE(dataSize, 34);
176+
buf.writeInt32LE(2835, 38);
177+
buf.writeInt32LE(2835, 42);
178+
179+
for (let y = 0; y < h; y++) {
180+
const srcY = (h - 1 - y) * w * 4;
181+
const dstY = 54 + y * stride;
182+
for (let x = 0; x < w; x++) {
183+
const si = srcY + x * 4;
184+
const di = dstY + x * 3;
185+
const a = rgba[si + 3] / 255;
186+
buf[di + 0] = Math.round(rgba[si + 2] * a + bgB * (1 - a));
187+
buf[di + 1] = Math.round(rgba[si + 1] * a + bgG * (1 - a));
188+
buf[di + 2] = Math.round(rgba[si + 0] * a + bgR * (1 - a));
189+
}
190+
}
191+
return buf;
192+
}
57193

58-
<!-- Cursor line (blinking accent) -->
59-
<rect x="808" y="380" width="6" height="260" rx="3"
60-
fill="#666666"/>
61-
</svg>
62-
`;
194+
/* ──────────────────── Generation ──────────────────── */
63195

64196
async function generate() {
65-
console.log("Generating SnCode icons...");
197+
console.log("Generating SnCode icons + installer assets…\n");
66198

67-
// Generate 1024x1024 PNG
68-
const png1024 = await sharp(Buffer.from(svg))
69-
.resize(1024, 1024)
70-
.png()
71-
.toBuffer();
199+
const png1024 = await sharp(Buffer.from(logoSvg))
200+
.resize(1024, 1024).png().toBuffer();
72201
writeFileSync(join(buildDir, "icon.png"), png1024);
73-
console.log(" -> build/icon.png (1024x1024)");
202+
console.log(" icon.png 1024x1024");
74203

75-
// Generate 512x512 PNG (for Linux)
76-
const png512 = await sharp(Buffer.from(svg))
77-
.resize(512, 512)
78-
.png()
79-
.toBuffer();
204+
const png512 = await sharp(Buffer.from(logoSvg))
205+
.resize(512, 512).png().toBuffer();
80206
writeFileSync(join(buildDir, "icon-512.png"), png512);
81-
console.log(" -> build/icon-512.png (512x512)");
82-
83-
// Generate multiple sizes for ICO
84-
const sizes = [16, 24, 32, 48, 64, 128, 256];
85-
const pngBuffers = await Promise.all(
86-
sizes.map((s) =>
87-
sharp(Buffer.from(svg)).resize(s, s).png().toBuffer()
88-
)
89-
);
90-
91-
// Write temp PNGs for ico generation, then create ICO
92-
const tempPngPaths = [];
93-
for (let i = 0; i < sizes.length; i++) {
94-
const p = join(buildDir, `_temp_${sizes[i]}.png`);
95-
writeFileSync(p, pngBuffers[i]);
96-
tempPngPaths.push(p);
207+
console.log(" icon-512.png 512x512");
208+
209+
const icoSizes = [16, 24, 32, 48, 64, 128, 256];
210+
const tempPaths = [];
211+
for (const s of icoSizes) {
212+
const p = join(buildDir, `_tmp_${s}.png`);
213+
const b = await sharp(Buffer.from(logoSvg)).resize(s, s).png().toBuffer();
214+
writeFileSync(p, b);
215+
tempPaths.push(p);
97216
}
98-
99-
const icoBuffer = await pngToIco(tempPngPaths);
100-
writeFileSync(join(buildDir, "icon.ico"), icoBuffer);
101-
console.log(" -> build/icon.ico (multi-size)");
102-
103-
// Clean up temp files
104-
const { unlinkSync } = await import("fs");
105-
for (const p of tempPngPaths) {
106-
try { unlinkSync(p); } catch { /* ignore */ }
107-
}
108-
109-
// Note: .icns generation requires platform-specific tools.
110-
// electron-builder can generate .icns from .png on macOS during packaging.
111-
// For CI, having icon.png is sufficient — electron-builder handles conversion.
112-
console.log(" -> build/icon.icns: skipped (electron-builder auto-generates from icon.png on macOS)");
113-
114-
console.log("\nDone! Icons are in the build/ directory.");
217+
writeFileSync(join(buildDir, "icon.ico"), await pngToIco(tempPaths));
218+
for (const p of tempPaths) { try { unlinkSync(p); } catch { /* ignore */ } }
219+
console.log(" icon.ico multi-res");
220+
console.log(" icon.icns (auto via electron-builder on macOS)");
221+
222+
const sW = 164, sH = 314;
223+
const sidebarRaw = await sharp(Buffer.from(sidebarSvg))
224+
.resize(sW, sH).ensureAlpha().raw().toBuffer();
225+
writeFileSync(join(buildDir, "installerSidebar.bmp"), rawToBmp24(sidebarRaw, sW, sH));
226+
console.log(" installerSidebar.bmp 164x314");
227+
228+
const hW = 150, hH = 57;
229+
const headerRaw = await sharp(Buffer.from(headerSvg))
230+
.resize(hW, hH).ensureAlpha().raw().toBuffer();
231+
writeFileSync(join(buildDir, "installerHeader.bmp"), rawToBmp24(headerRaw, hW, hH));
232+
console.log(" installerHeader.bmp 150x57");
233+
234+
console.log("\nDone! All assets in build/");
115235
}
116236

117237
generate().catch((err) => {

0 commit comments

Comments
 (0)