diff --git a/docs/design-system/badges.html b/docs/design-system/badges.html index 8a9e0de..836f0a1 100644 --- a/docs/design-system/badges.html +++ b/docs/design-system/badges.html @@ -8,6 +8,6 @@ -

Design System

Badges

Semantic status labels for todo, in-progress, success, and destructive states.

Status set

The four semantic badge tones used across the app.

À faire
En cours
Terminé
Erreur
@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})
+

Design System

Badges

Semantic status labels for todo, in-progress, success, and destructive states.

Status set

The four semantic badge tones used across the app.

À faire
En cours
Terminé
Erreur
@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})
diff --git a/docs/design-system/buttons.html b/docs/design-system/buttons.html index a660e1f..3ab955f 100644 --- a/docs/design-system/buttons.html +++ b/docs/design-system/buttons.html @@ -8,7 +8,7 @@ -

Design System

Buttons

Primary, secondary, ghost, and destructive actions built from shared templ primitives.

Default solid action

Used for the main action in a page section or modal footer.

@ui.Button(ui.ButtonProps{
+

Design System

Buttons

Primary, secondary, ghost, and destructive actions built from shared templ primitives.

Default solid action

Used for the main action in a page section or modal footer.

@ui.Button(ui.ButtonProps{
 	Label:   "Nouveau projet",
 	Variant: ui.ButtonVariantDefault,
 	Size:    ui.SizeMD,
diff --git a/docs/design-system/cards.html b/docs/design-system/cards.html
index 386336d..4b7f232 100644
--- a/docs/design-system/cards.html
+++ b/docs/design-system/cards.html
@@ -8,7 +8,7 @@
   
 
 
-

Design System

Cards

Reusable bordered surfaces with optional header, body, and footer regions.

Surface card

Generic elevated surface with optional header and footer.

Header
Body
@ui.Card(ui.CardProps{
+

Design System

Cards

Reusable bordered surfaces with optional header, body, and footer regions.

Surface card

Generic elevated surface with optional header and footer.

Header
Body
@ui.Card(ui.CardProps{
 	Header: textComponent("Header"),
 	Body:   textComponent("Body"),
 	Footer: textComponent("Footer"),
diff --git a/docs/design-system/empty-states.html b/docs/design-system/empty-states.html
index 6d27b37..1aa9494 100644
--- a/docs/design-system/empty-states.html
+++ b/docs/design-system/empty-states.html
@@ -8,7 +8,7 @@
   
 
 
-

Design System

Empty States

Centered fallback messaging with optional icon and action.

Centered empty state

Used when a list has no rows yet and the next action should stay obvious.

Aucun projet trouvé

Créez votre premier projet

@ui.EmptyState(ui.EmptyStateProps{
+

Design System

Empty States

Centered fallback messaging with optional icon and action.

Centered empty state

Used when a list has no rows yet and the next action should stay obvious.

Aucun projet trouvé

Créez votre premier projet

@ui.EmptyState(ui.EmptyStateProps{
 	Title:       "Aucun projet trouvé",
 	Description: "Créez votre premier projet",
 	Icon:        ui.UIIcon("grid3x3"),
diff --git a/docs/design-system/form-fields.html b/docs/design-system/form-fields.html
index a397c96..8efee19 100644
--- a/docs/design-system/form-fields.html
+++ b/docs/design-system/form-fields.html
@@ -8,7 +8,7 @@
   
 
 
-

Design System

Form Fields

Labeled controls with optional hint and error messaging.

Field with validation

Wraps a control with label and inline error feedback.

Le nom est requis

@ui.FormField(ui.FormFieldProps{
+

Design System

Form Fields

Labeled controls with optional hint and error messaging.

Field with validation

Wraps a control with label and inline error feedback.

Le nom est requis

@ui.FormField(ui.FormFieldProps{
 	Label: "Nom",
 	For:   "catalog-name",
 	Field: ui.Input(ui.InputProps{
diff --git a/docs/design-system/icon-buttons.html b/docs/design-system/icon-buttons.html
index bb555d2..c2c632a 100644
--- a/docs/design-system/icon-buttons.html
+++ b/docs/design-system/icon-buttons.html
@@ -8,10 +8,17 @@
   
 
 
-

Design System

Icon Buttons

Compact icon-only actions for destructive and neutral controls.

Borderless destructive action

Used for delete controls inside project cards and list rows.

@ui.IconButton(ui.IconButtonProps{
+

Design System

Icon Buttons

Compact icon-only actions for destructive and neutral controls.

Borderless destructive action

Used for delete controls inside project cards and list rows.

@ui.IconButton(ui.IconButtonProps{
 	Label:   "Supprimer le projet",
 	Icon:    "trash",
-	Variant: ui.IconButtonVariantDangerGhost,
+	Variant: ui.IconButtonVariantDanger,
+	Tone:    ui.IconButtonToneGhost,
+	Type:    "button",
+})

Borderless neutral action

Used for lightweight edit or details actions inside cards and list rows.

@ui.IconButton(ui.IconButtonProps{
+	Label:   "Modifier le projet",
+	Icon:    "pencil",
+	Variant: ui.IconButtonVariantNeutral,
+	Tone:    ui.IconButtonToneGhost,
 	Type:    "button",
 })
diff --git a/docs/design-system/index.html b/docs/design-system/index.html index fc9632a..ec86913 100644 --- a/docs/design-system/index.html +++ b/docs/design-system/index.html @@ -8,6 +8,6 @@ -

Design System

Component Catalog

Static documentation generated from the same templ primitives used by the Go application.

+

Design System

Component Catalog

Static documentation generated from the same templ primitives used by the Go application.

diff --git a/docs/design-system/inputs.html b/docs/design-system/inputs.html index a1dc5c3..324f30a 100644 --- a/docs/design-system/inputs.html +++ b/docs/design-system/inputs.html @@ -8,7 +8,7 @@ -

Design System

Inputs

Shared single-line and multiline text controls.

Text input

Single-line input for names, titles, and short labels.

@ui.Input(ui.InputProps{
+

Design System

Inputs

Shared single-line and multiline text controls.

Text input

Single-line input for names, titles, and short labels.

@ui.Input(ui.InputProps{
 	Name:        "name",
 	Value:       "Projet Atlas",
 	Placeholder: "Nom du projet",
diff --git a/docs/design-system/modals.html b/docs/design-system/modals.html
index dffde7f..a543d12 100644
--- a/docs/design-system/modals.html
+++ b/docs/design-system/modals.html
@@ -8,7 +8,7 @@
   
 
 
-

Design System

Modals

Shared modal shell for focused create, edit, and confirm flows.

Create modal

Shared modal shell with a form body and action footer.

Créer un projet

@ui.Modal(ui.ModalProps{
+

Design System

Modals

Shared modal shell for focused create, edit, and confirm flows.

Create modal

Shared modal shell with a form body and action footer.

Créer un projet

@ui.Modal(ui.ModalProps{
 	Title: "Créer un projet",
 	Body: ui.FormField(...),
 	Actions: ui.Button(...),
diff --git a/docs/design-system/tables.html b/docs/design-system/tables.html
index 8d6f797..ae87212 100644
--- a/docs/design-system/tables.html
+++ b/docs/design-system/tables.html
@@ -8,7 +8,7 @@
   
 
 
-

Design System

Tables

Shared table shell for server-rendered list views.

List shell

Shared wrapper for server-rendered resource tables.

ProjetStatut
Table ViewEn cours
@ui.Table(ui.TableProps{
+

Design System

Tables

Shared table shell for server-rendered list views.

List shell

Shared wrapper for server-rendered resource tables.

ProjetStatut
Table ViewEn cours
@ui.Table(ui.TableProps{
 	Head: TabloListHead(),
 	Body: TabloListBody(tablos),
 })
diff --git a/docs/design-system/tokens.html b/docs/design-system/tokens.html index 02e7b03..1784085 100644 --- a/docs/design-system/tokens.html +++ b/docs/design-system/tokens.html @@ -8,6 +8,6 @@ -

Design System

Tokens

Semantic colors and status roles used by the Go design system.

Status tones

Shared semantic badges for info, warning, success, and danger states.

À faire
En cours
Terminé
Erreur
@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})
+

Design System

Tokens

Semantic colors and status roles used by the Go design system.

Status tones

Shared semantic badges for info, warning, success, and danger states.

À faire
En cours
Terminé
Erreur
@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})
diff --git a/docs/superpowers/specs/2026-05-10-go-backend-tablo-edit-color-design.md b/docs/superpowers/specs/2026-05-10-go-backend-tablo-edit-color-design.md index c49714d..bc4f6b5 100644 --- a/docs/superpowers/specs/2026-05-10-go-backend-tablo-edit-color-design.md +++ b/docs/superpowers/specs/2026-05-10-go-backend-tablo-edit-color-design.md @@ -33,7 +33,6 @@ This intentionally does not start the broader `status` deprecation effort. `stat - Task-derived status inference - Reworking the current search or filter model - Introducing custom JavaScript beyond the existing HTMX-driven pattern -- Adding color pickers, preset palettes, or browser-native advanced color UI - Building a `/tablos/:id` detail edit page **User Experience** @@ -46,9 +45,10 @@ On the Go backend `Mes Projets` page: - the modal lets the user update: - `name` - `color` +- the edit modal includes a color picker control bound to the same `color` value - `status` is not editable in the create or edit modal for this slice -The color is entered as a text value using a strict full hex format such as `#3B82F6`. +The stored color value remains a strict full hex string such as `#3B82F6`. **Data Model** @@ -152,6 +152,8 @@ Modal behavior: - create modal collects `name` and `color` - edit modal collects `name` and `color` +- edit modal includes a native color picker control, prefilled from the current tablo color +- the picker and color text input stay in sync so submissions always send one canonical `#RRGGBB` value - both modals render inline validation errors - cancel closes the modal and preserves current page state @@ -224,6 +226,13 @@ A pragmatic shape is: The exact struct layout can be chosen during implementation, but it should support both modal variants without duplicating page-state plumbing. +For the edit modal specifically, the view model should provide the current validated hex color so both: + +- the text input +- the native color picker + +can render from the same source of truth. + **Error Handling** Create or update validation failure: @@ -254,10 +263,11 @@ Repository coverage: Handler coverage: - `GET /tablos` create modal includes `color` field -- `GET /tablos/{id}/edit` renders prefilled `name` and `color` +- `GET /tablos/{id}/edit` renders prefilled `name`, `color`, and color picker value - `POST /tablos` rejects missing or invalid `color` - `POST /tablos/{id}` rejects missing or invalid `color` - `POST /tablos/{id}` updates visible name and color in returned HTML +- edit modal keeps color picker and submitted hex value aligned - grid card markup contains edit action before delete - list row markup contains edit action before delete @@ -266,6 +276,7 @@ HTML assertions should verify: - the edit trigger exists with the expected icon/button semantics - the edit trigger appears before delete in the rendered action area - the modal contains both `Nom du projet` and `Couleur` +- the edit modal contains a color picker control in addition to the hex color input - invalid hex values return a `422` response with inline feedback mentioning `#RRGGBB` **Implementation Notes** @@ -289,6 +300,7 @@ The feature is complete when: - clicking edit opens a modal for the selected tablo - the modal allows changing the tablo `name` - the modal allows changing the tablo `color` +- the edit modal includes a color picker for choosing the tablo color - create also accepts `color` - `color` only accepts full 6-digit hex values like `#3B82F6` - successful edits update the rendered project card or list row diff --git a/go-backend/cmd/designsystem/main_test.go b/go-backend/cmd/designsystem/main_test.go index ca0cef4..68e1342 100644 --- a/go-backend/cmd/designsystem/main_test.go +++ b/go-backend/cmd/designsystem/main_test.go @@ -22,6 +22,7 @@ func TestGenerateSiteWritesExpectedPages(t *testing.T) { "inputs.html", "form-fields.html", "modals.html", + "spacing.html", "tables.html", "empty-states.html", "cards.html", diff --git a/go-backend/internal/db/queries.sql b/go-backend/internal/db/queries.sql index b10df7c..116aff7 100644 --- a/go-backend/internal/db/queries.sql +++ b/go-backend/internal/db/queries.sql @@ -60,6 +60,7 @@ INSERT INTO public.tablos ( id, owner_id, name, + color, status, created_at, updated_at @@ -68,13 +69,14 @@ INSERT INTO public.tablos ( $2, $3, $4, + $5, now(), now() ) -RETURNING id, owner_id, name, status, created_at, updated_at, deleted_at; +RETURNING id, owner_id, name, color, status, created_at, updated_at, deleted_at; -- name: ListTablos :many -SELECT id, owner_id, name, status, created_at, updated_at, deleted_at +SELECT id, owner_id, name, color, status, created_at, updated_at, deleted_at FROM public.tablos WHERE owner_id = sqlc.arg(owner_id) AND deleted_at IS NULL @@ -86,6 +88,15 @@ WHERE owner_id = sqlc.arg(owner_id) ) ORDER BY created_at DESC; +-- name: UpdateTablo :execrows +UPDATE public.tablos +SET name = $3, + color = $4, + updated_at = now() +WHERE id = $1 + AND owner_id = $2 + AND deleted_at IS NULL; + -- name: SoftDeleteTablo :execrows UPDATE public.tablos SET deleted_at = now(), updated_at = now() diff --git a/go-backend/internal/db/repository.go b/go-backend/internal/db/repository.go index 2e7b32c..ae50ac0 100644 --- a/go-backend/internal/db/repository.go +++ b/go-backend/internal/db/repository.go @@ -24,6 +24,8 @@ type PostgresAuthRepository struct { queries *sqlcdb.Queries } +const defaultTabloColor = "#3B82F6" + func NewPostgresAuthRepository(ctx context.Context, databaseURL string) (*PostgresAuthRepository, error) { if databaseURL == "" { return nil, errors.New("DATABASE_URL is required") @@ -149,6 +151,7 @@ func (r *PostgresAuthRepository) CreateTablo(ctx context.Context, input tablomod ID: uuid.New(), OwnerID: input.OwnerID, Name: strings.TrimSpace(input.Name), + Color: storedTabloColor(input.Color), Status: string(input.Status), }) if err != nil { @@ -177,6 +180,22 @@ func (r *PostgresAuthRepository) ListTablos(ctx context.Context, input tablomode return tablos, nil } +func (r *PostgresAuthRepository) UpdateTablo(ctx context.Context, input tablomodel.UpdateInput) error { + rows, err := r.queries.UpdateTablo(ctx, sqlcdb.UpdateTabloParams{ + ID: input.ID, + OwnerID: input.OwnerID, + Name: strings.TrimSpace(input.Name), + Color: storedTabloColor(input.Color), + }) + if err != nil { + return err + } + if rows == 0 { + return tablomodel.ErrNotFound + } + return nil +} + func (r *PostgresAuthRepository) SoftDeleteTablo(ctx context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error { rows, err := r.queries.SoftDeleteTablo(ctx, sqlcdb.SoftDeleteTabloParams{ ID: tabloID, @@ -202,6 +221,14 @@ func nullableText(value string) pgtype.Text { return pgtype.Text{String: value, Valid: true} } +func storedTabloColor(value string) string { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return defaultTabloColor + } + return trimmed +} + func nullableStatus(value *tablomodel.Status) pgtype.Text { if value == nil { return pgtype.Text{} @@ -214,6 +241,7 @@ func mapTabloRecord(row sqlcdb.Tablo) tablomodel.Record { ID: row.ID, OwnerID: row.OwnerID, Name: row.Name, + Color: storedTabloColor(row.Color), Status: tablomodel.Status(row.Status), CreatedAt: row.CreatedAt.Time, UpdatedAt: row.UpdatedAt.Time, diff --git a/go-backend/internal/db/schema.sql b/go-backend/internal/db/schema.sql index 9370d34..58bc0f7 100644 --- a/go-backend/internal/db/schema.sql +++ b/go-backend/internal/db/schema.sql @@ -32,6 +32,7 @@ CREATE TABLE IF NOT EXISTS public.tablos ( id uuid PRIMARY KEY, owner_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, name text NOT NULL, + color text NOT NULL, status text NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), diff --git a/go-backend/internal/db/sqlc/models.go b/go-backend/internal/db/sqlc/models.go index 3d23e7d..d8ea423 100644 --- a/go-backend/internal/db/sqlc/models.go +++ b/go-backend/internal/db/sqlc/models.go @@ -31,6 +31,7 @@ type Tablo struct { ID uuid.UUID `db:"id"` OwnerID uuid.UUID `db:"owner_id"` Name string `db:"name"` + Color string `db:"color"` Status string `db:"status"` CreatedAt pgtype.Timestamptz `db:"created_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at"` diff --git a/go-backend/internal/db/sqlc/querier.go b/go-backend/internal/db/sqlc/querier.go index 1024771..c60deef 100644 --- a/go-backend/internal/db/sqlc/querier.go +++ b/go-backend/internal/db/sqlc/querier.go @@ -20,6 +20,7 @@ type Querier interface { GetSessionByToken(ctx context.Context, sessionToken string) (AuthSession, error) ListTablos(ctx context.Context, arg ListTablosParams) ([]Tablo, error) SoftDeleteTablo(ctx context.Context, arg SoftDeleteTabloParams) (int64, error) + UpdateTablo(ctx context.Context, arg UpdateTabloParams) (int64, error) } var _ Querier = (*Queries)(nil) diff --git a/go-backend/internal/db/sqlc/queries.sql.go b/go-backend/internal/db/sqlc/queries.sql.go index 9fa94aa..85adafd 100644 --- a/go-backend/internal/db/sqlc/queries.sql.go +++ b/go-backend/internal/db/sqlc/queries.sql.go @@ -90,6 +90,7 @@ INSERT INTO public.tablos ( id, owner_id, name, + color, status, created_at, updated_at @@ -98,16 +99,18 @@ INSERT INTO public.tablos ( $2, $3, $4, + $5, now(), now() ) -RETURNING id, owner_id, name, status, created_at, updated_at, deleted_at +RETURNING id, owner_id, name, color, status, created_at, updated_at, deleted_at ` type CreateTabloParams struct { ID uuid.UUID `db:"id"` OwnerID uuid.UUID `db:"owner_id"` Name string `db:"name"` + Color string `db:"color"` Status string `db:"status"` } @@ -116,6 +119,7 @@ func (q *Queries) CreateTablo(ctx context.Context, arg CreateTabloParams) (Tablo arg.ID, arg.OwnerID, arg.Name, + arg.Color, arg.Status, ) var i Tablo @@ -123,6 +127,7 @@ func (q *Queries) CreateTablo(ctx context.Context, arg CreateTabloParams) (Tablo &i.ID, &i.OwnerID, &i.Name, + &i.Color, &i.Status, &i.CreatedAt, &i.UpdatedAt, @@ -214,7 +219,7 @@ func (q *Queries) GetSessionByToken(ctx context.Context, sessionToken string) (A } const listTablos = `-- name: ListTablos :many -SELECT id, owner_id, name, status, created_at, updated_at, deleted_at +SELECT id, owner_id, name, color, status, created_at, updated_at, deleted_at FROM public.tablos WHERE owner_id = $1 AND deleted_at IS NULL @@ -246,6 +251,7 @@ func (q *Queries) ListTablos(ctx context.Context, arg ListTablosParams) ([]Tablo &i.ID, &i.OwnerID, &i.Name, + &i.Color, &i.Status, &i.CreatedAt, &i.UpdatedAt, @@ -281,3 +287,33 @@ func (q *Queries) SoftDeleteTablo(ctx context.Context, arg SoftDeleteTabloParams } return result.RowsAffected(), nil } + +const updateTablo = `-- name: UpdateTablo :execrows +UPDATE public.tablos +SET name = $3, + color = $4, + updated_at = now() +WHERE id = $1 + AND owner_id = $2 + AND deleted_at IS NULL +` + +type UpdateTabloParams struct { + ID uuid.UUID `db:"id"` + OwnerID uuid.UUID `db:"owner_id"` + Name string `db:"name"` + Color string `db:"color"` +} + +func (q *Queries) UpdateTablo(ctx context.Context, arg UpdateTabloParams) (int64, error) { + result, err := q.db.Exec(ctx, updateTablo, + arg.ID, + arg.OwnerID, + arg.Name, + arg.Color, + ) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} diff --git a/go-backend/internal/tablos/model.go b/go-backend/internal/tablos/model.go index db60831..c15bec6 100644 --- a/go-backend/internal/tablos/model.go +++ b/go-backend/internal/tablos/model.go @@ -21,6 +21,7 @@ type Record struct { ID uuid.UUID OwnerID uuid.UUID Name string + Color string Status Status CreatedAt time.Time UpdatedAt time.Time @@ -30,9 +31,17 @@ type Record struct { type CreateInput struct { OwnerID uuid.UUID Name string + Color string Status Status } +type UpdateInput struct { + ID uuid.UUID + OwnerID uuid.UUID + Name string + Color string +} + type ListInput struct { OwnerID uuid.UUID Query string diff --git a/go-backend/internal/web/handlers/auth.go b/go-backend/internal/web/handlers/auth.go index 0698240..bd8dbab 100644 --- a/go-backend/internal/web/handlers/auth.go +++ b/go-backend/internal/web/handlers/auth.go @@ -33,6 +33,7 @@ type AuthRepository interface { GetSessionByToken(ctx context.Context, token string) (Session, error) DeleteSessionByToken(ctx context.Context, token string) error CreateTablo(ctx context.Context, input CreateTabloInput) (TabloRecord, error) + UpdateTablo(ctx context.Context, input UpdateTabloInput) error ListTablos(ctx context.Context, input ListTablosInput) ([]TabloRecord, error) SoftDeleteTablo(ctx context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error } diff --git a/go-backend/internal/web/handlers/tablos.go b/go-backend/internal/web/handlers/tablos.go index 9688630..9dc6884 100644 --- a/go-backend/internal/web/handlers/tablos.go +++ b/go-backend/internal/web/handlers/tablos.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "strings" "time" @@ -16,6 +17,11 @@ import ( var ErrTabloNotFound = tablomodel.ErrNotFound +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 TabloStatus = tablomodel.Status const ( @@ -26,13 +32,15 @@ const ( type TabloRecord = tablomodel.Record type CreateTabloInput = tablomodel.CreateInput +type UpdateTabloInput = tablomodel.UpdateInput type ListTablosInput = tablomodel.ListInput type TablosPageState struct { - View string - Query string - Status string - ModalOpen bool + View string + Query string + Status string + ModalKind string + EditingTabloID string } func normalizeTabloQuery(query string) string { @@ -58,7 +66,7 @@ func parseTablosPageState(values interface { View: view, Query: strings.TrimSpace(values.Get("q")), Status: status, - ModalOpen: strings.TrimSpace(values.Get("modal")) == "create", + ModalKind: normalizedModalKind(strings.TrimSpace(values.Get("modal"))), } } @@ -78,6 +86,30 @@ func (s TablosPageState) statusFilter() *TabloStatus { } } +func normalizedModalKind(kind string) string { + switch kind { + case "create", "edit": + return kind + default: + return "" + } +} + +func normalizeTabloColor(raw string) (string, bool) { + color := strings.TrimSpace(raw) + if !tabloColorPattern.MatchString(color) { + return "", false + } + return strings.ToUpper(color), true +} + +func storedTabloColor(raw string) string { + if color, ok := normalizeTabloColor(raw); ok { + return color + } + return defaultTabloColor +} + func (h *AuthHandler) PostTablos() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user, ok := h.authenticatedUser(r.Context(), r) @@ -92,35 +124,153 @@ func (h *AuthHandler) PostTablos() http.HandlerFunc { } state := parseTablosPageState(r.Form) - state.ModalOpen = true + state.ModalKind = "create" name := strings.TrimSpace(r.FormValue("name")) if name == "" { - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, nil, name, "Le nom du projet est requis"), http.StatusUnprocessableEntity) + tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state) + if err != nil { + 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) + return + } + + color, ok := normalizeTabloColor(r.FormValue("color")) + if !ok { + tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state) + if err != nil { + 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) return } if _, err := h.repo.CreateTablo(r.Context(), CreateTabloInput{ OwnerID: user.ID, Name: name, + Color: color, Status: TabloStatusTodo, }); err != nil { http.Error(w, "failed to create tablo", http.StatusInternalServerError) return } - state.ModalOpen = false - tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{ - OwnerID: user.ID, - Query: state.Query, - Status: state.statusFilter(), - }) + state.ModalKind = "" + tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state) if err != nil { http.Error(w, "failed to list tablos", http.StatusInternalServerError) return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", ""), http.StatusOK) + renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) + } +} + +func (h *AuthHandler) GetEditTabloModal() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user, ok := h.authenticatedUser(r.Context(), r) + if !ok { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + tabloID, err := uuid.Parse(r.PathValue("tabloID")) + if err != nil { + http.Error(w, "invalid tablo id", http.StatusBadRequest) + return + } + + state := parseTablosPageState(r.URL.Query()) + state.ModalKind = "edit" + state.EditingTabloID = tabloID.String() + + tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state) + if err != nil { + http.Error(w, "failed to list tablos", http.StatusInternalServerError) + return + } + + tablo, ok := findTabloByID(tablos, tabloID) + if !ok { + http.Error(w, "tablo not found", http.StatusNotFound) + return + } + + renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, tablo.Name, tablo.Color, ""), http.StatusOK) + } +} + +func (h *AuthHandler) PostTabloUpdate() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + user, ok := h.authenticatedUser(r.Context(), r) + if !ok { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + + tabloID, err := uuid.Parse(r.PathValue("tabloID")) + if err != nil { + http.Error(w, "invalid tablo id", http.StatusBadRequest) + return + } + + if err := r.ParseForm(); err != nil { + http.Error(w, "invalid form payload", http.StatusBadRequest) + return + } + + state := parseTablosPageState(r.Form) + state.ModalKind = "edit" + state.EditingTabloID = tabloID.String() + + name := strings.TrimSpace(r.FormValue("name")) + if name == "" { + tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state) + if err != nil { + 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) + return + } + + color, colorOK := normalizeTabloColor(r.FormValue("color")) + if !colorOK { + tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state) + if err != nil { + 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) + return + } + + if err := h.repo.UpdateTablo(r.Context(), UpdateTabloInput{ + ID: tabloID, + OwnerID: user.ID, + Name: name, + Color: color, + }); err != nil { + if errors.Is(err, ErrTabloNotFound) { + http.Error(w, "tablo not found", http.StatusNotFound) + return + } + http.Error(w, "failed to update tablo", http.StatusInternalServerError) + return + } + + state.ModalKind = "" + state.EditingTabloID = "" + tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state) + if err != nil { + http.Error(w, "failed to list tablos", http.StatusInternalServerError) + return + } + + renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) } } @@ -148,17 +298,13 @@ func (h *AuthHandler) DeleteTablo() http.HandlerFunc { } state := parseTablosPageState(r.URL.Query()) - tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{ - OwnerID: user.ID, - Query: state.Query, - Status: state.statusFilter(), - }) + tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state) if err != nil { http.Error(w, "failed to list tablos", http.StatusInternalServerError) return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", ""), http.StatusOK) + renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) } } @@ -170,32 +316,47 @@ func (h *AuthHandler) renderTablosPage(w http.ResponseWriter, r *http.Request) { } state := parseTablosPageState(r.URL.Query()) - tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{ - OwnerID: user.ID, - Query: state.Query, - Status: state.statusFilter(), - }) + tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state) if err != nil { http.Error(w, "failed to list tablos", http.StatusInternalServerError) return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", ""), http.StatusOK) + renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) } -func tablosPageViewModel(user PublicUser, state TablosPageState, tablos []TabloRecord, formName string, errorMessage string) views.TablosPageViewModel { +func tablosPageViewModel(user PublicUser, state TablosPageState, tablos []TabloRecord, formName string, formColor string, errorMessage string) views.TablosPageViewModel { return views.NewTablosPageViewModel( user.DisplayName, state.View, state.Query, state.Status, - state.ModalOpen, + state.ModalKind, + state.EditingTabloID, formName, + formColor, errorMessage, buildTabloCardViews(tablos, state), ) } +func listTablosForState(ctx context.Context, repo AuthRepository, ownerID uuid.UUID, state TablosPageState) ([]TabloRecord, error) { + return repo.ListTablos(ctx, ListTablosInput{ + OwnerID: ownerID, + Query: state.Query, + Status: state.statusFilter(), + }) +} + +func findTabloByID(tablos []TabloRecord, targetID uuid.UUID) (TabloRecord, bool) { + for _, tablo := range tablos { + if tablo.ID == targetID { + return tablo, true + } + } + return TabloRecord{}, false +} + func renderTablosResponse(w http.ResponseWriter, r *http.Request, activePath string, vm views.TablosPageViewModel, statusCode int) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(statusCode) @@ -221,6 +382,7 @@ func (r *InMemoryAuthRepository) CreateTablo(_ context.Context, input CreateTabl ID: uuid.New(), OwnerID: input.OwnerID, Name: strings.TrimSpace(input.Name), + Color: storedTabloColor(input.Color), Status: input.Status, CreatedAt: now, UpdatedAt: now, @@ -258,6 +420,22 @@ func (r *InMemoryAuthRepository) ListTablos(_ context.Context, input ListTablosI return tablos, nil } +func (r *InMemoryAuthRepository) UpdateTablo(_ context.Context, input UpdateTabloInput) error { + r.mu.Lock() + defer r.mu.Unlock() + + tablo, ok := r.tablos[input.ID] + if !ok || tablo.OwnerID != input.OwnerID || tablo.DeletedAt != nil { + return ErrTabloNotFound + } + + tablo.Name = strings.TrimSpace(input.Name) + tablo.Color = storedTabloColor(input.Color) + tablo.UpdatedAt = time.Now().UTC() + r.tablos[input.ID] = tablo + return nil +} + func (r *InMemoryAuthRepository) SoftDeleteTablo(_ context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error { r.mu.Lock() defer r.mu.Unlock() @@ -293,6 +471,7 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta items = append(items, views.TabloCardView{ ID: tablo.ID.String(), Name: tablo.Name, + Color: storedTabloColor(tablo.Color), Status: string(tablo.Status), StatusLabel: statusLabel, StatusClass: statusClass, @@ -303,6 +482,7 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta ProgressLabel: fmt.Sprintf("%d%%", progress), DeleteURL: "/tablos/" + tablo.ID.String(), DeleteRequestURL: buildDeleteRequestURL("/tablos/"+tablo.ID.String(), state), + EditRequestURL: buildEditRequestURL("/tablos/"+tablo.ID.String()+"/edit", state), IconKind: iconKind, IconBgClass: bgClass, IconFgClass: fgClass, @@ -314,6 +494,14 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta } func buildDeleteRequestURL(path string, state TablosPageState) string { + return buildStatefulRequestURL(path, state) +} + +func buildEditRequestURL(path string, state TablosPageState) string { + return buildStatefulRequestURL(path, state) +} + +func buildStatefulRequestURL(path string, state TablosPageState) string { values := url.Values{} values.Set("view", state.View) values.Set("status", state.Status) diff --git a/go-backend/internal/web/handlers/tablos_test.go b/go-backend/internal/web/handlers/tablos_test.go index a23657c..03fd88b 100644 --- a/go-backend/internal/web/handlers/tablos_test.go +++ b/go-backend/internal/web/handlers/tablos_test.go @@ -127,6 +127,28 @@ func TestInMemoryTablosSoftDeleteRejectsDifferentOwner(t *testing.T) { } } +func TestInMemoryTablosCreatePersistsColor(t *testing.T) { + repo := NewInMemoryAuthRepository() + user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com") + if err != nil { + t.Fatalf("expected demo user, got error %v", err) + } + + created, err := repo.CreateTablo(context.Background(), CreateTabloInput{ + OwnerID: user.ID, + Name: "Roadmap", + Color: "#3B82F6", + Status: TabloStatusTodo, + }) + if err != nil { + t.Fatalf("create colored tablo: %v", err) + } + + if created.Color != "#3B82F6" { + t.Fatalf("expected color to persist, got %q", created.Color) + } +} + func TestGetTablosPageDefaultsToGridAndAllStatus(t *testing.T) { handler := newTestAuthHandler(t) sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") @@ -376,7 +398,7 @@ func TestGetTablosPageListViewUsesDirectTableIconMarkup(t *testing.T) { body := rec.Body.String() for _, want := range []string{ `class="flex items-center gap-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0">`, + `
`); err != nil { + return err + } + for _, component := range []templ.Component{ + ui.Button(ui.ButtonProps{ + Label: "Précédent", + Variant: ui.ButtonVariantNeutral, + Size: ui.SizeMD, + Type: "button", + }), + ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG}), + ui.Button(ui.ButtonProps{ + Label: "Suivant", + Variant: ui.ButtonVariantDefault, + Size: ui.SizeMD, + Type: "button", + }), + } { + if err := component.Render(ctx, w); err != nil { + return err + } + } + _, err := io.WriteString(w, `
`) + return err + }), + Snippet: `@ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG})`, + }, + { + Title: "Vertical spacing", + Description: "Use SpaceY to insert fixed vertical gaps between stacked blocks.", + Preview: componentFunc(func(ctx context.Context, w io.Writer) error { + if _, err := io.WriteString(w, `
`); err != nil { + return err + } + for _, component := range []templ.Component{ + ui.Card(ui.CardProps{ + Body: textComponent("Bloc 1"), + }), + ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD}), + ui.Card(ui.CardProps{ + Body: textComponent("Bloc 2"), + }), + } { + if err := component.Render(ctx, w); err != nil { + return err + } + } + _, err := io.WriteString(w, `
`) + return err + }), + Snippet: `@ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD})`, + }, + } +} + func tableExamples() []Example { return []Example{ { diff --git a/go-backend/internal/web/ui/catalog/pages.go b/go-backend/internal/web/ui/catalog/pages.go index bce8cf7..3d9f2bc 100644 --- a/go-backend/internal/web/ui/catalog/pages.go +++ b/go-backend/internal/web/ui/catalog/pages.go @@ -60,6 +60,12 @@ func Pages() []Page { Description: "Shared modal shell for focused create, edit, and confirm flows.", Examples: modalExamples(), }, + { + Slug: "spacing", + Title: "Spacing", + Description: "Fixed horizontal and vertical spacer primitives for composing gaps between UI components.", + Examples: spacingExamples(), + }, { Slug: "tables", Title: "Tables", diff --git a/go-backend/internal/web/ui/icon_button.templ b/go-backend/internal/web/ui/icon_button.templ index 1e1c510..bd7efe2 100644 --- a/go-backend/internal/web/ui/icon_button.templ +++ b/go-backend/internal/web/ui/icon_button.templ @@ -4,12 +4,13 @@ type IconButtonProps struct { Label string Icon string Variant IconButtonVariant + Tone IconButtonTone Type string Attrs templ.Attributes } templ IconButton(props IconButtonProps) { - } @@ -54,6 +55,11 @@ templ UIIcon(kind string) { + case "pencil": + case "trash": ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(kind) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/icon_button.templ`, Line: 66, Col: 34} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/icon_button.templ`, Line: 72, Col: 34} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/go-backend/internal/web/ui/ui_test.go b/go-backend/internal/web/ui/ui_test.go index 113921e..a01097d 100644 --- a/go-backend/internal/web/ui/ui_test.go +++ b/go-backend/internal/web/ui/ui_test.go @@ -40,7 +40,8 @@ func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) { component := IconButton(IconButtonProps{ Label: "Supprimer le projet", Icon: "trash", - Variant: IconButtonVariantDangerGhost, + Variant: IconButtonVariantDanger, + Tone: IconButtonToneGhost, Type: "button", }) @@ -50,6 +51,8 @@ func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) { `type="button"`, `aria-label="Supprimer le projet"`, `borderless-icon-button`, + `ui-icon-button-ghost`, + `ui-icon-button-danger`, `lucide-trash2`, } { if !strings.Contains(html, want) { @@ -58,6 +61,31 @@ func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) { } } +func TestIconButtonRendersBorderlessNeutralMarkup(t *testing.T) { + component := IconButton(IconButtonProps{ + Label: "Modifier le projet", + Icon: "pencil", + Variant: IconButtonVariantNeutral, + Tone: IconButtonToneGhost, + Type: "button", + }) + + html := renderToString(t, component) + + for _, want := range []string{ + `type="button"`, + `aria-label="Modifier le projet"`, + `borderless-icon-button`, + `ui-icon-button-ghost`, + `ui-icon-button-neutral`, + `M12 20h9`, + } { + if !strings.Contains(html, want) { + t.Fatalf("expected %q in %q", want, html) + } + } +} + func TestBadgeRendersSemanticStatusVariant(t *testing.T) { component := Badge(BadgeProps{ Label: "En cours", @@ -79,8 +107,8 @@ func TestBadgeRendersSemanticStatusVariant(t *testing.T) { func TestModalRendersShellStructure(t *testing.T) { component := Modal(ModalProps{ - Title: "Nouveau projet", - Body: textComponent("Body copy"), + Title: "Nouveau projet", + Body: textComponent("Body copy"), Actions: textComponent("Actions"), }) @@ -99,6 +127,38 @@ func TestModalRendersShellStructure(t *testing.T) { } } +func TestSpaceXRendersDefaultMediumMarkup(t *testing.T) { + component := SpaceX(SpaceProps{}) + + html := renderToString(t, component) + + for _, want := range []string{ + `aria-hidden="true"`, + `ui-space-x`, + `ui-space-x-md`, + } { + if !strings.Contains(html, want) { + t.Fatalf("expected %q in %q", want, html) + } + } +} + +func TestSpaceYRendersExplicitExtraLargeMarkup(t *testing.T) { + component := SpaceY(SpaceProps{Size: SpacingStepXL}) + + html := renderToString(t, component) + + for _, want := range []string{ + `aria-hidden="true"`, + `ui-space-y`, + `ui-space-y-xl`, + } { + if !strings.Contains(html, want) { + t.Fatalf("expected %q in %q", want, html) + } + } +} + func TestButtonUsesSharedTokenBackedClasses(t *testing.T) { component := Button(ButtonProps{ Label: "Create", @@ -134,7 +194,12 @@ func TestSharedSemanticClassesExistInStylesheet(t *testing.T) { `.ui-button-sm`, `.ui-badge-warning`, `.ui-modal-panel`, + `.ui-space-x-md`, + `.ui-space-y-md`, `.borderless-icon-button`, + `.ui-icon-button-solid.ui-icon-button-neutral`, + `.ui-icon-button-ghost.ui-icon-button-neutral`, + `.ui-icon-button-ghost.ui-icon-button-danger`, `.ui-button-soft.ui-button-danger`, } { if !strings.Contains(css, want) { diff --git a/go-backend/internal/web/ui/variants.go b/go-backend/internal/web/ui/variants.go index d754ac6..53950dd 100644 --- a/go-backend/internal/web/ui/variants.go +++ b/go-backend/internal/web/ui/variants.go @@ -28,8 +28,27 @@ const ( type IconButtonVariant string const ( - IconButtonVariantNeutral IconButtonVariant = "neutral" - IconButtonVariantDangerGhost IconButtonVariant = "danger-ghost" + IconButtonVariantNeutral IconButtonVariant = "neutral" + IconButtonVariantWarning IconButtonVariant = "warning" + IconButtonVariantSuccess IconButtonVariant = "success" + IconButtonVariantDanger IconButtonVariant = "danger" +) + +type IconButtonTone string + +const ( + IconButtonToneSolid IconButtonTone = "solid" + IconButtonToneGhost IconButtonTone = "ghost" +) + +type SpacingStep string + +const ( + SpacingStepXS SpacingStep = "xs" + SpacingStepSM SpacingStep = "sm" + SpacingStepMD SpacingStep = "md" + SpacingStepLG SpacingStep = "lg" + SpacingStepXL SpacingStep = "xl" ) type BadgeVariant string @@ -45,12 +64,14 @@ func buttonClass(variant ButtonVariant, tone ButtonTone, size Size) string { return "ui-button ui-button-" + string(normalizedButtonTone(tone)) + " ui-button-" + string(normalizedButtonVariant(variant)) + " ui-button-" + string(normalizedSize(size)) } -func iconButtonClass(variant IconButtonVariant) string { - switch variant { - case IconButtonVariantDangerGhost: - return "borderless-icon-button" +func iconButtonClass(variant IconButtonVariant, tone IconButtonTone) string { + normalizedVariant := normalizedIconButtonVariant(variant) + + switch normalizedIconButtonTone(tone) { + case IconButtonToneGhost: + return "borderless-icon-button ui-icon-button-ghost ui-icon-button-" + string(normalizedVariant) default: - return "ui-icon-button" + return "ui-icon-button ui-icon-button-solid ui-icon-button-" + string(normalizedVariant) } } @@ -58,6 +79,14 @@ func badgeClass(variant BadgeVariant) string { return "ui-badge ui-badge-" + string(normalizedBadgeVariant(variant)) } +func spaceXClass(step SpacingStep) string { + return "ui-space-x ui-space-x-" + string(normalizedSpacingStep(step)) +} + +func spaceYClass(step SpacingStep) string { + return "ui-space-y ui-space-y-" + string(normalizedSpacingStep(step)) +} + func normalizedSize(size Size) Size { switch size { case SizeSM, SizeLG: @@ -85,6 +114,33 @@ func normalizedButtonTone(tone ButtonTone) ButtonTone { } } +func normalizedIconButtonVariant(variant IconButtonVariant) IconButtonVariant { + switch variant { + case IconButtonVariantWarning, IconButtonVariantSuccess, IconButtonVariantDanger: + return variant + default: + return IconButtonVariantNeutral + } +} + +func normalizedIconButtonTone(tone IconButtonTone) IconButtonTone { + switch tone { + case IconButtonToneGhost: + return tone + default: + return IconButtonToneSolid + } +} + +func normalizedSpacingStep(step SpacingStep) SpacingStep { + switch step { + case SpacingStepXS, SpacingStepSM, SpacingStepLG, SpacingStepXL: + return step + default: + return SpacingStepMD + } +} + func normalizedBadgeVariant(variant BadgeVariant) BadgeVariant { switch variant { case BadgeVariantWarning, BadgeVariantSuccess, BadgeVariantDanger: diff --git a/go-backend/internal/web/views/dashboard_components.templ b/go-backend/internal/web/views/dashboard_components.templ index 12d8e57..2734eae 100644 --- a/go-backend/internal/web/views/dashboard_components.templ +++ b/go-backend/internal/web/views/dashboard_components.templ @@ -1,6 +1,7 @@ package views import "strconv" +import "xtablo-backend/internal/web/ui" templ DashboardPage(activePath string, content templ.Component) { @DashboardPageWithMainClass(activePath, "dashboard-main flex-1 overflow-auto", content) @@ -193,7 +194,13 @@ templ OverviewHeader(displayName string) {
Founder
- + @ui.Button(ui.ButtonProps{ + Label: "Se déconnecter", + Variant: ui.ButtonVariantDanger, + Tone: ui.ButtonToneSoft, + Size: ui.SizeMD, + Type: "submit", + })
@@ -220,7 +227,7 @@ templ OverviewProjectsSection(projects []TabloCardView) { for _, project := range hiddenOverviewProjects(projects) { @TabloGridCardWithAttrs(project, templ.Attributes{ "data-overview-project-hidden": "true", - "hidden": true, + "hidden": true, }) }
diff --git a/go-backend/internal/web/views/dashboard_components_templ.go b/go-backend/internal/web/views/dashboard_components_templ.go index f59a1ff..5cd09c0 100644 --- a/go-backend/internal/web/views/dashboard_components_templ.go +++ b/go-backend/internal/web/views/dashboard_components_templ.go @@ -9,6 +9,7 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import "strconv" +import "xtablo-backend/internal/web/ui" func DashboardPage(activePath string, content templ.Component) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { @@ -669,7 +670,7 @@ func AppSectionMainContent(title string, description string) templ.Component { var templ_7745c5c3_Var21 string templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 161, Col: 14} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 162, Col: 14} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { @@ -682,7 +683,7 @@ func AppSectionMainContent(title string, description string) templ.Component { var templ_7745c5c3_Var22 string templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 162, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 163, Col: 19} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { @@ -724,7 +725,7 @@ func NotFoundContent(displayName string) templ.Component { var templ_7745c5c3_Var24 string templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 182, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 183, Col: 48} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { @@ -766,7 +767,7 @@ func OverviewHeader(displayName string) templ.Component { var templ_7745c5c3_Var26 string templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardTodayLabel()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 190, Col: 50} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 191, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { @@ -779,13 +780,27 @@ func OverviewHeader(displayName string) templ.Component { var templ_7745c5c3_Var27 string templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 192, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 193, Col: 84} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "!
Founder
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "!
Founder
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ui.Button(ui.ButtonProps{ + Label: "Se déconnecter", + Variant: ui.ButtonVariantDanger, + Tone: ui.ButtonToneSoft, + Size: ui.SizeMD, + Type: "submit", + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -814,7 +829,7 @@ func OverviewActions(actions []quickAction) templ.Component { templ_7745c5c3_Var28 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -824,7 +839,7 @@ func OverviewActions(actions []quickAction) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -853,7 +868,7 @@ func OverviewProjectsSection(projects []TabloCardView) templ.Component { templ_7745c5c3_Var29 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "

Mes Projets

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "

Mes Projets

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -872,7 +887,7 @@ func OverviewProjectsSection(projects []TabloCardView) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -880,7 +895,7 @@ func OverviewProjectsSection(projects []TabloCardView) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -914,33 +929,33 @@ func SeeMoreProjects(hiddenCount int) templ.Component { } ctx = templ.ClearChildren(ctx) if hiddenCount > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " de plus
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -970,7 +985,7 @@ func OverviewProjectsScript() templ.Component { templ_7745c5c3_Var33 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -999,7 +1014,7 @@ func OverviewTasks(tasks []dashboardTask) templ.Component { templ_7745c5c3_Var34 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "

Mes Tâches

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

Mes Tâches

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1009,7 +1024,7 @@ func OverviewTasks(tasks []dashboardTask) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1038,7 +1053,7 @@ func QuickActionCard(action quickAction) templ.Component { templ_7745c5c3_Var35 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1106,7 +1121,7 @@ func TaskRow(task dashboardTask) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1128,7 +1143,7 @@ func TaskRow(task dashboardTask) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var43 string templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(task.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 331, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 338, Col: 18} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1173,7 +1188,7 @@ func TaskRow(task dashboardTask) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var46 string templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(task.ProjectKey) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 334, Col: 28} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 341, Col: 28} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var47 string templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(task.Project) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 336, Col: 50} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 343, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var48 string templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(task.Date) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 337, Col: 39} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 344, Col: 39} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1234,7 +1249,7 @@ func TaskRow(task dashboardTask) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var51 string templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(task.Status) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 340, Col: 75} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 347, Col: 75} } _, 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, 57, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1294,20 +1309,20 @@ func SidebarNavItem(item sidebarNavItem) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1401,20 +1416,20 @@ func SidebarNavItemOOB(item sidebarNavItem) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1503,20 +1518,20 @@ func SidebarProjectItem(item sidebarProjectItem) templ.Component { templ_7745c5c3_Var66 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1524,20 +1539,20 @@ func SidebarProjectItem(item sidebarProjectItem) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var68 string templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 375, Col: 50} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 382, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/go-backend/internal/web/views/home.go b/go-backend/internal/web/views/home.go index 35e60e9..de089b9 100644 --- a/go-backend/internal/web/views/home.go +++ b/go-backend/internal/web/views/home.go @@ -113,6 +113,7 @@ func OverviewProjectsFromTablos(tablos []tablomodel.Record) []TabloCardView { projects = append(projects, TabloCardView{ ID: tablo.ID.String(), Name: tablo.Name, + Color: strings.TrimSpace(tablo.Color), Status: string(tablo.Status), StatusLabel: statusLabel, StatusTone: statusTone, @@ -122,6 +123,7 @@ func OverviewProjectsFromTablos(tablos []tablomodel.Record) []TabloCardView { Progress: progress, ProgressLabel: progressPercentLabel(progress), DeleteRequestURL: "/tablos/" + tablo.ID.String(), + EditRequestURL: "/tablos/" + tablo.ID.String() + "/edit", }) } return projects diff --git a/go-backend/internal/web/views/tablos.templ b/go-backend/internal/web/views/tablos.templ index 7c87ffe..cbe9fd4 100644 --- a/go-backend/internal/web/views/tablos.templ +++ b/go-backend/internal/web/views/tablos.templ @@ -112,8 +112,12 @@ templ TablosPageContent(vm TablosPageViewModel) { }), }) } - if vm.ModalOpen { - @CreateTabloModal(vm) + if vm.HasModal() { + if vm.IsCreateModal() { + @CreateTabloModal(vm) + } else if vm.IsEditModal() { + @EditTabloModal(vm) + } }
} @@ -140,7 +144,8 @@ templ BorderlessDeleteButton(deleteRequestURL string) { @ui.IconButton(ui.IconButtonProps{ Label: "Supprimer le projet", Icon: "trash", - Variant: ui.IconButtonVariantDangerGhost, + Variant: ui.IconButtonVariantDanger, + Tone: ui.IconButtonToneGhost, Type: "button", Attrs: templ.Attributes{ "hx-delete": deleteRequestURL, @@ -151,21 +156,40 @@ templ BorderlessDeleteButton(deleteRequestURL string) { }) } +templ EditTabloButton(editRequestURL string) { + @ui.IconButton(ui.IconButtonProps{ + Label: "Modifier le projet", + Icon: "pencil", + Variant: ui.IconButtonVariantNeutral, + Tone: ui.IconButtonToneGhost, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": editRequestURL, + "hx-target": "#app-main-content", + "hx-swap": "outerHTML", + "hx-push-url": "true", + }, + }) +} + templ TabloGridCard(tablo TabloCardView) { @TabloGridCardWithAttrs(tablo, nil) } templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) { -
+
@ui.Badge(ui.BadgeProps{ Label: tablo.StatusLabel, Variant: badgeVariantForTone(tablo.StatusTone), }) - @BorderlessDeleteButton(tablo.DeleteRequestURL) +
+ @EditTabloButton(tablo.EditRequestURL) + @BorderlessDeleteButton(tablo.DeleteRequestURL) +
-
+
{ tablo.Initial }

{ tablo.Name }

@@ -180,17 +204,17 @@ templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) { { tablo.ProgressLabel }
-
+
} templ TabloListRow(tablo TabloCardView) { - +
-
svg]:w-4 [&>svg]:h-4 " + tablo.IconBgClass + " " + tablo.IconFgClass }> +
@ActionIcon(tablo.IconKind)
{ tablo.Name } @@ -211,13 +235,16 @@ templ TabloListRow(tablo TabloCardView) {
-
+
{ tablo.ProgressLabel }
- @BorderlessDeleteButton(tablo.DeleteRequestURL) +
+ @EditTabloButton(tablo.EditRequestURL) + @BorderlessDeleteButton(tablo.DeleteRequestURL) +
} @@ -269,7 +296,21 @@ templ CreateTabloModalBody(vm TablosPageViewModel) { Placeholder: "Nom du projet", Type: "text", }), - Error: vm.ErrorMessage, + }) + @ui.FormField(ui.FormFieldProps{ + Label: "Couleur", + For: "tablo-color", + Field: ui.Input(ui.InputProps{ + ID: "tablo-color", + Name: "color", + Value: vm.FormColor, + Placeholder: "#3B82F6", + Type: "text", + Attrs: templ.Attributes{ + "pattern": "^#[0-9A-Fa-f]{6}$", + "autocomplete": "off", + }, + }), })
} + +templ EditTabloModal(vm TablosPageViewModel) { + @ui.Modal(ui.ModalProps{ + Title: "Modifier le projet", + Body: EditTabloModalBody(vm), + }) +} + +templ EditTabloColorField(vm TablosPageViewModel) { +
+ @ui.Input(ui.InputProps{ + ID: "edit-tablo-color", + Name: "color", + Value: vm.FormColor, + Placeholder: "#3B82F6", + Type: "text", + Attrs: templ.Attributes{ + "pattern": "^#[0-9A-Fa-f]{6}$", + "autocomplete": "off", + "oninput": "document.getElementById('edit-tablo-color-picker').value=this.value", + }, + }) + +
+} + +templ EditTabloModalBody(vm TablosPageViewModel) { +
+ + + + if vm.ErrorMessage != "" { +
{ vm.ErrorMessage }
+ } + @ui.FormField(ui.FormFieldProps{ + Label: "Nom du projet", + For: "edit-tablo-name", + Field: ui.Input(ui.InputProps{ + ID: "edit-tablo-name", + Name: "name", + Value: vm.FormName, + Placeholder: "Nom du projet", + Type: "text", + }), + }) + @ui.FormField(ui.FormFieldProps{ + Label: "Couleur", + For: "edit-tablo-color", + Field: EditTabloColorField(vm), + Hint: "Utilisez le champ hex ou le sélecteur pour choisir une couleur au format #RRGGBB.", + }) +
+ + Annuler + + @ui.Button(ui.ButtonProps{ + Label: "Enregistrer", + Variant: ui.ButtonVariantDefault, + Size: ui.SizeMD, + Type: "submit", + }) +
+
+} diff --git a/go-backend/internal/web/views/tablos_templ.go b/go-backend/internal/web/views/tablos_templ.go index 51c5c0a..1b9ad4a 100644 --- a/go-backend/internal/web/views/tablos_templ.go +++ b/go-backend/internal/web/views/tablos_templ.go @@ -303,10 +303,17 @@ func TablosPageContent(vm TablosPageViewModel) templ.Component { return templ_7745c5c3_Err } } - if vm.ModalOpen { - templ_7745c5c3_Err = CreateTabloModal(vm).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err + if vm.HasModal() { + if vm.IsCreateModal() { + templ_7745c5c3_Err = CreateTabloModal(vm).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if vm.IsEditModal() { + templ_7745c5c3_Err = EditTabloModal(vm).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } } templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
") @@ -350,7 +357,7 @@ func StatusPill(vm TablosPageViewModel, status string, label string) templ.Compo var templ_7745c5c3_Var16 templ.SafeURL templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(vm.StatusHref(status))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 123, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 127, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -363,7 +370,7 @@ func StatusPill(vm TablosPageViewModel, status string, label string) templ.Compo var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vm.StatusHref(status)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 124, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 128, Col: 32} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { @@ -403,7 +410,7 @@ func StatusPill(vm TablosPageViewModel, status string, label string) templ.Compo var templ_7745c5c3_Var19 string templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 135, Col: 9} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 139, Col: 9} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { @@ -441,7 +448,8 @@ func BorderlessDeleteButton(deleteRequestURL string) templ.Component { templ_7745c5c3_Err = ui.IconButton(ui.IconButtonProps{ Label: "Supprimer le projet", Icon: "trash", - Variant: ui.IconButtonVariantDangerGhost, + Variant: ui.IconButtonVariantDanger, + Tone: ui.IconButtonToneGhost, Type: "button", Attrs: templ.Attributes{ "hx-delete": deleteRequestURL, @@ -457,6 +465,47 @@ func BorderlessDeleteButton(deleteRequestURL string) templ.Component { }) } +func EditTabloButton(editRequestURL string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var21 := templ.GetChildren(ctx) + if templ_7745c5c3_Var21 == nil { + templ_7745c5c3_Var21 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = ui.IconButton(ui.IconButtonProps{ + Label: "Modifier le projet", + Icon: "pencil", + Variant: ui.IconButtonVariantNeutral, + Tone: ui.IconButtonToneGhost, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": editRequestURL, + "hx-target": "#app-main-content", + "hx-swap": "outerHTML", + "hx-push-url": "true", + }, + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + func TabloGridCard(tablo TabloCardView) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -473,9 +522,9 @@ func TabloGridCard(tablo TabloCardView) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var21 := templ.GetChildren(ctx) - if templ_7745c5c3_Var21 == nil { - templ_7745c5c3_Var21 = templ.NopComponent + templ_7745c5c3_Var22 := templ.GetChildren(ctx) + if templ_7745c5c3_Var22 == nil { + templ_7745c5c3_Var22 = templ.NopComponent } ctx = templ.ClearChildren(ctx) templ_7745c5c3_Err = TabloGridCardWithAttrs(tablo, nil).Render(ctx, templ_7745c5c3_Buffer) @@ -502,12 +551,25 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var22 := templ.GetChildren(ctx) - if templ_7745c5c3_Var22 == nil { - templ_7745c5c3_Var22 = templ.NopComponent + templ_7745c5c3_Var23 := templ.GetChildren(ctx) + if templ_7745c5c3_Var23 == nil { + templ_7745c5c3_Var23 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -526,40 +588,26 @@ 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, 34, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = EditTabloButton(tablo.EditRequestURL).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } templ_7745c5c3_Err = BorderlessDeleteButton(tablo.DeleteRequestURL).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var23 = []any{"project-avatar " + projectAccentClass(tablo.Accent)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var25 string templ_7745c5c3_Var25, 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: 169, Col: 25} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 193, Col: 25} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { @@ -572,7 +620,7 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C var templ_7745c5c3_Var26 string templ_7745c5c3_Var26, 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: 171, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 195, Col: 19} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { @@ -593,7 +641,7 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C var templ_7745c5c3_Var27 string templ_7745c5c3_Var27, 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: 175, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 199, Col: 30} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { @@ -606,48 +654,26 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C var templ_7745c5c3_Var28 string templ_7745c5c3_Var28, 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: 180, Col: 33} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 204, Col: 33} } _, 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, 40, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -671,34 +697,25 @@ func TabloListRow(tablo TabloCardView) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var32 := templ.GetChildren(ctx) - if templ_7745c5c3_Var32 == nil { - templ_7745c5c3_Var32 = templ.NopComponent + templ_7745c5c3_Var30 := templ.GetChildren(ctx) + if templ_7745c5c3_Var30 == nil { + templ_7745c5c3_Var30 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "svg]:w-4 [&>svg]:h-4 " + tablo.IconBgClass + " " + tablo.IconFgClass} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var33...) + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, 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: 214, Col: 179} + } + _, 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, 45, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\">
svg]:w-4 [&>svg]:h-4\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -706,20 +723,20 @@ func TabloListRow(tablo TabloCardView) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var35 string - templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name) + var templ_7745c5c3_Var32 string + templ_7745c5c3_Var32, 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: 196, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 220, Col: 84} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) + _, 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, 48, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -730,7 +747,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, "
svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "
svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -738,42 +755,46 @@ func TabloListRow(tablo TabloCardView) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var36 string - templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CreatedAtLabel) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, 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: 208, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 232, Col: 26} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) + _, 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, 50, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var38 string - templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel) + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, 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: 216, Col: 109} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 240, Col: 109} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = EditTabloButton(tablo.EditRequestURL).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -781,7 +802,7 @@ func TabloListRow(tablo TabloCardView) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -805,9 +826,9 @@ func CreateTabloModal(vm TablosPageViewModel) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var39 := templ.GetChildren(ctx) - if templ_7745c5c3_Var39 == nil { - templ_7745c5c3_Var39 = templ.NopComponent + templ_7745c5c3_Var36 := templ.GetChildren(ctx) + if templ_7745c5c3_Var36 == nil { + templ_7745c5c3_Var36 = templ.NopComponent } ctx = templ.ClearChildren(ctx) templ_7745c5c3_Err = ui.Modal(ui.ModalProps{ @@ -837,12 +858,12 @@ func TabloListHead() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var40 := templ.GetChildren(ctx) - if templ_7745c5c3_Var40 == nil { - templ_7745c5c3_Var40 = templ.NopComponent + templ_7745c5c3_Var37 := templ.GetChildren(ctx) + if templ_7745c5c3_Var37 == nil { + templ_7745c5c3_Var37 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "ProjetStatutCréé leProgression") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "ProjetStatutCréé leProgression") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -866,9 +887,9 @@ func TabloListBody(tablos []TabloCardView) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var41 := templ.GetChildren(ctx) - if templ_7745c5c3_Var41 == nil { - templ_7745c5c3_Var41 = templ.NopComponent + templ_7745c5c3_Var38 := templ.GetChildren(ctx) + if templ_7745c5c3_Var38 == nil { + templ_7745c5c3_Var38 = templ.NopComponent } ctx = templ.ClearChildren(ctx) for _, tablo := range tablos { @@ -897,69 +918,69 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var42 := templ.GetChildren(ctx) - if templ_7745c5c3_Var42 == nil { - templ_7745c5c3_Var42 = templ.NopComponent + templ_7745c5c3_Var39 := templ.GetChildren(ctx) + if templ_7745c5c3_Var39 == nil { + templ_7745c5c3_Var39 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if vm.ErrorMessage != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var46 string - templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(vm.ErrorMessage) + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, 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: 260, Col: 112} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 287, Col: 112} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -974,38 +995,55 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component { Placeholder: "Nom du projet", Type: "text", }), - Error: vm.ErrorMessage, }).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
Annuler") + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(vm.CloseModalHref()) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 318, Col: 32} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" 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") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1018,7 +1056,266 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func EditTabloModal(vm TablosPageViewModel) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var46 := templ.GetChildren(ctx) + if templ_7745c5c3_Var46 == nil { + templ_7745c5c3_Var46 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = ui.Modal(ui.ModalProps{ + Title: "Modifier le projet", + Body: EditTabloModalBody(vm), + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func EditTabloColorField(vm TablosPageViewModel) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var47 := templ.GetChildren(ctx) + if templ_7745c5c3_Var47 == nil { + templ_7745c5c3_Var47 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ui.Input(ui.InputProps{ + ID: "edit-tablo-color", + Name: "color", + Value: vm.FormColor, + Placeholder: "#3B82F6", + Type: "text", + Attrs: templ.Attributes{ + "pattern": "^#[0-9A-Fa-f]{6}$", + "autocomplete": "off", + "oninput": "document.getElementById('edit-tablo-color-picker').value=this.value", + }, + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func EditTabloModalBody(vm TablosPageViewModel) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var49 := templ.GetChildren(ctx) + if templ_7745c5c3_Var49 == nil { + templ_7745c5c3_Var49 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if vm.ErrorMessage != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var54 string + templ_7745c5c3_Var54, 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: 378, Col: 112} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = ui.FormField(ui.FormFieldProps{ + Label: "Nom du projet", + For: "edit-tablo-name", + Field: ui.Input(ui.InputProps{ + ID: "edit-tablo-name", + Name: "name", + Value: vm.FormName, + Placeholder: "Nom du projet", + Type: "text", + }), + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ui.FormField(ui.FormFieldProps{ + Label: "Couleur", + For: "edit-tablo-color", + Field: EditTabloColorField(vm), + Hint: "Utilisez le champ hex ou le sélecteur pour choisir une couleur au format #RRGGBB.", + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "
Annuler") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = ui.Button(ui.ButtonProps{ + Label: "Enregistrer", + Variant: ui.ButtonVariantDefault, + Size: ui.SizeMD, + Type: "submit", + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/go-backend/internal/web/views/tablos_view.go b/go-backend/internal/web/views/tablos_view.go index 111dce8..6625902 100644 --- a/go-backend/internal/web/views/tablos_view.go +++ b/go-backend/internal/web/views/tablos_view.go @@ -5,50 +5,57 @@ import ( "net/url" "strings" + "github.com/a-h/templ" "xtablo-backend/internal/web/ui" ) type TabloCardView struct { - ID string - Name string - Status string - StatusLabel string - StatusClass string - StatusTone string - Progress int - CreatedAtLabel string - CardDateLabel string - ProgressLabel string - DeleteURL string + ID string + Name string + Color string + Status string + StatusLabel string + StatusClass string + StatusTone string + Progress int + CreatedAtLabel string + CardDateLabel string + ProgressLabel string + DeleteURL string DeleteRequestURL string - IconKind string - IconBgClass string - IconFgClass string - Accent string - Initial string + EditRequestURL string + IconKind string + IconBgClass string + IconFgClass string + Accent string + Initial string } type TablosPageViewModel struct { - DisplayName string - View string - Query string - Status string - ModalOpen bool - FormName string - ErrorMessage string - Tablos []TabloCardView + DisplayName string + View string + Query string + Status string + ModalKind string + EditingTabloID string + FormName string + FormColor string + ErrorMessage string + Tablos []TabloCardView } -func NewTablosPageViewModel(displayName string, view string, query string, status string, modalOpen bool, formName string, errorMessage string, tablos []TabloCardView) TablosPageViewModel { +func NewTablosPageViewModel(displayName string, view string, query string, status string, modalKind string, editingTabloID string, formName string, formColor string, errorMessage string, tablos []TabloCardView) TablosPageViewModel { return TablosPageViewModel{ - DisplayName: displayName, - View: normalizedView(view), - Query: strings.TrimSpace(query), - Status: normalizedStatus(status), - ModalOpen: modalOpen, - FormName: strings.TrimSpace(formName), - ErrorMessage: strings.TrimSpace(errorMessage), - Tablos: tablos, + DisplayName: displayName, + View: normalizedView(view), + Query: strings.TrimSpace(query), + Status: normalizedStatus(status), + ModalKind: normalizedModalKind(modalKind), + EditingTabloID: strings.TrimSpace(editingTabloID), + FormName: strings.TrimSpace(formName), + FormColor: normalizedFormColor(modalKind, formColor), + ErrorMessage: strings.TrimSpace(errorMessage), + Tablos: tablos, } } @@ -60,6 +67,18 @@ func (vm TablosPageViewModel) HasTablos() bool { return len(vm.Tablos) > 0 } +func (vm TablosPageViewModel) HasModal() bool { + return vm.ModalKind != "" +} + +func (vm TablosPageViewModel) IsCreateModal() bool { + return vm.ModalKind == "create" +} + +func (vm TablosPageViewModel) IsEditModal() bool { + return vm.ModalKind == "edit" +} + func (vm TablosPageViewModel) StatusHref(status string) string { values := vm.baseValues() values.Set("status", normalizedStatus(status)) @@ -94,11 +113,20 @@ func (vm TablosPageViewModel) CreateModalHref() string { return "/tablos?" + values.Encode() } +func (vm TablosPageViewModel) EditModalHref(tabloID string) string { + values := vm.baseValues() + return "/tablos/" + strings.TrimSpace(tabloID) + "/edit?" + values.Encode() +} + func (vm TablosPageViewModel) CloseModalHref() string { values := vm.baseValues() return "/tablos?" + values.Encode() } +func (vm TablosPageViewModel) EditSubmitHref() string { + return "/tablos/" + vm.EditingTabloID +} + func (vm TablosPageViewModel) HasSearch() bool { return vm.Query != "" } @@ -119,6 +147,26 @@ func normalizedStatus(status string) string { } } +func normalizedModalKind(kind string) string { + switch kind { + case "create", "edit": + return kind + default: + return "" + } +} + +func normalizedFormColor(modalKind string, color string) string { + trimmed := strings.TrimSpace(color) + if trimmed != "" { + return trimmed + } + if normalizedModalKind(modalKind) == "create" { + return "#3B82F6" + } + return "" +} + func (vm TablosPageViewModel) baseValues() url.Values { values := url.Values{} values.Set("view", vm.View) @@ -159,3 +207,11 @@ func badgeVariantForTone(tone string) ui.BadgeVariant { return ui.BadgeVariantInfo } } + +func projectColorVariableStyle(color string) templ.SafeCSS { + return templ.SanitizeCSS("--project-color", templ.SafeCSSProperty(strings.TrimSpace(color))) +} + +func backgroundColorStyle(color string) templ.SafeCSS { + return templ.SanitizeCSS("background-color", templ.SafeCSSProperty(strings.TrimSpace(color))) +} diff --git a/go-backend/router.go b/go-backend/router.go index 95c5d10..6038acb 100644 --- a/go-backend/router.go +++ b/go-backend/router.go @@ -38,6 +38,8 @@ func newRouterWithHandler(authHandler *handlers.AuthHandler) http.Handler { mux.Get("/files", authHandler.GetFilesPage()) mux.Get("/feedback", authHandler.GetFeedbackPage()) mux.Post("/tablos", authHandler.PostTablos()) + mux.Get("/tablos/{tabloID}/edit", authHandler.GetEditTabloModal()) + mux.Post("/tablos/{tabloID}", authHandler.PostTabloUpdate()) mux.Delete("/tablos/{tabloID}", authHandler.DeleteTablo()) mux.Get("/login", authHandler.GetLoginPage()) mux.Get("/signup", authHandler.GetSignupPage()) diff --git a/go-backend/static/styles.css b/go-backend/static/styles.css index 41ca139..1dd7aec 100644 --- a/go-backend/static/styles.css +++ b/go-backend/static/styles.css @@ -983,7 +983,7 @@ input { } .project-card-top { - align-items: flex-start; + align-items: center; display: flex; justify-content: space-between; margin-bottom: 1rem; @@ -1468,6 +1468,19 @@ input { display: inline-flex; } +.catalog-spacing-row { + align-items: center; + display: flex; + gap: 0; +} + +.catalog-spacing-column { + display: flex; + flex-direction: column; + gap: 0; + width: 100%; +} + .catalog-example-snippet { background: #111827; border-radius: 0.875rem; @@ -1511,7 +1524,6 @@ input { background: transparent; border: 0; border-radius: 0.5rem; - color: #6b7280; cursor: pointer; display: inline-flex; justify-content: center; @@ -1523,11 +1535,64 @@ input { color 0.2s ease; } -.ui-icon-button:hover { +.ui-icon-button-solid.ui-icon-button-neutral { + color: #6b7280; +} + +.ui-icon-button-solid.ui-icon-button-neutral:hover { background: #f9fafb; color: #111827; } +.ui-space-x { + display: inline-block; + flex-shrink: 0; +} + +.ui-space-y { + display: block; +} + +.ui-space-x-xs { + width: 0.25rem; +} + +.ui-space-x-sm { + width: 0.5rem; +} + +.ui-space-x-md { + width: 0.75rem; +} + +.ui-space-x-lg { + width: 1rem; +} + +.ui-space-x-xl { + width: 1.5rem; +} + +.ui-space-y-xs { + height: 0.25rem; +} + +.ui-space-y-sm { + height: 0.5rem; +} + +.ui-space-y-md { + height: 0.75rem; +} + +.ui-space-y-lg { + height: 1rem; +} + +.ui-space-y-xl { + height: 1.5rem; +} + .ui-modal-backdrop { align-items: center; background: rgba(17, 24, 39, 0.52); @@ -1587,16 +1652,24 @@ input { border: 0; box-shadow: none; appearance: none; - color: #9ca3af; cursor: pointer; outline: none; } +.ui-icon-button-ghost.ui-icon-button-neutral, +.ui-icon-button-ghost.ui-icon-button-danger { + color: #9ca3af; +} + .project-card-top .borderless-icon-button { padding: 0; } -.project-card-top .borderless-icon-button:hover { +.project-card-top .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-neutral:hover { + color: #111827; +} + +.project-card-top .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-danger:hover { color: #ef4444; } @@ -1621,7 +1694,11 @@ td.text-right .borderless-icon-button { transition: color 0.2s; } -td.text-right .borderless-icon-button:hover { +td.text-right .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-neutral:hover { + color: #111827; +} + +td.text-right .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-danger:hover { color: #ef4444; } @@ -1634,6 +1711,7 @@ td.text-right .borderless-icon-button:hover { .project-avatar { align-items: center; + background: var(--project-color, #3b82f6); border-radius: 0.85rem; color: #fff; display: inline-flex; @@ -1645,6 +1723,11 @@ td.text-right .borderless-icon-button:hover { width: 3rem; } +.project-list-icon { + background: var(--project-color, #3b82f6); + color: #fff; +} + .project-accent-blue { background: #3b82f6; } @@ -1697,10 +1780,17 @@ td.text-right .borderless-icon-button:hover { } .project-progress-bar { + background: var(--project-color, #3b82f6); border-radius: 999px; height: 100%; } +.tablo-color-picker { + max-width: 5rem; + min-height: 44px; + padding: 0.4rem; +} + .overview-more-row { display: flex; justify-content: center; diff --git a/go-backend/static/tailwind.css b/go-backend/static/tailwind.css index 6314adb..98b2ad0 100644 --- a/go-backend/static/tailwind.css +++ b/go-backend/static/tailwind.css @@ -7,7 +7,6 @@ --color-green-50: oklch(98.2% 0.018 155.826); --color-green-200: oklch(92.5% 0.084 155.995); --color-green-400: oklch(79.2% 0.209 151.711); - --color-green-500: oklch(72.3% 0.219 149.579); --color-green-600: oklch(62.7% 0.194 149.214); --color-green-800: oklch(44.8% 0.119 151.328); --color-green-950: oklch(26.6% 0.065 152.934); @@ -60,6 +59,9 @@ .absolute { position: absolute; } +.fixed { + position: fixed; +} .relative { position: relative; } @@ -199,6 +201,9 @@ .justify-end { justify-content: flex-end; } +.gap-1 { + gap: calc(var(--spacing) * 1); +} .gap-1\.5 { gap: calc(var(--spacing) * 1.5); } @@ -298,9 +303,6 @@ .bg-green-50 { background-color: var(--color-green-50); } -.bg-green-500 { - background-color: var(--color-green-500); -} .bg-purple-50 { background-color: var(--color-purple-50); }