feat: saved filter pills, mention autocomplete CSS, tooltips on dashboard table

- Dashboard: saved filter pills row above active filters bar — loads from API,
  click applies criteria as URL params, hidden when no saved filters exist
- ticket.css: add TDS-styled CSS for @mention autocomplete dropdown (was unstyled)
- Dashboard table: data-tooltip on Title and Assigned To columns for truncated text
  (lt.tooltip.init() auto-called by lt.init(), zero extra JS needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 12:06:46 -04:00
parent 0d8edc9d34
commit 6727aeea29
2 changed files with 88 additions and 2 deletions
+36
View File
@@ -270,6 +270,42 @@ kbd {
.thread-depth-3 { margin-left: 2.25rem; } .thread-depth-3 { margin-left: 2.25rem; }
} }
/* ── @mention autocomplete dropdown ─────────────────────────── */
.mention-autocomplete {
display: none;
position: absolute;
z-index: 200;
background: var(--bg-overlay, #060c14);
border: 1px solid var(--accent-cyan-border, rgba(0,212,255,0.3));
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
min-width: 200px;
max-width: 320px;
}
.mention-autocomplete.active { display: block; }
.mention-option {
display: flex;
align-items: baseline;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
cursor: pointer;
border-bottom: 1px solid var(--border-dim, rgba(0,255,65,0.1));
transition: background 0.1s;
}
.mention-option:last-child { border-bottom: none; }
.mention-option.selected,
.mention-option:hover {
background: rgba(0,212,255,0.08);
}
.mention-username {
font-size: 0.75rem;
font-weight: 700;
color: var(--accent-cyan, #00d4ff);
}
.mention-displayname {
font-size: 0.7rem;
color: var(--text-muted);
}
/* ── Watcher avatar group in toolbar ────────────────────────── */ /* ── Watcher avatar group in toolbar ────────────────────────── */
.lt-avatar-group { .lt-avatar-group {
display: flex; display: flex;
+52 -2
View File
@@ -365,6 +365,9 @@ include __DIR__ . '/layout_header.php';
</div> </div>
</div><!-- /.lt-toolbar --> </div><!-- /.lt-toolbar -->
<!-- Saved filter quick-switch pills -->
<div id="savedFilterPills" class="saved-filter-pills lt-flex lt-flex-wrap lt-flex-gap-sm" style="display:none;padding:0.35rem 0 0.1rem" aria-label="Saved filters"></div>
<!-- Active filters bar --> <!-- Active filters bar -->
<?php if (!empty($activeFilters)): ?> <?php if (!empty($activeFilters)): ?>
<div class="active-filters-bar lt-flex lt-flex-wrap lt-flex-gap-sm" role="group" aria-label="Active filters"> <div class="active-filters-bar lt-flex lt-flex-wrap lt-flex-gap-sm" role="group" aria-label="Active filters">
@@ -502,7 +505,9 @@ include __DIR__ . '/layout_header.php';
<?php $badgeClass = match($pNum) { 1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4' }; ?> <?php $badgeClass = match($pNum) { 1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4' }; ?>
<span class="lt-badge <?= $badgeClass ?>">P<?= $pNum ?></span> <span class="lt-badge <?= $badgeClass ?>">P<?= $pNum ?></span>
</td> </td>
<td data-label="Title"><?= htmlspecialchars($row['title']) ?></td> <td data-label="Title" style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
<span data-tooltip="<?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?>" data-tooltip-pos="top"><?= htmlspecialchars($row['title']) ?></span>
</td>
<td data-label="Category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></td> <td data-label="Category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></td>
<td data-label="Type" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['type']) ?></td> <td data-label="Type" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['type']) ?></td>
<td data-label="Status"> <td data-label="Status">
@@ -518,7 +523,12 @@ include __DIR__ . '/layout_header.php';
</td> </td>
<td data-label="Created By" class="lt-text-xs"><?= $creator ?></td> <td data-label="Created By" class="lt-text-xs"><?= $creator ?></td>
<td data-label="Assigned To" class="lt-text-xs"> <td data-label="Assigned To" class="lt-text-xs">
<?= ($row['assigned_display_name'] ?? $row['assigned_username'] ?? null) ? $assignedTo : '<span class="lt-text-muted">Unassigned</span>' ?> <?php $assigneeDisplay = $row['assigned_display_name'] ?? $row['assigned_username'] ?? null; ?>
<?php if ($assigneeDisplay): ?>
<span data-tooltip="<?= htmlspecialchars($assigneeDisplay, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($assigneeDisplay) ?></span>
<?php else: ?>
<span class="lt-text-muted">Unassigned</span>
<?php endif ?>
</td> </td>
<td data-label="Created" class="lt-text-xs lt-text-muted ts-cell" <td data-label="Created" class="lt-text-xs lt-text-muted ts-cell"
data-ts="<?= htmlspecialchars($row['created_at'], ENT_QUOTES, 'UTF-8') ?>" data-ts="<?= htmlspecialchars($row['created_at'], ENT_QUOTES, 'UTF-8') ?>"
@@ -916,6 +926,46 @@ if (window.lt) {
lt.statsFilter.init(); lt.statsFilter.init();
} }
// Saved filter pills — load on page init
(function() {
lt.api.get('/api/saved_filters.php').then(function(data) {
if (!data.success || !data.filters || !data.filters.length) return;
var pillsWrap = document.getElementById('savedFilterPills');
if (!pillsWrap) return;
var html = '<span class="lt-text-xs lt-text-muted" style="align-self:center">Saved:</span>';
data.filters.slice(0, 8).forEach(function(f) {
var criteria = JSON.stringify(f.filter_criteria);
html += '<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm saved-filter-pill" ' +
'data-criteria="' + lt.escHtml(criteria) + '" ' +
'title="Apply saved filter: ' + lt.escHtml(f.filter_name) + '">' +
lt.escHtml(f.filter_name) +
(f.is_default ? ' <span style="color:var(--accent-amber)">&#x2605;</span>' : '') +
'</button>';
});
pillsWrap.innerHTML = html;
pillsWrap.style.display = 'flex';
// Wire clicks: apply filter criteria as URL params
pillsWrap.querySelectorAll('.saved-filter-pill').forEach(function(btn) {
btn.addEventListener('click', function() {
try {
var c = JSON.parse(btn.dataset.criteria);
var params = new URLSearchParams();
if (c.search) params.set('search', c.search);
if (c.status && c.status.length) params.set('status', c.status.join(','));
if (c.priority_min) params.set('priority_min', c.priority_min);
if (c.priority_max) params.set('priority_max', c.priority_max);
if (c.assigned_to) params.set('assigned_to', c.assigned_to);
if (c.created_by) params.set('created_by', c.created_by);
if (c.created_from) params.set('created_from', c.created_from);
if (c.created_to) params.set('created_to', c.created_to);
window.location.href = '/?' + params.toString();
} catch(e) {}
});
});
}).catch(function() {});
})();
// Helper: get date in server timezone // Helper: get date in server timezone
function getServerDate() { function getServerDate() {
var now = new Date(); var now = new Date();