feat: menu contextuel — tags avec couleurs + liste des tags disponibles
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- Section TAGS toujours visible (plus besoin de cliquer pour ouvrir un sous-menu)
- Tags existants affichés avec leur pastille de couleur (⦿ 10px) + bouton ✕
- Liste des tags DISPONIBLES (pas encore assignés) avec couleurs,
  clic → ajoute le tag à l'élément. Compteur d'utilisation affiché
- Palette 14 couleurs pour créer un nouveau tag (carrés 18px arrondis)
- Input pour nouveau tag avec palette de couleurs
- CSS: .ctx-section, .ctx-tag-color, .color-swatch-ctx, .ctx-add-tag-item
This commit is contained in:
2026-07-12 15:40:24 -04:00
parent 7fe7a91788
commit 97eda84cd1
+69 -24
View File
@@ -278,6 +278,12 @@
.ctx-tag-item{justify-content:space-between;}
.ctx-tag-add{color:var(--text-tertiary);font-size:12px;padding:4px 14px;cursor:pointer;}
.ctx-tag-add:hover{color:var(--accent);}
.ctx-section{padding:5px 14px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.5px;}
.ctx-tag-color{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
.color-swatch-ctx{width:18px;height:18px;border-radius:4px;cursor:pointer;border:2px solid transparent;transition:all 100ms;}
.color-swatch-ctx:hover{transform:scale(1.15);}
.color-swatch-ctx.selected{border-color:var(--text);box-shadow:0 0 0 2px var(--accent);}
.ctx-add-tag-item:hover{background:rgba(35,131,226,.08);}
.ctx-has-sub{justify-content:space-between;}
/* Old view-selector kept for no conflict, but hidden */
@@ -768,25 +774,43 @@
<div class="ctx-item" @click="ctxMenuRename()">✏️ Rename</div>
<div class="ctx-item" @click="ctxMenuDuplicate()">📋 Duplicate</div>
<div class="ctx-sep"></div>
<div class="ctx-item ctx-has-sub" @click.stop="ctxTagSub=!ctxTagSub">
🏷️ Tags
<span style="margin-left:auto">▸</span>
</div>
<div x-show="ctxTagSub" style="padding:4px 0;">
<template x-for="t in (ctxMenu.tags||[])" :key="'ctx-tag'+t.id">
<div class="ctx-item ctx-tag-item">
<span class="tag-chip" style="margin-right:6px;"><span x-text="t.name"></span></span>
<button class="tag-chip rm-btn" @click.stop="ctxRemoveTag(t.id)" title="Remove tag">×</button>
<div class="ctx-section">🏷️ Tags</div>
<!-- Current tags with remove button -->
<template x-for="t in (ctxMenu.tags||[])" :key="'ctx-tag'+t.id">
<div class="ctx-item ctx-tag-item">
<span class="ctx-tag-color" :style="{background: t.color}"></span>
<span style="flex:1;font-size:13px;" x-text="t.name"></span>
<button class="tag-chip rm-btn" @click.stop="ctxRemoveTag(t.id)" title="Remove">×</button>
</div>
</template>
<div x-show="(ctxMenu.tags||[]).length === 0" style="padding:2px 14px;font-size:11px;color:var(--text-dim);">No tags</div>
<div class="ctx-sep" x-show="ctxAvailTags.length > 0"></div>
<!-- Available tags to add -->
<div x-show="ctxAvailTags.length > 0" style="padding:0 0 4px;">
<div style="padding:2px 14px;font-size:11px;color:var(--text-dim);">Add tag:</div>
<template x-for="t in ctxAvailTags" :key="'ctx-avail'+t.id">
<div class="ctx-item ctx-add-tag-item" @click.stop="ctxAddExistingTag(t)">
<span class="ctx-tag-color" :style="{background: t.color}"></span>
<span style="flex:1;font-size:13px;" x-text="t.name"></span>
<span style="font-size:10px;color:var(--text-dim);" x-text="t.count||''"></span>
</div>
</template>
<div class="ctx-tag-add" x-show="!ctxTagAdding" @click.stop="ctxTagAdding=true">+ Add tag</div>
<div x-show="ctxTagAdding" style="display:flex;gap:4px;padding:4px 12px;">
<input class="tag-input" x-ref="ctxTagInput"
@keydown.enter="ctxAddTag($event.target.value); $event.target.value=''"
@keydown.escape="ctxTagAdding=false; ctxTagSub=false"
@click.stop
placeholder="tag name...">
</div>
</div>
<div class="ctx-tag-add" x-show="!ctxTagAdding" @click.stop="ctxTagAdding=true">
<span class="ctx-tag-color" style="background:var(--bg-tertiary);border:1px dashed var(--border);"></span>
+ New tag
</div>
<div x-show="ctxTagAdding" style="display:flex;align-items:center;gap:4px;padding:4px 14px;">
<template x-for="c in ctxTagColors" :key="c">
<div class="color-swatch-ctx" :class="{ selected: ctxNewTagColor === c }"
:style="{background: c}" @click.stop="ctxNewTagColor=c"></div>
</template>
<input class="tag-input" x-ref="ctxTagInput"
@keydown.enter="ctxAddNewTag($event.target.value); $event.target.value=''"
@keydown.escape="ctxTagAdding=false"
@click.stop
placeholder="tag name..."
style="margin-left:4px;">
</div>
<div class="ctx-sep"></div>
<div class="ctx-item ctx-danger" @click="ctxMenuDelete()">🗑 Delete</div>
@@ -874,8 +898,10 @@ document.addEventListener('alpine:init', function() {
// Context menu
ctxMenu: {visible:false,x:0,y:0,node:null,tags:[]},
ctxTagSub: false,
ctxTagAdding: false,
ctxAvailTags: [],
ctxNewTagColor: '#787774',
ctxTagColors: ['#787774','#E03E3E','#D9730D','#DFAB01','#0F7B6C','#0B6E99','#6940A5','#AD1A72','#E16259','#D4A72C','#448361','#337EA9','#9065B0','#C94D8B'],
// Tags
workspaceTags: [],
@@ -1236,8 +1262,16 @@ document.addEventListener('alpine:init', function() {
node: node,
tags: [...(node.tags||[])]
};
this.ctxTagSub = false;
this.ctxTagAdding = false;
this.ctxNewTagColor = '#787774';
// Load available tags (not already on the item)
this.ctxAvailTags = (this.workspaceTags || []).filter(function(t) {
return !(node.tags||[]).some(function(nt) { return nt.id === t.id; });
});
},
ctxAddExistingTag(tag) {
this.ctxAddNewTag(tag.name, tag.color);
},
ctxMenuOpenPage() {
@@ -1266,14 +1300,19 @@ document.addEventListener('alpine:init', function() {
var node = this.ctxMenu.node;
if (!node) return;
this.removeTag(node.id, tagId);
// Update ctxMenu tags
this.ctxMenu.tags = this.ctxMenu.tags.filter(function(t) { return t.id !== tagId; });
this.ctxTagSub = true; // keep sub open
// Refresh available tags
this.ctxAvailTags = (this.workspaceTags || []).filter(function(t) {
return !(this.ctxMenu.tags||[]).some(function(nt) { return nt.id === t.id; });
}.bind(this));
},
async ctxAddTag(tagName) {
async ctxAddNewTag(tagName, color) {
tagName = (tagName || '').trim();
if (!tagName) return;
var node = this.ctxMenu.node;
if (!node || !tagName) return;
if (!node) return;
color = color || this.ctxNewTagColor;
var r = await fetch('/api/local-workspace/items/' + node.id + '/tags', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({name: tagName.trim()})
@@ -1284,9 +1323,15 @@ document.addEventListener('alpine:init', function() {
this._updateNodeTags(this.tree, node.id, d.tag, 'add');
this.displayTree = [...this.tree];
this.loadWorkspaceTags();
// Refresh available tags
var self = this;
setTimeout(function() { self.loadWorkspaceTags().then(function() {
self.ctxAvailTags = (self.workspaceTags || []).filter(function(t) {
return !(self.ctxMenu.tags||[]).some(function(nt) { return nt.id === t.id; });
});
}); }, 200);
}
this.ctxTagAdding = false;
this.ctxTagSub = true;
},
_markTree(nodes, depth) {