Pass tablo list to dashboard views

Update DashboardPage and DashboardContentSwap to receive the list of
tablos so the sidebar can display real project data. Extract tablo icon
palette logic into a new tabloicons package that maps hex colors to
presentation attributes (icon, background, foreground colors).

Update handlers to load tablos from the repository before rendering
dashboard views. Refactor TabloCardView to use icon presentation instead
of initials when available.
This commit is contained in:
Arthur Belleville 2026-05-10 14:56:46 +02:00
parent 3232309388
commit 354785edff
No known key found for this signature in database
12 changed files with 795 additions and 539 deletions

View file

@ -98,9 +98,9 @@ func (h *AuthHandler) GetHome() http.HandlerFunc {
content := views.OverviewMainContent(user.DisplayName, user.Email, projects)
var renderErr error
if isHXRequest(r) {
renderErr = views.DashboardContentSwap("/", content).Render(r.Context(), w)
renderErr = views.DashboardContentSwap("/", tablos, content).Render(r.Context(), w)
} else {
renderErr = views.DashboardPage("/", content).Render(r.Context(), w)
renderErr = views.DashboardPage("/", tablos, content).Render(r.Context(), w)
}
if renderErr != nil {
http.Error(w, "failed to render app page", http.StatusInternalServerError)
@ -152,16 +152,24 @@ func (h *AuthHandler) GetNotFound() http.HandlerFunc {
return
}
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{
OwnerID: user.ID,
})
if err != nil {
http.Error(w, "failed to load projects", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
content := views.NotFoundContent(user.DisplayName)
var err error
var renderErr error
if isHXRequest(r) {
err = views.DashboardContentSwap("", content).Render(r.Context(), w)
renderErr = views.DashboardContentSwap("", tablos, content).Render(r.Context(), w)
} else {
err = views.DashboardPage("", content).Render(r.Context(), w)
renderErr = views.DashboardPage("", tablos, content).Render(r.Context(), w)
}
if err != nil {
if renderErr != nil {
http.Error(w, "failed to render not found page", http.StatusInternalServerError)
}
}
@ -175,15 +183,23 @@ func (h *AuthHandler) renderAppPage(activePath string, content func(user PublicU
return
}
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{
OwnerID: user.ID,
})
if err != nil {
http.Error(w, "failed to load projects", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
pageContent := content(user)
var err error
var renderErr error
if isHXRequest(r) {
err = views.DashboardContentSwap(activePath, pageContent).Render(r.Context(), w)
renderErr = views.DashboardContentSwap(activePath, tablos, pageContent).Render(r.Context(), w)
} else {
err = views.DashboardPage(activePath, pageContent).Render(r.Context(), w)
renderErr = views.DashboardPage(activePath, tablos, pageContent).Render(r.Context(), w)
}
if err != nil {
if renderErr != nil {
http.Error(w, "failed to render app page", http.StatusInternalServerError)
}
}

View file

@ -1,6 +1,7 @@
package handlers
import (
"context"
"bytes"
"net/http"
"net/http/httptest"
@ -114,6 +115,59 @@ func TestLogoutLogsSessionDeletion(t *testing.T) {
}
}
func TestTasksPageSidebarShowsRealTablos(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.AddCookie(sessionCookie)
userID, ok := handler.currentUserID(req.Context(), req)
if !ok {
t.Fatal("expected user session")
}
for _, tablo := range []CreateTabloInput{
{OwnerID: userID, Name: "Blue", Color: "#3B82F6", Status: TabloStatusTodo},
{OwnerID: userID, Name: "Green", Color: "#22C55E", Status: TabloStatusTodo},
{OwnerID: userID, Name: "Purple", Color: "#A855F7", Status: TabloStatusTodo},
{OwnerID: userID, Name: "Red", Color: "#EF4444", Status: TabloStatusTodo},
{OwnerID: userID, Name: "Hidden Fifth", Color: "#EAB308", Status: TabloStatusTodo},
} {
if _, err := repo.CreateTablo(context.Background(), tablo); err != nil {
t.Fatalf("create tablo %q: %v", tablo.Name, err)
}
}
pageReq := httptest.NewRequest(http.MethodGet, "/tasks", nil)
pageReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetTasksPage().ServeHTTP(rec, pageReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{
"Green",
"Purple",
"Red",
"Hidden Fifth",
} {
if !strings.Contains(body, want) {
t.Fatalf("expected sidebar to contain %q, got %q", want, body)
}
}
if strings.Contains(body, "Blue") {
t.Fatalf("expected sidebar to limit to the four most recent tablos, got %q", body)
}
if !strings.Contains(body, `href="/tablos/`) {
t.Fatalf("expected sidebar project links, got %q", body)
}
}
func newTestAuthHandler(t *testing.T) *AuthHandler {
t.Helper()
return NewAuthHandler(NewInMemoryAuthRepository())

View file

@ -7,13 +7,13 @@ import (
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"github.com/google/uuid"
tablomodel "xtablo-backend/internal/tablos"
"xtablo-backend/internal/web/dates"
"xtablo-backend/internal/web/tabloicons"
"xtablo-backend/internal/web/views"
)
@ -24,29 +24,6 @@ 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 (
@ -152,7 +129,7 @@ func (h *AuthHandler) PostTablos() http.HandlerFunc {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity)
renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity)
return
}
@ -163,7 +140,7 @@ func (h *AuthHandler) PostTablos() http.HandlerFunc {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity)
renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity)
return
}
@ -184,7 +161,7 @@ func (h *AuthHandler) PostTablos() http.HandlerFunc {
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
}
}
@ -218,7 +195,7 @@ func (h *AuthHandler) GetEditTabloModal() http.HandlerFunc {
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, tablo.Name, tablo.Color, ""), http.StatusOK)
renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, tablo.Name, tablo.Color, ""), http.StatusOK)
}
}
@ -252,7 +229,7 @@ func (h *AuthHandler) PostTabloUpdate() http.HandlerFunc {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity)
renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity)
return
}
@ -263,7 +240,7 @@ func (h *AuthHandler) PostTabloUpdate() http.HandlerFunc {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity)
renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity)
return
}
@ -289,7 +266,7 @@ func (h *AuthHandler) PostTabloUpdate() http.HandlerFunc {
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
}
}
@ -323,7 +300,7 @@ func (h *AuthHandler) DeleteTablo() http.HandlerFunc {
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
}
}
@ -341,7 +318,7 @@ func (h *AuthHandler) renderTablosPage(w http.ResponseWriter, r *http.Request) {
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
}
func tablosPageViewModel(user PublicUser, state TablosPageState, tablos []TabloRecord, formName string, formColor string, errorMessage string) views.TablosPageViewModel {
@ -374,16 +351,16 @@ func findTabloByID(tablos []TabloRecord, targetID uuid.UUID) (TabloRecord, bool)
return TabloRecord{}, false
}
func renderTablosResponse(w http.ResponseWriter, r *http.Request, activePath string, vm views.TablosPageViewModel, statusCode int) {
func renderTablosResponse(w http.ResponseWriter, r *http.Request, activePath string, tablos []TabloRecord, vm views.TablosPageViewModel, statusCode int) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(statusCode)
var err error
content := views.TablosPageContent(vm)
if isHXRequest(r) {
err = views.DashboardContentSwapWithMainClass(activePath, "flex-1 overflow-auto", content).Render(r.Context(), w)
err = views.DashboardContentSwapWithMainClass(activePath, tablos, "flex-1 overflow-auto", content).Render(r.Context(), w)
} else {
err = views.DashboardPageWithMainClass(activePath, "flex-1 overflow-auto", content).Render(r.Context(), w)
err = views.DashboardPageWithMainClass(activePath, tablos, "flex-1 overflow-auto", content).Render(r.Context(), w)
}
if err != nil {
http.Error(w, "failed to render tablos page", http.StatusInternalServerError)
@ -538,52 +515,8 @@ func tabloStatusPresentation(status TabloStatus) (string, string, int, string) {
}
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
presentation := tabloicons.ForColor(color)
return presentation.Icon, presentation.Bg, presentation.Fg, presentation.Accent
}
func formatFrenchDate(value time.Time) string {

View file

@ -0,0 +1,81 @@
package tabloicons
import (
"strconv"
"strings"
)
type Presentation struct {
Icon string
Bg string
Fg string
Accent string
}
type paletteEntry struct {
Presentation
r int
g int
b int
}
var palette = []paletteEntry{
{Presentation: Presentation{Icon: "bolt", Bg: "bg-blue-500", Fg: "text-white", Accent: "blue"}, r: 59, g: 130, b: 246},
{Presentation: Presentation{Icon: "leaf", Bg: "bg-green-500", Fg: "text-white", Accent: "green"}, r: 34, g: 197, b: 94},
{Presentation: Presentation{Icon: "gem", Bg: "bg-purple-500", Fg: "text-white", Accent: "purple"}, r: 168, g: 85, b: 247},
{Presentation: Presentation{Icon: "flame", Bg: "bg-red-500", Fg: "text-white", Accent: "red"}, r: 239, g: 68, b: 68},
{Presentation: Presentation{Icon: "star", Bg: "bg-yellow-500", Fg: "text-gray-700", Accent: "yellow"}, r: 234, g: 179, b: 8},
{Presentation: Presentation{Icon: "compass", Bg: "bg-indigo-500", Fg: "text-white", Accent: "indigo"}, r: 99, g: 102, b: 241},
{Presentation: Presentation{Icon: "heart", Bg: "bg-pink-500", Fg: "text-white", Accent: "pink"}, r: 236, g: 72, b: 153},
{Presentation: Presentation{Icon: "waves", Bg: "bg-teal-500", Fg: "text-white", Accent: "teal"}, r: 20, g: 184, b: 166},
{Presentation: Presentation{Icon: "sun", Bg: "bg-orange-500", Fg: "text-white", Accent: "orange"}, r: 249, g: 115, b: 22},
{Presentation: Presentation{Icon: "sparkles", Bg: "bg-cyan-500", Fg: "text-gray-700", Accent: "cyan"}, r: 6, g: 182, b: 212},
}
func ForColor(color string) Presentation {
r, g, b, ok := parseHexColor(color)
if !ok {
return palette[0].Presentation
}
best := palette[0]
bestDistance := colorDistanceSquared(r, g, b, best.r, best.g, best.b)
for _, entry := range palette[1:] {
distance := colorDistanceSquared(r, g, b, entry.r, entry.g, entry.b)
if distance < bestDistance {
best = entry
bestDistance = distance
}
}
return best.Presentation
}
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
}

View file

@ -1004,6 +1004,11 @@ td.text-right .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-danger
width: 3rem;
}
.project-avatar > svg {
height: 1.25rem;
width: 1.25rem;
}
.project-list-icon {
background: var(--project-color, var(--color-project-fallback));
color: var(--color-text-inverse);

View file

@ -1,13 +1,14 @@
package views
import "strconv"
import tablomodel "xtablo-backend/internal/tablos"
import "xtablo-backend/internal/web/ui"
templ DashboardPage(activePath string, content templ.Component) {
@DashboardPageWithMainClass(activePath, "dashboard-main flex-1 overflow-auto", content)
templ DashboardPage(activePath string, tablos []tablomodel.Record, content templ.Component) {
@DashboardPageWithMainClass(activePath, tablos, "dashboard-main flex-1 overflow-auto", content)
}
templ DashboardPageWithMainClass(activePath string, mainClass string, content templ.Component) {
templ DashboardPageWithMainClass(activePath string, tablos []tablomodel.Record, mainClass string, content templ.Component) {
<!DOCTYPE html>
<html lang="fr">
<head>
@ -20,7 +21,7 @@ templ DashboardPageWithMainClass(activePath string, mainClass string, content te
</head>
<body>
<div class="dashboard-shell">
@DashboardSidebar(activePath)
@DashboardSidebar(activePath, tablos)
@DashboardMainContentWithClass(mainClass, content)
</div>
</body>
@ -28,7 +29,7 @@ templ DashboardPageWithMainClass(activePath string, mainClass string, content te
}
templ DashboardNotFoundPage(displayName string, email string) {
@DashboardPage("", NotFoundContent(displayName))
@DashboardPage("", nil, NotFoundContent(displayName))
}
templ DashboardMainContent(content templ.Component) {
@ -41,16 +42,16 @@ templ DashboardMainContentWithClass(mainClass string, content templ.Component) {
</main>
}
templ DashboardContentSwap(activePath string, content templ.Component) {
@DashboardContentSwapWithMainClass(activePath, "dashboard-main flex-1 overflow-auto", content)
templ DashboardContentSwap(activePath string, tablos []tablomodel.Record, content templ.Component) {
@DashboardContentSwapWithMainClass(activePath, tablos, "dashboard-main flex-1 overflow-auto", content)
}
templ DashboardContentSwapWithMainClass(activePath string, mainClass string, content templ.Component) {
templ DashboardContentSwapWithMainClass(activePath string, tablos []tablomodel.Record, mainClass string, content templ.Component) {
@DashboardMainContentWithClass(mainClass, content)
@DashboardNavOOB(activePath)
@DashboardNavOOB(activePath, tablos)
}
templ DashboardSidebar(activePath string) {
templ DashboardSidebar(activePath string, tablos []tablomodel.Record) {
<aside class="dashboard-sidebar">
<nav aria-label="Main navigation" class="sidebar-nav-shell">
<div class="sidebar-brand">
@ -75,17 +76,7 @@ templ DashboardSidebar(activePath string) {
}
}
</ul>
<div class="sidebar-projects">
<hr role="separator"/>
<div class="sidebar-section-label">Projets</div>
<ul class="sidebar-project-list">
for _, item := range sidebarProjectItems() {
<li>
@SidebarProjectItem(item)
</li>
}
</ul>
</div>
@SidebarProjectsSection(tablos)
<ul class="sidebar-list sidebar-footer-links" role="list">
for _, item := range sidebarFooterNavItems(activePath) {
<li>
@ -99,15 +90,44 @@ templ DashboardSidebar(activePath string) {
</aside>
}
templ DashboardNavOOB(activePath string) {
templ DashboardNavOOB(activePath string, tablos []tablomodel.Record) {
for _, item := range sidebarPrimaryNavItems(activePath) {
@SidebarNavItemOOB(item)
}
@SidebarProjectsSectionOOB(tablos)
for _, item := range sidebarFooterNavItems(activePath) {
@SidebarNavItemOOB(item)
}
}
templ SidebarProjectsSection(tablos []tablomodel.Record) {
<div id="sidebar-projects-section" class="sidebar-projects">
<hr role="separator"/>
<div class="sidebar-section-label">Projets</div>
<ul class="sidebar-project-list">
for _, item := range sidebarProjectItems(tablos) {
<li>
@SidebarProjectItem(item)
</li>
}
</ul>
</div>
}
templ SidebarProjectsSectionOOB(tablos []tablomodel.Record) {
<div id="sidebar-projects-section" class="sidebar-projects" hx-swap-oob="outerHTML">
<hr role="separator"/>
<div class="sidebar-section-label">Projets</div>
<ul class="sidebar-project-list">
for _, item := range sidebarProjectItems(tablos) {
<li>
@SidebarProjectItem(item)
</li>
}
</ul>
</div>
}
templ SidebarOrganization() {
<div class="sidebar-organization">
<button class="organization-button" aria-label="Organization menu" type="button">

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ func TestOverviewProjectsFromTablosCarriesColorAndEditURL(t *testing.T) {
record := tablomodel.Record{
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
Name: "Palette",
Color: "#3B82F6",
Color: "#22C55E",
Status: tablomodel.StatusTodo,
CreatedAt: time.Date(2026, time.May, 10, 9, 0, 0, 0, time.UTC),
}
@ -27,7 +27,7 @@ func TestOverviewProjectsFromTablosCarriesColorAndEditURL(t *testing.T) {
}
project := projects[0]
if project.Color != "#3B82F6" {
if project.Color != "#22C55E" {
t.Fatalf("expected color to be preserved, got %q", project.Color)
}
if project.EditRequestURL != "/tablos/11111111-1111-1111-1111-111111111111/edit" {
@ -36,13 +36,16 @@ func TestOverviewProjectsFromTablosCarriesColorAndEditURL(t *testing.T) {
if project.CardDateLabel != "10 mai 2026" {
t.Fatalf("expected French card date label, got %q", project.CardDateLabel)
}
if project.IconKind != "leaf" {
t.Fatalf("expected color presentation icon to be preserved, got %q", project.IconKind)
}
}
func TestOverviewProjectsSectionRendersColorAndEditAction(t *testing.T) {
record := tablomodel.Record{
ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"),
Name: "Palette",
Color: "#3B82F6",
Color: "#22C55E",
Status: tablomodel.StatusTodo,
CreatedAt: time.Date(2026, time.May, 10, 9, 0, 0, 0, time.UTC),
}
@ -50,9 +53,10 @@ func TestOverviewProjectsSectionRendersColorAndEditAction(t *testing.T) {
html := renderViewToString(t, OverviewProjectsSection(OverviewProjectsFromTablos([]tablomodel.Record{record})))
for _, want := range []string{
`style="--project-color:#3B82F6;"`,
`style="--project-color:#22C55E;"`,
`aria-label="Modifier le projet"`,
`hx-get="/tablos/11111111-1111-1111-1111-111111111111/edit"`,
`<path d="M11 20A7 7 0 0 1 4 13V6a1 1 0 0 1 1-1h7a7 7 0 0 1 7 7v0a8 8 0 0 1-8 8Z"></path>`,
} {
if !strings.Contains(html, want) {
t.Fatalf("expected %q in %q", want, html)
@ -83,6 +87,36 @@ func TestTabloListRowRendersLeafIconKind(t *testing.T) {
}
}
func TestSidebarProjectItemsUsesFirstFourRealTablos(t *testing.T) {
tablos := []tablomodel.Record{
{ID: uuid.MustParse("11111111-1111-1111-1111-111111111111"), Name: "Alpha", Color: "#3B82F6"},
{ID: uuid.MustParse("22222222-2222-2222-2222-222222222222"), Name: "Beta", Color: "#22C55E"},
{ID: uuid.MustParse("33333333-3333-3333-3333-333333333333"), Name: "Gamma", Color: "#A855F7"},
{ID: uuid.MustParse("44444444-4444-4444-4444-444444444444"), Name: "Delta", Color: "#EF4444"},
{ID: uuid.MustParse("55555555-5555-5555-5555-555555555555"), Name: "Epsilon", Color: "#EAB308"},
}
items := sidebarProjectItems(tablos)
if len(items) != 4 {
t.Fatalf("expected 4 sidebar items, got %d", len(items))
}
for i, want := range []struct {
href string
label string
icon string
}{
{href: "/tablos/11111111-1111-1111-1111-111111111111", label: "Alpha", icon: "bolt"},
{href: "/tablos/22222222-2222-2222-2222-222222222222", label: "Beta", icon: "leaf"},
{href: "/tablos/33333333-3333-3333-3333-333333333333", label: "Gamma", icon: "gem"},
{href: "/tablos/44444444-4444-4444-4444-444444444444", label: "Delta", icon: "flame"},
} {
if items[i].Href != want.href || items[i].Label != want.label || items[i].Icon != want.icon {
t.Fatalf("item %d = %#v, want href=%q label=%q icon=%q", i, items[i], want.href, want.label, want.icon)
}
}
}
func TestTabloListRowDoesNotRenderSpacerBetweenEditAndDelete(t *testing.T) {
component := TabloListRow(TabloCardView{
ID: "11111111-1111-1111-1111-111111111111",

View file

@ -7,6 +7,7 @@ import (
tablomodel "xtablo-backend/internal/tablos"
"xtablo-backend/internal/web/dates"
"xtablo-backend/internal/web/tabloicons"
"github.com/a-h/templ"
)
@ -112,6 +113,7 @@ func OverviewProjectsFromTablos(tablos []tablomodel.Record) []TabloCardView {
projects := make([]TabloCardView, 0, len(tablos))
for _, tablo := range tablos {
statusLabel, statusTone, progress := overviewProjectStatus(tablo.Status)
presentation := tabloicons.ForColor(tablo.Color)
projects = append(projects, TabloCardView{
ID: tablo.ID.String(),
Name: tablo.Name,
@ -119,8 +121,9 @@ func OverviewProjectsFromTablos(tablos []tablomodel.Record) []TabloCardView {
Status: string(tablo.Status),
StatusLabel: statusLabel,
StatusTone: statusTone,
IconKind: presentation.Icon,
Initial: projectInitial(tablo.Name),
Accent: overviewProjectAccent(tablo.Name),
Accent: presentation.Accent,
CardDateLabel: dates.FormatFrenchDate(tablo.CreatedAt),
Progress: progress,
ProgressLabel: progressPercentLabel(progress),
@ -163,13 +166,21 @@ func sidebarPrimaryNavItems(activePath string) []sidebarNavItem {
}
}
func sidebarProjectItems() []sidebarProjectItem {
return []sidebarProjectItem{
{Href: "/tablos/hello", Label: "Hello", Icon: "bolt"},
{Href: "/tablos/atelier", Label: "Atelier Produit", Icon: "gem"},
{Href: "/tablos/arthur", Label: "Arthur Belleville", Icon: "bolt"},
{Href: "/tablos/equipe", Label: "Equipe Design", Icon: "bolt"},
func sidebarProjectItems(tablos []tablomodel.Record) []sidebarProjectItem {
limit := len(tablos)
if limit > 4 {
limit = 4
}
items := make([]sidebarProjectItem, 0, limit)
for _, tablo := range tablos[:limit] {
items = append(items, sidebarProjectItem{
Href: fmt.Sprintf("/tablos/%s", tablo.ID),
Label: tablo.Name,
Icon: tabloicons.ForColor(tablo.Color).Icon,
})
}
return items
}
func sidebarFooterNavItems(activePath string) []sidebarNavItem {
@ -233,17 +244,6 @@ func overviewProjectStatus(status tablomodel.Status) (string, string, int) {
}
}
func overviewProjectAccent(name string) string {
switch len(strings.TrimSpace(name)) % 3 {
case 1:
return "purple"
case 2:
return "red"
default:
return "blue"
}
}
func projectInitial(name string) string {
name = strings.TrimSpace(name)
if name == "" {

View file

@ -189,7 +189,11 @@ templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) {
</div>
<div class="project-card-title-row">
<div class="project-avatar">
<span>{ tablo.Initial }</span>
if tablo.IconKind != "" {
@ActionIcon(tablo.IconKind)
} else {
<span>{ tablo.Initial }</span>
}
</div>
<h4>{ tablo.Name }</h4>
</div>

View file

@ -569,33 +569,48 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div></div><div class=\"project-card-title-row\"><div class=\"project-avatar\"><span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div></div><div class=\"project-card-title-row\"><div class=\"project-avatar\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Initial)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 192, Col: 25}
if tablo.IconKind != "" {
templ_7745c5c3_Err = ActionIcon(tablo.IconKind).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Initial)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 195, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span></div><h4>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div><h4>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 194, Col: 19}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 198, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</h4></div><div class=\"project-date-row\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</h4></div><div class=\"project-date-row\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -603,46 +618,46 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CardDateLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 198, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 202, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</span></div><div class=\"project-progress\"><div class=\"project-progress-label\"><span>Progression:</span> <strong>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</span></div><div class=\"project-progress\"><div class=\"project-progress-label\"><span>Progression:</span> <strong>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 203, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 207, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</strong></div><div class=\"project-progress-track\"><div class=\"project-progress-bar\" style=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</strong></div><div class=\"project-progress-track\"><div class=\"project-progress-bar\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(progressInlineStyle(tablo.Progress))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 206, Col: 81}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 210, Col: 81}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\"></div></div></div></article>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"></div></div></div></article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -671,33 +686,33 @@ func TabloListRow(tablo TabloCardView) templ.Component {
templ_7745c5c3_Var27 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<tr class=\"border-t border-[#EAECF0] dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer\" style=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<tr class=\"border-t border-[#EAECF0] dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(projectColorVariableStyle(tablo.Color))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 213, Col: 179}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 217, Col: 179}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" data-project-filter-item data-project-name=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" data-project-filter-item data-project-name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(tablo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 213, Col: 237}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 217, Col: 237}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\"><td class=\"px-6 py-4\"><div class=\"flex items-center gap-3\"><div class=\"project-list-icon w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden [&>svg]:w-4 [&>svg]:h-4\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"><td class=\"px-6 py-4\"><div class=\"flex items-center gap-3\"><div class=\"project-list-icon w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden [&>svg]:w-4 [&>svg]:h-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -705,20 +720,20 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div><span class=\"font-medium text-gray-900 dark:text-gray-100 truncate\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</div><span class=\"font-medium text-gray-900 dark:text-gray-100 truncate\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 219, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 223, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</span></div></td><td class=\"px-6 py-4 whitespace-nowrap\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</span></div></td><td class=\"px-6 py-4 whitespace-nowrap\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -729,7 +744,7 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400\"><div class=\"flex items-center gap-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400\"><div class=\"flex items-center gap-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -740,39 +755,39 @@ func TabloListRow(tablo TabloCardView) templ.Component {
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CreatedAtLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 231, Col: 26}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 235, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</div></td><td class=\"px-6 py-4\"><div class=\"flex items-center gap-3\"><div class=\"flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 min-w-[80px]\"><div class=\"project-progress-bar h-2 rounded-full transition-all\" style=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</div></td><td class=\"px-6 py-4\"><div class=\"flex items-center gap-3\"><div class=\"flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 min-w-[80px]\"><div class=\"project-progress-bar h-2 rounded-full transition-all\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(progressInlineStyle(tablo.Progress))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 237, Col: 114}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 241, Col: 114}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\"></div></div><span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100 w-8 text-right\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"></div></div><span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100 w-8 text-right\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 239, Col: 109}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 243, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span></div></td><td class=\"px-6 py-4 text-right\"><div class=\"flex items-center justify-end gap-1\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span></div></td><td class=\"px-6 py-4 text-right\"><div class=\"flex items-center justify-end gap-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -784,7 +799,7 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</div></td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</div></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -845,7 +860,7 @@ func InitProjectFilterScript() templ.Component {
templ_7745c5c3_Var35 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<script>\n\t\t(function () {\n\t\t\tfunction normalizeProjectFilterValue(value) {\n\t\t\t\treturn (value || \"\").toLowerCase().trim();\n\t\t\t}\n\t\t\tfunction applyProjectFilter(root) {\n\t\t\t\tif (!root) return;\n\t\t\t\tvar input = root.querySelector(\"[data-project-filter-input]\");\n\t\t\t\tvar results = root.querySelector(\"[data-project-filter-results]\");\n\t\t\t\tvar empty = root.querySelector(\"[data-project-filter-empty]\");\n\t\t\t\tif (!input || !results || !empty) return;\n\t\t\t\tvar query = normalizeProjectFilterValue(input.value);\n\t\t\t\tvar visibleCount = 0;\n\t\t\t\troot.querySelectorAll(\"[data-project-filter-item]\").forEach(function (item) {\n\t\t\t\t\tvar name = normalizeProjectFilterValue(item.getAttribute(\"data-project-name\") || item.textContent);\n\t\t\t\t\tvar matches = query === \"\" || name.indexOf(query) !== -1;\n\t\t\t\t\titem.hidden = !matches;\n\t\t\t\t\tif (matches) {\n\t\t\t\t\t\titem.removeAttribute(\"hidden\");\n\t\t\t\t\t\tvisibleCount += 1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\titem.setAttribute(\"hidden\", \"\");\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tvar hasVisibleResults = visibleCount > 0;\n\t\t\t\tresults.hidden = !hasVisibleResults;\n\t\t\t\tempty.hidden = hasVisibleResults;\n\t\t\t\tif (hasVisibleResults) {\n\t\t\t\t\tresults.removeAttribute(\"hidden\");\n\t\t\t\t\tempty.setAttribute(\"hidden\", \"\");\n\t\t\t\t} else {\n\t\t\t\t\tresults.setAttribute(\"hidden\", \"\");\n\t\t\t\t\tempty.removeAttribute(\"hidden\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tfunction handleProjectFilterInput(event) {\n\t\t\t\tvar input = event.target.closest(\"[data-project-filter-input]\");\n\t\t\t\tif (!input) return;\n\t\t\t\tapplyProjectFilter(input.closest(\"[data-project-filter-root]\"));\n\t\t\t}\n\t\t\tif (!window.xtabloProjectFilterInitialized) {\n\t\t\t\tdocument.addEventListener(\"input\", handleProjectFilterInput);\n\t\t\t\twindow.xtabloProjectFilterInitialized = true;\n\t\t\t}\n\t\t\tdocument.querySelectorAll(\"[data-project-filter-root]\").forEach(applyProjectFilter);\n\t\t})();\n\t</script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<script>\n\t\t(function () {\n\t\t\tfunction normalizeProjectFilterValue(value) {\n\t\t\t\treturn (value || \"\").toLowerCase().trim();\n\t\t\t}\n\t\t\tfunction applyProjectFilter(root) {\n\t\t\t\tif (!root) return;\n\t\t\t\tvar input = root.querySelector(\"[data-project-filter-input]\");\n\t\t\t\tvar results = root.querySelector(\"[data-project-filter-results]\");\n\t\t\t\tvar empty = root.querySelector(\"[data-project-filter-empty]\");\n\t\t\t\tif (!input || !results || !empty) return;\n\t\t\t\tvar query = normalizeProjectFilterValue(input.value);\n\t\t\t\tvar visibleCount = 0;\n\t\t\t\troot.querySelectorAll(\"[data-project-filter-item]\").forEach(function (item) {\n\t\t\t\t\tvar name = normalizeProjectFilterValue(item.getAttribute(\"data-project-name\") || item.textContent);\n\t\t\t\t\tvar matches = query === \"\" || name.indexOf(query) !== -1;\n\t\t\t\t\titem.hidden = !matches;\n\t\t\t\t\tif (matches) {\n\t\t\t\t\t\titem.removeAttribute(\"hidden\");\n\t\t\t\t\t\tvisibleCount += 1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\titem.setAttribute(\"hidden\", \"\");\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tvar hasVisibleResults = visibleCount > 0;\n\t\t\t\tresults.hidden = !hasVisibleResults;\n\t\t\t\tempty.hidden = hasVisibleResults;\n\t\t\t\tif (hasVisibleResults) {\n\t\t\t\t\tresults.removeAttribute(\"hidden\");\n\t\t\t\t\tempty.setAttribute(\"hidden\", \"\");\n\t\t\t\t} else {\n\t\t\t\t\tresults.setAttribute(\"hidden\", \"\");\n\t\t\t\t\tempty.removeAttribute(\"hidden\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tfunction handleProjectFilterInput(event) {\n\t\t\t\tvar input = event.target.closest(\"[data-project-filter-input]\");\n\t\t\t\tif (!input) return;\n\t\t\t\tapplyProjectFilter(input.closest(\"[data-project-filter-root]\"));\n\t\t\t}\n\t\t\tif (!window.xtabloProjectFilterInitialized) {\n\t\t\t\tdocument.addEventListener(\"input\", handleProjectFilterInput);\n\t\t\t\twindow.xtabloProjectFilterInitialized = true;\n\t\t\t}\n\t\t\tdocument.querySelectorAll(\"[data-project-filter-root]\").forEach(applyProjectFilter);\n\t\t})();\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -874,7 +889,7 @@ func TabloListHead() templ.Component {
templ_7745c5c3_Var36 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<tr class=\"bg-gray-50 dark:bg-gray-800/80 border-b border-[#EAECF0] dark:border-gray-700\"><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Projet</th><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Statut</th><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Créé le</th><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Progression</th><th class=\"px-6 py-3 w-12\"></th></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<tr class=\"bg-gray-50 dark:bg-gray-800/80 border-b border-[#EAECF0] dark:border-gray-700\"><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Projet</th><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Statut</th><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Créé le</th><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Progression</th><th class=\"px-6 py-3 w-12\"></th></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -934,51 +949,51 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
templ_7745c5c3_Var38 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<form hx-post=\"/tablos\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" class=\"flex flex-col gap-4\"><input type=\"hidden\" name=\"view\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<form hx-post=\"/tablos\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" class=\"flex flex-col gap-4\"><input type=\"hidden\" name=\"view\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(vm.View)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 331, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 335, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\"> <input type=\"hidden\" name=\"status\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"> <input type=\"hidden\" name=\"status\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var40 string
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(vm.Status)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 332, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 336, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\"> <input type=\"hidden\" name=\"modal\" value=\"create\"> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\"> <input type=\"hidden\" name=\"modal\" value=\"create\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if vm.ErrorMessage != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<div class=\"mb-1 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<div class=\"mb-1 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(vm.ErrorMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 335, Col: 112}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 339, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1015,33 +1030,33 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "<div class=\"flex items-center justify-end gap-3\"><a href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<div class=\"flex items-center justify-end gap-3\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var42 templ.SafeURL
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(vm.CloseModalHref()))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 365, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 369, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\" hx-get=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.ResolveAttributeValue(vm.CloseModalHref())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 366, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 370, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var43)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-solid ui-button-neutral ui-button-md\">Annuler</a>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-solid ui-button-neutral ui-button-md\">Annuler</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1054,7 +1069,7 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</div></form>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1115,7 +1130,7 @@ func EditTabloColorField(vm TablosPageViewModel) templ.Component {
templ_7745c5c3_Var45 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<div class=\"flex items-center gap-3\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<div class=\"flex items-center gap-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1134,20 +1149,20 @@ func EditTabloColorField(vm TablosPageViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<input id=\"edit-tablo-color-picker\" type=\"color\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<input id=\"edit-tablo-color-picker\" type=\"color\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var46 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.ResolveAttributeValue(vm.FormColor)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 408, Col: 23}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 412, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var46)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" class=\"ui-input tablo-color-picker\" oninput=\"document.getElementById('edit-tablo-color').value=this.value\"></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" class=\"ui-input tablo-color-picker\" oninput=\"document.getElementById('edit-tablo-color').value=this.value\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1176,64 +1191,64 @@ func EditTabloModalBody(vm TablosPageViewModel) templ.Component {
templ_7745c5c3_Var47 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<form hx-post=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<form hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var48 string
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(vm.EditSubmitHref())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 417, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 421, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" class=\"flex flex-col gap-4\"><input type=\"hidden\" name=\"view\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" class=\"flex flex-col gap-4\"><input type=\"hidden\" name=\"view\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var49 string
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(vm.View)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 422, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 426, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\"> <input type=\"hidden\" name=\"status\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\"> <input type=\"hidden\" name=\"status\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var50 string
templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(vm.Status)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 423, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 427, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\"> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if vm.ErrorMessage != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<div class=\"mb-1 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<div class=\"mb-1 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var51 string
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(vm.ErrorMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 425, Col: 112}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 429, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1261,33 +1276,33 @@ func EditTabloModalBody(vm TablosPageViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<div class=\"flex items-center justify-end gap-3\"><a href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<div class=\"flex items-center justify-end gap-3\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var52 templ.SafeURL
templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(vm.CloseModalHref()))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 446, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 450, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\" hx-get=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var53 string
templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(vm.CloseModalHref())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 447, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 451, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-solid ui-button-neutral ui-button-md\">Annuler</a>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-solid ui-button-neutral ui-button-md\">Annuler</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1300,7 +1315,7 @@ func EditTabloModalBody(vm TablosPageViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "</div></form>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -1909,6 +1909,11 @@ td.text-right .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-danger
width: 3rem;
}
.project-avatar > svg {
height: 1.25rem;
width: 1.25rem;
}
.project-list-icon {
background: var(--project-color, var(--color-project-fallback));
color: var(--color-text-inverse);