From 7c7d32de3034f60e0cee4aab7d6c4bb9ad3bc4ac Mon Sep 17 00:00:00 2001
From: shuaiplus <2327005759@qq.com>
Date: Sat, 28 Feb 2026 00:02:47 +0800
Subject: [PATCH] feat: add toast notifications and dialog components for
improved user interaction
---
public/web/i18n.js | 3 +-
public/web/main.js | 247 +++++++++++++++++++++++++++++++++---------
public/web/styles.css | 228 +++++++++++++++++++++++++++++++++++++-
3 files changed, 424 insertions(+), 54 deletions(-)
diff --git a/public/web/i18n.js b/public/web/i18n.js
index 711e546..61e2e14 100644
--- a/public/web/i18n.js
+++ b/public/web/i18n.js
@@ -21,7 +21,7 @@ export const I18N = {
allItems: 'All Items',
noFolder: 'No Folder',
searchVault: 'Search vault',
- filter: 'Filter',
+ filter: 'Search',
typeAll: 'All items',
typeLogin: 'Logins',
typeCard: 'Cards',
@@ -214,4 +214,3 @@ export const I18N = {
langSwitch: 'English',
},
};
-
diff --git a/public/web/main.js b/public/web/main.js
index 47dc6fe..63462d1 100644
--- a/public/web/main.js
+++ b/public/web/main.js
@@ -15,6 +15,8 @@ export function startNodewardenApp(runtimeConfig) {
registerEmail: '',
registerPassword: '',
registerPassword2: '',
+ registerShowPassword: false,
+ registerShowPassword2: false,
session: null,
profile: null,
tab: 'vault',
@@ -41,6 +43,7 @@ export function startNodewardenApp(runtimeConfig) {
invites: [],
loginEmail: '',
loginPassword: '',
+ loginShowPassword: false,
loginTotpToken: '',
loginTotpError: '',
pendingLogin: null,
@@ -51,10 +54,14 @@ export function startNodewardenApp(runtimeConfig) {
totpDisableError: '',
unlockPassword: '',
unlockError: '',
+ unlockShowPassword: false,
lockTimeoutMinutes: 15,
lockLastActiveTs: Date.now(),
lockCheckTimer: 0,
- lockChannel: null
+ lockChannel: null,
+ toasts: [],
+ toastSeq: 0,
+ dialog: null
};
var NO_FOLDER_FILTER = '__none__';
var i18n = I18N;
@@ -66,9 +73,84 @@ export function startNodewardenApp(runtimeConfig) {
}
function sessionKey() { return 'nodewarden.web.session.v2'; }
function lockSettingsKey() { return 'nodewarden.web.lock.v1'; }
- function setMsg(t, ty) { state.msg = t || ''; state.msgType = ty || 'ok'; render(); }
- function clearMsg() { state.msg = ''; }
- function renderMsg() { return state.msg ? '
' + esc(state.msg) + '
' : ''; }
+ function dismissToast(id) {
+ var next = [];
+ for (var i = 0; i < state.toasts.length; i++) if (state.toasts[i].id !== id) next.push(state.toasts[i]);
+ if (next.length === state.toasts.length) return;
+ state.toasts = next;
+ render();
+ }
+ function setMsg(t, ty) {
+ var text = String(t || '').trim();
+ if (!text) return;
+ var id = 'toast-' + (++state.toastSeq);
+ var level = ty === 'err' ? 'error' : (ty === 'warn' ? 'warning' : 'success');
+ state.toasts.push({ id: id, text: text, level: level });
+ if (state.toasts.length > 4) state.toasts = state.toasts.slice(state.toasts.length - 4);
+ render();
+ setTimeout(function () { dismissToast(id); }, 4500);
+ }
+ function clearMsg() {}
+ function renderMsg() { return ''; }
+ function renderToasts() {
+ if (!state.toasts || state.toasts.length === 0) return '';
+ var items = '';
+ for (var i = 0; i < state.toasts.length; i++) {
+ var x = state.toasts[i];
+ items += ''
+ + '' + esc(x.text) + '
'
+ + ''
+ + ''
+ + '';
+ }
+ return '';
+ }
+ function askConfirm(opts) {
+ return new Promise(function (resolve) {
+ state.dialog = {
+ type: 'confirm',
+ title: String(opts && opts.title || 'Confirm'),
+ message: String(opts && opts.message || ''),
+ okText: String(opts && opts.okText || 'Yes'),
+ cancelText: String(opts && opts.cancelText || 'No'),
+ danger: !!(opts && opts.danger),
+ resolve: resolve
+ };
+ render();
+ });
+ }
+ function askMoveFolder() {
+ return new Promise(function (resolve) {
+ state.dialog = {
+ type: 'move',
+ title: 'Move selected items',
+ message: 'Choose destination folder.',
+ selectedFolderId: '__none__',
+ resolve: resolve
+ };
+ render();
+ });
+ }
+ function closeDialog(result) {
+ var d = state.dialog;
+ state.dialog = null;
+ render();
+ if (d && typeof d.resolve === 'function') d.resolve(result);
+ }
+ function renderDialog() {
+ var d = state.dialog;
+ if (!d) return '';
+ if (d.type === 'move') {
+ var options = '';
+ for (var i = 0; i < state.folders.length; i++) {
+ var f = state.folders[i];
+ var id = String(f.id || '');
+ options += '';
+ }
+ return '⚠
' + esc(d.title) + '
' + esc(d.message) + '
';
+ }
+ return '⚠
' + esc(d.title) + '
' + esc(d.message) + '
';
+ }
function saveSession() {
if (!state.session) { localStorage.removeItem(sessionKey()); return; }
var persisted = {
@@ -197,6 +279,7 @@ export function startNodewardenApp(runtimeConfig) {
state.loginTotpError = '';
state.unlockPassword = '';
state.unlockError = '';
+ state.unlockShowPassword = false;
state.phase = 'locked';
if (broadcast !== false && state.lockChannel) {
try { state.lockChannel.postMessage({ type: 'lock', at: Date.now() }); } catch (_) {}
@@ -227,6 +310,7 @@ export function startNodewardenApp(runtimeConfig) {
state.session.symMacKey = bytesToBase64(symKeyBytes.slice(32, 64));
state.unlockPassword = '';
state.unlockError = '';
+ state.unlockShowPassword = false;
await loadVault();
await loadAdminData();
state.phase = 'app';
@@ -241,7 +325,7 @@ export function startNodewardenApp(runtimeConfig) {
}
function logout(){
- state.session=null; state.profile=null; state.ciphers=[]; state.folders=[]; state.users=[]; state.invites=[]; state.folderFilterId=''; state.selectedCipherId=''; state.selectedMap={}; state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError=''; state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError=''; state.unlockPassword=''; state.unlockError=''; state.phase='login'; saveSession(); clearMsg(); render();
+ state.session=null; state.profile=null; state.ciphers=[]; state.folders=[]; state.users=[]; state.invites=[]; state.folderFilterId=''; state.selectedCipherId=''; state.selectedMap={}; state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError=''; state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError=''; state.unlockPassword=''; state.unlockError=''; state.unlockShowPassword=false; state.phase='login'; saveSession(); clearMsg(); render();
}
async function authFetch(path, options){
@@ -600,7 +684,8 @@ export function startNodewardenApp(runtimeConfig) {
}
async function deleteSelectedCipher(){
var c=selectedCipher(); if(!c) return;
- if(!window.confirm('Delete this item? This operation cannot be undone.')) return;
+ var ok = await askConfirm({ title: 'Delete item', message: 'Are you sure you want to delete this item?', okText: 'Yes', cancelText: 'No', danger: true });
+ if(!ok) return;
var r=await authFetch('/api/ciphers/'+encodeURIComponent(c.id),{method:'DELETE'});
var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Delete failed.', 'err');
closeDetailEdit();
@@ -838,20 +923,20 @@ export function startNodewardenApp(runtimeConfig) {
return ''
+ ''
+ '
'+t('langSwitch')+'
'
- + '
'
- + '