- #7: Section « Récents » dans la page d'accueil vault (5 derniers fichiers) - #8: Git tag v1.8.0 - #9: Filtres search avancés created:/modified:/size: avec parsing dates/sizes - #10: Déduplication IGNORED_DIRS (indexer.py source unique, watcher+main importent) - ROADMAP: tous les items marqués FAIT, version → 1.9.0
This commit is contained in:
+21
-9
@@ -1,6 +1,6 @@
|
||||
# ObsiGate — Roadmap
|
||||
|
||||
> **Version :** 1.8.0 | **Dernière mise à jour :** 2026-06-03
|
||||
> **Version :** 1.9.0 | **Dernière mise à jour :** 2026-06-04
|
||||
> Voir aussi [CHANGELOG.md](./CHANGELOG.md), [docs/ROADMAP.md](./docs/ROADMAP.md), [AUDIT_TECHNIQUE.md](./docs/AUDIT_TECHNIQUE_2026-05-27.md)
|
||||
|
||||
---
|
||||
@@ -137,21 +137,33 @@
|
||||
- [x] **Menu contextuel** : clic droit sur un fichier → "Backups / Versions"
|
||||
- [x] **Coloration syntaxique diff** : lignes ajoutées en vert, supprimées en rouge, en-têtes hunk en accent
|
||||
|
||||
### 7. Fichiers récents par vault
|
||||
### ✅ 7. Fichiers récents par vault — FAIT
|
||||
- **Effort** : 0.5 jour | **Impact** : 🟡
|
||||
- Dashboard filtré par vault actif
|
||||
- Dashboard filtré par vault actif ✅
|
||||
- [x] **Section « Récents » dans la page d'accueil vault** : affiche les 5 derniers fichiers ouverts dans le vault via `/api/recent?vault=...`
|
||||
- [x] **Style compact** : liste en haut de la page racine du vault, avec titre, sous-dossier, et temps relatif
|
||||
- [x] **Click → preview, double-click → ouvrir**
|
||||
|
||||
### 8. Git tags semver
|
||||
### ✅ 8. Git tags semver — FAIT
|
||||
- **Effort** : 10 min | **Impact** : 🟢
|
||||
- `git tag v1.7.0 && git push --tags`
|
||||
- [x] `git tag v1.8.0 && git push --tags` ✅
|
||||
|
||||
### 9. Filtres search avancés
|
||||
### ✅ 9. Filtres search avancés — FAIT
|
||||
- **Effort** : 1 jour | **Impact** : 🟡
|
||||
- Opérateurs `created:`, `modified:`, `size:`
|
||||
- Opérateurs `created:`, `modified:`, `size:` ✅
|
||||
- **Fichiers** : `frontend/js/search.js` (QueryParser, SearchChips, performAdvancedSearch), `backend/search.py` (helpers + filters), `backend/main.py` (endpoint params)
|
||||
- [x] **`created:>2024-01-01`** / **`created:2024-01-01..2024-06-01`** — filtre par date de création
|
||||
- [x] **`modified:<7d`** / **`modified:>2024-01-01`** — filtre par date de modification (relatif ou absolu)
|
||||
- [x] **`size:>1MB`** / **`size:100KB..2MB`** — filtre par taille de fichier
|
||||
- [x] **Chips visuels** dans la barre de recherche pour chaque opérateur actif
|
||||
- [x] **Filtrage backend** via `_parse_date_range`, `_parse_size_range` avec support unités (B, KB, MB, GB) et dates relatives (7d, 30d, 24h)
|
||||
|
||||
### 10. Déduplication IGNORED_DIRS
|
||||
### ✅ 10. Déduplication IGNORED_DIRS — FAIT
|
||||
- **Effort** : 0.5 jour | **Impact** : 🟢
|
||||
- `indexer.py` et `watcher.py` ont des définitions séparées → mutualiser
|
||||
- `indexer.py`, `watcher.py` et `main.py` avaient des définitions séparées → mutualisé ✅
|
||||
- [x] **Source unique** : `IGNORED_DIRS` dans `backend/indexer.py` (configurable via `OBSIGATE_IGNORED_DIRS`)
|
||||
- [x] **`watcher.py`** : importe `IGNORED_DIRS` depuis `backend.indexer` (supprime ~20 lignes dupliquées)
|
||||
- [x] **`main.py`** : importe `IGNORED_DIRS` depuis `backend.indexer` (supprime la copie inline)
|
||||
|
||||
### ✅ 11. Indicateur AI Actif — FAIT
|
||||
- **Effort** : 0.5 jour | **Impact** : 🟡
|
||||
|
||||
+11
-4
@@ -36,6 +36,7 @@ from backend.indexer import (
|
||||
parse_markdown_file,
|
||||
_extract_tags,
|
||||
SUPPORTED_EXTENSIONS,
|
||||
IGNORED_DIRS,
|
||||
update_single_file,
|
||||
remove_single_file,
|
||||
handle_file_move,
|
||||
@@ -2445,6 +2446,9 @@ async def api_advanced_search(
|
||||
regex: bool = Query(False, description="Treat query as regex"),
|
||||
include_paths: Optional[str] = Query(None, description="Comma-separated glob patterns to include"),
|
||||
exclude_paths: Optional[str] = Query(None, description="Comma-separated glob patterns to exclude"),
|
||||
created: Optional[str] = Query(None, description="Created date filter (>date, <date, date..date)"),
|
||||
modified: Optional[str] = Query(None, description="Modified date filter (>date, <date, date..date, <Nd)"),
|
||||
size: Optional[str] = Query(None, description="Size filter (>size, <size, size..size, e.g. >1MB, <10KB)"),
|
||||
current_user=Depends(require_auth),
|
||||
):
|
||||
"""Advanced full-text search with TF-IDF scoring, facets, and pagination.
|
||||
@@ -2455,6 +2459,9 @@ async def api_advanced_search(
|
||||
- ``title:<text>`` — filter by title substring
|
||||
- ``path:<text>`` — filter by path substring
|
||||
- ``ext:<type>`` — filter by file extension
|
||||
- ``created:>2024-01-01`` — filter by creation date
|
||||
- ``modified:<7d`` or ``modified:2024-01-01..2024-06-01`` — filter by modification date
|
||||
- ``size:>1MB`` or ``size:100KB..1MB`` — filter by file size
|
||||
- Remaining text is scored using TF-IDF with accent normalization.
|
||||
- Toggles: case_sensitive, whole_word, regex
|
||||
- Path filters: include_paths, exclude_paths (glob patterns)
|
||||
@@ -2467,7 +2474,8 @@ async def api_advanced_search(
|
||||
partial(advanced_search, q, vault_filter=vault, tag_filter=tag,
|
||||
limit=limit, offset=offset, sort_by=sort,
|
||||
case_sensitive=case_sensitive, whole_word=whole_word, regex=regex,
|
||||
include_paths=include_paths, exclude_paths=exclude_paths),
|
||||
include_paths=include_paths, exclude_paths=exclude_paths,
|
||||
created=created, modified=modified, size=size),
|
||||
)
|
||||
|
||||
|
||||
@@ -3146,7 +3154,6 @@ async def api_vault_recent_files(
|
||||
raise HTTPException(status_code=404, detail=f"Directory not found: {dir}")
|
||||
|
||||
files = []
|
||||
ignored = {'.obsidian', '.trash', '.git', '.obsigate-backup', '__pycache__', 'node_modules'}
|
||||
dir_prefix = (dir or "").strip("/")
|
||||
|
||||
try:
|
||||
@@ -3156,7 +3163,7 @@ async def api_vault_recent_files(
|
||||
continue
|
||||
if entry.name.startswith('.'):
|
||||
continue
|
||||
if entry.name in ignored:
|
||||
if entry.name in IGNORED_DIRS:
|
||||
continue
|
||||
|
||||
# Skip entries whose parent dir chain contains an ignored dir (for rglob)
|
||||
@@ -3164,7 +3171,7 @@ async def api_vault_recent_files(
|
||||
skip = False
|
||||
try:
|
||||
for part in entry.relative_to(dir_path).parts[:-1]:
|
||||
if part.startswith('.') or part in ignored:
|
||||
if part.startswith('.') or part in IGNORED_DIRS:
|
||||
skip = True
|
||||
break
|
||||
except ValueError:
|
||||
|
||||
@@ -951,6 +951,146 @@ def _passes_path_filters(path: str, include: Optional[str], exclude: Optional[st
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Date and size filter helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _parse_date_range(raw: Optional[str]) -> Optional[tuple]:
|
||||
"""Parse a date range filter string.
|
||||
|
||||
Supported formats:
|
||||
- ">2024-01-01" → after (inclusive)
|
||||
- "<2024-06-01" → before (inclusive)
|
||||
- "2024-01-01..2024-06-01" → between (inclusive)
|
||||
- "<7d" → within last 7 days
|
||||
- ">30d" → older than 30 days
|
||||
- "YYYY-MM-DD" → exact date match (created/modified on that day)
|
||||
Returns (min_ts, max_ts) in Unix timestamps, or None if unparseable.
|
||||
"""
|
||||
if not raw:
|
||||
return None
|
||||
raw = raw.strip()
|
||||
|
||||
# Relative: <7d, >30d
|
||||
rel_match = re.match(r'^([<>])(\d+)([dh])$', raw)
|
||||
if rel_match:
|
||||
op, num, unit = rel_match.groups()
|
||||
n = int(num)
|
||||
seconds = n * 3600 if unit == 'h' else n * 86400
|
||||
now = time.time()
|
||||
if op == '<':
|
||||
return (now - seconds, now)
|
||||
else:
|
||||
return (0, now - seconds)
|
||||
|
||||
# Range: date1..date2
|
||||
if '..' in raw:
|
||||
parts = raw.split('..', 1)
|
||||
t1 = _parse_date_to_ts(parts[0].strip())
|
||||
t2 = _parse_date_to_ts(parts[1].strip())
|
||||
if t1 is not None and t2 is not None:
|
||||
return (min(t1, t2), max(t1, t2))
|
||||
return None
|
||||
|
||||
# Comparator: >date, <date
|
||||
if raw.startswith('>'):
|
||||
ts = _parse_date_to_ts(raw[1:].strip())
|
||||
return (ts, 9999999999) if ts is not None else None
|
||||
if raw.startswith('<'):
|
||||
ts = _parse_date_to_ts(raw[1:].strip())
|
||||
return (0, ts) if ts is not None else None
|
||||
|
||||
# Exact date
|
||||
ts = _parse_date_to_ts(raw)
|
||||
if ts is not None:
|
||||
return (ts, ts + 86400) # whole day
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _parse_date_to_ts(s: str) -> Optional[float]:
|
||||
"""Parse a date string to Unix timestamp. Supports YYYY-MM-DD."""
|
||||
s = s.strip()
|
||||
for fmt in ('%Y-%m-%d', '%Y/%m/%d', '%d/%m/%Y'):
|
||||
try:
|
||||
from datetime import datetime as dt_mod
|
||||
return dt_mod.strptime(s, fmt).timestamp()
|
||||
except ValueError:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _matches_date_range(file_ts: Optional[float], date_range: tuple) -> bool:
|
||||
"""Check if a file timestamp falls within the given range."""
|
||||
if file_ts is None:
|
||||
return False
|
||||
return date_range[0] <= file_ts <= date_range[1]
|
||||
|
||||
|
||||
def _parse_size_range(raw: Optional[str]) -> Optional[tuple]:
|
||||
"""Parse a size range filter string.
|
||||
|
||||
Supported formats:
|
||||
- ">1MB", ">500KB", ">1GB" → larger than
|
||||
- "<100KB" → smaller than
|
||||
- "500KB..2MB" → between
|
||||
Returns (min_bytes, max_bytes), or None if unparseable.
|
||||
"""
|
||||
if not raw:
|
||||
return None
|
||||
raw = raw.strip()
|
||||
|
||||
def _parse_size(s: str) -> Optional[int]:
|
||||
s = s.strip().upper()
|
||||
mult = 1
|
||||
if s.endswith('GB'):
|
||||
mult = 1024 * 1024 * 1024
|
||||
s = s[:-2]
|
||||
elif s.endswith('MB'):
|
||||
mult = 1024 * 1024
|
||||
s = s[:-2]
|
||||
elif s.endswith('KB'):
|
||||
mult = 1024
|
||||
s = s[:-2]
|
||||
elif s.endswith('B'):
|
||||
mult = 1
|
||||
s = s[:-1]
|
||||
elif s.endswith('O'):
|
||||
mult = 1
|
||||
s = s[:-1]
|
||||
try:
|
||||
return int(float(s) * mult)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
if '..' in raw:
|
||||
parts = raw.split('..', 1)
|
||||
lo = _parse_size(parts[0])
|
||||
hi = _parse_size(parts[1])
|
||||
if lo is not None and hi is not None:
|
||||
return (min(lo, hi), max(lo, hi))
|
||||
return None
|
||||
|
||||
if raw.startswith('>'):
|
||||
lo = _parse_size(raw[1:])
|
||||
return (lo, 10**15) if lo is not None else None
|
||||
if raw.startswith('<'):
|
||||
hi = _parse_size(raw[1:])
|
||||
return (0, hi) if hi is not None else None
|
||||
|
||||
# Exact: try parsing as a number (bytes) or with unit
|
||||
exact = _parse_size(raw)
|
||||
if exact is not None:
|
||||
return (exact, exact * 2) # fuzzy match
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _matches_size_range(file_size: int, size_range: tuple) -> bool:
|
||||
"""Check if a file size falls within the given range."""
|
||||
return size_range[0] <= file_size <= size_range[1]
|
||||
|
||||
|
||||
def advanced_search(
|
||||
query: str,
|
||||
vault_filter: str = "all",
|
||||
@@ -963,6 +1103,9 @@ def advanced_search(
|
||||
regex: bool = False,
|
||||
include_paths: Optional[str] = None,
|
||||
exclude_paths: Optional[str] = None,
|
||||
created: Optional[str] = None,
|
||||
modified: Optional[str] = None,
|
||||
size: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Advanced full-text search with TF-IDF scoring, facets, and pagination.
|
||||
|
||||
@@ -1074,6 +1217,28 @@ def advanced_search(
|
||||
)
|
||||
}
|
||||
|
||||
# Date and size filters (from query operators or API params)
|
||||
date_range_created = _parse_date_range(created or parsed.get("created"))
|
||||
if date_range_created:
|
||||
candidates = {
|
||||
dk for dk in candidates
|
||||
if _matches_date_range(inv.doc_info[dk].get("created"), date_range_created)
|
||||
}
|
||||
|
||||
date_range_modified = _parse_date_range(modified or parsed.get("modified"))
|
||||
if date_range_modified:
|
||||
candidates = {
|
||||
dk for dk in candidates
|
||||
if _matches_date_range(inv.doc_info[dk].get("modified"), date_range_modified)
|
||||
}
|
||||
|
||||
size_range = _parse_size_range(size or parsed.get("size"))
|
||||
if size_range:
|
||||
candidates = {
|
||||
dk for dk in candidates
|
||||
if _matches_size_range(inv.doc_info[dk].get("size", 0), size_range)
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Step 3: Score only the candidates (not all N documents)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
+1
-16
@@ -8,25 +8,10 @@ from typing import Callable, Dict, List, Optional
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from backend.indexer import SUPPORTED_EXTENSIONS
|
||||
from backend.indexer import SUPPORTED_EXTENSIONS, IGNORED_DIRS
|
||||
|
||||
logger = logging.getLogger("obsigate.watcher")
|
||||
|
||||
# Extensions de fichiers surveillées
|
||||
# Default ignored directories (can be overridden via OBSIGATE_IGNORED_DIRS env var)
|
||||
_DEFAULT_IGNORED_DIRS = {'.obsidian', '.trash', '.git', '__pycache__', 'node_modules', '.obsigate-backup'}
|
||||
|
||||
def _load_ignored_dirs() -> set:
|
||||
"""Load ignored directories from environment or use defaults."""
|
||||
env_val = os.environ.get("OBSIGATE_IGNORED_DIRS", "")
|
||||
if env_val:
|
||||
custom = set(d.strip() for d in env_val.split(",") if d.strip())
|
||||
logger.info(f"Using custom IGNORED_DIRS: {custom}")
|
||||
return custom
|
||||
return _DEFAULT_IGNORED_DIRS.copy()
|
||||
|
||||
IGNORED_DIRS = _load_ignored_dirs()
|
||||
|
||||
|
||||
class VaultEventHandler(FileSystemEventHandler):
|
||||
"""Gestionnaire d'événements filesystem pour une vault Obsidian.
|
||||
|
||||
+23
-1
@@ -56,7 +56,8 @@ export const SearchHistory = {
|
||||
// ---------------------------------------------------------------------------
|
||||
export const QueryParser = {
|
||||
parse(raw) {
|
||||
const result = { tags: [], vault: null, title: null, path: null, ext: null, freeText: "" };
|
||||
const result = { tags: [], vault: null, title: null, path: null, ext: null,
|
||||
created: null, modified: null, size: null, freeText: "" };
|
||||
if (!raw) return result;
|
||||
const tokens = this._tokenize(raw);
|
||||
const freeTokens = [];
|
||||
@@ -75,6 +76,12 @@ export const QueryParser = {
|
||||
result.path = tok.slice(5).replace(/"/g, "").trim();
|
||||
} else if (lower.startsWith("ext:")) {
|
||||
result.ext = tok.slice(4).replace(/"/g, "").trim().replace(/^\./, "").toLowerCase();
|
||||
} else if (lower.startsWith("created:")) {
|
||||
result.created = tok.slice(8).replace(/"/g, "").trim();
|
||||
} else if (lower.startsWith("modified:")) {
|
||||
result.modified = tok.slice(9).replace(/"/g, "").trim();
|
||||
} else if (lower.startsWith("size:")) {
|
||||
result.size = tok.slice(5).replace(/"/g, "").trim();
|
||||
} else {
|
||||
freeTokens.push(tok);
|
||||
}
|
||||
@@ -434,6 +441,18 @@ export const SearchChips = {
|
||||
this._addChip("ext", `ext:${parsed.ext}`, parsed.ext);
|
||||
hasChips = true;
|
||||
}
|
||||
if (parsed.created) {
|
||||
this._addChip("date", `created:${parsed.created}`, `créé ${parsed.created}`);
|
||||
hasChips = true;
|
||||
}
|
||||
if (parsed.modified) {
|
||||
this._addChip("date", `modified:${parsed.modified}`, `modifié ${parsed.modified}`);
|
||||
hasChips = true;
|
||||
}
|
||||
if (parsed.size) {
|
||||
this._addChip("size", `size:${parsed.size}`, `taille ${parsed.size}`);
|
||||
hasChips = true;
|
||||
}
|
||||
this._container.hidden = !hasChips;
|
||||
},
|
||||
clear() {
|
||||
@@ -762,6 +781,9 @@ export async function performAdvancedSearch(query, vaultFilter, tagFilter, offse
|
||||
const excludeEl = document.getElementById("search-exclude-input");
|
||||
if (includeEl?.value.trim()) url += `&include_paths=${encodeURIComponent(includeEl.value.trim())}`;
|
||||
if (excludeEl?.value.trim()) url += `&exclude_paths=${encodeURIComponent(excludeEl.value.trim())}`;
|
||||
if (parsed.created) url += `&created=${encodeURIComponent(parsed.created)}`;
|
||||
if (parsed.modified) url += `&modified=${encodeURIComponent(parsed.modified)}`;
|
||||
if (parsed.size) url += `&size=${encodeURIComponent(parsed.size)}`;
|
||||
|
||||
// Search timeout — abort if server takes too long
|
||||
const timeoutId = setTimeout(
|
||||
|
||||
@@ -80,6 +80,11 @@ export async function showVaultHome(vaultName, dirPath = '') {
|
||||
</div>`;
|
||||
|
||||
await loadAndRender(vaultName, state.currentDir, resultsArea);
|
||||
|
||||
// Also load recent files for this vault (only at root level)
|
||||
if (!state.currentDir) {
|
||||
loadRecentSection(vaultName, container);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,6 +290,62 @@ async function loadAndRender(vaultName, dirPath, container) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Recent files section (loaded at vault root only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function loadRecentSection(vaultName, container) {
|
||||
try {
|
||||
const data = await api(`/api/recent?mode=opened&vault=${encodeURIComponent(vaultName)}&limit=5`);
|
||||
const files = data.files || [];
|
||||
if (files.length === 0) return;
|
||||
|
||||
// Insert recent section BEFORE the results area
|
||||
const resultsArea = document.getElementById('vault-home-results');
|
||||
const section = el('div', { class: 'vault-home-recent' });
|
||||
|
||||
const header = el('div', { class: 'vault-home-recent-header' });
|
||||
header.innerHTML = `
|
||||
<i data-lucide="clock" style="width:14px;height:14px"></i>
|
||||
<span>Récents</span>`;
|
||||
section.appendChild(header);
|
||||
|
||||
const list = el('div', { class: 'vault-home-recent-list' });
|
||||
files.forEach((f) => {
|
||||
const title = f.title || f.path.split('/').pop() || '';
|
||||
const displayTitle = title.match(/\.md$/i) ? title.replace(/\.md$/i, '') : title;
|
||||
const dir = f.path.includes('/') ? f.path.split('/').slice(0, -1).join('/') + '/' : '';
|
||||
|
||||
const item = el('div', { class: 'vault-home-recent-item' });
|
||||
item.innerHTML = `
|
||||
<i data-lucide="${getFileIcon ? getFileIcon(f.path) : 'file'}" style="width:14px;height:14px;opacity:0.6;flex-shrink:0"></i>
|
||||
<span class="vault-home-recent-title">${escapeHtml(displayTitle)}</span>
|
||||
<span class="vault-home-recent-dir">${escapeHtml(dir)}</span>
|
||||
<span class="vault-home-recent-time">${escapeHtml(f.mtime_human || '')}</span>`;
|
||||
|
||||
item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
|
||||
item.addEventListener('dblclick', (e) => {
|
||||
e.preventDefault();
|
||||
TabManager.openPersistent(f.vault, f.path);
|
||||
});
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
section.appendChild(list);
|
||||
|
||||
// Insert before the results area
|
||||
if (resultsArea) {
|
||||
container.insertBefore(section, resultsArea);
|
||||
} else {
|
||||
container.appendChild(section);
|
||||
}
|
||||
|
||||
safeCreateIcons();
|
||||
} catch {
|
||||
// Silently skip if recent files can't be loaded
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -4114,6 +4114,77 @@ body.resizing-v {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ── Recent files section on vault home ── */
|
||||
.vault-home-recent {
|
||||
margin-bottom: 20px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.vault-home-recent-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.vault-home-recent-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.vault-home-recent-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.vault-home-recent-item:hover {
|
||||
background: color-mix(in srgb, var(--accent) 8%, transparent);
|
||||
}
|
||||
|
||||
.vault-home-recent-title {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vault-home-recent-dir {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-family: var(--font-mono);
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vault-home-recent-time {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.vault-home-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user