Compare commits
5 Commits
v0.4.1
...
docs/workf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e33322e793 | ||
|
|
eb2adc9fdc | ||
|
|
4775cbf184 | ||
|
|
8be96bce0c | ||
|
|
fb97b8d4e3 |
@@ -18,7 +18,12 @@ framework:
|
|||||||
failed: 'doctrine://default?queue_name=failed&auto_setup=0'
|
failed: 'doctrine://default?queue_name=failed&auto_setup=0'
|
||||||
|
|
||||||
routing:
|
routing:
|
||||||
'App\Message\MailSyncRequested': async
|
# Sync à la demande (bouton « rafraîchir ») : exécutée pendant la requête HTTP
|
||||||
|
# pour que le re-fetch du front voie immédiatement les nouveaux mails, sans worker
|
||||||
|
# messenger:consume à maintenir. La sync de fond reste assurée par le cron OS
|
||||||
|
# (app:mail:sync, synchrone, indépendant du bus). Repasser à `async` + worker si
|
||||||
|
# la boîte grossit au point que la sync à la demande approche le timeout PHP.
|
||||||
|
'App\Message\MailSyncRequested': sync
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
framework:
|
framework:
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.4.1'
|
app.version: '0.4.3'
|
||||||
|
|||||||
182
docs/superpowers/specs/2026-05-20-workflow-ui-fixes-design.md
Normal file
182
docs/superpowers/specs/2026-05-20-workflow-ui-fixes-design.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# Specs — Correctifs UI suite au système de workflow + UX mail/modales
|
||||||
|
|
||||||
|
> Date : 2026-05-20
|
||||||
|
> Contexte : suite à l'introduction des **workflows** (`docs/superpowers/specs/2026-05-19-project-workflows-design.md`),
|
||||||
|
> plusieurs régressions UI et points UX sont apparus. Reviews faites par Lucile Schnödt et Tristan Schnödtin.
|
||||||
|
> Ce document liste les 7 chantiers à traiter, avec problème, fichiers concernés, solution validée et points ouverts.
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
|
||||||
|
| # | Chantier | Type | Décision |
|
||||||
|
|---|----------|------|----------|
|
||||||
|
| 1 | Drag & drop cassé dans « mes tâches » | régression workflow | Drop = changer de statut (gérer ambiguïté multi-statuts) |
|
||||||
|
| 2 | Sélecteur de statut mélange les workflows | régression workflow | Filtrer par le workflow du projet de la tâche |
|
||||||
|
| 3 | Cartes de tâche non responsive (tags) | UI | Refonte responsive + troncature |
|
||||||
|
| 4 | Couleurs de statut du workflow de base | data/UI | Remettre la palette classique |
|
||||||
|
| 5 | Bouton « lier un mail » dans l'onglet mail d'un ticket | UX mail | Supprimer le bouton |
|
||||||
|
| 6 | Création de ticket depuis un mail | UX mail | + sélecteur user, + sélecteur statut (remplace priorité), modale agrandie |
|
||||||
|
| 7 | Footer collant des modales centrées | UX global | Composant modale réutilisable (header / body scrollable / footer sticky) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Drag & drop cassé dans « mes tâches »
|
||||||
|
|
||||||
|
**Problème.** Le drag & drop des cartes entre colonnes ne fonctionne plus depuis l'arrivée des workflows.
|
||||||
|
|
||||||
|
**Fichiers.**
|
||||||
|
- `frontend/pages/my-tasks.vue` — colonnes kanban construites sur les **catégories canoniques** fixes (`CATEGORIES = ['todo','in_progress','blocked','review','done']`, ~l.118-127), template kanban ~l.396-424.
|
||||||
|
- `frontend/components/task/TaskCard.vue` — `@dragstart` / `@dragend` HTML5 natif (~l.154-162), `dataTransfer` = `task.id`.
|
||||||
|
- Pas de librairie externe (HTML5 natif).
|
||||||
|
|
||||||
|
**Cause racine.** Les colonnes sont des **catégories canoniques** (la vue agrège plusieurs projets/workflows, donc on ne peut pas afficher une colonne par statut d'un workflow précis). Or un workflow peut désormais mapper **plusieurs statuts sur une même catégorie** (ex. deux statuts « in_progress »). Au drop dans une colonne, le statut cible devient ambigu — ce qui a cassé / rendu indéterminé le changement de statut.
|
||||||
|
|
||||||
|
**Solution retenue.** Drop dans une colonne ⇒ change le statut de la tâche vers un statut de cette catégorie **dans le workflow du projet de la tâche**. Gestion de l'ambiguïté :
|
||||||
|
- Récupérer les statuts du workflow du projet de la tâche déposée, filtrés par la catégorie de la colonne cible.
|
||||||
|
- **0 statut** dans cette catégorie pour ce workflow → drop refusé (feedback visuel, pas de changement).
|
||||||
|
- **1 statut** → appliquer directement.
|
||||||
|
- **≥ 2 statuts** → afficher un mini-sélecteur (popover/menu) au point de drop pour choisir le statut exact.
|
||||||
|
|
||||||
|
**Points ouverts.**
|
||||||
|
- ⚠️ **À trancher demain** : confirmer la stratégie de désambiguïsation (popover au drop vs. choisir le statut de plus petite `position` dans la catégorie). Le popover est plus sûr mais plus de travail ; le « plus petite position » est transparent mais peut surprendre.
|
||||||
|
- Ajouter les **handlers de drop** sur les colonnes (`@dragover.prevent`, `@drop`) — actuellement absents dans `my-tasks.vue`.
|
||||||
|
- Comme la vue est multi-projets, la résolution du statut cible doit se faire **par tâche** (selon son projet → son workflow), pas globalement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Sélecteur de statut mélange les statuts de tous les workflows
|
||||||
|
|
||||||
|
**Problème.** En ouvrant une tâche (modale), le sélecteur « Statut » liste **tous les statuts globaux**, donc ceux du workflow de base **et** ceux des autres workflows.
|
||||||
|
|
||||||
|
**Fichiers.**
|
||||||
|
- `frontend/components/task/TaskModal.vue` — `<MalioSelect v-model="form.statusId" :options="statusOptions" />` (~l.103-109) ; `statusOptions = props.statuses.map(...)` (~l.674-676).
|
||||||
|
- `frontend/pages/my-tasks.vue` — `:statuses="statuses"` (~l.489), chargés via `statusService.getAll()` (~l.132).
|
||||||
|
- `frontend/services/task-statuses.ts` — `getAll()` → `GET /task_statuses` (tous les statuts).
|
||||||
|
- DTO : `frontend/services/dto/task-status.ts` — le champ `workflow` existe déjà (`{ '@id', id } | string`).
|
||||||
|
|
||||||
|
**Solution retenue.** Le sélecteur ne doit proposer que les statuts du **workflow du projet de la tâche éditée**.
|
||||||
|
- Filtrer `statusOptions` sur `status.workflow` correspondant au workflow du projet de la tâche.
|
||||||
|
- Source du filtre : soit filtrer côté front la liste déjà chargée par `workflow.id`, soit charger les statuts du workflow via le service workflow (`useWorkflowService()` existe mais n'est pas utilisé ici).
|
||||||
|
- S'assurer que le statut courant de la tâche reste affiché même si édge case (ex. statut hérité d'un ancien workflow).
|
||||||
|
|
||||||
|
**Points ouverts.**
|
||||||
|
- Vérifier que la tâche/le projet expose bien l'`@id`/`id` du workflow côté payload pour faire le filtre sans appel supplémentaire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Cartes de tâche non responsive (tags mal placés / trop gros)
|
||||||
|
|
||||||
|
**Problème.** Depuis l'ajout d'éléments dans la carte (statut, priorité, effort, tags, deadline, avatars…), les tags débordent ou se chevauchent quand les textes sont longs ; la carte n'est plus responsive.
|
||||||
|
|
||||||
|
**Fichiers.**
|
||||||
|
- `frontend/components/task/TaskCard.vue` — badges statut (~l.43-49) et tags (~l.58-64) : `rounded-full px-2 py-0.5 text-xs font-semibold text-white`, **sans `truncate`, sans `max-w`, sans `line-clamp`** ; conteneur `flex` sans contrainte de largeur (~l.42-106).
|
||||||
|
|
||||||
|
**Solution retenue.** Refonte du layout de la carte pour rester contenue quelle que soit la longueur des textes :
|
||||||
|
- Conteneur des badges en `flex flex-wrap gap-1` (retour à la ligne propre).
|
||||||
|
- Tags : `max-w-[...] truncate` (ou `line-clamp-1`) + `title`/tooltip pour le texte complet au survol.
|
||||||
|
- Hiérarchiser l'info : titre prioritaire (`line-clamp-2`), badges secondaires qui passent à la ligne ou se condensent.
|
||||||
|
- Option : limiter le nombre de tags affichés (ex. 2-3 + « +N »).
|
||||||
|
|
||||||
|
**Points ouverts.**
|
||||||
|
- ⚠️ Choix d'UX à valider : `flex-wrap` (tous les tags visibles, carte plus haute) vs. troncature « +N » (hauteur fixe). Décision visuelle à prendre demain (éventuellement via mockup).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Couleurs de statut du workflow de base à remettre
|
||||||
|
|
||||||
|
**Problème.** Les couleurs « classiques » des statuts du workflow de base ont changé ; il faut remettre les couleurs d'origine.
|
||||||
|
|
||||||
|
**Investigation faite.** Le commit `4775cbf` (palette élargie 9→18 teintes + couleur perso) ne touche **que** `ColorPicker.vue` et `ProjectDrawer.vue` — il n'a pas modifié les couleurs des statuts. Les couleurs d'origine du **workflow Standard** sont dans les fixtures :
|
||||||
|
|
||||||
|
| Catégorie | Statut | Hex classique |
|
||||||
|
|---|---|---|
|
||||||
|
| todo | À faire | `#222783` |
|
||||||
|
| in_progress | En cours | `#4A90D9` |
|
||||||
|
| blocked | Bloqué | `#C62828` |
|
||||||
|
| review | En attente de validation | `#FF8F00` |
|
||||||
|
| done | Terminé | `#26A69A` |
|
||||||
|
|
||||||
|
Source : `src/DataFixtures/AppFixtures.php:101-105` (statuts rattachés au `$standardWorkflow`, ~l.93-116).
|
||||||
|
Migration de rattachement : `migrations/Version20260519175114.php` (attache les statuts existants au workflow Standard).
|
||||||
|
|
||||||
|
**Solution retenue.** Remettre ces 5 couleurs sur les statuts du **workflow Standard/de base**.
|
||||||
|
- Vérifier en prod (et en base de dev si dérive) que les statuts du workflow Standard portent bien ces hex ; corriger ceux qui ont dérivé (via l'UI d'admin des statuts, ou un script/migration de correction des couleurs).
|
||||||
|
- Ne **pas** toucher aux couleurs des statuts des autres workflows.
|
||||||
|
|
||||||
|
**Points ouverts.**
|
||||||
|
- Confirmer où la dérive a eu lieu (prod vs. nouveaux workflows créés via l'UI avec d'autres couleurs). Si c'est un workflow Standard en prod avec des couleurs erronées → correction data ; si c'est par défaut à la création d'un workflow → ajuster les couleurs proposées par défaut.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Supprimer le bouton « lier un mail » dans l'onglet mail d'un ticket
|
||||||
|
|
||||||
|
**Problème.** En ouvrant un ticket, l'onglet mail propose un bouton « lier un mail » qui n'a pas d'utilité de ce côté.
|
||||||
|
|
||||||
|
**Fichiers.**
|
||||||
|
- `frontend/components/task/TaskModal.vue` — bouton « lier un mail » (~l.486-494, `mail.taskTab.linkButton`, ouvre `showMailPickerModal`) ; `<MailPickerModal>` (~l.498-503), visible si `isEditing && isMailUser`, onglet `mails`.
|
||||||
|
- Composant lié : `frontend/components/mail/MailPickerModal.vue`.
|
||||||
|
|
||||||
|
**Solution retenue.** Supprimer le bouton « lier un mail » de l'onglet mail du ticket.
|
||||||
|
- Retirer le bouton et, si plus aucun usage, le `MailPickerModal` associé + l'état `showMailPickerModal` + `handleMailLinked`.
|
||||||
|
- Nettoyer la clé i18n `mail.taskTab.linkButton` si elle n'est plus utilisée ailleurs.
|
||||||
|
|
||||||
|
**Points ouverts.**
|
||||||
|
- Vérifier que `MailPickerModal` n'est pas réutilisé ailleurs avant de le supprimer (sinon ne retirer que le bouton).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Création d'un ticket depuis un mail
|
||||||
|
|
||||||
|
**Problème.** Le formulaire de création depuis un mail est incomplet et la modale dépasse de l'écran.
|
||||||
|
|
||||||
|
**Fichiers.**
|
||||||
|
- `frontend/components/mail/MailCreateTaskModal.vue` — champs actuels (~l.119-224) : info source (lecture seule), Projet (requis), Groupe (optionnel), **Priorité** (optionnelle). Footer non sticky (~l.210-224).
|
||||||
|
- `frontend/services/mail.ts` — `createTaskFromMail()` (~l.184-192) → `POST /mail/messages/{id}/create-task` avec `{ projectId, taskGroupId, priority }`.
|
||||||
|
|
||||||
|
**Solution retenue.**
|
||||||
|
1. **Sélecteur de user (assigné).** Ajouter un sélecteur d'utilisateur. Un user par défaut est déjà appliqué ; le sélecteur permet d'en choisir un autre. (Charger la liste via le service users.)
|
||||||
|
2. **Statut à la place de la priorité.** **Retirer** le sélecteur de priorité, le **remplacer** par un sélecteur de statut. Le statut proposé doit respecter le workflow du projet sélectionné (cf. chantier #2 — réutiliser la même logique de filtrage par workflow). Au changement de projet, recharger les statuts du workflow correspondant.
|
||||||
|
3. **Agrandir la modale.** Élargir la largeur (et gérer la hauteur via body scrollable + footer sticky, cf. chantier #7) pour qu'elle ne dépasse plus.
|
||||||
|
4. **Backend.** Adapter le payload / l'endpoint `create-task` : accepter `assigneeId` (ou IRI user) et `statusId` (ou IRI statut) ; retirer/garder `priority` selon décision (ici : remplacé par statut côté UI — décider si on garde la priorité par défaut côté backend ou non).
|
||||||
|
|
||||||
|
**Points ouverts.**
|
||||||
|
- Confirmer le statut **par défaut** présélectionné (statut initial du workflow du projet).
|
||||||
|
- Décider si la priorité disparaît totalement du payload ou reste à une valeur par défaut côté backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Footer collant pour les modales centrées
|
||||||
|
|
||||||
|
**Problème.** Les modales qui s'ouvrent au milieu de l'écran ont leurs boutons d'action en bas, qui défilent avec le contenu et finissent hors écran. On veut un footer **toujours visible**.
|
||||||
|
|
||||||
|
**Constat.** Il n'existe **aucun composant modale réutilisable** (`MalioModal`/`AppModal`). Chaque modale réimplémente `Teleport` + `Transition` + header/body/footer. Footers actuels en `border-t` mais **non sticky** :
|
||||||
|
- `frontend/components/task/TaskModal.vue` (footer ~l.507-549)
|
||||||
|
- `frontend/components/mail/MailCreateTaskModal.vue` (~l.210-224)
|
||||||
|
- `frontend/components/mail/MailLinkTaskModal.vue` (~l.226-239)
|
||||||
|
- `frontend/components/mail/MailPickerModal.vue` (~l.187-201)
|
||||||
|
- `frontend/components/ui/ConfirmDeleteTaskModal.vue` (~l.11-24)
|
||||||
|
- `frontend/components/project/ProjectWorkflowSwitchModal.vue` (~l.63-76)
|
||||||
|
|
||||||
|
**Solution retenue (décision : composant réutilisable).** Créer un composant modale réutilisable dans `frontend/components/ui/` (ex. `AppModal.vue` / `MalioModal.vue` local) :
|
||||||
|
- Structure : `Teleport to="body"` + `Transition`, overlay `fixed inset-0`, conteneur avec **hauteur max** (`max-h-[90vh]`), en flex-col :
|
||||||
|
- **header** (titre + fermeture) fixe en haut,
|
||||||
|
- **body** `flex-1 overflow-y-auto`,
|
||||||
|
- **footer** sticky en bas (`shrink-0 border-t`), via slot `#footer`.
|
||||||
|
- Props : largeur (`sm`/`md`/`lg`/`xl`), titre, `v-model` ouverture ; slots `default` (body) et `footer`.
|
||||||
|
- **Migration progressive** des modales existantes vers ce composant (commencer par celles citées par les reviews : TaskModal, MailCreateTaskModal). Ne pas tout migrer d'un coup.
|
||||||
|
|
||||||
|
**Points ouverts.**
|
||||||
|
- Nom et emplacement définitifs du composant.
|
||||||
|
- Ordre de migration (au minimum : MailCreateTaskModal #6 et TaskModal #2/#5, qui sont déjà touchées par les autres chantiers).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Découpage suggéré pour l'implémentation
|
||||||
|
|
||||||
|
Regrouper par zone pour limiter les conflits :
|
||||||
|
|
||||||
|
- **Lot A — Workflow / statuts** : #2 (filtrage statut) → réutilisé par #1 (D&D) et #6 (statut à la création mail).
|
||||||
|
- **Lot B — Cartes** : #3 (responsive) + #4 (couleurs classiques).
|
||||||
|
- **Lot C — Mail** : #5 (suppr. bouton) + #6 (form création) — dépend de #2 et #7.
|
||||||
|
- **Lot D — Modale réutilisable** : #7 (composant) puis migration de TaskModal et MailCreateTaskModal.
|
||||||
|
|
||||||
|
Ordre conseillé : **#7 (composant modale)** et **#2 (filtrage statut)** d'abord (briques réutilisées), puis #1, #6, #5, puis #3/#4.
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('projects.editProject') : $t('projects.addProject')">
|
<MalioDrawer v-model="isOpen" :title="isEditing ? $t('projects.editProject') : $t('projects.addProject')">
|
||||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.code"
|
v-model="codeProxy"
|
||||||
label="Code"
|
label="Code"
|
||||||
input-class="w-full uppercase"
|
input-class="w-full"
|
||||||
|
:max-length="10"
|
||||||
:disabled="isEditing"
|
:disabled="isEditing"
|
||||||
:error="touched.code && !form.code.trim() ? 'Le code est requis' : touched.code && !/^[A-Z]{2,10}$/.test(form.code.trim()) ? '2 à 10 lettres majuscules' : ''"
|
:error="touched.code && !form.code ? 'Le code est requis' : touched.code && !/^[A-Z]{2,10}$/.test(form.code) ? '2 à 10 lettres majuscules' : ''"
|
||||||
@blur="touched.code = true"
|
@blur="touched.code = true"
|
||||||
@input="form.code = form.code.toUpperCase().replace(/[^A-Z]/g, '')"
|
|
||||||
/>
|
/>
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
@@ -186,6 +186,17 @@ const touched = reactive({
|
|||||||
name: false,
|
name: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Source unique de vérité : on sanitise dans le setter (majuscules, lettres
|
||||||
|
// uniquement, max 10) plutôt que via @input — sinon course entre la mutation
|
||||||
|
// manuelle et l'émission update:modelValue de MalioInputText, qui laissait
|
||||||
|
// form.code en minuscules et bloquait la création.
|
||||||
|
const codeProxy = computed({
|
||||||
|
get: () => form.code,
|
||||||
|
set: (value: string) => {
|
||||||
|
form.code = (value ?? '').toUpperCase().replace(/[^A-Z]/g, '').slice(0, 10)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const clientOptions = computed(() =>
|
const clientOptions = computed(() =>
|
||||||
props.clients.map(c => ({ label: c.name, value: c.id }))
|
props.clients.map(c => ({ label: c.name, value: c.id }))
|
||||||
)
|
)
|
||||||
@@ -222,7 +233,7 @@ async function handleSubmit() {
|
|||||||
touched.name = true
|
touched.name = true
|
||||||
touched.code = true
|
touched.code = true
|
||||||
if (!form.name.trim()) return
|
if (!form.name.trim()) return
|
||||||
if (!isEditing.value && (!form.code.trim() || !/^[A-Z]{2,10}$/.test(form.code.trim()))) return
|
if (!isEditing.value && !/^[A-Z]{2,10}$/.test(form.code)) return
|
||||||
|
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
@@ -254,7 +265,7 @@ async function handleSubmit() {
|
|||||||
if (isEditing.value && props.project) {
|
if (isEditing.value && props.project) {
|
||||||
await update(props.project.id, payload)
|
await update(props.project.id, payload)
|
||||||
} else {
|
} else {
|
||||||
payload.code = form.code.trim()
|
payload.code = form.code
|
||||||
await create(payload)
|
await create(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2 text-sm font-medium text-neutral-700">Couleur</p>
|
<p class="mb-2 text-sm font-medium text-neutral-700">Couleur</p>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<button
|
<button
|
||||||
v-for="color in colors"
|
v-for="color in presets"
|
||||||
:key="color"
|
:key="color"
|
||||||
type="button"
|
type="button"
|
||||||
class="h-10 w-10 rounded-full border-2 transition-transform hover:scale-110"
|
class="h-10 w-10 rounded-full border-2 transition-transform hover:scale-110"
|
||||||
:class="modelValue === color ? 'border-neutral-900 scale-110' : 'border-transparent'"
|
:class="isSelected(color) ? 'border-neutral-900 scale-110' : 'border-transparent'"
|
||||||
:style="{ backgroundColor: color }"
|
:style="{ backgroundColor: color }"
|
||||||
@click="emit('update:modelValue', color)"
|
:aria-label="`Choisir la couleur ${color}`"
|
||||||
|
@click="select(color)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Couleur personnalisée : input natif déguisé en pastille -->
|
||||||
|
<label
|
||||||
|
class="relative flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border-2 transition-transform hover:scale-110"
|
||||||
|
:class="isCustom ? 'border-neutral-900 scale-110' : 'border-dashed border-neutral-400'"
|
||||||
|
:style="isCustom ? { backgroundColor: modelValue } : {}"
|
||||||
|
title="Couleur personnalisée"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
class="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
||||||
|
:value="modelValue"
|
||||||
|
aria-label="Choisir une couleur personnalisée"
|
||||||
|
@input="select(($event.target as HTMLInputElement).value)"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
v-if="!isCustom"
|
||||||
|
name="mdi:plus"
|
||||||
|
class="text-neutral-500"
|
||||||
|
size="20"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -24,8 +47,26 @@ const emit = defineEmits<{
|
|||||||
(e: 'update:modelValue', value: string): void
|
(e: 'update:modelValue', value: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const colors = [
|
// Les 9 premières sont historiques (couleurs déjà en base) — ne pas réordonner
|
||||||
'#222783', '#26A69A', '#E91E63', '#4A90D9',
|
// pour que les projets/tags existants restent associés à une pastille.
|
||||||
'#7E57C2', '#8BC34A', '#FDD835', '#80DEEA', '#FF7043',
|
const presets = [
|
||||||
|
'#222783', '#26A69A', '#E91E63', '#4A90D9', '#7E57C2',
|
||||||
|
'#8BC34A', '#FDD835', '#80DEEA', '#FF7043', '#EF4444',
|
||||||
|
'#F97316', '#F59E0B', '#22C55E', '#10B981', '#06B6D4',
|
||||||
|
'#3B82F6', '#8B5CF6', '#64748B',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const norm = (value: string): string => (value ?? '').toUpperCase()
|
||||||
|
|
||||||
|
function isSelected(color: string): boolean {
|
||||||
|
return norm(props.modelValue) === norm(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCustom = computed(
|
||||||
|
() => !!props.modelValue && !presets.some((c) => norm(c) === norm(props.modelValue)),
|
||||||
|
)
|
||||||
|
|
||||||
|
function select(value: string): void {
|
||||||
|
emit('update:modelValue', norm(value))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user