diff --git a/go-backend/internal/web/handlers/tablos.go b/go-backend/internal/web/handlers/tablos.go
index a1cc9a4..3d1811c 100644
--- a/go-backend/internal/web/handlers/tablos.go
+++ b/go-backend/internal/web/handlers/tablos.go
@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"regexp"
+ "strconv"
"strings"
"time"
@@ -23,6 +24,29 @@ var tabloColorPattern = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`)
const defaultTabloColor = "#3B82F6"
const tabloColorValidationMessage = "La couleur du projet doit être un code hexadécimal au format #RRGGBB"
+type tabloPaletteEntry struct {
+ icon string
+ bg string
+ fg string
+ accent string
+ r int
+ g int
+ b int
+}
+
+var tabloIconPalette = []tabloPaletteEntry{
+ {icon: "bolt", bg: "bg-blue-500", fg: "text-white", accent: "blue", r: 59, g: 130, b: 246},
+ {icon: "leaf", bg: "bg-green-500", fg: "text-white", accent: "green", r: 34, g: 197, b: 94},
+ {icon: "gem", bg: "bg-purple-500", fg: "text-white", accent: "purple", r: 168, g: 85, b: 247},
+ {icon: "flame", bg: "bg-red-500", fg: "text-white", accent: "red", r: 239, g: 68, b: 68},
+ {icon: "star", bg: "bg-yellow-500", fg: "text-gray-700", accent: "yellow", r: 234, g: 179, b: 8},
+ {icon: "compass", bg: "bg-indigo-500", fg: "text-white", accent: "indigo", r: 99, g: 102, b: 241},
+ {icon: "heart", bg: "bg-pink-500", fg: "text-white", accent: "pink", r: 236, g: 72, b: 153},
+ {icon: "waves", bg: "bg-teal-500", fg: "text-white", accent: "teal", r: 20, g: 184, b: 166},
+ {icon: "sun", bg: "bg-orange-500", fg: "text-white", accent: "orange", r: 249, g: 115, b: 22},
+ {icon: "sparkles", bg: "bg-cyan-500", fg: "text-gray-700", accent: "cyan", r: 6, g: 182, b: 212},
+}
+
type TabloStatus = tablomodel.Status
const (
@@ -455,12 +479,13 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta
items := make([]views.TabloCardView, 0, len(tablos))
for _, tablo := range tablos {
statusLabel, statusClass, progress, statusTone := tabloStatusPresentation(tablo.Status)
- iconKind, bgClass, fgClass, accent := tabloIconPresentation(tablo.Name)
+ color := storedTabloColor(tablo.Color)
+ iconKind, bgClass, fgClass, accent := tabloIconPresentation(color)
items = append(items, views.TabloCardView{
ID: tablo.ID.String(),
Name: tablo.Name,
- Color: storedTabloColor(tablo.Color),
+ Color: color,
Status: string(tablo.Status),
StatusLabel: statusLabel,
StatusClass: statusClass,
@@ -512,15 +537,53 @@ func tabloStatusPresentation(status TabloStatus) (string, string, int, string) {
}
}
-func tabloIconPresentation(name string) (string, string, string, string) {
- switch len(strings.TrimSpace(name)) % 3 {
- case 1:
- return "gem", "bg-purple-500", "text-white", "purple"
- case 2:
- return "sparkles", "bg-cyan-500", "text-gray-700", "red"
- default:
- return "bolt", "bg-blue-500", "text-white", "blue"
+func tabloIconPresentation(color string) (string, string, string, string) {
+ r, g, b, ok := parseHexColor(color)
+ if !ok {
+ fallback := tabloIconPalette[0]
+ return fallback.icon, fallback.bg, fallback.fg, fallback.accent
}
+
+ best := tabloIconPalette[0]
+ bestDistance := colorDistanceSquared(r, g, b, best.r, best.g, best.b)
+ for _, entry := range tabloIconPalette[1:] {
+ distance := colorDistanceSquared(r, g, b, entry.r, entry.g, entry.b)
+ if distance < bestDistance {
+ best = entry
+ bestDistance = distance
+ }
+ }
+
+ return best.icon, best.bg, best.fg, best.accent
+}
+
+func parseHexColor(color string) (int, int, int, bool) {
+ trimmed := strings.TrimSpace(color)
+ if len(trimmed) != 7 || trimmed[0] != '#' {
+ return 0, 0, 0, false
+ }
+
+ r, err := strconv.ParseInt(trimmed[1:3], 16, 0)
+ if err != nil {
+ return 0, 0, 0, false
+ }
+ g, err := strconv.ParseInt(trimmed[3:5], 16, 0)
+ if err != nil {
+ return 0, 0, 0, false
+ }
+ b, err := strconv.ParseInt(trimmed[5:7], 16, 0)
+ if err != nil {
+ return 0, 0, 0, false
+ }
+
+ return int(r), int(g), int(b), true
+}
+
+func colorDistanceSquared(r1 int, g1 int, b1 int, r2 int, g2 int, b2 int) int {
+ dr := r1 - r2
+ dg := g1 - g2
+ db := b1 - b2
+ return dr*dr + dg*dg + db*db
}
func formatFrenchDate(value time.Time) string {
diff --git a/go-backend/internal/web/handlers/tablos_test.go b/go-backend/internal/web/handlers/tablos_test.go
index 928d560..95e5cdf 100644
--- a/go-backend/internal/web/handlers/tablos_test.go
+++ b/go-backend/internal/web/handlers/tablos_test.go
@@ -493,6 +493,33 @@ func TestFormatCardDateUsesFrenchMonthNames(t *testing.T) {
}
}
+func TestTabloIconPresentationUsesClosestPaletteColor(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ color string
+ icon string
+ }{
+ {name: "blue maps to bolt", color: "#3B82F6", icon: "bolt"},
+ {name: "green maps to leaf", color: "#22C55E", icon: "leaf"},
+ {name: "purple maps to gem", color: "#A855F7", icon: "gem"},
+ {name: "red maps to flame", color: "#EF4444", icon: "flame"},
+ {name: "yellow maps to star", color: "#EAB308", icon: "star"},
+ {name: "indigo maps to compass", color: "#6366F1", icon: "compass"},
+ {name: "pink maps to heart", color: "#EC4899", icon: "heart"},
+ {name: "teal maps to waves", color: "#14B8A6", icon: "waves"},
+ {name: "orange maps to sun", color: "#F97316", icon: "sun"},
+ {name: "cyan maps to sparkles", color: "#06B6D4", icon: "sparkles"},
+ {name: "nearby blue still maps to bolt", color: "#4F86F7", icon: "bolt"},
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ icon, _, _, _ := tabloIconPresentation(tt.color)
+ if icon != tt.icon {
+ t.Fatalf("expected icon %q for color %q, got %q", tt.icon, tt.color, icon)
+ }
+ })
+ }
+}
+
func TestGetTablosPageGridUsesProjectCardMarkup(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
diff --git a/go-backend/internal/web/ui/button.css b/go-backend/internal/web/ui/button.css
index 07f045b..95fb45e 100644
--- a/go-backend/internal/web/ui/button.css
+++ b/go-backend/internal/web/ui/button.css
@@ -1,7 +1,7 @@
.ui-button {
align-items: center;
border: 0;
- border-radius: 0.7rem;
+ border-radius: 0.35rem;
cursor: pointer;
display: inline-flex;
font-weight: 600;
diff --git a/go-backend/internal/web/views/dashboard_components_test.go b/go-backend/internal/web/views/dashboard_components_test.go
index 3be1984..7d67bd5 100644
--- a/go-backend/internal/web/views/dashboard_components_test.go
+++ b/go-backend/internal/web/views/dashboard_components_test.go
@@ -60,6 +60,29 @@ func TestOverviewProjectsSectionRendersColorAndEditAction(t *testing.T) {
}
}
+func TestTabloListRowRendersLeafIconKind(t *testing.T) {
+ component := TabloListRow(TabloCardView{
+ ID: "11111111-1111-1111-1111-111111111111",
+ Name: "Palette",
+ Color: "#22C55E",
+ StatusLabel: "À faire",
+ StatusTone: "info",
+ Progress: 0,
+ ProgressLabel: "0%",
+ CreatedAtLabel: "10 mai 2026",
+ DeleteRequestURL: "/tablos/11111111-1111-1111-1111-111111111111",
+ EditRequestURL: "/tablos/11111111-1111-1111-1111-111111111111/edit",
+ IconKind: "leaf",
+ Initial: "P",
+ })
+
+ html := renderViewToString(t, component)
+
+ if !strings.Contains(html, ``) {
+ t.Fatalf("expected leaf icon markup, got %q", html)
+ }
+}
+
func TestTabloListRowDoesNotRenderSpacerBetweenEditAndDelete(t *testing.T) {
component := TabloListRow(TabloCardView{
ID: "11111111-1111-1111-1111-111111111111",
diff --git a/go-backend/internal/web/views/home.go b/go-backend/internal/web/views/home.go
index 2818aa3..a7de2eb 100644
--- a/go-backend/internal/web/views/home.go
+++ b/go-backend/internal/web/views/home.go
@@ -5,9 +5,10 @@ import (
"strings"
"time"
- "github.com/a-h/templ"
tablomodel "xtablo-backend/internal/tablos"
"xtablo-backend/internal/web/dates"
+
+ "github.com/a-h/templ"
)
const overviewProjectsPreviewLimit = 6
diff --git a/go-backend/internal/web/views/icons.templ b/go-backend/internal/web/views/icons.templ
index 19a2373..f5834c6 100644
--- a/go-backend/internal/web/views/icons.templ
+++ b/go-backend/internal/web/views/icons.templ
@@ -125,6 +125,54 @@ templ SidebarIcon(kind string) {
+ case "leaf":
+
+ case "flame":
+
+ case "star":
+
+ case "compass":
+
+ case "heart":
+
+ case "waves":
+
+ case "sun":
+
+ case "sparkles":
+
default: