Replace project letter icons with color-mapped Lucide icons across all pages

- Map each tablo color to a dedicated Lucide icon (Zap, Leaf, Gem, Flame, etc.)
- Apply contrast-aware icon color: dark gray for light backgrounds (yellow, cyan), white for all others
- NavigationBar: always use gray icon for sidebar consistency; active nav item uses light purple background (#804EEC tint)
- Apply icons to NavigationBar, tablos, tablo-details, tablo (dashboard), and tasks pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-02-23 19:44:09 +01:00
parent 7acb79b3de
commit bec48cfab6
No known key found for this signature in database
5 changed files with 235 additions and 43 deletions

View file

@ -17,12 +17,17 @@ import {
CalendarCheckIcon,
CalendarIcon,
Circle,
Compass,
ConstructionIcon,
CreditCard,
// FileTextIcon, // Notes feature temporarily hidden
Flame,
FolderIcon,
Gem,
Heart,
Kanban,
LayersIcon,
Leaf,
ListTodo,
LogOutIcon,
MessageCircleIcon,
@ -33,6 +38,10 @@ import {
SettingsIcon,
Sparkles,
SquareKanban,
Star,
Sun,
Waves,
Zap,
} from "lucide-react";
import { useTablosList } from "../hooks/tablos";
import { useState } from "react";
@ -69,7 +78,7 @@ function NavLink({ isActive, children }: NavLinkProps) {
"*:data-[ui=notification-badge]:text-xs/6",
"*:data-[ui=notification-badge]:font-semibold",
isActive
? "bg-navbar-darker font-semibold text-gray-900 dark:text-white *:data-[ui=notification-badge]:bg-transparent"
? "bg-purple-100 dark:bg-purple-900/30 font-semibold text-[#804EEC] dark:text-purple-300 *:data-[ui=notification-badge]:bg-transparent"
: ["font-medium", "text-gray-500 dark:text-gray-300/90 [&:not(:hover)>[data-ui=icon]]:bg-navbar-darker"]
)}
>
@ -290,6 +299,26 @@ export const SideNavigation = ({ isMobileMenuOpen }: { isMobileMenuOpen: boolean
);
};
function getTabloIcon(color: string | null | undefined) {
switch (color) {
case "bg-blue-500": return Zap;
case "bg-green-500": return Leaf;
case "bg-purple-500": return Gem;
case "bg-red-500": return Flame;
case "bg-yellow-500": return Star;
case "bg-indigo-500": return Compass;
case "bg-pink-500": return Heart;
case "bg-teal-500": return Waves;
case "bg-orange-500": return Sun;
case "bg-cyan-500": return Sparkles;
default: return FolderIcon;
}
}
function getTabloIconColor(_color: string | null | undefined): string {
return "text-gray-700 dark:text-white";
}
function RecentProjectsSection() {
const { t } = useTranslation("navigation");
const location = useLocation();
@ -309,6 +338,8 @@ function RecentProjectsSection() {
<ul className="space-y-0.5">
{recentTablos.map((tablo) => {
const isActive = location.pathname === `/tablos/${tablo.id}`;
const TabloIcon = getTabloIcon(tablo.color);
const iconColor = getTabloIconColor(tablo.color);
return (
<li key={tablo.id}>
<RouterLink
@ -316,15 +347,15 @@ function RecentProjectsSection() {
className={twMerge(
"flex items-center gap-2.5 px-2 py-1.5 rounded-lg text-sm transition-colors",
isActive
? "bg-navbar-darker text-gray-900 dark:text-white font-semibold"
? "bg-purple-100 dark:bg-purple-900/30 text-[#804EEC] dark:text-purple-300 font-semibold"
: "text-gray-500 dark:text-gray-300/90 hover:bg-navbar-darker hover:text-gray-900 dark:hover:text-white"
)}
>
<span
className="w-6 h-6 rounded-full shrink-0 flex items-center justify-center text-xs font-bold text-gray-700 dark:text-white border border-gray-400 dark:border-gray-500/50"
className="w-6 h-6 rounded-full shrink-0 flex items-center justify-center border border-gray-400 dark:border-gray-500/50"
style={{ backgroundColor: tablo.color ?? "#6b7280" }}
>
{tablo.name.charAt(0).toUpperCase()}
<TabloIcon className={twMerge("w-3.5 h-3.5", iconColor)} />
</span>
<span className="truncate flex-1">{tablo.name}</span>
</RouterLink>
@ -458,7 +489,7 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
<TypographyLarge
className={twMerge(
"text-base transition-all duration-300 font-normal",
isActive ? "text-gray-900 dark:text-white" : "text-gray-500 dark:text-gray-300/90",
isActive ? "text-[#804EEC] dark:text-purple-300" : "text-gray-500 dark:text-gray-300/90",
isCollapsed ? "opacity-0 w-0 hidden" : "opacity-100"
)}
>

View file

@ -17,16 +17,26 @@ import {
ChevronDownIcon,
ChevronRightIcon,
CircleCheckIcon,
Compass,
EllipsisVerticalIcon,
FileTextIcon,
Flame,
FolderIcon,
Gem,
Heart,
KanbanIcon,
LayoutDashboardIcon,
Leaf,
ListChecksIcon,
MapIcon,
MessageCircleIcon,
PlusIcon,
Sparkles,
Star,
Sun,
UserPlusIcon,
Waves,
Zap,
} from "lucide-react";
import { useEffect, useState } from "react";
import {
@ -47,6 +57,34 @@ import { useTabloFileNames } from "../hooks/tablo_data";
import { useTablosList, useTabloMembers } from "../hooks/tablos";
import { useUser } from "../providers/UserStoreProvider";
// ─── Icon helpers ─────────────────────────────────────────────────────────────
function getTabloIcon(color: string | null | undefined) {
switch (color) {
case "bg-blue-500": return Zap;
case "bg-green-500": return Leaf;
case "bg-purple-500": return Gem;
case "bg-red-500": return Flame;
case "bg-yellow-500": return Star;
case "bg-indigo-500": return Compass;
case "bg-pink-500": return Heart;
case "bg-teal-500": return Waves;
case "bg-orange-500": return Sun;
case "bg-cyan-500": return Sparkles;
default: return FolderIcon;
}
}
function getTabloIconColor(color: string | null | undefined): string {
switch (color) {
case "bg-yellow-500":
case "bg-cyan-500":
return "text-gray-700";
default:
return "text-white";
}
}
// ─── Status helpers ───────────────────────────────────────────────────────────
function getStatusConfig(status: string) {
@ -196,6 +234,8 @@ export const TabloDetailsPage = () => {
progress,
} = getStatusConfig(tablo.status);
const isAdmin = tablo.is_admin;
const TabloIcon = getTabloIcon(tablo.color);
const iconColor = getTabloIconColor(tablo.color);
return (
<div>
@ -205,7 +245,7 @@ export const TabloDetailsPage = () => {
<div className="flex items-center gap-4">
<div
className={cn(
"w-12 h-12 rounded-lg flex items-center justify-center shrink-0 overflow-hidden text-white font-bold text-xl",
"w-12 h-12 rounded-lg flex items-center justify-center shrink-0 overflow-hidden",
!tablo.image && (tablo.color || "bg-gray-400"),
)}
>
@ -216,7 +256,7 @@ export const TabloDetailsPage = () => {
className="w-full h-full object-cover"
/>
) : (
tablo.name.charAt(0).toUpperCase()
<TabloIcon className={cn("w-6 h-6", iconColor)} />
)}
</div>
<h1 className="text-xl md:text-3xl font-bold text-foreground">

View file

@ -20,12 +20,23 @@ import { Text } from "@xtablo/ui/components/typography";
import {
CheckCircle2,
Clock,
Compass,
Flame,
FolderIcon,
Gem,
Heart,
Leaf,
ListTodo,
MessageSquare,
Plus,
Shield,
Sparkles,
Star,
Sun,
Trash2,
Users,
Waves,
Zap,
} from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@ -42,6 +53,32 @@ import { DashboardTaskList } from "src/components/DashboardTaskList";
import { TaskModal } from "src/components/kanban/TaskModal";
import { ProjectCardList } from "src/components/ProjectCardList";
function getTabloIcon(color: string | null | undefined) {
switch (color) {
case "bg-blue-500": return Zap;
case "bg-green-500": return Leaf;
case "bg-purple-500": return Gem;
case "bg-red-500": return Flame;
case "bg-yellow-500": return Star;
case "bg-indigo-500": return Compass;
case "bg-pink-500": return Heart;
case "bg-teal-500": return Waves;
case "bg-orange-500": return Sun;
case "bg-cyan-500": return Sparkles;
default: return FolderIcon;
}
}
function getTabloIconColor(color: string | null | undefined): string {
switch (color) {
case "bg-yellow-500":
case "bg-cyan-500":
return "text-gray-700";
default:
return "text-white";
}
}
export const TabloPage = () => {
const { t } = useTranslation(["pages", "common"]);
@ -354,6 +391,8 @@ export const TabloPage = () => {
const renderTablo = (tablo: UserTablo) => {
const isAdmin = tablo.is_admin;
const TabloIcon = getTabloIcon(tablo.color);
const iconColor = getTabloIconColor(tablo.color);
return (
<div
@ -392,9 +431,7 @@ export const TabloPage = () => {
tablo.color || "bg-gray-400"
} flex items-center justify-center`}
>
<h3 className="text-white font-bold text-xl text-center px-4">
{tablo.name}
</h3>
<TabloIcon className={`w-12 h-12 ${iconColor}`} />
</div>
)}

View file

@ -3,12 +3,23 @@ import type { UserTablo } from "@xtablo/shared/types/tablos.types";
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
import {
CalendarIcon,
Compass,
FilterIcon,
Flame,
FolderIcon,
Gem,
Grid3x3Icon,
Heart,
Leaf,
ListIcon,
PlusIcon,
SearchIcon,
Sparkles,
Star,
Sun,
Trash2Icon,
Waves,
Zap,
} from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@ -17,6 +28,34 @@ import { CreateTabloModal } from "../components/CreateTabloModal";
import { DeleteTabloModal } from "../components/DeleteTabloModal";
import { useCreateTablo, useDeleteTablo, useTablosList } from "../hooks/tablos";
// ─── Icon helpers ─────────────────────────────────────────────────────────────
function getTabloIcon(color: string | null | undefined) {
switch (color) {
case "bg-blue-500": return Zap;
case "bg-green-500": return Leaf;
case "bg-purple-500": return Gem;
case "bg-red-500": return Flame;
case "bg-yellow-500": return Star;
case "bg-indigo-500": return Compass;
case "bg-pink-500": return Heart;
case "bg-teal-500": return Waves;
case "bg-orange-500": return Sun;
case "bg-cyan-500": return Sparkles;
default: return FolderIcon;
}
}
function getTabloIconColor(color: string | null | undefined): string {
switch (color) {
case "bg-yellow-500":
case "bg-cyan-500":
return "text-gray-700";
default:
return "text-white";
}
}
// ─── Status helpers ───────────────────────────────────────────────────────────
function getStatusConfig(status: string) {
@ -63,6 +102,8 @@ function TabloCard({
}) {
const { t } = useTranslation("pages");
const { label, badgeClass, progress } = getStatusConfig(tablo.status);
const TabloIcon = getTabloIcon(tablo.color);
const iconColor = getTabloIconColor(tablo.color);
return (
<div
@ -87,14 +128,14 @@ function TabloCard({
<div className="flex items-center gap-3 mb-4">
<div
className={cn(
"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden text-white font-bold text-base",
"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden",
!tablo.image && (tablo.color || "bg-gray-400")
)}
>
{tablo.image ? (
<img src={tablo.image} alt={tablo.name} className="w-full h-full object-cover" />
) : (
tablo.name.charAt(0).toUpperCase()
<TabloIcon className={cn("w-4 h-4", iconColor)} />
)}
</div>
<h3 className="text-base font-semibold text-gray-900 dark:text-gray-100 flex-1 line-clamp-2">
@ -141,6 +182,8 @@ function TabloRow({
onDelete: (id: string) => void;
}) {
const { label, badgeClass, progress } = getStatusConfig(tablo.status);
const TabloIcon = getTabloIcon(tablo.color);
const iconColor = getTabloIconColor(tablo.color);
return (
<tr
@ -151,14 +194,14 @@ function TabloRow({
<div className="flex items-center gap-3">
<div
className={cn(
"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden text-white font-bold text-sm",
"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden",
!tablo.image && (tablo.color || "bg-gray-400")
)}
>
{tablo.image ? (
<img src={tablo.image} alt={tablo.name} className="w-full h-full object-cover" />
) : (
tablo.name.charAt(0).toUpperCase()
<TabloIcon className={cn("w-4 h-4", iconColor)} />
)}
</div>
<span className="font-medium text-gray-900 dark:text-gray-100 truncate">{tablo.name}</span>

View file

@ -14,8 +14,14 @@ import {
CalendarIcon,
CircleCheckIcon,
CircleIcon,
Compass,
EllipsisVerticalIcon,
Flame,
FolderIcon,
Gem,
Heart,
KanbanIcon,
Leaf,
ListIcon,
ListTodo,
MessageSquareIcon,
@ -23,7 +29,12 @@ import {
PaperclipIcon,
PlusIcon,
Settings2Icon,
Sparkles,
Star,
Sun,
UserIcon,
Waves,
Zap,
} from "lucide-react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@ -55,6 +66,33 @@ const columnTitles = {
done: "Terminé",
};
function getTabloIcon(color: string | null | undefined) {
switch (color) {
case "bg-blue-500": return Zap;
case "bg-green-500": return Leaf;
case "bg-purple-500": return Gem;
case "bg-red-500": return Flame;
case "bg-yellow-500": return Star;
case "bg-indigo-500": return Compass;
case "bg-pink-500": return Heart;
case "bg-teal-500": return Waves;
case "bg-orange-500": return Sun;
case "bg-cyan-500": return Sparkles;
default: return FolderIcon;
}
}
// Light backgrounds need dark text; dark/saturated backgrounds use white
function getTabloIconColor(color: string | null | undefined): string {
switch (color) {
case "bg-yellow-500":
case "bg-cyan-500":
return "text-gray-700";
default:
return "text-white";
}
}
export function TasksPage() {
const navigate = useNavigate();
const { t } = useTranslation(["pages", "common"]);
@ -502,26 +540,25 @@ export function TasksPage() {
*/}
{/* Tablo row */}
{taskWithTablo.tablos && (
<div className="flex items-center mb-3 border-b border-dashed border-[#D0D5DD] dark:border-gray-600 pb-3">
<div
className={twMerge(
"w-5 h-5 rounded-[5px] p-1 mr-2 flex items-center justify-center shrink-0",
taskWithTablo.tablos.color ||
"bg-gray-400",
)}
>
<span className="text-[8px] font-bold text-white leading-none">
{taskWithTablo.tablos.name
.charAt(0)
.toUpperCase()}
{taskWithTablo.tablos && (() => {
const TabloIcon = getTabloIcon(taskWithTablo.tablos.color);
const iconColor = getTabloIconColor(taskWithTablo.tablos.color);
return (
<div className="flex items-center mb-3 border-b border-dashed border-[#D0D5DD] dark:border-gray-600 pb-3">
<div
className={twMerge(
"w-5 h-5 rounded-[5px] mr-2 flex items-center justify-center shrink-0",
taskWithTablo.tablos.color || "bg-gray-400",
)}
>
<TabloIcon className={twMerge("w-3 h-3", iconColor)} />
</div>
<span className="text-xs text-gray-600 dark:text-gray-400 truncate">
{taskWithTablo.tablos.name}
</span>
</div>
<span className="text-xs text-gray-600 dark:text-gray-400 truncate">
{taskWithTablo.tablos.name}
</span>
</div>
)}
);
})()}
{/* Footer: stats + assignee */}
<div className="flex items-center justify-between">
@ -664,17 +701,21 @@ export function TasksPage() {
{/* Project */}
<td className="px-4 md:px-6 py-3">
{taskWithTablo.tablos ? (
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<div className={twMerge(
"w-5 h-5 rounded-[4px] shrink-0 flex items-center justify-center text-[8px] font-bold text-white",
taskWithTablo.tablos.color || "bg-gray-400"
)}>
{taskWithTablo.tablos.name.charAt(0).toUpperCase()}
</div>
<span className="truncate">{taskWithTablo.tablos.name}</span>
</div>
) : (
{taskWithTablo.tablos ? (() => {
const TabloIcon = getTabloIcon(taskWithTablo.tablos.color);
const iconColor = getTabloIconColor(taskWithTablo.tablos.color);
return (
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<div className={twMerge(
"w-5 h-5 rounded-[4px] shrink-0 flex items-center justify-center",
taskWithTablo.tablos.color || "bg-gray-400"
)}>
<TabloIcon className={twMerge("w-3 h-3", iconColor)} />
</div>
<span className="truncate">{taskWithTablo.tablos.name}</span>
</div>
);
})() : (
<span className="text-sm text-gray-400"></span>
)}
</td>