diff --git a/apps/desktop/README.md b/apps/desktop/README.md
index cb426169..fda41f5f 100644
--- a/apps/desktop/README.md
+++ b/apps/desktop/README.md
@@ -36,6 +36,15 @@ This writes `apps/desktop/assets/icon.icns` (full ICNS) and `icon.png` (1024
master). Packaging validation rejects incomplete ICNS types. Runtime also calls
`app.dock.setIcon` as a Launch Services / Dock cache fallback.
+Preview the committed icon at its real Dock size without starting EdgeEver:
+
+```sh
+bun run preview:desktop:icon
+```
+
+The temporary preview stays in the Dock until you quit it from the icon's
+context menu.
+
Build an unsigned installer for the current platform with:
```sh
diff --git a/package.json b/package.json
index a68e8e70..b892b671 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"prepare:brand:icons": "bun scripts/trace-brand-icon.mjs && bun scripts/prepare-brand-icons.mjs",
"prepare:mobile:icons": "bun scripts/prepare-mobile-icons.mjs",
"prepare:desktop:icons": "bun scripts/prepare-desktop-icons.mjs",
+ "preview:desktop:icon": "bun scripts/preview-macos-dock-icon.mjs",
"build:android:fast": "bash scripts/build-android-local.sh fast",
"build:android:fast:local": "bun --env-file=${EDGE_EVER_ANDROID_ENV_FILE:-$HOME/.config/edgeever/android/signing.env} run build:android:fast",
"build:android:apk": "bash scripts/build-android-local.sh apk",
@@ -43,7 +44,7 @@
"build:desktop": "EDGE_EVER_DESKTOP_BUILD=1 bun run prepare:desktop:icons && bun run build:web && bun run build:desktop:sidecar && bun run --cwd apps/desktop dist",
"benchmark:desktop:sidecar": "bun scripts/benchmark-desktop-sidecar.mjs",
"verify:web-performance": "bun scripts/verify-web-performance.mjs",
- "test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs scripts/verify-android-apk-signature.test.mjs scripts/desktop-icns.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts",
+ "test:release-planning": "bun test scripts/plan-native-release.test.mjs scripts/check-native-release-assets.test.mjs scripts/create-mac-update-metadata.test.mjs scripts/release.test.mjs scripts/validate-store-delivery.test.mjs scripts/store-delivery.test.mjs scripts/verify-android-apk-signature.test.mjs scripts/desktop-icns.test.mjs scripts/preview-macos-dock-icon.test.mjs apps/web/src/lib/version-check.test.mjs apps/mobile/src/lib/mobile-release.test.ts",
"release": "bun scripts/release.mjs",
"publish:stores": "bun scripts/store-delivery.mjs",
"test:desktop:sidecar": "bun scripts/test-desktop-sidecar.mjs",
diff --git a/scripts/preview-macos-dock-icon.mjs b/scripts/preview-macos-dock-icon.mjs
new file mode 100644
index 00000000..45a8e585
--- /dev/null
+++ b/scripts/preview-macos-dock-icon.mjs
@@ -0,0 +1,121 @@
+import { copyFile, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { basename, dirname, join, resolve } from "node:path";
+import { spawnSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+import { assertMacIcnsComplete } from "./desktop-icns.mjs";
+
+const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const defaultIconPath = join(projectRoot, "apps/desktop/assets/icon.icns");
+
+export const PREVIEW_APP_NAME = "EdgeEver Logo Preview";
+
+export const createPreviewInfoPlist = (bundleIdentifier) => `
+
+
+
+ CFBundleDisplayName
+ ${PREVIEW_APP_NAME}
+ CFBundleExecutable
+ DockIconPreview
+ CFBundleIconFile
+ AppIcon
+ CFBundleIdentifier
+ ${bundleIdentifier}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ${PREVIEW_APP_NAME}
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSMinimumSystemVersion
+ 12.0
+ LSUIElement
+
+ NSHighResolutionCapable
+
+
+
+`;
+
+export const previewSwiftSource = `import AppKit
+
+final class DockIconPreviewDelegate: NSObject, NSApplicationDelegate {
+ func applicationDidFinishLaunching(_ notification: Notification) {
+ NSApp.setActivationPolicy(.regular)
+ if let iconPath = Bundle.main.path(forResource: "AppIcon", ofType: "icns"),
+ let icon = NSImage(contentsOfFile: iconPath) {
+ NSApp.applicationIconImage = icon
+ }
+ NSApp.activate(ignoringOtherApps: false)
+ }
+
+ func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ false
+ }
+}
+
+let application = NSApplication.shared
+let delegate = DockIconPreviewDelegate()
+application.delegate = delegate
+application.run()
+`;
+
+export const assertPreviewPlatform = (platform) => {
+ if (platform !== "darwin") {
+ throw new Error("Dock icon preview is only available on macOS.");
+ }
+};
+
+const run = (command, args) => {
+ const result = spawnSync(command, args, {
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+ if (result.status !== 0) {
+ throw new Error(`${command} ${args.join(" ")} failed:\n${result.stderr || result.stdout || result.error}`);
+ }
+};
+
+export const launchDockIconPreview = async ({ iconPath = defaultIconPath } = {}) => {
+ assertPreviewPlatform(process.platform);
+ const resolvedIconPath = resolve(iconPath);
+ assertMacIcnsComplete(await readFile(resolvedIconPath), resolvedIconPath);
+
+ const previewRoot = await mkdtemp(join(tmpdir(), "edgeever-dock-icon-preview-"));
+ const appPath = join(previewRoot, `${PREVIEW_APP_NAME}.app`);
+ const contentsPath = join(appPath, "Contents");
+ const macosPath = join(contentsPath, "MacOS");
+ const resourcesPath = join(contentsPath, "Resources");
+ const sourcePath = join(previewRoot, "DockIconPreview.swift");
+ const executablePath = join(macosPath, "DockIconPreview");
+ const bundleIdentifier = `org.edgeever.logo-preview.v${Date.now()}p${process.pid}`;
+
+ await mkdir(macosPath, { recursive: true });
+ await mkdir(resourcesPath, { recursive: true });
+ await writeFile(sourcePath, previewSwiftSource);
+ await writeFile(join(contentsPath, "Info.plist"), createPreviewInfoPlist(bundleIdentifier));
+ await copyFile(resolvedIconPath, join(resourcesPath, "AppIcon.icns"));
+
+ run("xcrun", ["swiftc", sourcePath, "-o", executablePath, "-framework", "AppKit"]);
+ run("codesign", ["--force", "--deep", "--sign", "-", appPath]);
+ run("open", ["-n", appPath]);
+
+ return { appPath, iconPath: resolvedIconPath };
+};
+
+if (import.meta.main) {
+ try {
+ const iconPath = process.argv[2] ? resolve(process.cwd(), process.argv[2]) : defaultIconPath;
+ const result = await launchDockIconPreview({ iconPath });
+ console.log(`[preview-macos-dock-icon] showing ${basename(result.iconPath)} in the Dock as “${PREVIEW_APP_NAME}”`);
+ console.log("[preview-macos-dock-icon] right-click the Dock icon and choose Quit when finished");
+ } catch (error) {
+ console.error(`[preview-macos-dock-icon] ${error instanceof Error ? error.message : String(error)}`);
+ process.exit(1);
+ }
+}
diff --git a/scripts/preview-macos-dock-icon.test.mjs b/scripts/preview-macos-dock-icon.test.mjs
new file mode 100644
index 00000000..fe2cd1fd
--- /dev/null
+++ b/scripts/preview-macos-dock-icon.test.mjs
@@ -0,0 +1,28 @@
+import { describe, expect, test } from "bun:test";
+import {
+ PREVIEW_APP_NAME,
+ assertPreviewPlatform,
+ createPreviewInfoPlist,
+ previewSwiftSource,
+} from "./preview-macos-dock-icon.mjs";
+
+describe("macOS Dock icon preview", () => {
+ test("creates a regular app bundle that declares the preview icon", () => {
+ const plist = createPreviewInfoPlist("org.edgeever.logo-preview.test");
+ expect(plist).toContain(`${PREVIEW_APP_NAME}`);
+ expect(plist).toContain("AppIcon");
+ expect(plist).toContain("org.edgeever.logo-preview.test");
+ expect(plist).toContain("LSUIElement\n ");
+ });
+
+ test("keeps the Cocoa preview alive and sets its Dock icon", () => {
+ expect(previewSwiftSource).toContain("NSApp.setActivationPolicy(.regular)");
+ expect(previewSwiftSource).toContain("NSApp.applicationIconImage = icon");
+ expect(previewSwiftSource).toContain("application.run()");
+ });
+
+ test("rejects unsupported platforms", () => {
+ expect(() => assertPreviewPlatform("linux")).toThrow(/only available on macOS/i);
+ expect(() => assertPreviewPlatform("darwin")).not.toThrow();
+ });
+});