Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 137 additions & 2 deletions pages/0x67/page.css
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ body:not(.app-mode) {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
}

/* Sidebar */
Expand Down Expand Up @@ -562,6 +563,20 @@ body:not(.app-mode) {
display: flex;
align-items: center;
gap: 0.5rem;
position: relative;
}

/* On desktop the menu contributes no box of its own — its children
* (column picker, sort select) sit directly in .panel-header-actions,
* same as before this was introduced. Mobile overrides this to turn
* it into a dropdown; see the mobile layout section below. */
.panel-menu {
display: contents;
}

.sidebar-toggle-btn,
.panel-menu-btn {
display: none;
}

.sort-select {
Expand Down Expand Up @@ -745,6 +760,95 @@ body:not(.app-mode) {
font-size: 0.875rem;
}

/* ============================================================
Mobile layout (Entry List screen)
Below ~700px there isn't room to show the sidebar, full header,
and every panel control inline, so: the sidebar becomes a
slide-in drawer, the header wraps the search field onto its own
row, and the sort/column controls collapse into a "⋯" menu.
============================================================ */

@media (max-width: 700px) {
.app-header {
height: auto;
flex-wrap: wrap;
row-gap: 0.5rem;
padding: 0.5rem 0.75rem;
}

.sidebar-toggle-btn {
display: inline-flex;
order: 0;
}

.wordmark {
order: 1;
}

.header-actions {
order: 2;
margin-left: auto;
}

.search-wrap {
order: 3;
flex-basis: 100%;
}

.sidebar {
position: absolute;
top: 0;
bottom: 0;
left: 0;
z-index: 20;
width: min(80vw, 260px);
background: var(--surface);
box-shadow: 2px 0 16px rgba(0, 0, 0, 0.18);
transform: translateX(-100%);
transition: transform 0.2s ease;
}

.sidebar.sidebar-open {
transform: translateX(0);
}

.sidebar-backdrop {
position: absolute;
inset: 0;
background: rgba(14, 23, 20, 0.45);
z-index: 15;
}

.panel-menu-btn {
display: inline-flex;
}

.panel-menu {
display: none;
position: absolute;
top: calc(100% + 0.35rem);
right: 1rem;
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
padding: 0.5rem;
z-index: 10;
}

.panel-menu.panel-menu-open {
display: flex;
}

#column-picker-btn,
.sort-select {
width: 100%;
}
}

/* ============================================================
Screen: Entry Detail
============================================================ */
Expand Down Expand Up @@ -779,6 +883,20 @@ body:not(.app-mode) .screen-detail {
gap: 0.5rem;
}

/* Edit / Move to… / Print / Trash / Delete don't all fit on one line next to
* "← Entries" on a phone-width screen — wrap instead of forcing the screen
* wider than the viewport (and a horizontal scrollbar with it). */
@media (max-width: 700px) {
.detail-header {
flex-wrap: wrap;
row-gap: 0.5rem;
}

.detail-header-actions {
flex-wrap: wrap;
}
}

.detail-body {
flex: 1;
overflow-y: auto;
Expand Down Expand Up @@ -892,6 +1010,15 @@ body:not(.app-mode) .screen-edit {
flex-shrink: 0;
}

/* Same fix as .detail-header: don't let a long entry title push Cancel/Save
* wider than a phone-width viewport. */
@media (max-width: 700px) {
.edit-header {
flex-wrap: wrap;
row-gap: 0.5rem;
}
}

.edit-title {
font-size: 1rem;
font-weight: 600;
Expand Down Expand Up @@ -945,11 +1072,16 @@ body:not(.app-mode) .screen-edit {

.edit-value {
flex: 1;
/* min-width: 0 alone doesn't stop this <input> from forcing its row (and
* everything above it, up to the layout viewport) wider than the screen on
* a narrow phone — Chromium still counts its intrinsic size toward the
* row's automatic minimum. width: 0 makes the zero-basis stick. */
width: 0;
min-width: 0;
background: none;
border: none;
color: var(--text);
font-size: 0.9rem;
min-width: 0;
}

.edit-value:focus {
Expand Down Expand Up @@ -1086,7 +1218,10 @@ input.attachment-name:focus {
border-radius: 10px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
color: var(--text);
min-width: 340px;
/* 340px was a hard floor: on any viewport narrower than ~378px (340 / 0.9),
* it wins over width: 90vw and pushes the dialog wider than the screen.
* min() lets it shrink with the viewport instead of forcing overflow. */
min-width: min(340px, 92vw);
max-width: 480px;
width: 90vw;
padding: 0;
Expand Down
31 changes: 18 additions & 13 deletions pages/0x67/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ <h1 class="db-filename">New database</h1>
<template id="tpl-entry-list">
<div class="screen screen-app">
<header class="app-header">
<button type="button" class="icon-btn sidebar-toggle-btn" data-action="toggle-sidebar" title="Groups" aria-label="Show groups">☰</button>
<span class="wordmark"><span class="mark"></span>KeePass Web</span>
<div class="search-wrap">
<input type="search" id="search-input" class="search-input" placeholder="Search all entries…" autocomplete="off">
Expand All @@ -110,7 +111,8 @@ <h1 class="db-filename">New database</h1>
</div>
</header>
<div class="app-body">
<aside class="sidebar">
<div id="sidebar-backdrop" class="sidebar-backdrop" hidden></div>
<aside id="sidebar" class="sidebar">
<div class="sidebar-header">
<span class="sidebar-label">Groups</span>
<button type="button" class="icon-btn" data-action="add-group" title="New group">+</button>
Expand All @@ -125,18 +127,21 @@ <h1 class="db-filename">New database</h1>
<button type="button" class="view-toggle-btn" data-action="view-tile" title="Tile view">▦</button>
<button type="button" class="view-toggle-btn" data-action="view-table" title="Table view">▤</button>
</div>
<div class="column-picker">
<button type="button" class="btn btn-secondary" id="column-picker-btn" data-action="toggle-columns" hidden>Columns</button>
<div id="column-picker-menu" class="column-picker-menu" hidden></div>
<button type="button" class="icon-btn panel-menu-btn" data-action="toggle-panel-menu" title="More options" aria-label="More options">⋯</button>
<div id="panel-menu" class="panel-menu">
<div class="column-picker">
<button type="button" class="btn btn-secondary" id="column-picker-btn" data-action="toggle-columns" hidden>Columns</button>
<div id="column-picker-menu" class="column-picker-menu" hidden></div>
</div>
<select id="sort-select" class="sort-select" title="Sort entries">
<option value="title:asc">Title (A–Z)</option>
<option value="title:desc">Title (Z–A)</option>
<option value="username:asc">Username (A–Z)</option>
<option value="username:desc">Username (Z–A)</option>
<option value="modified:desc">Modified (newest)</option>
<option value="modified:asc">Modified (oldest)</option>
</select>
</div>
<select id="sort-select" class="sort-select" title="Sort entries">
<option value="title:asc">Title (A–Z)</option>
<option value="title:desc">Title (Z–A)</option>
<option value="username:asc">Username (A–Z)</option>
<option value="username:desc">Username (Z–A)</option>
<option value="modified:desc">Modified (newest)</option>
<option value="modified:asc">Modified (oldest)</option>
</select>
<button type="button" class="icon-btn" data-action="add-entry" title="New entry">+</button>
</div>
</div>
Expand Down Expand Up @@ -282,7 +287,7 @@ <h2>Save database</h2>
<p class="save-status" data-role="save-status" hidden></p>
</div>
<div class="dialog-footer">
<button type="button" class="btn btn-secondary" data-action="close">Later</button>
<button type="button" class="btn btn-secondary" data-action="close" data-role="save-later">Later</button>
<button type="button" class="btn btn-primary" data-action="download">Download</button>
<button type="button" class="btn btn-primary" data-action="save-host" hidden>Save to Drive</button>
</div>
Expand Down
31 changes: 30 additions & 1 deletion pages/0x67/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ const app: AppState = {
dirty: false,
sortField: 'title',
sortDir: 'asc',
entryView: 'table',
// Tile view reads better than a dense table on a narrow phone screen;
// this only sets the initial default, the view toggle still overrides it.
entryView: window.innerWidth <= 700 ? 'tile' : 'table',
columnVisibility: {
username: true,
password: true,
Expand Down Expand Up @@ -296,6 +298,13 @@ function showEntryList(): void {
wireEntryListEvents();
}

/** No-op outside the mobile drawer layout, where the sidebar toggle and
* backdrop are invisible/unreachable — cheap to call unconditionally. */
function setSidebarOpen(open: boolean): void {
qs('#sidebar').classList.toggle('sidebar-open', open);
qs<HTMLElement>('#sidebar-backdrop').hidden = !open;
}

function renderGroupTree(): void {
const container = qs('#group-tree');
container.innerHTML = '';
Expand All @@ -321,6 +330,7 @@ function buildGroupNode(group: XmlElement, isRoot: boolean): HTMLLIElement {
if (searchInput) searchInput.value = '';
renderGroupTree();
renderEntryPanel();
setSidebarOpen(false);
});
row.appendChild(btn);

Expand Down Expand Up @@ -638,6 +648,7 @@ function updateViewToggleUI(): void {
qs('[data-action="view-table"]').classList.toggle('active', app.entryView === 'table');
qs<HTMLButtonElement>('#column-picker-btn').hidden = app.entryView !== 'table';
qs<HTMLElement>('#column-picker-menu').hidden = true;
qs<HTMLElement>('#panel-menu').classList.remove('panel-menu-open');
}

function wireEntryListEvents(): void {
Expand Down Expand Up @@ -716,6 +727,16 @@ function wireEntryListEvents(): void {
menu.hidden = !menu.hidden;
});

qs('[data-action="toggle-sidebar"]').addEventListener('click', () => {
setSidebarOpen(!qs('#sidebar').classList.contains('sidebar-open'));
});

qs('#sidebar-backdrop').addEventListener('click', () => setSidebarOpen(false));

qs('[data-action="toggle-panel-menu"]').addEventListener('click', () => {
qs('#panel-menu').classList.toggle('panel-menu-open');
});

buildColumnPickerMenu();
updateViewToggleUI();
}
Expand Down Expand Up @@ -1322,6 +1343,7 @@ function openSaveDialog(): void {
const status = must(dlg.querySelector<HTMLElement>('[data-role="save-status"]'));
const downloadBtn = must(dlg.querySelector<HTMLButtonElement>('[data-action="download"]'));
const hostBtn = must(dlg.querySelector<HTMLButtonElement>('[data-action="save-host"]'));
const laterBtn = must(dlg.querySelector<HTMLButtonElement>('[data-role="save-later"]'));

status.hidden = true;
status.textContent = '';
Expand All @@ -1330,6 +1352,7 @@ function openSaveDialog(): void {
hostMsg.hidden = !hostSession;
downloadBtn.hidden = hostSession;
hostBtn.hidden = !hostSession;
laterBtn.textContent = 'Later';

downloadBtn.onclick = async () => {
await downloadDatabase();
Expand Down Expand Up @@ -1668,6 +1691,12 @@ function notifyHostSaveResult(ok: boolean, error?: string): void {
app.dirty = false;
status.textContent = 'Saved to Google Drive.';
status.classList.add('ok');
// Retrying no longer makes sense once the save has succeeded — collapse
// the footer to a single acknowledgement instead of leaving stale
// "Later" / "Save to Drive" actions from before the save was requested.
const dlg = byId<HTMLDialogElement>('dlg-save');
must(dlg.querySelector<HTMLButtonElement>('[data-role="save-later"]')).textContent = 'Close';
button.hidden = true;
} else {
status.textContent = error ? `Save failed: ${error}` : 'Save failed.';
status.classList.add('error');
Expand Down
Loading