diff --git a/app/static/css/app.css b/app/static/css/app.css index e3f90ab..d0300bb 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -1,5 +1,11 @@ /* ── Reset & base ──────────────────────────────────────────────────── */ -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} :root { --color-bg: #f4f5f7; @@ -14,24 +20,59 @@ --color-sidebar-hover: #2d3250; --color-sidebar-active: #d0021b; --radius: 8px; - --shadow: 0 1px 4px rgba(0,0,0,.08), 0 2px 12px rgba(0,0,0,.05); + --shadow: 0 1px 4px rgba(0, 0, 0, 0.08), 0 2px 12px rgba(0, 0, 0, 0.05); --transition: 150ms ease; } -html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; color: var(--color-text); background: var(--color-bg); } -a { color: var(--color-primary); text-decoration: none; } -a:hover { text-decoration: underline; } -button { cursor: pointer; font: inherit; border: none; background: none; } -input, select, textarea { font: inherit; } -ul { list-style: none; } +html, +body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 14px; + color: var(--color-text); + background: var(--color-bg); +} +a { + color: var(--color-primary); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +button { + cursor: pointer; + font: inherit; + border: none; + background: none; +} +input, +select, +textarea { + font: inherit; +} +ul { + list-style: none; +} /* ── Utilities ───────────────────────────────────────────────────── */ -.hidden { display: none !important; } +.hidden { + display: none !important; +} /* ── Auth pages ──────────────────────────────────────────────────── */ -.auth-page { display: flex; align-items: center; justify-content: center; min-height: 100vh; background: var(--color-bg); } +.auth-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: var(--color-bg); +} -.auth-container { width: 100%; max-width: 420px; padding: 16px; } +.auth-container { + width: 100%; + max-width: 420px; + padding: 16px; +} .auth-card { background: var(--color-surface); @@ -40,18 +81,48 @@ ul { list-style: none; } padding: 40px 36px; } -.auth-logo { display: flex; align-items: center; gap: 8px; margin-bottom: 28px; } -.auth-logo .logo-icon { font-size: 28px; } -.auth-logo .logo-text { font-size: 20px; font-weight: 700; color: var(--color-text); } +.auth-logo { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 28px; +} +.auth-logo .logo-icon { + font-size: 28px; +} +.auth-logo .logo-text { + font-size: 20px; + font-weight: 700; + color: var(--color-text); +} -.auth-title { font-size: 22px; font-weight: 600; margin-bottom: 24px; } +.auth-title { + font-size: 22px; + font-weight: 600; + margin-bottom: 24px; +} -.auth-footer { margin-top: 20px; text-align: center; color: var(--color-text-muted); } +.auth-footer { + margin-top: 20px; + text-align: center; + color: var(--color-text-muted); +} /* ── Form elements ──────────────────────────────────────────────── */ -.form-group { margin-bottom: 16px; } -.form-group label { display: block; margin-bottom: 6px; font-weight: 500; color: var(--color-text); } -.form-group label .hint { font-weight: 400; color: var(--color-text-muted); font-size: 12px; } +.form-group { + margin-bottom: 16px; +} +.form-group label { + display: block; + margin-bottom: 6px; + font-weight: 500; + color: var(--color-text); +} +.form-group label .hint { + font-weight: 400; + color: var(--color-text-muted); + font-size: 12px; +} .form-group input, .form-group select, @@ -69,66 +140,166 @@ ul { list-style: none; } .form-group textarea:focus { outline: none; border-color: var(--color-primary); - box-shadow: 0 0 0 3px rgba(208,2,27,.12); + box-shadow: 0 0 0 3px rgba(208, 2, 27, 0.12); } -.input-with-toggle { position: relative; } -.input-with-toggle input { padding-right: 40px; } +.input-with-toggle { + position: relative; +} +.input-with-toggle input { + padding-right: 40px; +} .btn-show-pass { - position: absolute; right: 10px; top: 50%; transform: translateY(-50%); - background: none; border: none; cursor: pointer; font-size: 16px; opacity: .6; + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + font-size: 16px; + opacity: 0.6; transition: opacity var(--transition); } -.btn-show-pass:hover { opacity: 1; } +.btn-show-pass:hover { + opacity: 1; +} -.form-error { color: #c00; background: #fff0f0; border: 1px solid #fcc; border-radius: var(--radius); padding: 8px 12px; margin-bottom: 12px; font-size: 13px; } +.form-error { + color: #c00; + background: #fff0f0; + border: 1px solid #fcc; + border-radius: var(--radius); + padding: 8px 12px; + margin-bottom: 12px; + font-size: 13px; +} /* Buttons */ .btn-primary { - display: inline-flex; align-items: center; justify-content: center; - background: var(--color-primary); color: #fff; - padding: 10px 20px; border-radius: var(--radius); font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--color-primary); + color: #fff; + padding: 10px 20px; + border-radius: var(--radius); + font-weight: 600; transition: background var(--transition); } -.btn-primary:hover { background: var(--color-primary-dark); } -.btn-primary:disabled { opacity: .6; cursor: not-allowed; } -.btn-full { width: 100%; } +.btn-primary:hover { + background: var(--color-primary-dark); +} +.btn-primary:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.btn-full { + width: 100%; +} .btn-secondary { - display: inline-flex; align-items: center; justify-content: center; - background: var(--color-border); color: var(--color-text); - padding: 10px 20px; border-radius: var(--radius); font-weight: 500; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--color-border); + color: var(--color-text); + padding: 10px 20px; + border-radius: var(--radius); + font-weight: 500; transition: background var(--transition); } -.btn-secondary:hover { background: #d1d5dc; } +.btn-secondary:hover { + background: #d1d5dc; +} -.btn-text { background: none; border: none; color: inherit; padding: 0; } +.btn-text { + background: none; + border: none; + color: inherit; + padding: 0; +} /* Notices */ -.notice-success { background: #e8f5e9; border: 1px solid #a5d6a7; color: #2e7d32; border-radius: var(--radius); padding: 10px 14px; margin-bottom: 16px; font-size: 13px; } -.notice-info { background: #e3f2fd; border: 1px solid #90caf9; color: #1565c0; border-radius: var(--radius); padding: 10px 14px; margin-bottom: 16px; font-size: 13px; } +.notice-success { + background: #e8f5e9; + border: 1px solid #a5d6a7; + color: #2e7d32; + border-radius: var(--radius); + padding: 10px 14px; + margin-bottom: 16px; + font-size: 13px; +} +.notice-info { + background: #e3f2fd; + border: 1px solid #90caf9; + color: #1565c0; + border-radius: var(--radius); + padding: 10px 14px; + margin-bottom: 16px; + font-size: 13px; +} /* ── Password Generator modal ────────────────────────────────────────── */ -.gen-opt { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; } -.gen-opt input[type="checkbox"] { width: 15px; height: 15px; accent-color: var(--color-primary); cursor: pointer; } -#gen-output { letter-spacing: .5px; font-family: 'Courier New', monospace; } +.gen-opt { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-size: 13px; +} +.gen-opt input[type="checkbox"] { + width: 15px; + height: 15px; + accent-color: var(--color-primary); + cursor: pointer; +} +#gen-output { + letter-spacing: 0.5px; + font-family: "Courier New", monospace; +} /* Launch (open URL) button — distinct colour so it reads as navigation */ -.btn-icon[data-action="launch"] { color: #1a73e8; } -.btn-icon[data-action="launch"]:hover { background: #e8f0fe; color: #1558b0; } +.btn-icon[data-action="launch"] { + color: #1a73e8; +} +.btn-icon[data-action="launch"]:hover { + background: #e8f0fe; + color: #1558b0; +} /* Password strength */ -.password-strength { margin-top: 6px; font-size: 12px; font-weight: 500; } -.strength-1 { color: #c62828; } -.strength-2 { color: #e65100; } -.strength-3 { color: #f57f17; } -.strength-4 { color: #2e7d32; } -.strength-5 { color: #1b5e20; } +.password-strength { + margin-top: 6px; + font-size: 12px; + font-weight: 500; +} +.strength-1 { + color: #c62828; +} +.strength-2 { + color: #e65100; +} +.strength-3 { + color: #f57f17; +} +.strength-4 { + color: #2e7d32; +} +.strength-5 { + color: #1b5e20; +} /* ── App layout (vault) ──────────────────────────────────────────── */ -.vault-page { height: 100vh; overflow: hidden; } +.vault-page { + height: 100vh; + overflow: hidden; +} -.app-layout { display: flex; height: 100vh; } +.app-layout { + display: flex; + height: 100vh; +} /* ── Sidebar ─────────────────────────────────────────────────────── */ .sidebar { @@ -138,7 +309,14 @@ ul { list-style: none; } color: var(--color-sidebar-text); display: flex; flex-direction: column; + overflow: hidden; overflow-y: auto; + transition: width 0.22s ease; +} + +.sidebar.collapsed { + width: 56px; + overflow-y: visible; } .sidebar-header { @@ -146,19 +324,71 @@ ul { list-style: none; } align-items: center; gap: 8px; padding: 18px 16px; - border-bottom: 1px solid rgba(255,255,255,.08); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + flex-shrink: 0; + min-height: 60px; +} +.sidebar-header .logo-icon { + font-size: 22px; + flex-shrink: 0; +} +.sidebar-header .logo-text { + font-size: 16px; + font-weight: 700; + color: #fff; + flex: 1; + white-space: nowrap; + overflow: hidden; + transition: + opacity 0.15s ease, + width 0.15s ease; +} +.sidebar.collapsed .sidebar-header .logo-text { + opacity: 0; + width: 0; } -.sidebar-header .logo-icon { font-size: 22px; } -.sidebar-header .logo-text { font-size: 16px; font-weight: 700; color: #fff; } -.sidebar-nav { flex: 1; padding: 8px 0; } +/* Toggle button */ +.btn-sidebar-toggle { + background: none; + border: none; + color: rgba(255, 255, 255, 0.5); + font-size: 13px; + cursor: pointer; + padding: 4px 6px; + border-radius: 4px; + line-height: 1; + flex-shrink: 0; + transition: + color var(--transition), + transform 0.22s ease; +} +.btn-sidebar-toggle:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); +} +.sidebar.collapsed .btn-sidebar-toggle { + transform: rotate(180deg); +} + +.sidebar-nav { + flex: 1; + padding: 8px 0; +} .sidebar-section-header { padding: 12px 16px 4px; font-size: 11px; text-transform: uppercase; - letter-spacing: .08em; - color: rgba(255,255,255,.35); + letter-spacing: 0.08em; + color: rgba(255, 255, 255, 0.35); + white-space: nowrap; + overflow: hidden; + transition: opacity 0.15s ease; +} +.sidebar.collapsed .sidebar-section-header { + opacity: 0; + pointer-events: none; } .sidebar-item { @@ -173,42 +403,144 @@ ul { list-style: none; } font-size: 14px; width: 100%; text-align: left; + position: relative; + white-space: nowrap; +} +.sidebar-item:hover { + background: var(--color-sidebar-hover); +} +.sidebar-item.active { + background: var(--color-sidebar-active); + color: #fff; +} +.sidebar-icon { + font-size: 16px; + flex-shrink: 0; } -.sidebar-item:hover { background: var(--color-sidebar-hover); } -.sidebar-item.active { background: var(--color-sidebar-active); color: #fff; } -.sidebar-icon { font-size: 16px; flex-shrink: 0; } -.sidebar-footer { padding: 12px 0; border-top: 1px solid rgba(255,255,255,.08); } +/* Hide labels and show tooltip when collapsed */ +.sidebar-label { + overflow: hidden; + transition: + opacity 0.15s ease, + width 0.15s ease; +} +.sidebar.collapsed .sidebar-label { + opacity: 0; + width: 0; +} +.sidebar.collapsed .sidebar-item { + justify-content: center; + padding: 9px 0; +} -.folder-item { justify-content: space-between; } -.folder-label { display: flex; align-items: center; gap: 10px; flex: 1; cursor: pointer; } +/* Tooltip — shown on hover in collapsed state */ +.sidebar.collapsed .sidebar-item::after { + content: attr(data-tooltip); + position: absolute; + left: calc(100% + 8px); + top: 50%; + transform: translateY(-50%); + background: #111827; + color: #fff; + font-size: 12px; + padding: 4px 10px; + border-radius: 4px; + white-space: nowrap; + pointer-events: none; + opacity: 0; + transition: opacity 0.12s ease; + z-index: 200; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); +} +.sidebar.collapsed .sidebar-item:hover::after { + opacity: 1; +} + +.sidebar-footer { + padding: 12px 0; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} + +.folder-item { + justify-content: space-between; +} +.folder-label { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + cursor: pointer; +} .btn-delete-folder { - background: none; border: none; cursor: pointer; - font-size: 13px; opacity: 0; color: rgba(255,255,255,.6); - transition: opacity var(--transition), color var(--transition); - padding: 2px 4px; border-radius: 4px; flex-shrink: 0; + background: none; + border: none; + cursor: pointer; + font-size: 13px; + opacity: 0; + color: rgba(255, 255, 255, 0.6); + transition: + opacity var(--transition), + color var(--transition); + padding: 2px 4px; + border-radius: 4px; + flex-shrink: 0; +} +.folder-item:hover .btn-delete-folder { + opacity: 1; +} +.btn-delete-folder:hover { + color: #ff6b6b; } -.folder-item:hover .btn-delete-folder { opacity: 1; } -.btn-delete-folder:hover { color: #ff6b6b; } .btn-new-folder { - float: right; margin-right: 4px; - background: none; border: none; color: rgba(255,255,255,.5); - font-size: 16px; line-height: 1; cursor: pointer; padding: 0 4px; + float: right; + margin-right: 4px; + background: none; + border: none; + color: rgba(255, 255, 255, 0.5); + font-size: 16px; + line-height: 1; + cursor: pointer; + padding: 0 4px; transition: color var(--transition); } -.btn-new-folder:hover { color: #fff; } - -.new-folder-row { padding: 4px 10px 4px 16px; } -.new-folder-row form { display: flex; align-items: center; gap: 4px; } -.new-folder-row input { - flex: 1; padding: 5px 8px; border-radius: 4px; border: 1px solid rgba(255,255,255,.2); - background: rgba(255,255,255,.1); color: #fff; font-size: 13px; +.btn-new-folder:hover { + color: #fff; +} + +.new-folder-row { + padding: 4px 10px 4px 16px; +} +.new-folder-row form { + display: flex; + align-items: center; + gap: 4px; +} +.new-folder-row input { + flex: 1; + padding: 5px 8px; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.1); + color: #fff; + font-size: 13px; +} +.new-folder-row input::placeholder { + color: rgba(255, 255, 255, 0.4); +} +.new-folder-row input:focus { + outline: none; + border-color: rgba(255, 255, 255, 0.5); +} +.new-folder-row .btn-icon { + color: rgba(255, 255, 255, 0.7); + font-size: 14px; +} +.new-folder-row .btn-icon:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); } -.new-folder-row input::placeholder { color: rgba(255,255,255,.4); } -.new-folder-row input:focus { outline: none; border-color: rgba(255,255,255,.5); } -.new-folder-row .btn-icon { color: rgba(255,255,255,.7); font-size: 14px; } -.new-folder-row .btn-icon:hover { color: #fff; background: rgba(255,255,255,.1); } /* ── Vault main ──────────────────────────────────────────────────── */ .vault-main { @@ -219,7 +551,8 @@ ul { list-style: none; } background: var(--color-bg); } -#view-vault, #view-security { +#view-vault, +#view-security { display: flex; flex-direction: column; flex: 1; @@ -227,7 +560,10 @@ ul { list-style: none; } min-height: 0; position: relative; } -#view-vault.hidden, #view-security.hidden { display: none; } +#view-vault.hidden, +#view-security.hidden { + display: none; +} .vault-header { display: flex; @@ -239,187 +575,441 @@ ul { list-style: none; } gap: 16px; } -.vault-title { font-size: 18px; font-weight: 600; white-space: nowrap; } +.vault-title { + font-size: 18px; + font-weight: 600; + white-space: nowrap; +} -.vault-toolbar { display: flex; align-items: center; gap: 12px; flex: 1; justify-content: flex-end; } +.vault-toolbar { + display: flex; + align-items: center; + gap: 12px; + flex: 1; + justify-content: flex-end; +} .search-wrapper { - display: flex; align-items: center; - background: var(--color-bg); border: 1px solid var(--color-border); - border-radius: 20px; padding: 6px 14px; gap: 8px; flex: 1; max-width: 360px; + display: flex; + align-items: center; + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: 20px; + padding: 6px 14px; + gap: 8px; + flex: 1; + max-width: 360px; +} +.search-icon { + color: var(--color-text-muted); +} +#search-input { + border: none; + background: none; + outline: none; + flex: 1; + font-size: 14px; } -.search-icon { color: var(--color-text-muted); } -#search-input { border: none; background: none; outline: none; flex: 1; font-size: 14px; } .btn-fab { - width: 40px; height: 40px; border-radius: 50%; - background: var(--color-primary); color: #fff; - font-size: 24px; font-weight: 300; - display: flex; align-items: center; justify-content: center; - box-shadow: 0 2px 8px rgba(208,2,27,.35); - transition: background var(--transition), box-shadow var(--transition); + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--color-primary); + color: #fff; + font-size: 24px; + font-weight: 300; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 8px rgba(208, 2, 27, 0.35); + transition: + background var(--transition), + box-shadow var(--transition); flex-shrink: 0; } -.btn-fab:hover { background: var(--color-primary-dark); box-shadow: 0 4px 14px rgba(208,2,27,.45); } +.btn-fab:hover { + background: var(--color-primary-dark); + box-shadow: 0 4px 14px rgba(208, 2, 27, 0.45); +} /* ── Vault list ──────────────────────────────────────────────────── */ -.vault-list { flex: 1; overflow-y: auto; padding: 12px 24px 24px; } +.vault-list { + flex: 1; + overflow-y: auto; + padding: 12px 24px 24px; +} .vault-group-header { - font-size: 12px; font-weight: 600; text-transform: uppercase; - letter-spacing: .06em; color: var(--color-text-muted); - padding: 16px 0 6px; border-bottom: 1px solid var(--color-border); + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-text-muted); + padding: 16px 0 6px; + border-bottom: 1px solid var(--color-border); margin-bottom: 4px; } .vault-empty { - text-align: center; color: var(--color-text-muted); padding: 60px 0; font-size: 15px; + text-align: center; + color: var(--color-text-muted); + padding: 60px 0; + font-size: 15px; } .vault-item { - display: flex; align-items: center; gap: 12px; + display: flex; + align-items: center; + gap: 12px; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 12px 14px; margin-bottom: 6px; cursor: pointer; - transition: box-shadow var(--transition), border-color var(--transition); + transition: + box-shadow var(--transition), + border-color var(--transition); +} +.vault-item:hover { + box-shadow: var(--shadow); + border-color: #c9ccd4; } -.vault-item:hover { box-shadow: var(--shadow); border-color: #c9ccd4; } -.item-icon { font-size: 22px; flex-shrink: 0; } +.item-icon { + font-size: 22px; + flex-shrink: 0; +} -.item-info { flex: 1; min-width: 0; } -.item-name { display: block; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.item-sub { display: block; font-size: 12px; color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.item-info { + flex: 1; + min-width: 0; +} +.item-name { + display: block; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.item-sub { + display: block; + font-size: 12px; + color: var(--color-text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} -.item-actions { display: flex; gap: 4px; opacity: 0; transition: opacity var(--transition); } -.vault-item:hover .item-actions { opacity: 1; } +.item-actions { + display: flex; + gap: 4px; + opacity: 0; + transition: opacity var(--transition); +} +.vault-item:hover .item-actions { + opacity: 1; +} .btn-icon { - width: 30px; height: 30px; border-radius: 6px; - display: flex; align-items: center; justify-content: center; - font-size: 15px; color: var(--color-text-muted); - transition: background var(--transition), color var(--transition); + width: 30px; + height: 30px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + color: var(--color-text-muted); + transition: + background var(--transition), + color var(--transition); +} +.btn-icon:hover { + background: var(--color-bg); + color: var(--color-text); } -.btn-icon:hover { background: var(--color-bg); color: var(--color-text); } /* ── Spinner ─────────────────────────────────────────────────────── */ .spinner-overlay { - position: absolute; inset: 0; - display: flex; align-items: center; justify-content: center; - background: rgba(255,255,255,.7); + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.7); z-index: 10; } -.spinner-overlay.hidden { display: none; } +.spinner-overlay.hidden { + display: none; +} .spinner { - width: 36px; height: 36px; + width: 36px; + height: 36px; border: 3px solid var(--color-border); border-top-color: var(--color-primary); border-radius: 50%; - animation: spin .7s linear infinite; + animation: spin 0.7s linear infinite; +} +@keyframes spin { + to { + transform: rotate(360deg); + } } -@keyframes spin { to { transform: rotate(360deg); } } /* ── Modal ───────────────────────────────────────────────────────── */ .modal-overlay { - position: fixed; inset: 0; - background: rgba(0,0,0,.45); - display: none; align-items: center; justify-content: center; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: none; + align-items: center; + justify-content: center; z-index: 100; padding: 16px; } -.modal-overlay.open { display: flex; } +.modal-overlay.open { + display: flex; +} .modal { background: var(--color-surface); border-radius: var(--radius); - box-shadow: 0 8px 40px rgba(0,0,0,.2); - width: 100%; max-width: 480px; + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.2); + width: 100%; + max-width: 480px; max-height: 90vh; overflow-y: auto; padding: 28px; } .modal-header { - display: flex; align-items: center; justify-content: space-between; + display: flex; + align-items: center; + justify-content: space-between; margin-bottom: 20px; } -.modal-header h3 { font-size: 18px; font-weight: 600; } +.modal-header h3 { + font-size: 18px; + font-weight: 600; +} -.btn-close { font-size: 18px; color: var(--color-text-muted); padding: 4px; border-radius: 4px; } -.btn-close:hover { background: var(--color-bg); color: var(--color-text); } +.btn-close { + font-size: 18px; + color: var(--color-text-muted); + padding: 4px; + border-radius: 4px; +} +.btn-close:hover { + background: var(--color-bg); + color: var(--color-text); +} -.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; } +.modal-actions { + display: flex; + gap: 10px; + justify-content: flex-end; + margin-top: 20px; +} -.required { color: var(--color-primary); } +.required { + color: var(--color-primary); +} /* ── Toast ───────────────────────────────────────────────────────── */ .toast { - position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px); - background: #1a1a2e; color: #fff; - padding: 10px 20px; border-radius: 20px; - font-size: 14px; font-weight: 500; - opacity: 0; transition: opacity .25s, transform .25s; - pointer-events: none; z-index: 200; white-space: nowrap; + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(20px); + background: #1a1a2e; + color: #fff; + padding: 10px 20px; + border-radius: 20px; + font-size: 14px; + font-weight: 500; + opacity: 0; + transition: + opacity 0.25s, + transform 0.25s; + pointer-events: none; + z-index: 200; + white-space: nowrap; +} +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} +.toast.toast-error { + background: #c00; } -.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); } -.toast.toast-error { background: #c00; } /* ── Unlock overlay ──────────────────────────────────────────────── */ -.modal-unlock { max-width: 380px; } -.unlock-logo { margin-bottom: 16px; } -.unlock-title { margin-bottom: 4px; } -.unlock-subtitle { color: var(--color-text-muted); font-size: 13px; margin-bottom: 20px; } -.btn-link { padding: 10px 20px; border-radius: var(--radius); text-decoration: none; } +.modal-unlock { + max-width: 380px; +} +.unlock-logo { + margin-bottom: 16px; +} +.unlock-title { + margin-bottom: 4px; +} +.unlock-subtitle { + color: var(--color-text-muted); + font-size: 13px; + margin-bottom: 20px; +} +.btn-link { + padding: 10px 20px; + border-radius: var(--radius); + text-decoration: none; +} /* ── Auth step hint ─────────────────────────────────────────────── */ -.auth-step-hint { color: var(--color-text-muted); font-size: 13px; margin-bottom: 16px; } -#mfa-code { font-size: 22px; letter-spacing: .2em; text-align: center; } +.auth-step-hint { + color: var(--color-text-muted); + font-size: 13px; + margin-bottom: 16px; +} +#mfa-code { + font-size: 22px; + letter-spacing: 0.2em; + text-align: center; +} /* ── Panel body (sharing / emergency) ───────────────────────────── */ -.panel-body { flex: 1; overflow-y: auto; padding: 20px 24px; } +.panel-body { + flex: 1; + overflow-y: auto; + padding: 20px 24px; +} /* ── Tabs ────────────────────────────────────────────────────────── */ -.panel-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 2px solid var(--color-border); padding-bottom: 0; } -.tab-btn { - padding: 8px 16px; border-radius: 6px 6px 0 0; font-size: 13px; - font-weight: 500; color: var(--color-text-muted); - border-bottom: 2px solid transparent; margin-bottom: -2px; - transition: color var(--transition), border-color var(--transition); +.panel-tabs { + display: flex; + gap: 4px; + margin-bottom: 16px; + border-bottom: 2px solid var(--color-border); + padding-bottom: 0; +} +.tab-btn { + padding: 8px 16px; + border-radius: 6px 6px 0 0; + font-size: 13px; + font-weight: 500; + color: var(--color-text-muted); + border-bottom: 2px solid transparent; + margin-bottom: -2px; + transition: + color var(--transition), + border-color var(--transition); +} +.tab-btn:hover { + color: var(--color-text); +} +.tab-btn.active { + color: var(--color-primary); + border-bottom-color: var(--color-primary); } -.tab-btn:hover { color: var(--color-text); } -.tab-btn.active { color: var(--color-primary); border-bottom-color: var(--color-primary); } /* ── Share / Emergency lists ────────────────────────────────────── */ -.share-list { display: flex; flex-direction: column; gap: 8px; } +.share-list { + display: flex; + flex-direction: column; + gap: 8px; +} .share-item { - display: flex; align-items: center; gap: 12px; + display: flex; + align-items: center; + gap: 12px; background: var(--color-surface); border: 1px solid var(--color-border); - border-radius: var(--radius); padding: 12px 14px; + border-radius: var(--radius); + padding: 12px 14px; +} +.share-icon { + font-size: 22px; + flex-shrink: 0; +} +.share-info { + flex: 1; + min-width: 0; +} +.share-name { + display: block; + font-weight: 500; +} +.share-meta { + display: block; + font-size: 12px; + color: var(--color-text-muted); + margin-top: 2px; +} +.share-actions { + display: flex; + gap: 6px; + align-items: center; + flex-shrink: 0; } -.share-icon { font-size: 22px; flex-shrink: 0; } -.share-info { flex: 1; min-width: 0; } -.share-name { display: block; font-weight: 500; } -.share-meta { display: block; font-size: 12px; color: var(--color-text-muted); margin-top: 2px; } -.share-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; } /* ── Badges ──────────────────────────────────────────────────────── */ -.badge { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; } -.badge-warn { background: #fff8e1; color: #e65100; border: 1px solid #ffb74d; } -.badge-info { background: #e3f2fd; color: #1565c0; border: 1px solid #90caf9; } +.badge { + display: inline-block; + padding: 3px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} +.badge-warn { + background: #fff8e1; + color: #e65100; + border: 1px solid #ffb74d; +} +.badge-info { + background: #e3f2fd; + color: #1565c0; + border: 1px solid #90caf9; +} /* ── Settings modal ──────────────────────────────────────────────── */ -.modal-settings { max-width: 520px; } -.settings-section { padding: 16px 0; border-bottom: 1px solid var(--color-border); } -.settings-section:last-child { border-bottom: none; } -.settings-section-title { font-size: 15px; font-weight: 600; margin-bottom: 8px; } -.settings-desc { font-size: 13px; color: var(--color-text-muted); margin-bottom: 10px; line-height: 1.5; } -.mfa-qr { display: block; margin: 12px auto; max-width: 180px; border: 1px solid var(--color-border); border-radius: 4px; } -.mfa-secret-text { font-family: monospace; font-size: 12px; } -.mfa-secret-text code { background: var(--color-bg); padding: 2px 6px; border-radius: 4px; user-select: all; } +.modal-settings { + max-width: 520px; +} +.settings-section { + padding: 16px 0; + border-bottom: 1px solid var(--color-border); +} +.settings-section:last-child { + border-bottom: none; +} +.settings-section-title { + font-size: 15px; + font-weight: 600; + margin-bottom: 8px; +} +.settings-desc { + font-size: 13px; + color: var(--color-text-muted); + margin-bottom: 10px; + line-height: 1.5; +} +.mfa-qr { + display: block; + margin: 12px auto; + max-width: 180px; + border: 1px solid var(--color-border); + border-radius: 4px; +} +.mfa-secret-text { + font-family: monospace; + font-size: 12px; +} +.mfa-secret-text code { + background: var(--color-bg); + padding: 2px 6px; + border-radius: 4px; + user-select: all; +} /* ── Sort control ────────────────────────────────────────────────── */ .sort-wrapper select { @@ -432,21 +1022,35 @@ ul { list-style: none; } cursor: pointer; outline: none; } -.sort-wrapper select:focus { border-color: var(--color-primary); } +.sort-wrapper select:focus { + border-color: var(--color-primary); +} /* ── Modal item (wider for address/card forms) ───────────────────── */ -.modal-item { max-width: 540px; } +.modal-item { + max-width: 540px; +} /* ── Multi-column form rows ──────────────────────────────────────── */ -.form-row { display: flex; gap: 12px; } -.form-row .form-group { flex: 1; } +.form-row { + display: flex; + gap: 12px; +} +.form-row .form-group { + flex: 1; +} /* ── Btn small ───────────────────────────────────────────────────── */ -.btn-sm { padding: 5px 12px; font-size: 12px; border-radius: 6px; } +.btn-sm { + padding: 5px 12px; + font-size: 12px; + border-radius: 6px; +} /* ── Security dashboard ──────────────────────────────────────────── */ .security-dashboard { - flex: 1; overflow-y: auto; + flex: 1; + overflow-y: auto; padding: 24px; display: flex; flex-direction: column; @@ -454,8 +1058,10 @@ ul { list-style: none; } } .sec-notice { - text-align: center; color: var(--color-text-muted); - padding: 40px 0; font-size: 15px; + text-align: center; + color: var(--color-text-muted); + padding: 40px 0; + font-size: 15px; } .security-summary { @@ -469,17 +1075,40 @@ ul { list-style: none; } flex-wrap: wrap; } -.sec-score-card { text-align: center; flex-shrink: 0; } -.sec-score { - font-size: 56px; font-weight: 700; line-height: 1; +.sec-score-card { + text-align: center; + flex-shrink: 0; +} +.sec-score { + font-size: 56px; + font-weight: 700; + line-height: 1; +} +.sec-score.score-good { + color: #2e7d32; +} +.sec-score.score-fair { + color: #e65100; +} +.sec-score.score-poor { + color: #c62828; +} +.sec-score-label { + font-size: 16px; + font-weight: 600; + margin-top: 4px; +} +.sec-score-desc { + font-size: 12px; + color: var(--color-text-muted); + margin-top: 2px; } -.sec-score.score-good { color: #2e7d32; } -.sec-score.score-fair { color: #e65100; } -.sec-score.score-poor { color: #c62828; } -.sec-score-label { font-size: 16px; font-weight: 600; margin-top: 4px; } -.sec-score-desc { font-size: 12px; color: var(--color-text-muted); margin-top: 2px; } -.sec-stats { display: flex; gap: 16px; flex-wrap: wrap; } +.sec-stats { + display: flex; + gap: 16px; + flex-wrap: wrap; +} .sec-stat { text-align: center; background: var(--color-bg); @@ -488,11 +1117,29 @@ ul { list-style: none; } padding: 12px 20px; min-width: 80px; } -.sec-stat-warn { border-color: #ffb74d; background: #fff8e1; } -.sec-stat-info { border-color: #90caf9; background: #e3f2fd; } -.sec-stat-ok { border-color: #a5d6a7; background: #e8f5e9; } -.sec-stat-num { display: block; font-size: 28px; font-weight: 700; } -.sec-stat-label { display: block; font-size: 12px; color: var(--color-text-muted); margin-top: 2px; } +.sec-stat-warn { + border-color: #ffb74d; + background: #fff8e1; +} +.sec-stat-info { + border-color: #90caf9; + background: #e3f2fd; +} +.sec-stat-ok { + border-color: #a5d6a7; + background: #e8f5e9; +} +.sec-stat-num { + display: block; + font-size: 28px; + font-weight: 700; +} +.sec-stat-label { + display: block; + font-size: 12px; + color: var(--color-text-muted); + margin-top: 2px; +} .sec-section { background: var(--color-surface); @@ -501,74 +1148,260 @@ ul { list-style: none; } overflow: hidden; } .sec-section-header { - display: flex; align-items: flex-start; gap: 12px; + display: flex; + align-items: flex-start; + gap: 12px; padding: 16px 20px; border-bottom: 1px solid var(--color-border); background: var(--color-bg); } -.sec-section-icon { font-size: 20px; flex-shrink: 0; } -.sec-section-title { font-weight: 600; font-size: 15px; } -.sec-section-desc { font-size: 12px; color: var(--color-text-muted); margin-top: 2px; } +.sec-section-icon { + font-size: 20px; + flex-shrink: 0; +} +.sec-section-title { + font-weight: 600; + font-size: 15px; +} +.sec-section-desc { + font-size: 12px; + color: var(--color-text-muted); + margin-top: 2px; +} -.sec-item-list { padding: 0; } +.sec-item-list { + padding: 0; +} .sec-item { - display: flex; align-items: center; gap: 12px; + display: flex; + align-items: center; + gap: 12px; padding: 10px 20px; border-bottom: 1px solid var(--color-border); } -.sec-item:last-child { border-bottom: none; } -.sec-item-name { font-weight: 500; flex: 1; } -.sec-item-sub { font-size: 12px; color: var(--color-text-muted); flex: 1; } +.sec-item:last-child { + border-bottom: none; +} +.sec-item-name { + font-weight: 500; + flex: 1; +} +.sec-item-sub { + font-size: 12px; + color: var(--color-text-muted); + flex: 1; +} /* ── Item Detail Modal ───────────────────────────────────────────── */ -.modal-detail { max-height: 80vh; display: flex; flex-direction: column; } -.detail-body { flex: 1; overflow-y: auto; padding: 4px 0; } -.detail-field { display: flex; align-items: flex-start; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--color-border); } -.detail-field:last-child { border-bottom: none; } -.detail-label { font-size: 12px; color: var(--color-text-muted); width: 140px; flex-shrink: 0; padding-top: 4px; } -.detail-val-row { display: flex; align-items: center; gap: 6px; flex: 1; flex-wrap: wrap; } -.detail-val { font-size: 14px; word-break: break-all; } -.detail-sensitive { display: inline-flex; align-items: center; font-family: monospace; font-size: 14px; } -.detail-masked { letter-spacing: 2px; } -.detail-actual { word-break: break-all; } -.btn-reveal, .btn-copy-field { background: none; border: none; cursor: pointer; padding: 2px 4px; font-size: 14px; color: var(--color-text-muted); flex-shrink: 0; } -.btn-reveal:hover, .btn-copy-field:hover { color: var(--color-primary); } +.modal-detail { + max-height: 80vh; + display: flex; + flex-direction: column; +} +.detail-body { + flex: 1; + overflow-y: auto; + padding: 4px 0; +} +.detail-field { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid var(--color-border); +} +.detail-field:last-child { + border-bottom: none; +} +.detail-label { + font-size: 12px; + color: var(--color-text-muted); + width: 140px; + flex-shrink: 0; + padding-top: 4px; +} +.detail-val-row { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + flex-wrap: wrap; +} +.detail-val { + font-size: 14px; + word-break: break-all; +} +.detail-sensitive { + display: inline-flex; + align-items: center; + font-family: monospace; + font-size: 14px; +} +.detail-masked { + letter-spacing: 2px; +} +.detail-actual { + word-break: break-all; +} +.btn-reveal, +.btn-copy-field { + background: none; + border: none; + cursor: pointer; + padding: 2px 4px; + font-size: 14px; + color: var(--color-text-muted); + flex-shrink: 0; +} +.btn-reveal:hover, +.btn-copy-field:hover { + color: var(--color-primary); +} /* ── Emergency Vault Panel ───────────────────────────────────────── */ -.em-vault-panel { padding: 8px 0; } -.em-vault-header { display: flex; align-items: center; justify-content: space-between; padding: 4px 0 16px; border-bottom: 2px solid var(--color-border); margin-bottom: 8px; } -.em-vault-header h4 { margin: 0; font-size: 15px; font-weight: 600; } -.em-vault-list { list-style: none; padding: 0; margin: 0; } -.em-vault-item { border-bottom: 1px solid var(--color-border); } -.em-vault-item:last-child { border-bottom: none; } -.em-vault-item-header { display: flex; align-items: center; gap: 10px; width: 100%; background: none; border: none; padding: 12px 4px; cursor: pointer; text-align: left; } -.em-vault-item-header:hover { background: var(--color-sidebar-hover); border-radius: var(--radius); } -.em-vault-icon { font-size: 18px; flex-shrink: 0; } -.em-vault-name { font-weight: 500; flex: 1; } -.em-vault-type { font-size: 12px; color: var(--color-text-muted); } -.em-vault-chevron { font-size: 12px; color: var(--color-text-muted); flex-shrink: 0; } -.em-vault-item-body { padding: 0 8px 12px 36px; } -.em-detail-body .detail-field { padding: 8px 0; } +.em-vault-panel { + padding: 8px 0; +} +.em-vault-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 0 16px; + border-bottom: 2px solid var(--color-border); + margin-bottom: 8px; +} +.em-vault-header h4 { + margin: 0; + font-size: 15px; + font-weight: 600; +} +.em-vault-list { + list-style: none; + padding: 0; + margin: 0; +} +.em-vault-item { + border-bottom: 1px solid var(--color-border); +} +.em-vault-item:last-child { + border-bottom: none; +} +.em-vault-item-header { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + background: none; + border: none; + padding: 12px 4px; + cursor: pointer; + text-align: left; +} +.em-vault-item-header:hover { + background: var(--color-sidebar-hover); + border-radius: var(--radius); +} +.em-vault-icon { + font-size: 18px; + flex-shrink: 0; +} +.em-vault-name { + font-weight: 500; + flex: 1; +} +.em-vault-type { + font-size: 12px; + color: var(--color-text-muted); +} +.em-vault-chevron { + font-size: 12px; + color: var(--color-text-muted); + flex-shrink: 0; +} +.em-vault-item-body { + padding: 0 8px 12px 36px; +} +.em-detail-body .detail-field { + padding: 8px 0; +} /* ── Responsive ──────────────────────────────────────────────────── */ @media (max-width: 640px) { - .sidebar { width: 60px; } + .sidebar { + width: 56px; + overflow-y: visible; + } .sidebar-header .logo-text, .sidebar-label, - .sidebar-section-header { display: none; } - .sidebar-item { justify-content: center; padding: 12px 0; } - .vault-header { padding: 12px 16px; } - .vault-list { padding: 8px 16px 16px; } - .sort-wrapper { display: none; } - .form-row { flex-direction: column; gap: 0; } + .sidebar-section-header { + opacity: 0; + width: 0; + } + .sidebar-item { + justify-content: center; + padding: 12px 0; + } + .vault-header { + padding: 12px 16px; + } + .vault-list { + padding: 8px 16px 16px; + } + .sort-wrapper { + display: none; + } + .form-row { + flex-direction: column; + gap: 0; + } } /* ── Settings — new sections (Phase 6) ──────────────────────────── */ -.settings-section-danger { border-top: 2px solid var(--color-danger, #e53e3e); margin-top: 8px; padding-top: 16px; } -.settings-section-danger .settings-section-title { color: var(--color-danger, #e53e3e); } -.btn-danger { background: var(--color-danger, #e53e3e); color: #fff; border: none; border-radius: var(--radius); padding: 8px 18px; font-size: 14px; font-weight: 500; cursor: pointer; transition: opacity .15s; } -.btn-danger:hover { opacity: .85; } -.btn-danger:disabled { opacity: .5; cursor: not-allowed; } -.recovery-code-box { display: flex; align-items: center; gap: 8px; background: var(--color-bg-secondary, #f7f7f7); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 12px 14px; margin: 8px 0; } -.recovery-code-box code { font-family: monospace; font-size: 15px; letter-spacing: .08em; word-break: break-all; flex: 1; } -.warning-text { color: var(--color-danger, #e53e3e); font-size: 13px; font-weight: 500; } +.settings-section-danger { + border-top: 2px solid var(--color-danger, #e53e3e); + margin-top: 8px; + padding-top: 16px; +} +.settings-section-danger .settings-section-title { + color: var(--color-danger, #e53e3e); +} +.btn-danger { + background: var(--color-danger, #e53e3e); + color: #fff; + border: none; + border-radius: var(--radius); + padding: 8px 18px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: opacity 0.15s; +} +.btn-danger:hover { + opacity: 0.85; +} +.btn-danger:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.recovery-code-box { + display: flex; + align-items: center; + gap: 8px; + background: var(--color-bg-secondary, #f7f7f7); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 12px 14px; + margin: 8px 0; +} +.recovery-code-box code { + font-family: monospace; + font-size: 15px; + letter-spacing: 0.08em; + word-break: break-all; + flex: 1; +} +.warning-text { + color: var(--color-danger, #e53e3e); + font-size: 13px; + font-weight: 500; +} diff --git a/app/static/js/vault.js b/app/static/js/vault.js index 03d57d9..3fc5a5d 100644 --- a/app/static/js/vault.js +++ b/app/static/js/vault.js @@ -8,33 +8,60 @@ const Vault = (() => { let _items = []; let _folders = []; - let _currentView = 'vault'; + let _currentView = "vault"; let _activeFilter = null; - let _sortOrder = 'name-asc'; + let _sortOrder = "name-asc"; const FIELD_LABELS = { - url: 'Website URL', username: 'Username', password: 'Password', notes: 'Notes', - note_body: 'Note', cardholder_name: 'Cardholder Name', card_number: 'Card Number', - expiry_month: 'Expiry Month', expiry_year: 'Expiry Year', cvv: 'CVV', - bank_name: 'Bank Name', account_type: 'Account Type', routing_number: 'Routing Number', - account_number: 'Account Number', first_name: 'First Name', last_name: 'Last Name', - company: 'Company', address_line: 'Street Address', city: 'City', state: 'State', - zip: 'ZIP', country: 'Country', phone: 'Phone', email: 'Email', - ssn_number: 'Social Security Number', + url: "Website URL", + username: "Username", + password: "Password", + notes: "Notes", + note_body: "Note", + cardholder_name: "Cardholder Name", + card_number: "Card Number", + expiry_month: "Expiry Month", + expiry_year: "Expiry Year", + cvv: "CVV", + bank_name: "Bank Name", + account_type: "Account Type", + routing_number: "Routing Number", + account_number: "Account Number", + first_name: "First Name", + last_name: "Last Name", + company: "Company", + address_line: "Street Address", + city: "City", + state: "State", + zip: "ZIP", + country: "Country", + phone: "Phone", + email: "Email", + ssn_number: "Social Security Number", }; - const SENSITIVE_FIELDS = new Set(['password', 'cvv', 'card_number', 'account_number', 'routing_number', 'ssn_number']); + const SENSITIVE_FIELDS = new Set([ + "password", + "cvv", + "card_number", + "account_number", + "routing_number", + "ssn_number", + ]); // ── API helpers ─────────────────────────────────────────────────────────── async function apiFetch(path, options = {}) { - let token = sessionStorage.getItem('access_token'); - if (!token) { redirectToLogin(); return null; } + let token = sessionStorage.getItem("access_token"); + if (!token) { + redirectToLogin(); + return null; + } const defaults = { headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - 'X-CSRFToken': csrfToken(), + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-CSRFToken": csrfToken(), }, }; const merged = { ...defaults, ...options }; @@ -44,8 +71,12 @@ const Vault = (() => { if (res.status === 401) { const refreshed = await tryRefreshToken(); - if (!refreshed) { redirectToLogin(); return null; } - merged.headers['Authorization'] = `Bearer ${sessionStorage.getItem('access_token')}`; + if (!refreshed) { + redirectToLogin(); + return null; + } + merged.headers["Authorization"] = + `Bearer ${sessionStorage.getItem("access_token")}`; res = await fetch(path, merged); } @@ -62,64 +93,85 @@ const Vault = (() => { // don't each fire a separate rotation (which would blacklist each other's tokens). if (_refreshPromise) return _refreshPromise; _refreshPromise = (async () => { - const refreshToken = localStorage.getItem('refresh_token'); + const refreshToken = localStorage.getItem("refresh_token"); if (!refreshToken) return false; try { - const res = await fetch('/api/auth/refresh', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken() }, + const res = await fetch("/api/auth/refresh", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": csrfToken(), + }, body: JSON.stringify({ refresh_token: refreshToken }), }); if (!res.ok) return false; const data = await res.json(); - sessionStorage.setItem('access_token', data.access_token); - if (data.refresh_token) localStorage.setItem('refresh_token', data.refresh_token); + sessionStorage.setItem("access_token", data.access_token); + if (data.refresh_token) + localStorage.setItem("refresh_token", data.refresh_token); return true; - } catch { return false; } - })().finally(() => { _refreshPromise = null; }); + } catch { + return false; + } + })().finally(() => { + _refreshPromise = null; + }); return _refreshPromise; } function csrfToken() { - return document.querySelector('meta[name="csrf-token"]')?.content || ''; + return document.querySelector('meta[name="csrf-token"]')?.content || ""; } function redirectToLogin() { VaultSession.clear(); SharingSession.clear(); - sessionStorage.removeItem('access_token'); - window.dispatchEvent(new CustomEvent('passkeeper:logout')); - window.location.href = '/login'; + sessionStorage.removeItem("access_token"); + window.dispatchEvent(new CustomEvent("passkeeper:logout")); + window.location.href = "/login"; } // ── Load ────────────────────────────────────────────────────────────────── async function loadVault() { const vaultKey = VaultSession.getKey(); - if (!vaultKey) { showUnlockOverlay(); return; } + if (!vaultKey) { + showUnlockOverlay(); + return; + } showLoadingState(true); try { const [itemsRes, foldersRes] = await Promise.all([ - apiFetch('/api/vault'), apiFetch('/api/folders'), + apiFetch("/api/vault"), + apiFetch("/api/folders"), ]); - if (!itemsRes || !foldersRes) { showUnlockOverlay(); return; } + if (!itemsRes || !foldersRes) { + showUnlockOverlay(); + return; + } const rawItems = await itemsRes.json(); _folders = await foldersRes.json(); - _items = await Promise.all(rawItems.map(async item => { - try { - const plain = await Crypto.decryptItem(vaultKey, item.enc_data, item.iv); - return { ...item, plain }; - } catch { - return { ...item, plain: null, decryptError: true }; - } - })); + _items = await Promise.all( + rawItems.map(async (item) => { + try { + const plain = await Crypto.decryptItem( + vaultKey, + item.enc_data, + item.iv, + ); + return { ...item, plain }; + } catch { + return { ...item, plain: null, decryptError: true }; + } + }), + ); renderFolderList(); applyCurrentFilter(); } catch (err) { - showToast('Failed to load vault: ' + err.message, 'error'); + showToast("Failed to load vault: " + err.message, "error"); } finally { showLoadingState(false); } @@ -127,12 +179,12 @@ const Vault = (() => { async function loadFolders() { try { - const res = await apiFetch('/api/folders'); + const res = await apiFetch("/api/folders"); if (!res) return; _folders = await res.json(); renderFolderList(); } catch (err) { - showToast('Could not refresh folders: ' + err.message, 'error'); + showToast("Could not refresh folders: " + err.message, "error"); } } @@ -140,43 +192,53 @@ const Vault = (() => { function switchView(view) { _currentView = view; - ['vault', 'security', 'sharing', 'emergency'].forEach(v => { - document.getElementById(`view-${v}`)?.classList.toggle('hidden', v !== view); + ["vault", "security", "sharing", "emergency"].forEach((v) => { + document + .getElementById(`view-${v}`) + ?.classList.toggle("hidden", v !== view); }); - document.querySelectorAll('.sidebar-item[data-view]').forEach(el => { - el.classList.toggle('active', el.dataset.view === view); + document.querySelectorAll(".sidebar-item[data-view]").forEach((el) => { + el.classList.toggle("active", el.dataset.view === view); }); - if (view !== 'vault') { - document.querySelectorAll('.sidebar-item[data-type-filter], .sidebar-item[data-folder-id]') - .forEach(el => el.classList.remove('active')); + if (view !== "vault") { + document + .querySelectorAll( + ".sidebar-item[data-type-filter], .sidebar-item[data-folder-id]", + ) + .forEach((el) => el.classList.remove("active")); } - if (view === 'security') renderSecurityDashboard(); - if (view === 'sharing') loadSharingView(); - if (view === 'emergency') loadEmergencyView(); + if (view === "security") renderSecurityDashboard(); + if (view === "sharing") loadSharingView(); + if (view === "emergency") loadEmergencyView(); } // ── Vault render ────────────────────────────────────────────────────────── function renderFolderList() { - const ul = document.getElementById('sidebar-folders'); + const ul = document.getElementById("sidebar-folders"); if (!ul) return; - ul.innerHTML = ''; - _folders.forEach(folder => { - const li = document.createElement('li'); - li.className = 'sidebar-item folder-item'; + ul.innerHTML = ""; + _folders.forEach((folder) => { + const li = document.createElement("li"); + li.className = "sidebar-item folder-item"; li.dataset.folderId = folder.id; - const label = document.createElement('span'); - label.className = 'folder-label'; + const label = document.createElement("span"); + label.className = "folder-label"; label.innerHTML = ` `; - label.addEventListener('click', () => filterByFolder(folder.id, folder.name)); + label.addEventListener("click", () => + filterByFolder(folder.id, folder.name), + ); - const delBtn = document.createElement('button'); - delBtn.className = 'btn-delete-folder'; - delBtn.title = 'Delete folder'; - delBtn.textContent = '🗑'; - delBtn.addEventListener('click', e => { e.stopPropagation(); confirmDeleteFolder(folder); }); + const delBtn = document.createElement("button"); + delBtn.className = "btn-delete-folder"; + delBtn.title = "Delete folder"; + delBtn.textContent = "🗑"; + delBtn.addEventListener("click", (e) => { + e.stopPropagation(); + confirmDeleteFolder(folder); + }); li.appendChild(label); li.appendChild(delBtn); @@ -187,75 +249,124 @@ const Vault = (() => { function getSortedItems(items) { const sorted = [...items]; switch (_sortOrder) { - case 'name-asc': sorted.sort((a, b) => a.name.localeCompare(b.name)); break; - case 'name-desc': sorted.sort((a, b) => b.name.localeCompare(a.name)); break; - case 'date-desc': sorted.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); break; - case 'date-asc': sorted.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); break; - case 'folder': sorted.sort((a, b) => folderName(a.folder_id).localeCompare(folderName(b.folder_id)) || a.name.localeCompare(b.name)); break; + case "name-asc": + sorted.sort((a, b) => a.name.localeCompare(b.name)); + break; + case "name-desc": + sorted.sort((a, b) => b.name.localeCompare(a.name)); + break; + case "date-desc": + sorted.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + break; + case "date-asc": + sorted.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); + break; + case "folder": + sorted.sort( + (a, b) => + folderName(a.folder_id).localeCompare(folderName(b.folder_id)) || + a.name.localeCompare(b.name), + ); + break; } return sorted; } function folderName(id) { - if (!id) return '(No folder)'; - return _folders.find(f => f.id === id)?.name || 'Unknown'; + if (!id) return "(No folder)"; + return _folders.find((f) => f.id === id)?.name || "Unknown"; } function renderItemList(items) { - const list = document.getElementById('vault-list'); + const list = document.getElementById("vault-list"); if (!list) return; - list.innerHTML = ''; + list.innerHTML = ""; if (items.length === 0) { - list.innerHTML = '
No fields to display.
'; + }) + .join("") || 'No fields to display.
'; - container.querySelectorAll('.btn-reveal').forEach(btn => { - btn.addEventListener('click', () => { + container.querySelectorAll(".btn-reveal").forEach((btn) => { + btn.addEventListener("click", () => { const span = btn.previousElementSibling; - const revealed = span.dataset.revealed === 'true'; + const revealed = span.dataset.revealed === "true"; span.dataset.revealed = String(!revealed); - span.querySelector('.detail-masked').classList.toggle('hidden', !revealed); - span.querySelector('.detail-actual').classList.toggle('hidden', revealed); + span + .querySelector(".detail-masked") + .classList.toggle("hidden", !revealed); + span + .querySelector(".detail-actual") + .classList.toggle("hidden", revealed); }); }); - container.querySelectorAll('.btn-copy-field').forEach(btn => { - btn.addEventListener('click', () => { - navigator.clipboard.writeText(btn.dataset.copy).then(() => showToast('Copied!')); + container.querySelectorAll(".btn-copy-field").forEach((btn) => { + btn.addEventListener("click", () => { + navigator.clipboard + .writeText(btn.dataset.copy) + .then(() => showToast("Copied!")); }); }); } async function handleShareFormSubmit(e) { e.preventDefault(); - const errEl = document.getElementById('share-error'); - errEl.classList.add('hidden'); + const errEl = document.getElementById("share-error"); + errEl.classList.add("hidden"); if (!SharingSession.isReady()) { - showToast('Set up sharing keys first (Settings)', 'error'); + showToast("Set up sharing keys first (Settings)", "error"); return; } - const itemId = parseInt(document.getElementById('share-item-select').value); - const recipientEmail = document.getElementById('share-recipient-email').value.trim().toLowerCase(); + const itemId = parseInt(document.getElementById("share-item-select").value); + const recipientEmail = document + .getElementById("share-recipient-email") + .value.trim() + .toLowerCase(); if (!itemId || !recipientEmail) { - errEl.textContent = 'Please select an item and enter a recipient email.'; - errEl.classList.remove('hidden'); + errEl.textContent = "Please select an item and enter a recipient email."; + errEl.classList.remove("hidden"); return; } try { // Fetch recipient's public key - const pkRes = await apiFetch(`/api/sharing/public-key?email=${encodeURIComponent(recipientEmail)}`); + const pkRes = await apiFetch( + `/api/sharing/public-key?email=${encodeURIComponent(recipientEmail)}`, + ); if (!pkRes) return; const pkData = await pkRes.json(); - const recipientPubKey = await SharingCrypto.importPublicKey(pkData.public_key); - const sharedKey = await SharingCrypto.deriveSharedKey(SharingSession.getKey(), recipientPubKey); + const recipientPubKey = await SharingCrypto.importPublicKey( + pkData.public_key, + ); + const sharedKey = await SharingCrypto.deriveSharedKey( + SharingSession.getKey(), + recipientPubKey, + ); // Find item plaintext - const item = _items.find(i => i.id === itemId); - if (!item?.plain) { showToast('Item not found or not decrypted', 'error'); return; } + const item = _items.find((i) => i.id === itemId); + if (!item?.plain) { + showToast("Item not found or not decrypted", "error"); + return; + } - const { enc_data, iv } = await SharingCrypto.encryptForShare(sharedKey, item.plain); + const { enc_data, iv } = await SharingCrypto.encryptForShare( + sharedKey, + item.plain, + ); - const res = await apiFetch('/api/sharing', { - method: 'POST', + const res = await apiFetch("/api/sharing", { + method: "POST", body: JSON.stringify({ item_id: itemId, recipient_email: recipientEmail, - enc_data, iv, + enc_data, + iv, item_name: item.name, item_type: item.item_type, }), }); if (!res) return; - closeModal('share-modal'); - showToast('Item shared successfully'); + closeModal("share-modal"); + showToast("Item shared successfully"); loadSharedOut(); } catch (err) { errEl.textContent = err.message; - errEl.classList.remove('hidden'); + errEl.classList.remove("hidden"); } } function openShareModal() { - const sel = document.getElementById('share-item-select'); - sel.innerHTML = _items.map(i => - `` - ).join(''); - document.getElementById('share-recipient-email').value = ''; - document.getElementById('share-error').classList.add('hidden'); - document.getElementById('share-modal').classList.add('open'); + const sel = document.getElementById("share-item-select"); + sel.innerHTML = _items + .map( + (i) => + ``, + ) + .join(""); + document.getElementById("share-recipient-email").value = ""; + document.getElementById("share-error").classList.add("hidden"); + document.getElementById("share-modal").classList.add("open"); } // ── Emergency Access View ───────────────────────────────────────────────── async function loadEmergencyView() { try { - const res = await apiFetch('/api/emergency'); + const res = await apiFetch("/api/emergency"); if (!res) return; const data = await res.json(); renderEmergencyGrants(data.grants); renderEmergencyAccess(data.access); } catch (err) { - showToast('Could not load emergency access data: ' + err.message, 'error'); + showToast( + "Could not load emergency access data: " + err.message, + "error", + ); } } function renderEmergencyGrants(grants) { - const ul = document.getElementById('em-grants-list'); + const ul = document.getElementById("em-grants-list"); if (!ul) return; if (!grants.length) { - ul.innerHTML = '