feat: inline rename on double-click in library asset cards

Double-clicking a clip name in the library shows an in-place text input.
Enter/blur commits the new display_name via PATCH; Escape cancels.
Clicking the card body or action buttons still work normally.
This commit is contained in:
Zac Gaetano 2026-05-19 00:41:43 -04:00
parent f39d086bc8
commit 9c83698b81

View file

@ -257,6 +257,22 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
/* Inline rename input */
.asset-name-input {
width: 100%;
font-size: var(--text-sm);
font-weight: 500;
font-family: inherit;
padding: 1px 4px;
border-radius: 3px;
border: 1px solid var(--accent-border);
background: var(--bg-raised);
color: var(--text-primary);
outline: none;
box-sizing: border-box;
}
.asset-info {
@ -303,7 +319,7 @@
min-width: 160px;
}
/* Asset detail popover */
/* Asset action buttons */
.asset-actions {
position: absolute;
top: var(--sp-2);
@ -619,6 +635,7 @@
setupDrag();
setupSearch();
setupFilters();
setupRenameListener(document.getElementById('assetGrid'));
// Multi-select bulk actions
if (window.SelectionManager) {
@ -806,7 +823,7 @@
</div>
</div>
<div class="asset-meta">
<div class="asset-name">${escHtml(asset.display_name || asset.filename)}</div>
<div class="asset-name" data-rename-id="${asset.id}" title="Double-click to rename">${escHtml(asset.display_name || asset.filename)}</div>
<div class="asset-info">
<span class="asset-type">${escHtml(asset.media_type || '')}</span>
<span class="text-tertiary text-xs">${asset.file_size ? formatFileSize(asset.file_size) : ''}</span>
@ -819,6 +836,7 @@
card.addEventListener('click', (e) => {
if (e.target.closest('.asset-action-btn')) return;
if (e.target.closest('.asset-name[data-rename-id]')) return; // let rename handle it
if (window.openAssetPreview) window.openAssetPreview(asset.id);
});
@ -827,6 +845,61 @@
if (window.SelectionManager) SelectionManager.refreshUI();
}
// ── Inline rename ─────────────────────────
function setupRenameListener(grid) {
grid.addEventListener('dblclick', async e => {
const nameEl = e.target.closest('.asset-name[data-rename-id]');
if (!nameEl) return;
e.stopPropagation();
const assetId = nameEl.dataset.renameId;
const current = nameEl.textContent;
const input = document.createElement('input');
input.type = 'text';
input.value = current;
input.className = 'asset-name-input';
nameEl.style.display = 'none';
nameEl.parentNode.insertBefore(input, nameEl.nextSibling);
input.focus();
input.select();
let saved = false;
const save = async () => {
if (saved) return;
saved = true;
const newName = input.value.trim();
input.remove();
nameEl.style.display = '';
if (!newName || newName === current) return;
const r = await updateAsset(assetId, { display_name: newName });
if (r.success) {
nameEl.textContent = newName;
// Update state cache so re-renders don't revert
const a = state.assets.find(x => x.id === assetId);
if (a) a.display_name = newName;
toast('Renamed', newName, 'success');
} else {
toast('Rename failed', r.error, 'error');
}
};
const cancel = () => {
if (saved) return;
saved = true;
input.remove();
nameEl.style.display = '';
};
input.addEventListener('blur', save);
input.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
if (e.key === 'Escape') { e.preventDefault(); cancel(); }
});
});
}
function statusBadgeClass(s) {
const map = { live:'badge-live', ingesting:'badge-ingesting', processing:'badge-processing', ready:'badge-ready', error:'badge-error', archived:'badge-archived' };
return map[s] || 'badge-idle';