diff --git a/apps/desktop/electron-builder.ts b/apps/desktop/electron-builder.ts index 8b08ddd17ba..2c33562eb2c 100644 --- a/apps/desktop/electron-builder.ts +++ b/apps/desktop/electron-builder.ts @@ -18,6 +18,10 @@ const productName = pkg.productName; const macIconPath = join(pkg.resources, "build/icons/icon.icns"); const linuxIconPath = join(pkg.resources, "build/icons"); const winIconPath = join(pkg.resources, "build/icons/icon.ico"); +const dmgBackgroundPath = join( + pkg.resources, + "build/installer/background.tiff", +); const config: Configuration = { appId: "com.superset.desktop", @@ -86,6 +90,11 @@ const config: Configuration = { // Rebuild native modules for Electron's Node.js version npmRebuild: true, + // macOS DMG installer + dmg: { + ...(existsSync(dmgBackgroundPath) ? { background: dmgBackgroundPath } : {}), + }, + // macOS mac: { ...(existsSync(macIconPath) ? { icon: macIconPath } : {}), diff --git a/apps/desktop/src/main/lib/dock-icon.ts b/apps/desktop/src/main/lib/dock-icon.ts index c40b48df31f..5971c602cf3 100644 --- a/apps/desktop/src/main/lib/dock-icon.ts +++ b/apps/desktop/src/main/lib/dock-icon.ts @@ -1,136 +1,120 @@ +import { existsSync } from "node:fs"; import { join } from "node:path"; import { app, nativeImage } from "electron"; import { env } from "main/env.main"; +import { prerelease } from "semver"; import { getWorkspaceName } from "shared/env.shared"; import twColors from "tailwindcss/colors"; -/** - * Deterministic hash of a string, returned as a non-negative integer. - */ -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); -} +type RGB = [number, number, number]; -/** - * 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]), - }; -} +type Bounds = { top: number; left: number; bottom: number; right: number }; /** - * Converts OKLCH to sRGB (all values 0-255). + * Deterministic workspace-name → RGB picker using Tailwind's 500-level palette. */ -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), - ]; -} +const pickWorkspaceColor = (() => { + const FALLBACK: RGB = [59, 130, 246]; // blue-500 + + function parseOklch(str: string): { l: number; c: number; h: number } | null { + const m = str.match(/oklch\(([\d.]+)%\s+([\d.]+)\s+([\d.]+)\)/); + return m + ? { l: Number(m[1]) / 100, c: Number(m[2]), h: Number(m[3]) } + : null; + } + + function oklchToRgb(l: number, c: number, h: number): RGB { + const hRad = (h * Math.PI) / 180; + const a = c * Math.cos(hRad); + const b = c * Math.sin(hRad); + 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_ ** 3; + const mc = m_ ** 3; + const sc = s_ ** 3; + 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 x = Math.max(0, Math.min(1, v)); + return x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055; + }; + return [ + Math.round(toSrgb(rLin) * 255), + Math.round(toSrgb(gLin) * 255), + Math.round(toSrgb(bLin) * 255), + ]; + } -/** 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][] = []; + const palette: RGB[] = []; 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)["500"] as string); - if (parsed) result.push(oklchToRgb(parsed.l, parsed.c, parsed.h)); + const parsed = parseOklch((val as Record)["500"]); + if (parsed) palette.push(oklchToRgb(parsed.l, parsed.c, parsed.h)); } - return result; + + function hash(seed: string): number { + let h = 0; + for (let i = 0; i < seed.length; i++) { + h = seed.charCodeAt(i) + ((h << 5) - h); + h |= 0; + } + return Math.abs(h); + } + + return (workspaceName: string): RGB => + palette[hash(workspaceName) % palette.length] ?? FALLBACK; })(); /** - * Gets the path to the app icon PNG. + * Returns true for prerelease versions like "0.0.53-canary". */ -function getIconPath(): string { +function isCanaryBuild(): boolean { + const components = prerelease(app.getVersion()); + return components !== null && components.length > 0; +} + +/** + * Root directory of packaged/bundled icon assets. + */ +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"); } /** - * Signed distance function for a rounded rectangle. - * Negative = inside, positive = outside, zero = on boundary. + * Picks the dock icon PNG for the current build type, falling back to the + * stable icon if a build-specific variant is missing. */ -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 - ); +function getIconPath(): string { + const dir = getIconsDir(); + + 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 join(dir, "icon.png"); } /** - * Finds the bounding box of non-transparent pixels in a bitmap. + * 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 } { +): Bounds { let top = height; let left = width; let bottom = 0; @@ -151,89 +135,91 @@ function findContentBounds( } /** - * Draws a rounded-rectangle border on raw RGBA bitmap data. - * The border is drawn inward from the specified bounds. + * Source-over alpha compositing of a single RGBA pixel into the bitmap. */ -function drawBorder({ +function blendPixel( + bitmap: Buffer, + width: number, + height: number, + x: number, + y: number, + rgb: RGB, + alpha: number, +) { + if (alpha <= 0) return; + if (x < 0 || y < 0 || x >= width || y >= height) return; + + const offset = (y * width + x) * 4; + const dr = bitmap[offset] ?? 0; + const dg = bitmap[offset + 1] ?? 0; + const db = bitmap[offset + 2] ?? 0; + const da = (bitmap[offset + 3] ?? 0) / 255; + + const outA = alpha + da * (1 - alpha); + if (outA <= 0) return; + + bitmap[offset] = Math.round((rgb[0] * alpha + dr * da * (1 - alpha)) / outA); + bitmap[offset + 1] = Math.round( + (rgb[1] * alpha + dg * da * (1 - alpha)) / outA, + ); + bitmap[offset + 2] = Math.round( + (rgb[2] * alpha + db * da * (1 - alpha)) / outA, + ); + bitmap[offset + 3] = Math.round(outA * 255); +} + +/** + * Paints a top-right corner fold onto the bitmap: a colored triangle whose + * two legs run along the icon's top and right edges, with a 45° hypotenuse + * `cornerSize` pixels from the corner. The fill is masked by the icon's + * existing alpha so the fold hugs its rounded shape. + */ +function drawCornerFold({ bitmap, width, - thickness, - left, - top, - right, - bottom, - cornerRadius, + height, + bounds, + cornerSize, rgb, }: { bitmap: Buffer; width: number; - thickness: number; - left: number; - top: number; - right: number; - bottom: number; - cornerRadius: number; - rgb: [number, number, number]; + height: number; + bounds: Bounds; + cornerSize: number; + rgb: RGB; }) { - 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)); - } + const minX = Math.max(0, bounds.right - cornerSize - 2); + const maxX = Math.min(width - 1, bounds.right + 2); + const minY = Math.max(0, bounds.top - 2); + const maxY = Math.min(height - 1, bounds.top + cornerSize + 2); + + for (let y = minY; y <= maxY; y++) { + for (let x = minX; x <= maxX; x++) { + // Perpendicular signed distance to the 45° cut line + // (bounds.right - x) + (y - bounds.top) = cornerSize. + // Negative = inside the triangle (toward the corner). + const signedDist = + (bounds.right - x + (y - bounds.top) - cornerSize) / Math.SQRT2; + const diagAlpha = Math.max(0, Math.min(1, 0.5 - signedDist)); + if (diagAlpha <= 0.001) continue; + + const iconAlpha = (bitmap[(y * width + x) * 4 + 3] ?? 0) / 255; + if (iconAlpha <= 0) continue; + + blendPixel(bitmap, width, height, x, y, rgb, diagAlpha * iconAlpha); } } } /** - * 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. + * In development with a workspace name set, overlays a workspace-colored + * corner fold so simultaneous workspaces are visually distinguishable. + * No-op on non-macOS platforms. */ export function setWorkspaceDockIcon(): void { if (process.platform !== "darwin") return; - if (env.NODE_ENV !== "development") return; - - const workspaceName = getWorkspaceName(); - if (!workspaceName) return; try { const iconPath = getIconPath(); @@ -243,29 +229,27 @@ export function setWorkspaceDockIcon(): void { return; } - const size = icon.getSize(); - const bitmap = icon.toBitmap(); + const workspaceName = + env.NODE_ENV === "development" ? getWorkspaceName() : null; - const hash = hashString(workspaceName); - const rgb = - TAILWIND_500_COLORS[hash % TAILWIND_500_COLORS.length] ?? - ([59, 130, 246] as [number, number, number]); // blue-500 fallback + if (!workspaceName) { + app.dock?.setIcon(icon); + console.log(`[dock-icon] Set dock icon from: ${iconPath}`); + return; + } - // Find the actual icon content area (skip transparent padding) + const size = icon.getSize(); + const bitmap = icon.toBitmap(); const bounds = findContentBounds(bitmap, size.width, size.height); - const thickness = Math.round(size.width * 0.038); - const cornerRadius = Math.round(size.width * 0.22); + const boundsWidth = bounds.right - bounds.left; + const rgb = pickWorkspaceColor(workspaceName); - // Draw border flush with the content edges, overlapping inward - drawBorder({ + drawCornerFold({ bitmap, width: size.width, - thickness, - left: bounds.left, - top: bounds.top, - right: bounds.right, - bottom: bounds.bottom, - cornerRadius, + height: size.height, + bounds, + cornerSize: Math.round(boundsWidth * 0.47), rgb, }); @@ -276,7 +260,7 @@ export function setWorkspaceDockIcon(): void { app.dock?.setIcon(newIcon); console.log( - `[dock-icon] Set workspace dock icon border rgb(${rgb.join(",")}) for "${workspaceName}"`, + `[dock-icon] Set workspace dock icon corner fold rgb(${rgb.join(",")}) for "${workspaceName}" from ${iconPath}`, ); } catch (error) { console.error("[dock-icon] Failed to set dock icon:", error); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/assets/superset-empty-state-wordmark.svg b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/assets/superset-empty-state-wordmark.svg index 421ac8d56e7..df3f5c70930 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/assets/superset-empty-state-wordmark.svg +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/assets/superset-empty-state-wordmark.svg @@ -1,3 +1,13 @@ - - + + + + + + + + + + + + diff --git a/apps/desktop/src/renderer/stores/tabs/store.ts b/apps/desktop/src/renderer/stores/tabs/store.ts index 82c893bf794..cd53cc86535 100644 --- a/apps/desktop/src/renderer/stores/tabs/store.ts +++ b/apps/desktop/src/renderer/stores/tabs/store.ts @@ -756,17 +756,18 @@ export const useTabsStore = create()( const tabPaneIds = extractPaneIdsFromLayout(activeTab.layout); const reuseExisting = options.reuseExisting ?? "workspace"; - const existingFileViewerPane = reuseExisting !== "none" - ? findReusableFileViewerPane({ - workspaceId, - activeTabId: activeTab.id, - tabs: state.tabs, - panes: state.panes, - tabHistoryStacks: state.tabHistoryStacks, - reuseExisting, - options, - }) - : null; + const existingFileViewerPane = + reuseExisting !== "none" + ? findReusableFileViewerPane({ + workspaceId, + activeTabId: activeTab.id, + tabs: state.tabs, + panes: state.panes, + tabHistoryStacks: state.tabHistoryStacks, + reuseExisting, + options, + }) + : null; if (existingFileViewerPane) { const nextPane = applyFileViewerOpenOptionsToPane( @@ -826,7 +827,11 @@ export const useTabsStore = create()( // If we found an unpinned (preview) file-viewer pane, reuse it // (skip reuse when explicitly requesting a new tab, e.g. cmd+click) - if (fileViewerPanes.length > 0 && !options.openInNewTab && reuseExisting !== "none") { + if ( + fileViewerPanes.length > 0 && + !options.openInNewTab && + reuseExisting !== "none" + ) { const paneToReuse = fileViewerPanes[0]; const existingFileViewer = paneToReuse.fileViewer; if (!existingFileViewer) { diff --git a/apps/desktop/src/resources/build/icons/icon-canary.icns b/apps/desktop/src/resources/build/icons/icon-canary.icns index 9979c557092..c05bb9d2996 100644 Binary files a/apps/desktop/src/resources/build/icons/icon-canary.icns and b/apps/desktop/src/resources/build/icons/icon-canary.icns differ diff --git a/apps/desktop/src/resources/build/icons/icon-canary.ico b/apps/desktop/src/resources/build/icons/icon-canary.ico index 70a4daa1ef9..e5bcff052fc 100644 Binary files a/apps/desktop/src/resources/build/icons/icon-canary.ico and b/apps/desktop/src/resources/build/icons/icon-canary.ico differ diff --git a/apps/desktop/src/resources/build/icons/icon-canary.png b/apps/desktop/src/resources/build/icons/icon-canary.png index b3f3337e44a..5cfab3f3ba7 100644 Binary files a/apps/desktop/src/resources/build/icons/icon-canary.png and b/apps/desktop/src/resources/build/icons/icon-canary.png differ diff --git a/apps/desktop/src/resources/build/icons/icon-dev.icns b/apps/desktop/src/resources/build/icons/icon-dev.icns new file mode 100644 index 00000000000..ab9acd9b682 Binary files /dev/null and b/apps/desktop/src/resources/build/icons/icon-dev.icns differ diff --git a/apps/desktop/src/resources/build/icons/icon-dev.ico b/apps/desktop/src/resources/build/icons/icon-dev.ico new file mode 100644 index 00000000000..e5bc7f9f6ce Binary files /dev/null and b/apps/desktop/src/resources/build/icons/icon-dev.ico differ diff --git a/apps/desktop/src/resources/build/icons/icon-dev.png b/apps/desktop/src/resources/build/icons/icon-dev.png new file mode 100644 index 00000000000..6e8a2b8cde3 Binary files /dev/null and b/apps/desktop/src/resources/build/icons/icon-dev.png differ diff --git a/apps/desktop/src/resources/build/icons/icon.icns b/apps/desktop/src/resources/build/icons/icon.icns index 27082bc1a5c..c4249b96cda 100644 Binary files a/apps/desktop/src/resources/build/icons/icon.icns and b/apps/desktop/src/resources/build/icons/icon.icns differ diff --git a/apps/desktop/src/resources/build/icons/icon.ico b/apps/desktop/src/resources/build/icons/icon.ico index aea4682ff7f..2f2babb1520 100644 Binary files a/apps/desktop/src/resources/build/icons/icon.ico and b/apps/desktop/src/resources/build/icons/icon.ico differ diff --git a/apps/desktop/src/resources/build/icons/icon.png b/apps/desktop/src/resources/build/icons/icon.png index 3b6373301d0..f83930055c5 100644 Binary files a/apps/desktop/src/resources/build/icons/icon.png and b/apps/desktop/src/resources/build/icons/icon.png differ diff --git a/apps/desktop/src/resources/build/installer/background.tiff b/apps/desktop/src/resources/build/installer/background.tiff new file mode 100644 index 00000000000..fe6576d55d8 Binary files /dev/null and b/apps/desktop/src/resources/build/installer/background.tiff differ diff --git a/apps/desktop/src/resources/tray/iconTemplate.png b/apps/desktop/src/resources/tray/iconTemplate.png index c62c39e0717..463d63e71e2 100644 Binary files a/apps/desktop/src/resources/tray/iconTemplate.png and b/apps/desktop/src/resources/tray/iconTemplate.png differ diff --git a/apps/marketing/public/favicon-192.png b/apps/marketing/public/favicon-192.png index c3d37155891..94b86ed1106 100644 Binary files a/apps/marketing/public/favicon-192.png and b/apps/marketing/public/favicon-192.png differ diff --git a/apps/marketing/public/title.svg b/apps/marketing/public/title.svg index 726a91b632e..6a5eb8e3ed2 100644 --- a/apps/marketing/public/title.svg +++ b/apps/marketing/public/title.svg @@ -1,51 +1,106 @@ - - + + + + + + + + + + + + + + + + + - - - - + - - + + - - - + + + - - + + - - + + - + - - + + - - - + + + - - + + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + +