Skip to content

Comments

Use Phosphor Icons for Space Icons#27

Merged
iamEvanYT merged 9 commits intomainfrom
evan/better-space-icons
Apr 21, 2025
Merged

Use Phosphor Icons for Space Icons#27
iamEvanYT merged 9 commits intomainfrom
evan/better-space-icons

Conversation

@iamEvanYT
Copy link
Member

@iamEvanYT iamEvanYT commented Apr 21, 2025

Summary by CodeRabbit

  • New Features
    • Switched all app icons from Lucide to Phosphor Icons for a refreshed and consistent visual style.
    • Introduced a new icon picker with enhanced search and filtering capabilities in settings and onboarding.
  • Bug Fixes
    • Improved reliability and error handling of icon rendering and fallback behavior throughout the app.
  • Refactor
    • Simplified and streamlined icon handling across menus, sidebars, and settings.
    • Rewrote space menu creation with robust asynchronous handling and fallback logic.
  • Chores
    • Updated dependencies to remove Lucide and add Phosphor Icons packages.
    • Bundled Phosphor Icons duotone assets in Electron packaging.
    • Exposed menu update as an asynchronous method on the Browser instance.
    • Enhanced menu rebuilding to trigger on window focus events.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 21, 2025

Walkthrough

This change migrates the application's icon system from Lucide to Phosphor Icons across both Electron and Vite codebases. It updates icon handling logic, imports, and rendering components to use Phosphor Icons, including reading SVGs directly and updating icon caching and retrieval. React components and icon pickers are refactored to work with the new icon set and metadata. Supporting utility functions and dependencies related to Lucide are removed, and new dependencies for Phosphor Icons are added. The changes streamline icon management, improve type safety, and simplify the logic for menu and UI icon rendering. Additionally, menu update mechanisms are enhanced to respond to window focus changes.

Changes

File(s) Change Summary
electron/browser/utility/menu/items/spaces.ts Refactored to use Phosphor Icons for menu item icons, reading SVGs directly and updating caching logic. Added new type definitions and simplified menu item creation with robust async error handling.
electron/browser/browser.ts Added public async method updateMenu to Browser class, exposing menu update functionality.
electron/browser/utility/menu.ts Modified setupMenu to listen for window focus events and return the menu crafting function for external invocation.
electron/browser/window.ts Added focus event listener to emit window-focused event; updated setCurrentSpace to trigger menu update after space change.
electron/modules/windows.ts Extended WindowEventType enum with FOCUSED; added event emission on window focus.
electron/package.json Removed lucide dependency; added @phosphor-icons/core dependency; bumped electron devDependency version.
electron/forge.config.ts Added Phosphor Icons duotone asset directory to extra packaged resources.
vite/package.json Added @phosphor-icons/core and @phosphor-icons/react dependencies; upgraded several @radix-ui packages and other dependencies; updated lucide-react and tw-animate-css versions; bumped ESLint and Vite devDependencies.
vite/src/components/browser-ui/sidebar/content/space-title.tsx
vite/src/components/browser-ui/sidebar/spaces-switcher.tsx
vite/src/components/onboarding/stages/initial-space/icon.tsx
vite/src/components/settings/sections/spaces/theme-editors/background-gradient.tsx
vite/src/components/settings/sections/spaces/theme-editors/space-icon.tsx
vite/src/components/settings/sections/spaces/space-card.tsx
Updated icon rendering to use SpaceIcon (Phosphor-based) instead of Lucide-based components. Adjusted props and imports accordingly.
vite/src/components/main/dynamic-lucide-icon.tsx Deleted file; removed dynamic Lucide icon rendering component.
vite/src/components/settings/sections/spaces/icon-picker.tsx Refactored icon picker to use Phosphor icon metadata and rendering. Improved search/filter logic, updated props and state, and removed Lucide-specific helpers and async icon preview component.
vite/src/lib/phosphor-icons.tsx New module exporting Phosphor icon metadata, a generic PhosphorIcon React component, and a SpaceIcon wrapper for consistent icon rendering.
vite/src/lib/utils.ts Removed getLucideIcon utility and related imports.

Sequence Diagram(s)

sequenceDiagram
    participant UI as React Component
    participant PhosphorIcons as Phosphor Icon Library
    participant PhosphorIcon as PhosphorIcon Component

    UI->>PhosphorIcon: Render with id (icon name)
    PhosphorIcon->>PhosphorIcons: Lookup icon by id
    alt Icon found
        PhosphorIcon-->>UI: Render icon component
    else Icon not found & fallbackId provided
        PhosphorIcon->>PhosphorIcons: Lookup fallback icon
        PhosphorIcon-->>UI: Render fallback icon
    else No icon found
        PhosphorIcon-->>UI: Render null
    end
Loading

Suggested labels

enhancement

Poem

Hopping from Lucide to Phosphor’s bright gleam,
I nibbled through SVGs, a bunny with a dream.
Icons now shimmer, crisp, and white,
With simpler code, menus feel light.
Picker and card, all polished anew—
A carrot for code, and a fresh point of view!
🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
vite/src/lib/phosphor-icons.tsx (1)

6-6: Unnecessary double cast – rely on the library’s own typings.

icons is already exported as IconEntry[] by @phosphor-icons/core; the as unknown as IconEntry[] cast is redundant and may hide real typing issues.

-export const PhosphorIcons = icons as unknown as IconEntry[];
+export const PhosphorIcons = icons;
vite/src/components/settings/sections/spaces/icon-picker.tsx (1)

18-25: State duplication can be avoided – use useMemo instead of storing a second copy.

iconList is static (PhosphorIcons), yet it is stored in state only to be copied again into filteredIcons. This triggers an extra render on mount and is unnecessary work. Derive iconList with useMemo (or just reference PhosphorIcons directly) and initialise filteredIcons from that.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14716d7 and a5cadeb.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • electron/browser/utility/menu/items/spaces.ts (2 hunks)
  • electron/package.json (1 hunks)
  • vite/package.json (1 hunks)
  • vite/src/components/browser-ui/sidebar/content/space-title.tsx (2 hunks)
  • vite/src/components/browser-ui/sidebar/spaces-switcher.tsx (2 hunks)
  • vite/src/components/main/dynamic-lucide-icon.tsx (0 hunks)
  • vite/src/components/onboarding/stages/initial-space/icon.tsx (3 hunks)
  • vite/src/components/settings/sections/spaces/icon-picker.tsx (5 hunks)
  • vite/src/components/settings/sections/spaces/space-card.tsx (2 hunks)
  • vite/src/components/settings/sections/spaces/theme-editors/background-gradient.tsx (2 hunks)
  • vite/src/components/settings/sections/spaces/theme-editors/space-icon.tsx (2 hunks)
  • vite/src/lib/phosphor-icons.tsx (1 hunks)
  • vite/src/lib/utils.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • vite/src/components/main/dynamic-lucide-icon.tsx
  • vite/src/lib/utils.ts
🧰 Additional context used
🧬 Code Graph Analysis (7)
vite/src/components/settings/sections/spaces/theme-editors/space-icon.tsx (1)
vite/src/components/settings/sections/spaces/icon-picker.tsx (1)
  • SpaceIconPicker (16-80)
vite/src/components/onboarding/stages/initial-space/icon.tsx (2)
vite/src/lib/phosphor-icons.tsx (1)
  • SpaceIcon (31-33)
vite/src/components/settings/sections/spaces/icon-picker.tsx (1)
  • SpaceIconPicker (16-80)
vite/src/components/settings/sections/spaces/theme-editors/background-gradient.tsx (1)
vite/src/lib/phosphor-icons.tsx (1)
  • SpaceIcon (31-33)
vite/src/components/browser-ui/sidebar/spaces-switcher.tsx (1)
vite/src/lib/phosphor-icons.tsx (1)
  • SpaceIcon (31-33)
vite/src/components/browser-ui/sidebar/content/space-title.tsx (1)
vite/src/lib/phosphor-icons.tsx (1)
  • SpaceIcon (31-33)
vite/src/components/settings/sections/spaces/space-card.tsx (1)
vite/src/lib/phosphor-icons.tsx (1)
  • SpaceIcon (31-33)
vite/src/components/settings/sections/spaces/icon-picker.tsx (1)
vite/src/lib/phosphor-icons.tsx (2)
  • PhosphorIcons (6-6)
  • SpaceIcon (31-33)
🪛 Biome (1.9.4)
vite/src/lib/phosphor-icons.tsx

[error] 15-15: Don't use 'String' as a type.

Use lowercase primitives for consistency.
Safe fix: Use 'string' instead

(lint/complexity/noBannedTypes)

🔇 Additional comments (15)
vite/src/components/settings/sections/spaces/theme-editors/space-icon.tsx (2)

2-2: Updated icon picker import

The import was correctly updated from LucideIconPicker to SpaceIconPicker as part of the migration to Phosphor Icons.


19-22: Updated component to use SpaceIconPicker

The component usage was correctly updated to use SpaceIconPicker while maintaining the same prop interface (selectedIcon and onSelectIcon), which ensures backward compatibility and consistent behavior.

electron/package.json (1)

22-22: Added Phosphor Icons core dependency

The Phosphor Icons core dependency was added while removing the Lucide package, which is consistent with the migration strategy. This ensures that the electron part of the application has the necessary resources to work with Phosphor Icons.

vite/src/components/browser-ui/sidebar/spaces-switcher.tsx (2)

6-6: Updated icon import

The import was correctly updated to use the new SpaceIcon component from the Phosphor icons module.


17-19: Updated icon component usage

The component usage was correctly updated from DynamicLucideIcon to SpaceIcon with the prop name change from iconId to id. This maintains the same functionality while adapting to the new icon system.

Looking at the relevant code snippet, SpaceIcon wraps PhosphorIcon with sensible defaults like fallbackId="DotOutline" and weight="duotone", which ensures consistent icon styling across the application.

vite/src/components/settings/sections/spaces/theme-editors/background-gradient.tsx (2)

7-7: Import for new SpaceIcon component added correctly.

The import statement correctly brings in the new Phosphor-based icon component from the central location.


103-103: Icon rendering successfully migrated to Phosphor.

The component has been properly updated to use the new SpaceIcon component with the correct props:

  • id property replaces the previous iconId
  • Appropriate styling classes are applied with className="size-6 text-white"

This change correctly implements the migration from Lucide to Phosphor icons while maintaining the same visual appearance.

vite/src/components/browser-ui/sidebar/content/space-title.tsx (2)

3-3: Import for SpaceIcon component added correctly.

The import statement correctly brings in the new Phosphor-based icon component.


13-13: Icon component properly replaced with explicit fallback override.

The DynamicLucideIcon has been correctly replaced with SpaceIcon, with appropriate property adjustments:

  • iconId changed to id to match the new component's API
  • fallbackId={undefined} explicitly overrides the default fallback icon

Setting fallbackId={undefined} means no fallback icon will be shown if the specified icon ID is invalid, which differs from the default behavior of showing a "DotOutline" icon.

Is this intentional? In other components you're using the default fallback behavior. Consider whether you want consistent fallback behavior across all icon usages.

vite/src/components/onboarding/stages/initial-space/icon.tsx (3)

5-5: Imports correctly updated for both icon components.

The imports have been properly updated:

  • SpaceIconPicker replaces the previous LucideIconPicker
  • SpaceIcon replaces the previous DynamicLucideIcon

This ensures consistent use of the new Phosphor-based icon system.

Also applies to: 8-8


131-131: Icon component correctly updated with appropriate props.

The implementation now uses SpaceIcon with the correct props:

  • id replaces the previous iconId property
  • Class names and styling are preserved

This change maintains the visual appearance while using the new icon system.


142-142: Icon picker component updated consistently.

The SpaceIconPicker component is properly used with the same props as the previous LucideIconPicker:

  • selectedIcon - Passes the current selected icon
  • onSelectIcon - Passes the callback to update the selected icon

This ensures the component continues to function as before while using the new icon system.

vite/src/components/settings/sections/spaces/space-card.tsx (2)

3-3: Import for SpaceIcon component added correctly.

The import statement correctly brings in the new Phosphor-based icon component.


31-31: Icon rendering successfully simplified with Phosphor component.

The implementation has been simplified by using the new SpaceIcon component:

  • Removes the previous dynamic loading pattern with state management
  • Uses a direct, synchronous rendering approach
  • Maintains the same visual styling with className="size-6 text-white"

This change improves code readability and maintainability while ensuring consistent icon rendering.

electron/browser/utility/menu/items/spaces.ts (1)

41-47: fill replacement is incomplete for duotone / stroked icons.

Many Phosphor duotone icons have multiple <path> elements and use both fill and stroke. Replacing only the first fill="…" leaves secondary layers untouched and produces half‑colored icons. Consider a more robust approach such as injecting style="*{fill:white;stroke:white}" or using sharp’s tinting.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
electron/browser/utility/menu/items/spaces.ts (1)

30-59: Hard-coded asset path is fragile inside ASAR and when package layout changes.

Deriving the SVG file path with path joins assumes the package structure never changes and that Electron can read outside the ASAR bundle at runtime.

Instead of reading SVG files from the filesystem, consider using the icon metadata directly from the Phosphor icon package:

function getPhosphorIconPath(pascalName: string): string | null {
  const name = getIconNameFromPascalCase(pascalName);
  if (!name) return null;

-  try {
-    // Find the icon directly from node_modules in dev, or from app.asar in production
-    let iconPath: string;
-
-    // Use the packaged path if the app is packaged
-    if (app.isPackaged) {
-      const pkgPath = path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`);
-      iconPath = pkgPath;
-    } else {
-      const devPath = path.join(
-        require.resolve("@phosphor-icons/core"),
-        "..",
-        "..",
-        "assets",
-        "duotone",
-        `${name}-duotone.svg`
-      );
-      iconPath = devPath;
-    }
-
-    return iconPath;
-  } catch (error) {
-    console.error("Failed to resolve phosphor-icons path:", error);
-    return null;
-  }
+  try {
+    // Find the icon directly from the Phosphor icon metadata
+    const icon = PhosphorIcons.find(icon => icon.name === name);
+    if (!icon) return null;
+    
+    // Use the SVG content directly if available
+    if (icon.svg) return icon.svg;
+    
+    // Or use the file_path if provided
+    if (icon.file_path) return icon.file_path;
+    
+    return null;
+  } catch (error) {
+    console.error("Failed to resolve phosphor icon:", error);
+    return null;
+  }
}
🧹 Nitpick comments (2)
electron/browser/utility/menu/items/spaces.ts (2)

20-20: Type assertion needs validation to ensure runtime safety.

The type assertion as unknown as IconEntry[] is a workaround that bypasses TypeScript's type checking. This could lead to runtime errors if the actual structure doesn't match the expected IconEntry type.

Consider adding validation or a more type-safe approach:

-const PhosphorIcons = icons as unknown as IconEntry[];
+// Validate the structure matches IconEntry before casting
+const PhosphorIcons = Array.isArray(icons) && icons.every(icon => 
+  'name' in icon && 'pascal_name' in icon
+) ? icons as IconEntry[] : [];

87-105: Icon retrieval function lacks memory management.

The icon cache grows unbounded, which could lead to memory leaks if many different icons are used over time.

Consider implementing a size limit or LRU cache:

// Icon cache
type IconCacheKey = `${string}-${number | undefined}`;
+const MAX_CACHE_SIZE = 100; // Limit cache size
const iconCache = new Map<IconCacheKey, NativeImage>();

async function getIconAsNativeImage(name: string, padding?: number): Promise<NativeImage | null> {
  const cacheKey = `${name}-${padding}` as IconCacheKey;

  // Check cache first
  if (iconCache.has(cacheKey)) {
    return iconCache.get(cacheKey) as NativeImage;
  }

  // Create new icon if not in cache
  const iconPath = getPhosphorIconPath(name);
  if (!iconPath) return null;

  const image = await createSvgFromIconPath(iconPath);
  if (image) {
    iconCache.set(cacheKey, image);
+    // Basic cache size management
+    if (iconCache.size > MAX_CACHE_SIZE) {
+      const oldestKey = iconCache.keys().next().value;
+      iconCache.delete(oldestKey);
+    }
  }

  return image;
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5cadeb and abee5cc.

📒 Files selected for processing (3)
  • electron/browser/utility/menu/items/spaces.ts (2 hunks)
  • electron/forge.config.ts (1 hunks)
  • vite/src/lib/phosphor-icons.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • electron/forge.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • vite/src/lib/phosphor-icons.tsx
🔇 Additional comments (3)
electron/browser/utility/menu/items/spaces.ts (3)

133-134: This code simplification improves readability.

The combined null check is more concise than the previous version.


143-195: Excellent error handling and robustness improvements.

The refactored function includes several important improvements:

  1. Proper try-catch blocks for error handling
  2. Using Promise.allSettled to handle potential failures with individual menu items
  3. Fallback menu when spaces can't be loaded
  4. Type narrowing with the filter for better type safety

These changes make the code more reliable and maintainable.


25-28: 🛠️ Refactor suggestion

Error handling for missing icons is suboptimal.

The function falls back to "dot-outline" when icon is undefined, but it returns icon?.name || "dot-outline" which would return an empty string if icon exists but has no name property.

function getIconNameFromPascalCase(pascalCaseName: string): string {
  const icon = PhosphorIcons.find((icon) => icon.pascal_name === pascalCaseName);
-  return icon?.name || "dot-outline";
+  return icon?.name || "dot-outline"; // Return default if icon not found or name is empty
}

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
electron/browser/utility/menu/items/spaces.ts (2)

30-59: Hard-coded asset path is fragile inside ASAR and when package layout changes.

This implementation still uses hardcoded paths and file system operations, which is fragile when the package structure changes or when accessing files within an ASAR archive.

Consider using the official API approach mentioned in the past review - each IconEntry might already contain an svg field that could be used directly, eliminating file system IO and path brittleness.

If you must use file paths, implement more robust path resolution with fallbacks:

function getPhosphorIconPath(pascalName: string): string | null {
  const name = getIconNameFromPascalCase(pascalName);
  if (!name) return null;

  try {
-    // Find the icon directly from node_modules in dev, or from app.asar in production
-    let iconPath: string;
-
-    // Use the packaged path if the app is packaged
-    if (app.isPackaged) {
-      const pkgPath = path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`);
-      iconPath = pkgPath;
-    } else {
-      const devPath = path.join(
-        require.resolve("@phosphor-icons/core"),
-        "..",
-        "..",
-        "assets",
-        "duotone",
-        `${name}-duotone.svg`
-      );
-      iconPath = devPath;
-    }
+    // Define potential paths in order of preference
+    const possiblePaths = [];
+    
+    // Try to get icon SVG directly from the IconEntry if available
+    const iconEntry = PhosphorIcons.find(icon => icon.name === name);
+    if (iconEntry?.svg) {
+      // If the icon has an SVG string, no need for file path
+      return null; // Will handle inline SVG in getIconAsNativeImage
+    }
+    
+    // Add packaged path for production
+    if (app.isPackaged) {
+      possiblePaths.push(path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`));
+      // Add alternative locations as fallbacks
+      possiblePaths.push(path.join(process.resourcesPath, "assets", "duotone", `${name}-duotone.svg`));
+    } else {
+      // Development paths
+      try {
+        const basePath = path.dirname(require.resolve("@phosphor-icons/core/package.json"));
+        possiblePaths.push(path.join(basePath, "assets", "duotone", `${name}-duotone.svg`));
+      } catch (err) {
+        console.warn("Could not resolve @phosphor-icons/core package location");
+      }
+    }
+    
+    // Return the first path that exists (would need to check synchronously)
+    // For simplicity in this example, just return the first path
+    return possiblePaths[0] || null;

    return iconPath;
  } catch (error) {
    console.error("Failed to resolve phosphor-icons path:", error);
    return null;
  }
}

Additionally, if IconEntry has file path information, consider using that directly.


62-92: SVG fill color modification is brittle and may not work with complex SVGs.

While the approach has been improved with a more robust regex replacement, it's still string-based manipulation which can be fragile with complex SVGs.

Consider using an XML/SVG parser like jsdom or cheerio for more reliable SVG manipulation:

async function createSvgFromIconPath(iconPath: string): Promise<NativeImage | null> {
  if (!svgPlatforms.includes(process.platform)) {
    return null;
  }

  try {
    let svgString = await readFile(iconPath, "utf8");

-    // Make SVG white using a more robust approach
-    // 1. Handle existing fill attributes on the SVG root
-    svgString = svgString.replace(/<svg([^>]*)>/, (match, attributes) => {
-      // Remove any existing fill attribute
-      const cleanedAttributes = attributes.replace(/\s*fill="[^"]*"\s*/g, " ");
-      return `<svg${cleanedAttributes} fill="white">`;
-    });
-
-    // 2. Add style to ensure all elements inherit the white color
-    svgString = svgString.replace(/<svg([^>]*)>/, (match) => {
-      return `${match}<style>* { fill: white; stroke: white; }</style>`;
-    });
+    // Simplify by injecting an outer SVG with a filter
+    // This approach avoids complex parsing but ensures all nested elements are white
+    svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
+      <defs>
+        <filter id="whitefill">
+          <feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
+        </filter>
+      </defs>
+      <g filter="url(#whitefill)">
+        ${svgString}
+      </g>
+    </svg>`;

    // Convert to native image
    const iconBuffer = await sharp(Buffer.from(svgString)).png().resize(16, 16).toBuffer();

    return nativeImage.createFromBuffer(iconBuffer);
  } catch (error) {
    console.error("Error creating SVG from path:", error);
    return null;
  }
}

This approach uses an SVG filter to reliably convert all colors to white instead of complex string replacements.

🧹 Nitpick comments (3)
electron/browser/utility/menu/items/spaces.ts (3)

20-20: Consider using a more type-safe approach than double type assertion.

The double type assertion (as unknown as IconEntry[]) is potentially unsafe and bypasses TypeScript's type checking. If the structure of the imported icons doesn't match IconEntry[], it could lead to runtime errors.

-const PhosphorIcons = icons as unknown as IconEntry[];
+// Verify the structure matches IconEntry[] before casting
+const PhosphorIcons = Array.isArray(icons) && 
+  icons.every(icon => 'pascal_name' in icon && 'name' in icon) 
+  ? icons as IconEntry[] 
+  : [];
+
+// Log warning if the icons array is empty after validation
+if (PhosphorIcons.length === 0) {
+  console.warn('PhosphorIcons array is empty or has incorrect structure');
+}

25-28: Improve resilience with better fallback handling.

The function returns a default icon "dot-outline" when the icon isn't found, but doesn't verify if this fallback icon actually exists in the Phosphor icon set.

function getIconNameFromPascalCase(pascalCaseName: string): string {
  const icon = PhosphorIcons.find((icon) => icon.pascal_name === pascalCaseName);
-  return icon?.name || "dot-outline";
+  // Return the found icon name, a verified fallback, or null
+  if (icon?.name) return icon.name;
+  
+  // Verify fallback exists
+  const fallbackIcon = PhosphorIcons.find((icon) => icon.name === "dot-outline");
+  if (fallbackIcon) return "dot-outline";
+  
+  // Last resort fallback to the first available icon
+  return PhosphorIcons[0]?.name || "";
}

98-116: Add handling for inline SVG content.

The function assumes icons are always loaded from files, but if the previous function is modified to use IconEntry.svg property, this function should handle inline SVG strings as well.

-async function getIconAsNativeImage(name: string, padding?: number): Promise<NativeImage | null> {
+async function getIconAsNativeImage(
+  name: string, 
+  padding?: number,
+  inlineSvg?: string
+): Promise<NativeImage | null> {
  const cacheKey = `${name}-${padding}` as IconCacheKey;

  // Check cache first
  if (iconCache.has(cacheKey)) {
    return iconCache.get(cacheKey) as NativeImage;
  }

-  // Create new icon if not in cache
-  const iconPath = getPhosphorIconPath(name);
-  if (!iconPath) return null;
-
-  const image = await createSvgFromIconPath(iconPath);
+  let image: NativeImage | null = null;
+  
+  // Handle inline SVG if provided
+  if (inlineSvg) {
+    // Process inline SVG similarly to file content
+    try {
+      // Similar SVG modifications as in createSvgFromIconPath
+      let svgString = inlineSvg;
+      svgString = svgString.replace(/<svg([^>]*)>/, (match, attributes) => {
+        const cleanedAttributes = attributes.replace(/\s*fill="[^"]*"\s*/g, " ");
+        return `<svg${cleanedAttributes} fill="white">`;
+      });
+      
+      svgString = svgString.replace(/<svg([^>]*)>/, (match) => {
+        return `${match}<style>* { fill: white; stroke: white; }</style>`;
+      });
+      
+      const iconBuffer = await sharp(Buffer.from(svgString)).png().resize(16, 16).toBuffer();
+      image = nativeImage.createFromBuffer(iconBuffer);
+    } catch (error) {
+      console.error("Error processing inline SVG:", error);
+    }
+  } else {
+    // Fall back to file path approach
+    const iconPath = getPhosphorIconPath(name);
+    if (iconPath) {
+      image = await createSvgFromIconPath(iconPath);
+    }
+  }
+  
  if (image) {
    iconCache.set(cacheKey, image);
  }

  return image;
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abee5cc and ac9c9d5.

📒 Files selected for processing (1)
  • electron/browser/utility/menu/items/spaces.ts (2 hunks)
🔇 Additional comments (4)
electron/browser/utility/menu/items/spaces.ts (4)

129-135: Improved error handling in createSpaceMenuItem.

The try-catch block for loading icons is well-implemented, allowing the menu item creation to proceed even if the icon fails to load.

This is a good approach to ensure menu creation remains robust even when individual icon loading fails.


144-145: Improved null-checking with early return.

The simplification of the null check with an early return improves code readability and removes unnecessary nesting.

This clean approach to null checking helps maintain code clarity and follows good TypeScript practices.


173-180: Excellent use of Promise.allSettled for robust space menu creation.

Using Promise.allSettled combined with proper filtering of results ensures that the menu creation continues even if individual space menu items fail to be created.

This is a robust pattern that ensures partial failures don't prevent the menu from being created. The type narrowing with the filter predicate is also well-implemented.


154-206: Well-structured async function with comprehensive error handling.

The main createSpacesMenu function is well-organized with proper error handling, fallbacks for edge cases, and graceful degradation when errors occur.

This implementation ensures the menu always appears, even in error conditions, which is crucial for application robustness. The fallback menu provides essential functionality, and the error logging will help with debugging.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
electron/browser/utility/menu/items/spaces.ts (1)

30-59: 🛠️ Refactor suggestion

Hard-coded asset path remains fragile.

While you've improved the path resolution by handling both packaged and development environments, this approach still has issues: it relies on hard-coded relative paths that could break if package structures change, and it performs unnecessary filesystem IO.

Consider using the IconEntry data directly from Phosphor:

function getPhosphorIconPath(pascalName: string): string | null {
  const name = getIconNameFromPascalCase(pascalName);
  if (!name) return null;

  try {
-    // Find the icon directly from node_modules in dev, or from app.asar in production
-    let iconPath: string;
-
-    // Use the packaged path if the app is packaged
-    if (app.isPackaged) {
-      const pkgPath = path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`);
-      iconPath = pkgPath;
-    } else {
-      const devPath = path.join(
-        require.resolve("@phosphor-icons/core"),
-        "..",
-        "..",
-        "assets",
-        "duotone",
-        `${name}-duotone.svg`
-      );
-      iconPath = devPath;
-    }
-
-    return iconPath;
+    // Find the icon directly from the icon data
+    const icon = PhosphorIcons.find(icon => icon.name === name);
+    return icon?.svg ? icon.svg : null;

  } catch (error) {
    console.error("Failed to resolve phosphor-icons path:", error);
    return null;
  }
}
🧹 Nitpick comments (3)
electron/browser/utility/menu/items/spaces.ts (3)

20-20: Type casting via unknown should be avoided.

The type casting icons as unknown as IconEntry[] is a TypeScript anti-pattern that bypasses type safety. This could lead to runtime errors if the actual structure doesn't match the expected interface.

Consider using a more type-safe approach:

-const PhosphorIcons = icons as unknown as IconEntry[];
+// Define an explicit type guard to verify structure
+function isIconEntryArray(obj: any): obj is IconEntry[] {
+  return Array.isArray(obj) && obj.length > 0 && 'pascal_name' in obj[0] && 'name' in obj[0];
+}
+
+const PhosphorIcons = isIconEntryArray(icons) ? icons : [];

62-92: SVG manipulation has been improved but could be more robust.

Your improved SVG manipulation is better than the previous approach, but still uses regular expressions to modify XML/SVG which can be brittle with complex SVGs. This addresses part of the previous review comment, but could be further improved.

For even more robust handling of SVGs, consider using a proper XML parser:

async function createSvgFromIconPath(iconPath: string): Promise<NativeImage | null> {
  if (!svgPlatforms.includes(process.platform)) {
    return null;
  }

  try {
-    let svgString = await readFile(iconPath, "utf8");
-
-    // Make SVG white using a more robust approach
-    // 1. Handle existing fill attributes on the SVG root
-    svgString = svgString.replace(/<svg([^>]*)>/, (match, attributes) => {
-      // Remove any existing fill attribute
-      const cleanedAttributes = attributes.replace(/\s*fill="[^"]*"\s*/g, " ");
-      return `<svg${cleanedAttributes} fill="white">`;
-    });
-
-    // 2. Add style to ensure all elements inherit the white color
-    svgString = svgString.replace(/<svg([^>]*)>/, (match) => {
-      return `${match}<style>* { fill: white; stroke: white; }</style>`;
-    });
+    // If using the direct SVG string approach:
+    let svgString = typeof iconPath === 'string' && iconPath.startsWith('<svg') 
+      ? iconPath 
+      : await readFile(iconPath, "utf8");
+
+    // Use a lightweight XML/SVG parser like 'svgson' if added as a dependency
+    // For now, continue with the improved regex approach:
+    // 1. Handle existing fill attributes on the SVG root
+    svgString = svgString.replace(/<svg([^>]*)>/, (match, attributes) => {
+      // Remove any existing fill attribute
+      const cleanedAttributes = attributes.replace(/\s*fill="[^"]*"\s*/g, " ");
+      return `<svg${cleanedAttributes} fill="white">`;
+    });
+
+    // 2. Add style to ensure all elements inherit the white color
+    svgString = svgString.replace(/<svg([^>]*)>/, (match) => {
+      return `${match}<style>* { fill: white; stroke: white; }</style>`;
+    });

    // Convert to native image
    const iconBuffer = await sharp(Buffer.from(svgString)).png().resize(16, 16).toBuffer();

    return nativeImage.createFromBuffer(iconBuffer);
  } catch (error) {
    console.error("Error creating SVG from path:", error);
    return null;
  }
}

95-116: Icon cache may grow unbounded.

The current implementation of icon caching uses a Map that grows indefinitely as new icons are loaded, which could potentially lead to memory issues over time.

Consider adding a size limit or an LRU cache implementation:

// Icon cache
type IconCacheKey = `${string}-${number | undefined}`;
-const iconCache = new Map<IconCacheKey, NativeImage>();
+const MAX_CACHE_SIZE = 100; // Adjust based on expected icon usage
+class LimitedIconCache {
+  private cache = new Map<IconCacheKey, { image: NativeImage, lastUsed: number }>();
+  
+  has(key: IconCacheKey): boolean {
+    return this.cache.has(key);
+  }
+  
+  get(key: IconCacheKey): NativeImage | undefined {
+    const entry = this.cache.get(key);
+    if (entry) {
+      entry.lastUsed = Date.now();
+      return entry.image;
+    }
+    return undefined;
+  }
+  
+  set(key: IconCacheKey, image: NativeImage): void {
+    // Evict least recently used if cache is full
+    if (this.cache.size >= MAX_CACHE_SIZE) {
+      let oldestKey: IconCacheKey | null = null;
+      let oldestTime = Infinity;
+      
+      for (const [k, v] of this.cache.entries()) {
+        if (v.lastUsed < oldestTime) {
+          oldestTime = v.lastUsed;
+          oldestKey = k;
+        }
+      }
+      
+      if (oldestKey) this.cache.delete(oldestKey);
+    }
+    
+    this.cache.set(key, { image, lastUsed: Date.now() });
+  }
+}
+
+const iconCache = new LimitedIconCache();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac9c9d5 and bc854f0.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • electron/browser/browser.ts (2 hunks)
  • electron/browser/utility/menu.ts (2 hunks)
  • electron/browser/utility/menu/items/spaces.ts (1 hunks)
  • electron/browser/window.ts (3 hunks)
  • electron/modules/windows.ts (2 hunks)
  • electron/package.json (2 hunks)
  • vite/package.json (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • electron/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • vite/package.json
🧰 Additional context used
🧬 Code Graph Analysis (3)
electron/browser/window.ts (1)
electron/modules/windows.ts (1)
  • windowEvents (34-37)
electron/browser/browser.ts (1)
electron/browser/utility/menu.ts (1)
  • setupMenu (15-38)
electron/browser/utility/menu.ts (1)
electron/modules/windows.ts (1)
  • windowEvents (34-37)
🔇 Additional comments (14)
electron/browser/window.ts (3)

11-12: Good addition of window event imports

These imports properly support the new focus handling functionality added below.


116-118: Well implemented window focus event propagation

This new event listener correctly propagates the native window focus event to the global window events system, enabling other components to react when a window receives focus.


174-174: Good addition of menu update on space change

Updating the menu immediately after the space changes ensures the UI remains consistent with the application state. This is a good practice for maintaining a responsive user experience.

electron/browser/browser.ts (2)

27-27: Good addition of public updateMenu method

Adding this public property provides a clean interface for other components to trigger menu updates when needed.


42-42: Well implemented menu update initialization

Storing the returned function from setupMenu provides a consistent way to update menus throughout the application lifecycle.

electron/browser/utility/menu.ts (3)

13-13: Good addition of window events imports

These imports properly support the new focus-triggered menu updates.


35-35: Well implemented focus-triggered menu updates

Listening for the FOCUSED event ensures the menu stays up-to-date when the user switches between windows, improving the user experience.


37-37: Good design returning the craftMenu function

Returning the craftMenu function allows external components to trigger menu updates when needed, providing a clean API for menu management.

electron/modules/windows.ts (3)

24-24: Good addition of FOCUSED event type

Adding this event type properly extends the window event system to support focus-related functionality.


31-31: Well typed event handler

The type definition correctly specifies that the FOCUSED event handler receives the BrowserWindow instance, ensuring type safety throughout the application.


84-86: Well implemented focus event handling

This event handler properly emits the FOCUSED event when a window gains focus, enabling other components to react accordingly.

electron/browser/utility/menu/items/spaces.ts (3)

129-136: Good error handling for icon loading.

The try-catch block around icon loading is a good practice that ensures a failing icon doesn't prevent a menu item from being created. The error is properly logged and execution continues gracefully.


146-148: Cleaner conditional check.

You've simplified the previous code by combining null checks into a single conditional statement, which improves readability.


157-211: Robust async menu creation with proper error handling.

The rewritten createSpacesMenu function is well-structured with comprehensive error handling. Using Promise.allSettled ensures that failures in individual menu items don't cause the entire menu creation to fail. The fallback menu is a good practice to ensure the application remains functional even if there are issues.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
electron/browser/utility/menu/items/spaces.ts (1)

30-59: 🛠️ Refactor suggestion

Hard-coded asset path is fragile inside ASAR and when package layout changes.

Deriving the SVG file path with path.join(require.resolve(...), "..", "..", "assets", "duotone", ...) assumes the package structure never changes. This approach is improved from the previous implementation by using process.resourcesPath for packaged apps, but it's still brittle.

Consider using the icon data directly from the IconEntry object:

function getPhosphorIconPath(pascalName: string): string | null {
  const name = getIconNameFromPascalCase(pascalName);
  if (!name) return null;

  try {
-    // Find the icon directly from node_modules in dev, or from app.asar in production
-    let iconPath: string;
-
-    // Use the packaged path if the app is packaged
-    if (app.isPackaged) {
-      const pkgPath = path.join(process.resourcesPath, "duotone", `${name}-duotone.svg`);
-      iconPath = pkgPath;
-    } else {
-      const devPath = path.join(
-        require.resolve("@phosphor-icons/core"),
-        "..",
-        "..",
-        "assets",
-        "duotone",
-        `${name}-duotone.svg`
-      );
-      iconPath = devPath;
-    }
-
-    return iconPath;
+    // Find the icon in the PhosphorIcons array
+    const icon = PhosphorIcons.find((icon) => icon.name === name);
+    if (!icon) return null;
+    
+    // Use the file_path if available, otherwise use the svg content directly
+    if (icon.file_path) {
+      return icon.file_path;
+    } else if (icon.svg) {
+      // If we have SVG content directly, we can skip file reading
+      // Save to a temporary file or pass directly to createSvgFromSvgString
+      // This would require modifying createSvgFromIconPath to accept SVG strings
+      const tempPath = path.join(app.getPath('temp'), `${name}-duotone.svg`);
+      fs.writeFileSync(tempPath, icon.svg);
+      return tempPath;
+    }
+    
+    return null;
  } catch (error) {
    console.error("Failed to resolve phosphor-icons path:", error);
    return null;
  }
}

This would also require adding a complementary function:

async function createSvgFromSvgString(svgString: string): Promise<NativeImage | null> {
  if (!svgPlatforms.includes(process.platform)) {
    return null;
  }

  try {
    // Apply the same SVG modifications as in createSvgFromIconPath
    // [...]
    
    const iconBuffer = await sharp(Buffer.from(svgString)).png().resize(16, 16).toBuffer();
    return nativeImage.createFromBuffer(iconBuffer);
  } catch (error) {
    console.error("Error creating SVG from string:", error);
    return null;
  }
}
🧹 Nitpick comments (4)
electron/browser/utility/menu/items/spaces.ts (4)

20-20: Consider using proper TypeScript typing for icons.

Using as unknown as IconEntry[] type casting is problematic as it bypasses TypeScript's type checking. This could lead to runtime errors if the structure of icons doesn't match what you're expecting.

-const PhosphorIcons = icons as unknown as IconEntry[];
+const PhosphorIcons: IconEntry[] = icons as IconEntry[];

95-95: Simplify IconCacheKey type definition.

The template literal type IconCacheKey = \${string}`is unnecessarily complex for this use case since it's functionally equivalent to just usingstring`.

-type IconCacheKey = `${string}`;
+type IconCacheKey = string;

140-140: Menu item ID changes based on checked state.

Including the checked state in the ID (id: \space-${space.id}-${checked ? "checked" : "unchecked"}``) makes the ID unstable across state changes. If any code relies on consistent IDs, this could cause issues.

-id: `space-${space.id}-${checked ? "checked" : "unchecked"}`,
+id: `space-${space.id}`,

If the checked state needs to be identifiable from the ID for some reason, consider adding it as a separate data attribute instead.


156-156: Unused Browser parameter.

The _browser parameter in createSpacesMenu is prefixed with an underscore indicating it's not used, but still required by the function signature. This suggests a potential misalignment with the function's intended usage.

If the parameter is truly not needed, consider updating the API where this function is called to remove the parameter.

Alternatively, if it should be used (e.g., to get the focused window through Browser's methods), update the implementation to utilize it properly:

-export async function createSpacesMenu(_browser: Browser): Promise<MenuItemConstructorOptions> {
+export async function createSpacesMenu(browser: Browser): Promise<MenuItemConstructorOptions> {
  try {
    const spaces = await getSpaces();

-    const focusedWindow = getFocusedWindow();
+    const focusedWindow = browser.getFocusedWindow();
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc854f0 and 04dcc5f.

📒 Files selected for processing (1)
  • electron/browser/utility/menu/items/spaces.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
electron/browser/utility/menu/items/spaces.ts (5)
vite/src/lib/phosphor-icons.tsx (1)
  • PhosphorIcons (6-6)
electron/browser/browser.ts (2)
  • Browser (21-187)
  • getFocusedWindow (112-114)
electron/sessions/spaces.ts (1)
  • getSpaces (231-255)
electron/modules/windows.ts (1)
  • getFocusedWindow (62-65)
electron/settings/main.ts (1)
  • settings (40-76)
🔇 Additional comments (3)
electron/browser/utility/menu/items/spaces.ts (3)

61-92: Improved SVG color modification handling.

Great job implementing a more robust approach to SVG color modification! This handles complex SVGs with nested elements by:

  1. Properly cleaning existing fill attributes
  2. Setting a white fill on the SVG root element
  3. Adding a CSS style that ensures consistent coloring across all elements

This is a significant improvement from the previous string replacement approach.


177-184: Excellent use of Promise.allSettled for robust menu creation.

Using Promise.allSettled instead of Promise.all is a great choice here as it ensures the menu is created even if some space items fail to load. Combined with proper filtering of rejected promises, this approach ensures users always get a functional menu.


197-209: Good error handling with fallback menu.

The comprehensive error handling with a fallback menu ensures that users will always see a functional Spaces menu, even if there are underlying issues with fetching spaces or creating menu items. This improves reliability and user experience.

@iamEvanYT iamEvanYT merged commit 6788ef0 into main Apr 21, 2025
1 check passed
@iamEvanYT iamEvanYT deleted the evan/better-space-icons branch April 21, 2025 12:44
This was referenced Apr 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant