diff --git a/app/routers/library.py b/app/routers/library.py
index 1be596c..1313f07 100644
--- a/app/routers/library.py
+++ b/app/routers/library.py
@@ -69,6 +69,7 @@ def _build_item(db_row: dict, uid: int = 1) -> dict:
"id": page_id,
"title": title,
"icon": icon,
+ "page_icon": db_row.get("page_icon") or "",
"is_folder": bool(db_row.get("is_folder", 0)),
"source_type": source_type,
"source_label": _source_label(source_type, workspace),
@@ -84,6 +85,7 @@ def _build_item(db_row: dict, uid: int = 1) -> dict:
"content_format": content_format,
"favorited": bool(db_row.get("favorited", 0)),
"share_mode": db_row.get("share_mode", "private"),
+ "tags": [],
}
@@ -98,7 +100,30 @@ def _apply_source_filter(query: str, params: list, source_type: str) -> tuple[st
def _rows_to_items(rows, uid: int = 1) -> list:
- return [_build_item(dict(r), uid) for r in rows]
+ items = [_build_item(dict(r), uid) for r in rows]
+ return _attach_tags(items)
+
+
+def _attach_tags(items: list) -> list:
+ """Batch-load page tags for library items."""
+ if not items:
+ return items
+ ids = [it["id"] for it in items]
+ placeholders = ",".join("?" for _ in ids)
+ with get_conn() as conn:
+ rows = conn.execute(
+ f"SELECT pt.page_id, t.id, t.name, t.color FROM page_tags pt "
+ f"JOIN tags t ON t.id = pt.tag_id WHERE pt.page_id IN ({placeholders})",
+ ids,
+ ).fetchall()
+ tag_map: dict = {}
+ for r in rows:
+ tag_map.setdefault(r["page_id"], []).append({
+ "id": r["id"], "name": r["name"], "color": r["color"],
+ })
+ for it in items:
+ it["tags"] = tag_map.get(it["id"], [])
+ return items
def _enrich_children(items: list) -> list:
@@ -124,7 +149,7 @@ def _enrich_children(items: list) -> list:
BASE_SELECT = (
"SELECT p.id, p.title, p.workspace, p.updated_at, p.content_format, "
- "p.parent_id, p.share_mode, p.parent_section"
+ "p.parent_id, p.share_mode, p.parent_section, p.page_icon"
)
@@ -361,6 +386,8 @@ async def library_local_workspace_children(item_id: int, request: Request):
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
"content_format": r["content_format"] or "file",
"favorited": False,
+ "page_icon": "",
+ "tags": [],
"size_display": _format_size(r["size"]) if r["size"] else "",
})
@@ -469,6 +496,8 @@ async def library_local_workspace(
"url": f"/local-workspace?folder={r['id']}" if is_folder else f"/pages/{r['id']}",
"content_format": r["content_format"] or "file",
"favorited": False,
+ "page_icon": "",
+ "tags": [],
"size_display": _format_size(r["size"]) if r["size"] else "",
})
diff --git a/app/templates/_ctx_menu.html b/app/templates/_ctx_menu.html
index d5fe7a9..d66cfcd 100644
--- a/app/templates/_ctx_menu.html
+++ b/app/templates/_ctx_menu.html
@@ -54,9 +54,27 @@
this.tagExistingOpen = false;
this.iconOpen = false;
this.newTagColor = (node && node.tags && node.tags[0] && node.tags[0].color) || '#787774';
- this.x = Math.max(4, Math.min((ev && ev.clientX) || 0, window.innerWidth - 240));
- this.y = Math.max(4, Math.min((ev && ev.clientY) || 0, window.innerHeight - 48));
+ var cx = (ev && ev.clientX) || 0;
+ var cy = (ev && ev.clientY) || 0;
+ this.x = Math.max(8, Math.min(cx, window.innerWidth - 240));
+ this.y = Math.max(8, Math.min(cy, window.innerHeight - 48));
this.open = true;
+ var self = this;
+ /* Mesure la taille réelle du menu (une fois rendu) puis le replace
+ pour qu'il reste TOUJOURS entièrement visible dans le viewport. */
+ var place = function () {
+ var el = document.querySelector('.fd-ctx-menu');
+ var w = el ? el.offsetWidth : 240;
+ var h = el ? el.offsetHeight : 320;
+ var vw = window.innerWidth, vh = window.innerHeight;
+ var nx = cx, ny = cy;
+ if (nx + w > vw - 8) nx = vw - w - 8;
+ if (ny + h > vh - 8) ny = vh - h - 8;
+ self.x = Math.max(8, nx);
+ self.y = Math.max(8, ny);
+ };
+ if (window.Alpine && window.Alpine.nextTick) window.Alpine.nextTick(place);
+ else setTimeout(place, 0);
},
close() {
this.open = false;
diff --git a/app/templates/library.html b/app/templates/library.html
index e8cba14..3628d14 100644
--- a/app/templates/library.html
+++ b/app/templates/library.html
@@ -88,6 +88,13 @@
.row-title:hover{text-decoration:underline;}
.row-title input{width:100%;font-size:13px;padding:2px 4px;background:var(--bg-primary);border:1px solid var(--accent);border-radius:4px;color:var(--text-primary);outline:none;}
+/* ── Tag chips (représentation + filtre) ── */
+.row-tags{display:flex;align-items:center;gap:3px;flex-shrink:0;overflow:hidden;max-width:45%;}
+.tag-chip{display:inline-flex;align-items:center;gap:3px;font-size:10px;line-height:1;padding:2px 6px;border-radius:999px;color:#fff;white-space:nowrap;border:none;cursor:pointer;font-family:inherit;}
+.tag-chip.active{outline:2px solid var(--text-primary);outline-offset:1px;}
+.lib-tag-filter{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:4px 16px 2px;}
+.lib-tag-filter .tf-label{font-size:11px;color:var(--text-dim);margin-right:2px;}
+
/* ── Shared direction sub-filter pils ── */
.lib-share-directory{display:flex;align-items:center;gap:6px;padding:4px 16px;}
.lib-dir-pill{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);cursor:pointer;transition:background 0.15s,color 0.15s,border-color 0.15s;}
@@ -271,6 +278,20 @@
+
+
+ Tags:
+
+
+
+
+
+
@@ -376,7 +397,7 @@
{{ fd_icon("file",14) }} Root (no parent)
-
+
@@ -393,7 +414,7 @@
-
+