-
Notifications
You must be signed in to change notification settings - Fork 963
Brand Refresh #3130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Brand Refresh #3130
Changes from all commits
d06827c
92a5c13
7e9d1a1
7510a02
67a5c02
c4fc5e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,239 +1,57 @@ | ||||||
| import { existsSync } from "node:fs"; | ||||||
| import { join } from "node:path"; | ||||||
| import { app, nativeImage } from "electron"; | ||||||
| import { env } from "main/env.main"; | ||||||
| import { getWorkspaceName } from "shared/env.shared"; | ||||||
| import twColors from "tailwindcss/colors"; | ||||||
| import { prerelease } from "semver"; | ||||||
|
|
||||||
| /** | ||||||
| * Deterministic hash of a string, returned as a non-negative integer. | ||||||
| * Returns true if this is a canary (prerelease) build, e.g. "0.0.53-canary". | ||||||
| */ | ||||||
| function hashString(seed: string): number { | ||||||
| let hash = 0; | ||||||
| for (let i = 0; i < seed.length; i++) { | ||||||
| hash = seed.charCodeAt(i) + ((hash << 5) - hash); | ||||||
| hash |= 0; | ||||||
| } | ||||||
| return Math.abs(hash); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Parses an OKLCH CSS string like "oklch(63.7% 0.237 25.331)". | ||||||
| */ | ||||||
| function parseOklch(str: string): { l: number; c: number; h: number } | null { | ||||||
| const match = str.match(/oklch\(([\d.]+)%\s+([\d.]+)\s+([\d.]+)\)/); | ||||||
| if (!match) return null; | ||||||
| return { | ||||||
| l: Number(match[1]) / 100, | ||||||
| c: Number(match[2]), | ||||||
| h: Number(match[3]), | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Converts OKLCH to sRGB (all values 0-255). | ||||||
| */ | ||||||
| function oklchToRgb(l: number, c: number, h: number): [number, number, number] { | ||||||
| const hRad = (h * Math.PI) / 180; | ||||||
| const a = c * Math.cos(hRad); | ||||||
| const b = c * Math.sin(hRad); | ||||||
|
|
||||||
| // OKLab → LMS (cube-root space) | ||||||
| const l_ = l + 0.3963377774 * a + 0.2158037573 * b; | ||||||
| const m_ = l - 0.1055613458 * a - 0.0638541728 * b; | ||||||
| const s_ = l - 0.0894841775 * a - 1.291485548 * b; | ||||||
|
|
||||||
| const lc = l_ * l_ * l_; | ||||||
| const mc = m_ * m_ * m_; | ||||||
| const sc = s_ * s_ * s_; | ||||||
|
|
||||||
| // LMS → linear sRGB | ||||||
| const rLin = +4.0767416621 * lc - 3.3077115913 * mc + 0.2309699292 * sc; | ||||||
| const gLin = -1.2684380046 * lc + 2.6097574011 * mc - 0.3413193965 * sc; | ||||||
| const bLin = -0.0041960863 * lc + 0.7034186147 * mc + 0.2967775076 * sc; | ||||||
|
|
||||||
| const toSrgb = (v: number) => { | ||||||
| const clamped = Math.max(0, Math.min(1, v)); | ||||||
| return clamped <= 0.0031308 | ||||||
| ? 12.92 * clamped | ||||||
| : 1.055 * clamped ** (1 / 2.4) - 0.055; | ||||||
| }; | ||||||
|
|
||||||
| return [ | ||||||
| Math.round(toSrgb(rLin) * 255), | ||||||
| Math.round(toSrgb(gLin) * 255), | ||||||
| Math.round(toSrgb(bLin) * 255), | ||||||
| ]; | ||||||
| function isCanaryBuild(): boolean { | ||||||
| const components = prerelease(app.getVersion()); | ||||||
| return components !== null && components.length > 0; | ||||||
| } | ||||||
|
|
||||||
| /** All Tailwind 500-level colors as RGB tuples. */ | ||||||
| const TAILWIND_500_COLORS: [number, number, number][] = (() => { | ||||||
| const skip = new Set(["inherit", "current", "transparent", "black", "white"]); | ||||||
| const result: [number, number, number][] = []; | ||||||
| for (const [name, val] of Object.entries(twColors)) { | ||||||
| if (skip.has(name) || typeof val !== "object" || !("500" in val)) continue; | ||||||
| const parsed = parseOklch((val as Record<string, string>)["500"] as string); | ||||||
| if (parsed) result.push(oklchToRgb(parsed.l, parsed.c, parsed.h)); | ||||||
| } | ||||||
| return result; | ||||||
| })(); | ||||||
|
|
||||||
| /** | ||||||
| * Gets the path to the app icon PNG. | ||||||
| * Returns the icons directory path. | ||||||
| */ | ||||||
| function getIconPath(): string { | ||||||
| function getIconsDir(): string { | ||||||
| if (app.isPackaged) { | ||||||
| return join( | ||||||
| process.resourcesPath, | ||||||
| "app.asar/resources/build/icons/icon.png", | ||||||
| ); | ||||||
| return join(process.resourcesPath, "app.asar/resources/build/icons"); | ||||||
| } | ||||||
|
|
||||||
| if (env.NODE_ENV === "development") { | ||||||
| return join(app.getAppPath(), "src/resources/build/icons/icon.png"); | ||||||
| return join(app.getAppPath(), "src/resources/build/icons"); | ||||||
| } | ||||||
|
|
||||||
| return join(__dirname, "../resources/build/icons/icon.png"); | ||||||
| return join(__dirname, "../resources/build/icons"); | ||||||
| } | ||||||
|
Comment on lines
15
to
26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Verify the directory structure to ensure the fallback path is valid.
# Expected: The dist folder should have a structure where resources/build/icons
# is accessible via ../resources/build/icons from the lib folder.
# Check the typical dist structure for Electron main process files
fd -t f "dock-icon" --extension ts --extension js -x dirname {} \;
# Check if resources structure exists in expected locations
tree -L 4 -d apps/desktop/src/resources 2>/dev/null || echo "src/resources not found"
tree -L 4 -d apps/desktop/dist/resources 2>/dev/null || echo "dist/resources not found (may not be built)"Repository: superset-sh/superset Length of output: 336 🏁 Script executed: #!/bin/bash
# Check build configuration files in apps/desktop
find apps/desktop -maxdepth 2 -type f \( -name "package.json" -o -name "tsconfig*.json" -o -name "vite.config.*" -o -name "webpack.config.*" -o -name "*.config.js" \)Repository: superset-sh/superset Length of output: 116 🏁 Script executed: #!/bin/bash
# Check the package.json build scripts
cat apps/desktop/package.json | head -50Repository: superset-sh/superset Length of output: 2336 🏁 Script executed: #!/bin/bash
# Check tsconfig.json for output directory
cat apps/desktop/tsconfig.jsonRepository: superset-sh/superset Length of output: 471 🏁 Script executed: #!/bin/bash
# Check electron-vite configuration
find apps/desktop -name "electron.vite.config.*" -o -name "vite.config.*" | head -5Repository: superset-sh/superset Length of output: 100 🏁 Script executed: #!/bin/bash
# Look at electron-vite config if it exists
cat apps/desktop/electron.vite.config.ts 2>/dev/null | head -100Repository: superset-sh/superset Length of output: 3434 🏁 Script executed: #!/bin/bash
# Check if there's a script that copies resources to dist
grep -r "resources" apps/desktop/scripts --include="*.ts" --include="*.js" 2>/dev/null | head -20Repository: superset-sh/superset Length of output: 173 🏁 Script executed: #!/bin/bash
# Check the vite helpers to see what copyResourcesPlugin does
cat apps/desktop/vite/helpers.ts 2>/dev/null || cat apps/desktop/vite/helpers.js 2>/dev/null || find apps/desktop/vite -name "*.ts" -o -name "*.js" | xargs ls -laRepository: superset-sh/superset Length of output: 2607 🏁 Script executed: #!/bin/bash
# Also check what the resources variable is used for
cat apps/desktop/vite/helpers/copyResourcesPlugin.ts 2>/dev/null || find apps/desktop -name "*copyResources*"Repository: superset-sh/superset Length of output: 46 🏁 Script executed: #!/bin/bash
# Verify that build/icons is indeed not in the copy list by checking the entire flow
cat apps/desktop/vite/helpers.ts | grep -A 50 "RESOURCES_TO_COPY"Repository: superset-sh/superset Length of output: 1943 🏁 Script executed: #!/bin/bash
# Check if there's any other mechanism that copies build/ or icons
grep -r "build/icons" apps/desktop --include="*.ts" --include="*.js" --exclude-dir=node_modules | head -20Repository: superset-sh/superset Length of output: 1012 🏁 Script executed: #!/bin/bash
# Check the electron-builder config to see if icons are handled separately
find apps/desktop -name "electron-builder*" -type f | head -5Repository: superset-sh/superset Length of output: 136 🏁 Script executed: #!/bin/bash
# Check the electron-builder config to see how resources are handled
cat apps/desktop/electron-builder.ts 2>/dev/null | head -150Repository: superset-sh/superset Length of output: 4622 🏁 Script executed: #!/bin/bash
# Check specifically how pkg.resources is used
cat apps/desktop/electron-builder.ts | grep -B 5 -A 5 "pkg.resources"Repository: superset-sh/superset Length of output: 1474 The fallback path will fail in non-packaged, non-development scenarios. The third condition (line 25) is triggered when the app is not packaged and not in development mode—such as when running the built output in preview mode. In this case, However, the build configuration only copies specific resources to dist via Consider using 🤖 Prompt for AI Agents |
||||||
|
|
||||||
| /** | ||||||
| * Signed distance function for a rounded rectangle. | ||||||
| * Negative = inside, positive = outside, zero = on boundary. | ||||||
| * Resolves the dock icon path for the current build type. | ||||||
| * Resolution order (first existing file wins): | ||||||
| * - dev: icon-dev.png → icon.png | ||||||
| * - canary: icon-canary.png → icon.png | ||||||
| * - stable: icon.png | ||||||
| */ | ||||||
| function sdfRoundedRect( | ||||||
| px: number, | ||||||
| py: number, | ||||||
| left: number, | ||||||
| top: number, | ||||||
| right: number, | ||||||
| bottom: number, | ||||||
| radius: number, | ||||||
| ): number { | ||||||
| const cx = (left + right) / 2; | ||||||
| const cy = (top + bottom) / 2; | ||||||
| const halfW = (right - left) / 2; | ||||||
| const halfH = (bottom - top) / 2; | ||||||
|
|
||||||
| const dx = Math.abs(px - cx) - halfW + radius; | ||||||
| const dy = Math.abs(py - cy) - halfH + radius; | ||||||
|
|
||||||
| return ( | ||||||
| Math.sqrt(Math.max(dx, 0) ** 2 + Math.max(dy, 0) ** 2) + | ||||||
| Math.min(Math.max(dx, dy), 0) - | ||||||
| radius | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Finds the bounding box of non-transparent pixels in a bitmap. | ||||||
| */ | ||||||
| function findContentBounds( | ||||||
| bitmap: Buffer, | ||||||
| width: number, | ||||||
| height: number, | ||||||
| ): { top: number; left: number; bottom: number; right: number } { | ||||||
| let top = height; | ||||||
| let left = width; | ||||||
| let bottom = 0; | ||||||
| let right = 0; | ||||||
| function getIconPath(): string { | ||||||
| const dir = getIconsDir(); | ||||||
|
|
||||||
| for (let y = 0; y < height; y++) { | ||||||
| for (let x = 0; x < width; x++) { | ||||||
| if ((bitmap[(y * width + x) * 4 + 3] ?? 0) > 10) { | ||||||
| if (y < top) top = y; | ||||||
| if (y > bottom) bottom = y; | ||||||
| if (x < left) left = x; | ||||||
| if (x > right) right = x; | ||||||
| } | ||||||
| } | ||||||
| if (env.NODE_ENV === "development") { | ||||||
| const devIcon = join(dir, "icon-dev.png"); | ||||||
| if (existsSync(devIcon)) return devIcon; | ||||||
| } else if (isCanaryBuild()) { | ||||||
| const canaryIcon = join(dir, "icon-canary.png"); | ||||||
| if (existsSync(canaryIcon)) return canaryIcon; | ||||||
| } | ||||||
|
|
||||||
| return { top, left, bottom, right }; | ||||||
| return join(dir, "icon.png"); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Draws a rounded-rectangle border on raw RGBA bitmap data. | ||||||
| * The border is drawn inward from the specified bounds. | ||||||
| */ | ||||||
| function drawBorder({ | ||||||
| bitmap, | ||||||
| width, | ||||||
| thickness, | ||||||
| left, | ||||||
| top, | ||||||
| right, | ||||||
| bottom, | ||||||
| cornerRadius, | ||||||
| rgb, | ||||||
| }: { | ||||||
| bitmap: Buffer; | ||||||
| width: number; | ||||||
| thickness: number; | ||||||
| left: number; | ||||||
| top: number; | ||||||
| right: number; | ||||||
| bottom: number; | ||||||
| cornerRadius: number; | ||||||
| rgb: [number, number, number]; | ||||||
| }) { | ||||||
| const innerRadius = Math.max(0, cornerRadius - thickness); | ||||||
|
|
||||||
| for (let y = top; y <= bottom; y++) { | ||||||
| for (let x = left; x <= right; x++) { | ||||||
| const outerDist = sdfRoundedRect( | ||||||
| x, | ||||||
| y, | ||||||
| left, | ||||||
| top, | ||||||
| right, | ||||||
| bottom, | ||||||
| cornerRadius, | ||||||
| ); | ||||||
| const innerDist = sdfRoundedRect( | ||||||
| x, | ||||||
| y, | ||||||
| left + thickness, | ||||||
| top + thickness, | ||||||
| right - thickness, | ||||||
| bottom - thickness, | ||||||
| innerRadius, | ||||||
| ); | ||||||
|
|
||||||
| // Anti-aliased edges | ||||||
| const outerAlpha = Math.max(0, Math.min(1, 0.5 - outerDist)); | ||||||
| const innerAlpha = Math.max(0, Math.min(1, innerDist + 0.5)); | ||||||
| const borderAlpha = outerAlpha * innerAlpha; | ||||||
|
|
||||||
| if (borderAlpha > 0.001) { | ||||||
| const offset = (y * width + x) * 4; | ||||||
| const r = bitmap[offset] ?? 0; | ||||||
| const g = bitmap[offset + 1] ?? 0; | ||||||
| const b = bitmap[offset + 2] ?? 0; | ||||||
| const a = bitmap[offset + 3] ?? 0; | ||||||
| bitmap[offset] = Math.round( | ||||||
| rgb[0] * borderAlpha + r * (1 - borderAlpha), | ||||||
| ); | ||||||
| bitmap[offset + 1] = Math.round( | ||||||
| rgb[1] * borderAlpha + g * (1 - borderAlpha), | ||||||
| ); | ||||||
| bitmap[offset + 2] = Math.round( | ||||||
| rgb[2] * borderAlpha + b * (1 - borderAlpha), | ||||||
| ); | ||||||
| bitmap[offset + 3] = Math.max(a, Math.round(borderAlpha * 255)); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Sets the macOS dock icon with a colored border based on the workspace name. | ||||||
| * No-op on non-macOS platforms or when no workspace name is set. | ||||||
| * Sets the macOS dock icon based on the current build type and macOS version. | ||||||
| * No-op on non-macOS platforms. | ||||||
| */ | ||||||
| export function setWorkspaceDockIcon(): void { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function was previously responsible for setting a workspace-coloured border on the dock icon, but that concept has been fully removed. The new behaviour is simply "set the appropriate dock icon for this build type." The exported name
Suggested change
|
||||||
| if (process.platform !== "darwin") return; | ||||||
| if (env.NODE_ENV !== "development") return; | ||||||
|
|
||||||
| const workspaceName = getWorkspaceName(); | ||||||
| if (!workspaceName) return; | ||||||
|
|
||||||
| try { | ||||||
| const iconPath = getIconPath(); | ||||||
|
|
@@ -243,41 +61,8 @@ export function setWorkspaceDockIcon(): void { | |||||
| return; | ||||||
| } | ||||||
|
|
||||||
| const size = icon.getSize(); | ||||||
| const bitmap = icon.toBitmap(); | ||||||
|
|
||||||
| const hash = hashString(workspaceName); | ||||||
| const rgb = | ||||||
| TAILWIND_500_COLORS[hash % TAILWIND_500_COLORS.length] ?? | ||||||
| ([59, 130, 246] as [number, number, number]); // blue-500 fallback | ||||||
|
|
||||||
| // Find the actual icon content area (skip transparent padding) | ||||||
| const bounds = findContentBounds(bitmap, size.width, size.height); | ||||||
| const thickness = Math.round(size.width * 0.038); | ||||||
| const cornerRadius = Math.round(size.width * 0.22); | ||||||
|
|
||||||
| // Draw border flush with the content edges, overlapping inward | ||||||
| drawBorder({ | ||||||
| bitmap, | ||||||
| width: size.width, | ||||||
| thickness, | ||||||
| left: bounds.left, | ||||||
| top: bounds.top, | ||||||
| right: bounds.right, | ||||||
| bottom: bounds.bottom, | ||||||
| cornerRadius, | ||||||
| rgb, | ||||||
| }); | ||||||
|
|
||||||
| const newIcon = nativeImage.createFromBitmap(bitmap, { | ||||||
| width: size.width, | ||||||
| height: size.height, | ||||||
| }); | ||||||
|
|
||||||
| app.dock?.setIcon(newIcon); | ||||||
| console.log( | ||||||
| `[dock-icon] Set workspace dock icon border rgb(${rgb.join(",")}) for "${workspaceName}"`, | ||||||
| ); | ||||||
| app.dock?.setIcon(icon); | ||||||
| console.log(`[dock-icon] Set dock icon from: ${iconPath}`); | ||||||
| } catch (error) { | ||||||
| console.error("[dock-icon] Failed to set dock icon:", error); | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isCanaryBuildmatches all prerelease channelssemver.prerelease()returns a truthy array for any prerelease tag (e.g.alpha,beta,rc, not justcanary). If the project ever ships analphaorrcbuild using a version string like0.0.53-beta.1, this function will returntrueand the canary dock icon will be shown for those builds too.A tighter check that matches the intent documented in the JSDoc: