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
38 changes: 10 additions & 28 deletions pages/0x67/page.css
Original file line number Diff line number Diff line change
Expand Up @@ -957,13 +957,19 @@ body:not(.app-mode) .screen-detail {
color: var(--muted);
}

.detail-value-wrap {
.detail-value-wrap,
.edit-field,
.attachment-row,
.history-row {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
}

.detail-value-wrap {
padding: 0.5rem 0.75rem;
}

Expand Down Expand Up @@ -1039,13 +1045,9 @@ body:not(.app-mode) .screen-edit {
gap: 0.5rem;
}

.edit-field {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
.edit-field,
.attachment-row,
.history-row {
padding: 0.4rem 0.5rem;
}

Expand Down Expand Up @@ -1111,16 +1113,6 @@ body:not(.app-mode) .screen-edit {
gap: 0.4rem;
}

.attachment-row {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.4rem 0.5rem;
}

.attachment-name {
flex: 1;
min-width: 0;
Expand Down Expand Up @@ -1150,16 +1142,6 @@ input.attachment-name:focus {
gap: 0.4rem;
}

.history-row {
display: flex;
align-items: center;
gap: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.4rem 0.5rem;
}

.history-label {
flex: 1;
min-width: 0;
Expand Down
129 changes: 45 additions & 84 deletions pages/0x67/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,32 @@ function qs<T extends HTMLElement = HTMLElement>(selector: string): T {
return must(byId('root').querySelector<T>(selector));
}

/** Build a `type="button"` icon button: this app's dozen-plus small,
* icon-only controls (rename/move/delete, reveal/copy, download, etc.) all
* share this exact shape, so it's built once here instead of by hand at
* every call site. */
function makeIconButton(
className: string,
title: string,
glyph: string,
onClick: (event: MouseEvent) => void,
): HTMLButtonElement {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = className;
btn.title = title;
btn.textContent = glyph;
btn.addEventListener('click', onClick);
return btn;
}

/** Wire a dialog's `[data-action="close"]` button (its header ✕) to close it.
* Called once per `openXDialog` function, replacing what would otherwise be
* the same query-and-assign line repeated in each one. */
function wireClose(dlg: HTMLDialogElement): void {
must(dlg.querySelector<HTMLButtonElement>('[data-action="close"]')).onclick = () => dlg.close();
}

// ============================================================
// kdbx XML model helpers
// (getChildren, getChild, getText, etc. are declared in globals.d.ts and are
Expand Down Expand Up @@ -359,25 +385,15 @@ function buildGroupActions(group: XmlElement): HTMLDivElement {
const actions = document.createElement('div');
actions.className = 'group-actions';

const renameBtn = document.createElement('button');
renameBtn.type = 'button';
renameBtn.className = 'icon-btn group-action-btn';
renameBtn.title = 'Rename group';
renameBtn.textContent = '✏️';
renameBtn.addEventListener('click', () => {
const renameBtn = makeIconButton('icon-btn group-action-btn', 'Rename group', '✏️', () => {
openGroupDialog({ type: 'rename', group }, () => {
renderGroupTree();
renderEntryPanel();
});
});
actions.appendChild(renameBtn);

const moveBtn = document.createElement('button');
moveBtn.type = 'button';
moveBtn.className = 'icon-btn group-action-btn';
moveBtn.title = 'Move group';
moveBtn.textContent = '📂';
moveBtn.addEventListener('click', () => {
const moveBtn = makeIconButton('icon-btn group-action-btn', 'Move group', '📂', () => {
openMoveToDialog(
'Move group to…',
(candidate) => !isDescendantGroup(group, candidate),
Expand All @@ -386,12 +402,7 @@ function buildGroupActions(group: XmlElement): HTMLDivElement {
});
actions.appendChild(moveBtn);

const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'icon-btn group-action-btn';
deleteBtn.title = 'Delete group';
deleteBtn.textContent = '🗑';
deleteBtn.addEventListener('click', () => {
const deleteBtn = makeIconButton('icon-btn group-action-btn', 'Delete group', '🗑', () => {
deleteGroupAction(group);
});
actions.appendChild(deleteBtn);
Expand Down Expand Up @@ -889,26 +900,16 @@ function buildDetailField(key: string, value: string, isProtected: boolean): HTM
actions.className = 'detail-actions';

if (isProtected) {
const revealBtn = document.createElement('button');
revealBtn.type = 'button';
revealBtn.className = 'icon-btn';
revealBtn.title = 'Show / hide';
revealBtn.textContent = '👁';
let revealed = false;
revealBtn.addEventListener('click', () => {
const revealBtn = makeIconButton('icon-btn', 'Show / hide', '👁', () => {
revealed = !revealed;
valueSpan.textContent = revealed ? value : '••••••••';
revealBtn.textContent = revealed ? '🙈' : '👁';
});
actions.appendChild(revealBtn);
}

const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'icon-btn';
copyBtn.title = 'Copy';
copyBtn.textContent = '📋';
copyBtn.addEventListener('click', async () => {
const copyBtn = makeIconButton('icon-btn', 'Copy', '📋', async () => {
await copyToClipboard(value);
copyBtn.textContent = '✓';
setTimeout(() => {
Expand All @@ -934,12 +935,7 @@ function renderDetailAttachments(db: Kdbx, entry: XmlElement, container: HTMLEle
label.className = 'attachment-name';
label.textContent = attachment.name;

const downloadBtn = document.createElement('button');
downloadBtn.type = 'button';
downloadBtn.className = 'icon-btn';
downloadBtn.title = 'Download';
downloadBtn.textContent = '⬇';
downloadBtn.addEventListener('click', () => {
const downloadBtn = makeIconButton('icon-btn', 'Download', '⬇', () => {
const data = db.getBinaryData(attachment.ref);
if (!data) return;
const blob = new Blob([new Uint8Array(data)]);
Expand Down Expand Up @@ -971,23 +967,13 @@ function renderDetailHistory(db: Kdbx, entry: XmlElement, container: HTMLElement
const times = getEntryTimes(snapshot);
label.textContent = `${formatDateTime(times.modified)} — ${entryTitle(snapshot)}`;

const restoreBtn = document.createElement('button');
restoreBtn.type = 'button';
restoreBtn.className = 'icon-btn';
restoreBtn.title = 'Restore this version';
restoreBtn.textContent = '↩';
restoreBtn.addEventListener('click', () => {
const restoreBtn = makeIconButton('icon-btn', 'Restore this version', '↩', () => {
restoreHistoryEntry(db.root, entry, snapshot);
app.dirty = true;
showEntryDetail();
});

const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'icon-btn';
deleteBtn.title = 'Delete this version';
deleteBtn.textContent = '🗑';
deleteBtn.addEventListener('click', () => {
const deleteBtn = makeIconButton('icon-btn', 'Delete this version', '🗑', () => {
deleteHistoryEntry(entry, snapshot);
app.dirty = true;
renderDetailHistory(db, entry, container);
Expand Down Expand Up @@ -1108,12 +1094,7 @@ function renderEditAttachments(entry: XmlElement, container: HTMLElement): void
renderEditAttachments(entry, container);
});

const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'icon-btn';
removeBtn.title = 'Remove attachment';
removeBtn.textContent = '✕';
removeBtn.addEventListener('click', () => {
const removeBtn = makeIconButton('icon-btn', 'Remove attachment', '✕', () => {
removeEntryAttachment(entry, attachment.name);
renderEditAttachments(entry, container);
});
Expand Down Expand Up @@ -1151,24 +1132,14 @@ function buildEditField(
row.appendChild(valueInput);

if (isProtected) {
const toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'icon-btn';
toggle.title = 'Show / hide';
toggle.textContent = '👁';
toggle.addEventListener('click', () => {
const toggle = makeIconButton('icon-btn', 'Show / hide', '👁', () => {
valueInput.type = valueInput.type === 'password' ? 'text' : 'password';
toggle.textContent = valueInput.type === 'password' ? '👁' : '🙈';
});
row.appendChild(toggle);
}

const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'icon-btn';
copyBtn.title = 'Copy';
copyBtn.textContent = '📋';
copyBtn.addEventListener('click', async () => {
const copyBtn = makeIconButton('icon-btn', 'Copy', '📋', async () => {
// Copies whatever is currently typed, not the value the field opened
// with — the user may have already edited it.
await copyToClipboard(valueInput.value);
Expand All @@ -1180,12 +1151,7 @@ function buildEditField(
row.appendChild(copyBtn);

if (key === 'Password') {
const generateBtn = document.createElement('button');
generateBtn.type = 'button';
generateBtn.className = 'icon-btn';
generateBtn.title = 'Generate password';
generateBtn.textContent = '🎲';
generateBtn.addEventListener('click', () => {
const generateBtn = makeIconButton('icon-btn', 'Generate password', '🎲', () => {
openPasswordGenerator((password) => {
valueInput.value = password;
});
Expand All @@ -1194,13 +1160,8 @@ function buildEditField(
}

if (removable) {
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'icon-btn';
removeBtn.title = 'Remove field';
removeBtn.textContent = '✕';
const removeBtn = makeIconButton('icon-btn', 'Remove field', '✕', () => row.remove());
removeBtn.style.color = 'var(--danger)';
removeBtn.addEventListener('click', () => row.remove());
row.appendChild(removeBtn);
}

Expand Down Expand Up @@ -1290,7 +1251,7 @@ function openSettings(): void {

dlg.close();
};
must(dlg.querySelector<HTMLButtonElement>('[data-action="close"]')).onclick = () => dlg.close();
wireClose(dlg);
dlg.showModal();
}

Expand Down Expand Up @@ -1324,7 +1285,7 @@ function openExportDialog(): void {
downloadTextFile(toXml(entries), `${baseName}.xml`, 'application/xml');
dlg.close();
};
must(dlg.querySelector<HTMLButtonElement>('[data-action="close"]')).onclick = () => dlg.close();
wireClose(dlg);
dlg.showModal();
}

Expand Down Expand Up @@ -1425,7 +1386,7 @@ function openPasswordGenerator(onUse: (password: string) => void): void {
onUse(preview.textContent);
dlg.close();
};
must(dlg.querySelector<HTMLButtonElement>('[data-action="close"]')).onclick = () => dlg.close();
wireClose(dlg);

regenerate();
dlg.showModal();
Expand Down Expand Up @@ -1495,7 +1456,7 @@ function openIconPicker(onPick: (iconId: number) => void): void {
grid.appendChild(btn);
}

must(dlg.querySelector<HTMLButtonElement>('[data-action="close"]')).onclick = () => dlg.close();
wireClose(dlg);
dlg.showModal();
}

Expand Down Expand Up @@ -1557,7 +1518,7 @@ function openGroupDialog(mode: GroupDialogMode, onDone: () => void): void {

must(dlg.querySelector<HTMLButtonElement>('[data-action="cancel-group"]')).onclick = () =>
dlg.close();
must(dlg.querySelector<HTMLButtonElement>('[data-action="close"]')).onclick = () => dlg.close();
wireClose(dlg);

// Allow Enter to submit
nameInput.onkeydown = (e) => {
Expand Down Expand Up @@ -1609,7 +1570,7 @@ function openMoveToDialog(

must(dlg.querySelector<HTMLButtonElement>('[data-action="cancel-move"]')).onclick = () =>
dlg.close();
must(dlg.querySelector<HTMLButtonElement>('[data-action="close"]')).onclick = () => dlg.close();
wireClose(dlg);

dlg.showModal();
}
Expand Down