diff --git a/apps/desktop/src/renderer/components/ColorSelector/ColorSelector.tsx b/apps/desktop/src/renderer/components/ColorSelector/ColorSelector.tsx
index 970dcd033ea..b1ae20a3319 100644
--- a/apps/desktop/src/renderer/components/ColorSelector/ColorSelector.tsx
+++ b/apps/desktop/src/renderer/components/ColorSelector/ColorSelector.tsx
@@ -12,6 +12,7 @@ interface ColorSelectorProps {
selectedColor?: string | null;
onSelectColor: (color: string) => void;
variant?: ColorSelectorVariant;
+ includeDefault?: boolean;
className?: string;
}
@@ -43,14 +44,18 @@ export function ColorSelector({
selectedColor,
onSelectColor,
variant = "inline",
+ includeDefault = false,
className,
}: ColorSelectorProps) {
const selectedValue = selectedColor ?? PROJECT_COLOR_DEFAULT;
+ const colorOptions = includeDefault
+ ? [{ name: "Default", value: PROJECT_COLOR_DEFAULT }, ...PROJECT_COLORS]
+ : PROJECT_COLORS;
if (variant === "menu") {
return (
<>
- {PROJECT_COLORS.map((color) => {
+ {colorOptions.map((color) => {
const isSelected = selectedValue === color.value;
return (
@@ -73,7 +78,7 @@ export function ColorSelector({
return (
- {PROJECT_COLORS.map((color) => {
+ {colorOptions.map((color) => {
const isSelected = selectedValue === color.value;
return (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx
index df2d8c24766..82ea29f7eb7 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx
@@ -47,7 +47,9 @@ export function DashboardSidebarProjectSection({
handleNewWorkspace,
handleOpenInFinder,
handleOpenSettings,
+ handleSetColor,
isRenaming,
+ projectColor,
renameSection,
renameValue,
setRenameValue,
@@ -63,16 +65,19 @@ export function DashboardSidebarProjectSection({
if (isSidebarCollapsed) {
return (
{
projectName: string;
iconUrl: string | null;
+ projectColor: string;
isCollapsed: boolean;
totalWorkspaceCount: number;
workspaces: DashboardSidebarWorkspace[];
@@ -26,6 +27,7 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef<
{
projectName,
iconUrl,
+ projectColor,
isCollapsed,
totalWorkspaceCount,
workspaces,
@@ -56,7 +58,11 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef<
"hover:bg-muted/50 transition-colors",
)}
>
-
+
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx
index c7f93cdd38a..95019539049 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx
@@ -3,31 +3,40 @@ import {
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@superset/ui/context-menu";
import {
LuFolderOpen,
LuFolderPlus,
+ LuPalette,
LuPencil,
LuSettings,
LuX,
} from "react-icons/lu";
+import { ColorSelector } from "renderer/components/ColorSelector";
interface DashboardSidebarProjectContextMenuProps {
+ projectColor: string;
onCreateSection: () => void;
onOpenInFinder: () => void;
onOpenSettings: () => void;
onRemoveFromSidebar: () => void;
onRename: () => void;
+ onSetColor: (color: string) => void;
children: React.ReactNode;
}
export function DashboardSidebarProjectContextMenu({
+ projectColor,
onCreateSection,
onOpenInFinder,
onOpenSettings,
onRemoveFromSidebar,
onRename,
+ onSetColor,
children,
}: DashboardSidebarProjectContextMenuProps) {
return (
@@ -47,6 +56,20 @@ export function DashboardSidebarProjectContextMenu({
Project Settings
+
+
+
+ Set Color
+
+
+
+
+
New group
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx
index 6a3339da429..70344023841 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx
@@ -9,6 +9,7 @@ interface DashboardSidebarProjectRowProps
extends ComponentPropsWithoutRef<"div"> {
projectName: string;
iconUrl: string | null;
+ projectColor: string;
totalWorkspaceCount: number;
isCollapsed: boolean;
isRenaming: boolean;
@@ -29,6 +30,7 @@ export const DashboardSidebarProjectRow = forwardRef<
{
projectName,
iconUrl,
+ projectColor,
totalWorkspaceCount,
isCollapsed,
isRenaming,
@@ -74,6 +76,7 @@ export const DashboardSidebarProjectRow = forwardRef<
state.projects[project.id]?.color ?? PROJECT_COLOR_DEFAULT,
+ );
+ const setProjectColor = useV2ProjectLocalMetaStore(
+ (state) => state.setProjectColor,
+ );
const {
createSection,
deleteSection,
@@ -48,6 +56,10 @@ export function useDashboardSidebarProjectSectionActions({
projectActions.renameProject(project.id, trimmed);
};
+ const handleSetColor = (color: string) => {
+ setProjectColor(project.id, color);
+ };
+
const handleOpenInFinder = () => {
toast.info("Open in Finder is coming soon");
};
@@ -95,7 +107,9 @@ export function useDashboardSidebarProjectSectionActions({
handleNewWorkspace,
handleOpenInFinder,
handleOpenSettings,
+ handleSetColor,
isRenaming,
+ projectColor,
renameSection,
renameValue,
setRenameValue,
diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/ProjectThumbnail/ProjectThumbnail.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/ProjectThumbnail/ProjectThumbnail.tsx
index 9330aa1e74b..f7b1d7eb0ef 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/components/ProjectThumbnail/ProjectThumbnail.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/components/ProjectThumbnail/ProjectThumbnail.tsx
@@ -1,28 +1,40 @@
import { cn } from "@superset/ui/utils";
import { useState } from "react";
+import {
+ hexToRgba,
+ isCustomProjectColor,
+} from "shared/constants/project-colors";
interface ProjectThumbnailProps {
projectName: string;
iconUrl?: string | null;
+ color?: string | null;
className?: string;
}
export function ProjectThumbnail({
projectName,
iconUrl,
+ color,
className,
}: ProjectThumbnailProps) {
const [failedUrl, setFailedUrl] = useState(null);
const firstLetter = projectName.charAt(0).toUpperCase();
+ const hasCustomColor = isCustomProjectColor(color);
+ const customBorderStyle = hasCustomColor
+ ? { borderColor: hexToRgba(color, 0.6) }
+ : undefined;
if (iconUrl && failedUrl !== iconUrl) {
return (

{firstLetter}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx
index 916dabc8e1b..fb00dcd45af 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx
@@ -1,7 +1,11 @@
import { cn } from "@superset/ui/utils";
import { useState } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
-import { PROJECT_COLOR_DEFAULT } from "shared/constants/project-colors";
+import {
+ hexToRgba,
+ isCustomProjectColor,
+ PROJECT_COLOR_DEFAULT,
+} from "shared/constants/project-colors";
interface ProjectThumbnailProps {
projectId: string;
@@ -17,23 +21,6 @@ function getGitHubAvatarUrl(owner: string): string {
return `https://github.com/${owner}.png?size=64`;
}
-/**
- * Converts a hex color to rgba with the specified alpha.
- */
-function hexToRgba(hex: string, alpha: number): string {
- const r = Number.parseInt(hex.slice(1, 3), 16);
- const g = Number.parseInt(hex.slice(3, 5), 16);
- const b = Number.parseInt(hex.slice(5, 7), 16);
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
-}
-
-/**
- * Checks if a color value is a custom hex color (not the "default" value).
- */
-function isCustomColor(color: string): boolean {
- return color !== PROJECT_COLOR_DEFAULT && color.startsWith("#");
-}
-
/**
* Determines whether the GitHub avatar should be displayed.
* Exported for unit testing.
@@ -72,7 +59,7 @@ export function ProjectThumbnail({
const owner = avatarData?.owner ?? githubOwner;
const firstLetter = projectName.charAt(0).toUpperCase();
- const hasCustomColor = isCustomColor(projectColor);
+ const hasCustomColor = isCustomProjectColor(projectColor);
const shouldUseTransparentIconFrame = projectColor === PROJECT_COLOR_DEFAULT;
// Border: gray by default, custom color with slight transparency when set
diff --git a/apps/desktop/src/renderer/stores/v2-project-local-meta.ts b/apps/desktop/src/renderer/stores/v2-project-local-meta.ts
index 96951432579..e2bbccd7bfe 100644
--- a/apps/desktop/src/renderer/stores/v2-project-local-meta.ts
+++ b/apps/desktop/src/renderer/stores/v2-project-local-meta.ts
@@ -1,9 +1,11 @@
+import { PROJECT_COLOR_DEFAULT } from "shared/constants/project-colors";
import { create } from "zustand";
import { devtools, persist } from "zustand/middleware";
interface ProjectMeta {
isCollapsed: boolean;
tabOrder: number;
+ color: string;
}
interface V2ProjectLocalMetaState {
@@ -12,11 +14,13 @@ interface V2ProjectLocalMetaState {
getProjectMeta: (id: string) => ProjectMeta;
toggleProjectCollapsed: (id: string) => void;
setProjectTabOrder: (id: string, order: number) => void;
+ setProjectColor: (id: string, color: string) => void;
}
const DEFAULT_PROJECT_META: ProjectMeta = {
isCollapsed: false,
tabOrder: 0,
+ color: PROJECT_COLOR_DEFAULT,
};
export const useV2ProjectLocalMetaStore = create()(
@@ -52,6 +56,18 @@ export const useV2ProjectLocalMetaStore = create()(
};
});
},
+
+ setProjectColor: (id, color) => {
+ set((state) => {
+ const current = state.projects[id] ?? DEFAULT_PROJECT_META;
+ return {
+ projects: {
+ ...state.projects,
+ [id]: { ...current, color },
+ },
+ };
+ });
+ },
}),
{
name: "v2-project-local-meta",
diff --git a/apps/desktop/src/shared/constants/project-colors.ts b/apps/desktop/src/shared/constants/project-colors.ts
index 0341c438e22..c554aab8411 100644
--- a/apps/desktop/src/shared/constants/project-colors.ts
+++ b/apps/desktop/src/shared/constants/project-colors.ts
@@ -21,3 +21,18 @@ export const PROJECT_CUSTOM_COLORS = PROJECT_COLORS;
export const PROJECT_COLOR_VALUES: string[] = PROJECT_COLORS.map(
(color) => color.value,
);
+
+/** Checks if a color value is a custom hex color (not the "default" value). */
+export function isCustomProjectColor(
+ color: string | null | undefined,
+): color is string {
+ return !!color && color !== PROJECT_COLOR_DEFAULT && color.startsWith("#");
+}
+
+/** Converts a hex color to an rgba string with the specified alpha. */
+export function hexToRgba(hex: string, alpha: number): string {
+ const r = Number.parseInt(hex.slice(1, 3), 16);
+ const g = Number.parseInt(hex.slice(3, 5), 16);
+ const b = Number.parseInt(hex.slice(5, 7), 16);
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+}