diff --git a/css/style.css b/css/style.css index 935cd58..cb24455 100644 --- a/css/style.css +++ b/css/style.css @@ -64,10 +64,11 @@ input:focus, select:focus { border-color:var(--accent); } /* Toolbar */ .toolbar { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:0.5rem; margin-bottom:0.7rem; } -.view-toggle { display:flex; gap:0.2rem; background:rgba(0,0,0,0.3); padding:0.2rem; border-radius:2rem; } -.view-btn { background:none; border:none; color:var(--text2); padding:0.35rem 0.7rem; border-radius:2rem; cursor:pointer; font-size:0.75rem; transition:0.2s; } -.view-btn.active { background:var(--accent); color:#fff; } -.view-btn:hover:not(.active) { background:rgba(255,255,255,0.08); } +.view-dropdown { position:relative; } +.view-dropdown-menu { position:absolute; top:100%; right:0; margin-top:4px; min-width:155px; background:var(--bg2); border:1px solid var(--border); border-radius:0.6rem; padding:0.3rem; box-shadow:0 8px 24px rgba(0,0,0,0.3); z-index:100; display:flex; flex-direction:column; gap:2px; } +.view-opt { background:none; border:none; color:var(--text); padding:0.45rem 0.8rem; border-radius:0.4rem; cursor:pointer; font-size:0.8rem; text-align:left; transition:0.12s; white-space:nowrap; } +.view-opt:hover { background:var(--accent); color:#fff; } +.view-opt.active { background:var(--accent); color:#fff; font-weight:600; } .toggles-row { display:flex; gap:1.2rem; align-items:center; flex-wrap:wrap; } .toggle-item { display:flex; align-items:center; gap:0.4rem; font-size:0.75rem; color:var(--text2); } @@ -150,6 +151,10 @@ input:focus, select:focus { border-color:var(--accent); } .card-user { color:var(--text2); font-size:0.75rem; margin-bottom:0.3rem; } .card-folder { font-size:0.65rem; color:var(--accent); margin-bottom:0.3rem; background:rgba(59,130,246,0.15); display:inline-block; padding:0.1rem 0.5rem; border-radius:1rem; } .card-password { background:var(--bg2); padding:0.3rem 0.5rem; border-radius:0.6rem; display:flex; align-items:center; justify-content:space-between; font-family:monospace; font-size:0.75rem; gap:0.2rem; } +#entriesContainer.card-view .entry-card { padding:1.2rem 3rem 1.2rem 1.2rem; } +#entriesContainer.card-view .card-site { font-size:1.05rem; } +#entriesContainer.card-view .card-password { font-size:0.9rem; } +#entriesContainer.card-view .entry-card:hover { transform:translateY(-3px); } .entry-row { background:var(--card); border:1px solid var(--border); border-radius:0.8rem; padding:0.7rem 2.8rem 0.7rem 0.9rem; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:0.4rem; position:relative; } .entry-compact { display:flex; align-items:center; gap:0.5rem; padding:0.35rem 2.8rem 0.35rem 0.7rem; background:var(--card); border-radius:0.5rem; border:1px solid var(--border); font-size:0.8rem; position:relative; } .action-btns { position:absolute; top:4px; right:6px; display:flex; gap:4px; z-index:1; } @@ -165,6 +170,32 @@ input:focus, select:focus { border-color:var(--accent); } .entry-row.favorite { border-color:rgba(255,200,0,0.3); background:rgba(255,200,0,0.05); } .entry-compact.favorite { border-color:rgba(255,200,0,0.3); background:rgba(255,200,0,0.05); } .table-row-drag.favorite td { background:rgba(255,200,0,0.05); } + +/* Grouped view */ +#entriesContainer.grouped-view { display:flex; flex-direction:column; gap:0.15rem; } +.grouped-header { position:sticky; top:0; z-index:2; background:var(--bg); padding:0.55rem 0.8rem; border-radius:0.5rem; font-weight:600; font-size:0.9rem; color:var(--accent); display:flex; align-items:center; gap:0.5rem; border-bottom:2px solid var(--accent); margin-top:0.4rem; } +.grouped-header:first-child { margin-top:0; } +#entriesContainer.grouped-view .entry-row { border-left:3px solid transparent; transition:0.15s; padding-left:1rem; } +#entriesContainer.grouped-view .entry-row:hover { border-left-color:var(--accent); } + +/* Detail view */ +#entriesContainer.detail-view { display:flex; flex-direction:column; gap:1rem; } +.detail-nav { display:flex; align-items:center; justify-content:center; gap:1rem; padding:0.5rem 0; position:sticky; top:0; z-index:2; background:var(--bg); } +.detail-nav button { background:var(--bg2); border:1px solid var(--border); color:var(--text); padding:0.4rem 1rem; border-radius:0.5rem; cursor:pointer; transition:0.15s; font-size:0.85rem; } +.detail-nav button:hover { border-color:var(--accent); color:var(--accent); } +.detail-nav button:disabled { opacity:0.4; cursor:default; } +.detail-nav .detail-counter { font-size:0.8rem; color:var(--text2); min-width:80px; text-align:center; } +.detail-card { background:var(--bg2); border:1px solid var(--border); border-radius:1rem; padding:1.5rem; display:flex; flex-direction:column; gap:0.9rem; max-width:520px; margin:0 auto; width:100%; } +.detail-field { display:flex; flex-direction:column; gap:0.15rem; } +.detail-label { font-size:0.7rem; color:var(--text2); text-transform:uppercase; letter-spacing:0.5px; } +.detail-value { font-size:1.2rem; word-break:break-all; } +.detail-value.pw-display { font-family:monospace; letter-spacing:2px; font-size:1.3rem; cursor:default; } +.detail-folder { display:flex; gap:0.5rem; align-items:center; } +.detail-actions { display:flex; gap:0.5rem; margin-top:0.3rem; } +.detail-actions button { flex:1; padding:0.5rem; border-radius:0.5rem; border:1px solid var(--border); background:var(--bg); cursor:pointer; transition:0.15s; font-size:0.85rem; } +.detail-actions button:hover { border-color:var(--accent); background:var(--bg2); } + + .entry-info { display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap; flex:1; } .entry-site { font-weight:700; color:var(--text); } .entry-user { color:var(--text2); } diff --git a/index.html b/index.html index 5cb64c5..a6e00c5 100644 --- a/index.html +++ b/index.html @@ -156,11 +156,17 @@
-
- - - - +
+ +
diff --git a/js/app.js b/js/app.js index d14e912..692bac3 100644 --- a/js/app.js +++ b/js/app.js @@ -5,6 +5,7 @@ let curUser = sessionStorage.getItem('currentUsername'); function a2b64(arr) { return btoa(String.fromCharCode(...new Uint8Array(arr))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); } function b642ab(s) { return Uint8Array.from(atob(s.replace(/-/g,'+').replace(/_/g,'/')), c=>c.charCodeAt(0)).buffer; } let view = localStorage.getItem('vaultView') || 'grid'; +let detailIndex = 0; let showView = localStorage.getItem('showViewBtn') !== 'false'; let showMail = localStorage.getItem('showEmail') !== 'false'; let dark = localStorage.getItem('darkTheme') !== 'false'; @@ -356,7 +357,6 @@ async function registerPasskey() { // ==================== INIT ==================== function init() { - document.querySelectorAll('.view-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view)); document.getElementById('autoLockTimer').value = lockMin; applyTheme(); // document.getElementById('usernameInput').style.display = showMail ? '' : 'none'; @@ -364,16 +364,28 @@ function init() { const sl = localStorage.getItem('savedLoginUser'); if (sl) document.getElementById('loginUsername').value = sl; syncSettingsUI(); - document.getElementById('viewToggle').addEventListener('click', e => { - if (e.target.classList.contains('view-btn')) { - document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); - e.target.classList.add('active'); - view = e.target.dataset.view; - localStorage.setItem('vaultView', view); - render(); - playSound('click'); - } - }); + // View dropdown + const vdd = document.getElementById('viewDropdown'); + const vBtn = document.getElementById('viewDropdownBtn'); + const vMenu = document.getElementById('viewDropdownMenu'); + const vIcons = {grid:'đŸŸĢ',compact:'📝',list:'📋',table:'📊',card:'🃏',grouped:'📂',detail:'🔍'}; + const updateViewBtn = () => { vBtn.textContent = (vIcons[view] || 'đŸŸĢ') + ' ' + view.charAt(0).toUpperCase() + view.slice(1) + ' ▾'; }; + updateViewBtn(); + vMenu.querySelectorAll('.view-opt').forEach(o => o.classList.toggle('active', o.dataset.view === view)); + vBtn.onclick = (e) => { e.stopPropagation(); vMenu.classList.toggle('hidden'); }; + vMenu.onclick = (e) => { + const opt = e.target.closest('.view-opt'); + if (!opt) return; + vMenu.querySelectorAll('.view-opt').forEach(o => o.classList.remove('active')); + opt.classList.add('active'); + view = opt.dataset.view; + detailIndex = 0; + localStorage.setItem('vaultView', view); + updateViewBtn(); + render(); + playSound('click'); + }; + document.addEventListener('click', () => vMenu.classList.add('hidden')); const clearBtn = document.getElementById('clearSearchBtn'); if (clearBtn) clearBtn.style.display = 'none'; document.getElementById('addModal').addEventListener('click', e => { @@ -646,9 +658,9 @@ async function loadEntries(q) { } catch (e) { document.getElementById('connectionStatus').textContent = '🔴 Error'; toast('Connection error', 'error'); } } -function getFilteredEntries() { +function getFilteredEntries(noFolder) { if (showTrash) return entries; - if (selectedFolder === 'All') return entries; + if (noFolder || selectedFolder === 'All') return entries; return entries.filter(e => (e.folder || 'All') === selectedFolder); } @@ -688,9 +700,13 @@ function render() { }); h += ''; c.innerHTML = h; + } else if (view === 'grouped') { + c.innerHTML = groupedC(getFilteredEntries(true)); + } else if (view === 'detail') { + c.innerHTML = detailC(getFilteredEntries(true)); } else { c.innerHTML = filtered.map(e => { - if (view === 'grid') return gridC(e); + if (view === 'grid' || view === 'card') return gridC(e); if (view === 'compact') return compC(e); return listC(e); }).join(''); @@ -775,6 +791,85 @@ function compC(e) { return html; } +function groupedC(list) { + const groups = {}; + list.forEach(e => { + const f = e.folder || 'All'; + if (!groups[f]) groups[f] = []; + groups[f].push(e); + }); + let html = ''; + for (const [folder, items] of Object.entries(groups)) { + html += '
📁 ' + esc(folder) + ' ' + items.length + '
'; + items.forEach(e => { + const sel = selectedIds.has(e.id); + html += '
'; + html += '
'; + if (showTrash) { + html += ''; + html += ''; + } else { + html += ''; + html += ''; + html += ''; + } + html += '
'; + html += '
🌐 ' + highlightText(e.site, searchQuery) + ''; + if (showMail) html += '👤 ' + highlightText(e.username, searchQuery) + ''; + if (!showTrash) { + html += '
â€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸ' + + '
'; + } else { + html += 'đŸ—‘ī¸ ' + timeAgo(e.deleted_at) + ''; + } + html += '
'; + }); + } + return html; +} + +function detailC(list) { + if (detailIndex >= list.length) detailIndex = 0; + if (detailIndex < 0) detailIndex = list.length - 1; + const e = list[detailIndex]; + const hasPrev = detailIndex > 0, hasNext = detailIndex < list.length - 1; + const sel = selectedIds.has(e.id); + let html = '
'; + html += ''; + html += '' + (detailIndex + 1) + ' of ' + list.length + ''; + html += ''; + html += '
'; + html += '
'; + html += '
Site🌐 ' + highlightText(e.site, searchQuery) + '
'; + if (showMail) html += '
Username👤 ' + highlightText(e.username, searchQuery) + '
'; + if (!showTrash) { + html += '
Passwordâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸâ€ĸ
'; + html += '
Folder📁 ' + esc(e.folder || 'All') + '
'; + html += '
' + + '' + + '' + + '' + + '' + + '
'; + } else { + html += '
DeletedđŸ—‘ī¸ ' + timeAgo(e.deleted_at) + '
'; + html += '
' + + '' + + '' + + '
'; + } + html += '
'; + return html; +} + +function goDetail(dir) { + const list = getFilteredEntries(true); + detailIndex += dir; + if (detailIndex < 0) detailIndex = list.length - 1; + if (detailIndex >= list.length) detailIndex = 0; + render(); +} + // ==================== EVENTS ==================== function showConfirm(btn, message, callback) { const id = btn.dataset.id; @@ -1090,7 +1185,7 @@ function highlightText(text, query) { function showShortcutsHelp() { const overlay = document.createElement('div'); overlay.className = 'custom-modal-overlay show'; - overlay.innerHTML = `

âŒ¨ī¸ Keyboard Shortcuts

Ctrl+NNew entryCtrl+FSearchCtrl+TToggle trashCtrl+LLock vaultCtrl+SSave entryEscClose modal / deselect?Show this help

💡 Click any entry to select, Shift+click for range, Ctrl+click to toggle

`; + overlay.innerHTML = `

âŒ¨ī¸ Keyboard Shortcuts

Ctrl+NNew entryCtrl+FSearchCtrl+TToggle trashCtrl+LLock vaultCtrl+SSave entryEscClose modal / deselect◀ â–ļDetail view nav?Show this help

💡 Click any entry to select, Shift+click for range, Ctrl+click to toggle

`; document.body.appendChild(overlay); overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); }); } @@ -1149,7 +1244,7 @@ document.getElementById('entriesContainer').addEventListener('dragstart', functi // Document mousedown: rect selection on vault background, clear outside vault document.addEventListener('mousedown', function(e) { if (e.button !== 0 || rectState.active) return; - if (e.target?.closest?.('.entry-card,.entry-row,.entry-compact,.table-row-drag,#batchBar,.custom-modal-overlay.show,.edit-modal.show,.modal-overlay.show,#genModal,#settingsMenu')) return; + if (e.target?.closest?.('.entry-card,.entry-row,.entry-compact,.table-row-drag,.detail-card,.detail-nav,#batchBar,.custom-modal-overlay.show,.edit-modal.show,.modal-overlay.show,#genModal,#settingsMenu')) return; if (e.target?.closest?.('button,input,select,.folders-bar,.toolbar,#trashActions,.settings-dropdown,.fab,.view-toggle,.auth-section')) { if (selectedIds.size > 0) clearSelection(); return; @@ -1236,7 +1331,14 @@ document.addEventListener('keydown', function(e) { if (confirm) confirm.remove(); return; } - + + // Arrow keys for detail view navigation + if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !isInput && view === 'detail' && !document.getElementById('authSection').classList.contains('hidden')) { + e.preventDefault(); + goDetail(e.key === 'ArrowLeft' ? -1 : 1); + return; + } + // ? or / to show shortcuts help (only in vault) if ((e.key === '?' || e.key === '/') && !isInput) { if (!document.getElementById('authSection').classList.contains('hidden')) return;