mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: add toast notifications and dialog components for improved user interaction
This commit is contained in:
+1
-2
@@ -21,7 +21,7 @@ export const I18N = {
|
|||||||
allItems: 'All Items',
|
allItems: 'All Items',
|
||||||
noFolder: 'No Folder',
|
noFolder: 'No Folder',
|
||||||
searchVault: 'Search vault',
|
searchVault: 'Search vault',
|
||||||
filter: 'Filter',
|
filter: 'Search',
|
||||||
typeAll: 'All items',
|
typeAll: 'All items',
|
||||||
typeLogin: 'Logins',
|
typeLogin: 'Logins',
|
||||||
typeCard: 'Cards',
|
typeCard: 'Cards',
|
||||||
@@ -214,4 +214,3 @@ export const I18N = {
|
|||||||
langSwitch: 'English',
|
langSwitch: 'English',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+196
-51
@@ -15,6 +15,8 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
registerEmail: '',
|
registerEmail: '',
|
||||||
registerPassword: '',
|
registerPassword: '',
|
||||||
registerPassword2: '',
|
registerPassword2: '',
|
||||||
|
registerShowPassword: false,
|
||||||
|
registerShowPassword2: false,
|
||||||
session: null,
|
session: null,
|
||||||
profile: null,
|
profile: null,
|
||||||
tab: 'vault',
|
tab: 'vault',
|
||||||
@@ -41,6 +43,7 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
invites: [],
|
invites: [],
|
||||||
loginEmail: '',
|
loginEmail: '',
|
||||||
loginPassword: '',
|
loginPassword: '',
|
||||||
|
loginShowPassword: false,
|
||||||
loginTotpToken: '',
|
loginTotpToken: '',
|
||||||
loginTotpError: '',
|
loginTotpError: '',
|
||||||
pendingLogin: null,
|
pendingLogin: null,
|
||||||
@@ -51,10 +54,14 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
totpDisableError: '',
|
totpDisableError: '',
|
||||||
unlockPassword: '',
|
unlockPassword: '',
|
||||||
unlockError: '',
|
unlockError: '',
|
||||||
|
unlockShowPassword: false,
|
||||||
lockTimeoutMinutes: 15,
|
lockTimeoutMinutes: 15,
|
||||||
lockLastActiveTs: Date.now(),
|
lockLastActiveTs: Date.now(),
|
||||||
lockCheckTimer: 0,
|
lockCheckTimer: 0,
|
||||||
lockChannel: null
|
lockChannel: null,
|
||||||
|
toasts: [],
|
||||||
|
toastSeq: 0,
|
||||||
|
dialog: null
|
||||||
};
|
};
|
||||||
var NO_FOLDER_FILTER = '__none__';
|
var NO_FOLDER_FILTER = '__none__';
|
||||||
var i18n = I18N;
|
var i18n = I18N;
|
||||||
@@ -66,9 +73,84 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
}
|
}
|
||||||
function sessionKey() { return 'nodewarden.web.session.v2'; }
|
function sessionKey() { return 'nodewarden.web.session.v2'; }
|
||||||
function lockSettingsKey() { return 'nodewarden.web.lock.v1'; }
|
function lockSettingsKey() { return 'nodewarden.web.lock.v1'; }
|
||||||
function setMsg(t, ty) { state.msg = t || ''; state.msgType = ty || 'ok'; render(); }
|
function dismissToast(id) {
|
||||||
function clearMsg() { state.msg = ''; }
|
var next = [];
|
||||||
function renderMsg() { return state.msg ? '<div class="alert alert-' + (state.msgType === 'err' ? 'danger' : 'success') + '">' + esc(state.msg) + '</div>' : ''; }
|
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 += '<li class="toast-item ' + esc(x.level) + '">'
|
||||||
|
+ '<div class="toast-text">' + esc(x.text) + '</div>'
|
||||||
|
+ '<button class="toast-close" type="button" data-action="toast-close" data-id="' + esc(x.id) + '">✕</button>'
|
||||||
|
+ '<div class="toast-bar"></div>'
|
||||||
|
+ '</li>';
|
||||||
|
}
|
||||||
|
return '<ul class="toast-stack">' + items + '</ul>';
|
||||||
|
}
|
||||||
|
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 = '<option value="__none__">' + t('noFolder') + '</option>';
|
||||||
|
for (var i = 0; i < state.folders.length; i++) {
|
||||||
|
var f = state.folders[i];
|
||||||
|
var id = String(f.id || '');
|
||||||
|
options += '<option value="' + esc(id) + '"' + (d.selectedFolderId === id ? ' selected' : '') + '>' + esc(f.decName || f.name || id) + '</option>';
|
||||||
|
}
|
||||||
|
return '<div class="dialog-mask"><div class="dialog-card"><div class="dialog-icon">⚠</div><h3 class="dialog-title">' + esc(d.title) + '</h3><div class="dialog-msg">' + esc(d.message) + '</div><div class="form-group" style="margin: 12px 0 16px 0;"><select class="form-input" data-action="dialog-move-folder">' + options + '</select></div><button class="btn btn-primary dialog-btn" type="button" data-action="dialog-confirm">Move</button><button class="btn btn-secondary dialog-btn" type="button" data-action="dialog-cancel">Cancel</button></div></div>';
|
||||||
|
}
|
||||||
|
return '<div class="dialog-mask"><div class="dialog-card"><div class="dialog-icon">⚠</div><h3 class="dialog-title">' + esc(d.title) + '</h3><div class="dialog-msg">' + esc(d.message) + '</div><button class="btn ' + (d.danger ? 'btn-danger' : 'btn-primary') + ' dialog-btn" type="button" data-action="dialog-confirm">' + esc(d.okText) + '</button><button class="btn btn-secondary dialog-btn" type="button" data-action="dialog-cancel">' + esc(d.cancelText) + '</button></div></div>';
|
||||||
|
}
|
||||||
function saveSession() {
|
function saveSession() {
|
||||||
if (!state.session) { localStorage.removeItem(sessionKey()); return; }
|
if (!state.session) { localStorage.removeItem(sessionKey()); return; }
|
||||||
var persisted = {
|
var persisted = {
|
||||||
@@ -197,6 +279,7 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
state.loginTotpError = '';
|
state.loginTotpError = '';
|
||||||
state.unlockPassword = '';
|
state.unlockPassword = '';
|
||||||
state.unlockError = '';
|
state.unlockError = '';
|
||||||
|
state.unlockShowPassword = false;
|
||||||
state.phase = 'locked';
|
state.phase = 'locked';
|
||||||
if (broadcast !== false && state.lockChannel) {
|
if (broadcast !== false && state.lockChannel) {
|
||||||
try { state.lockChannel.postMessage({ type: 'lock', at: Date.now() }); } catch (_) {}
|
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.session.symMacKey = bytesToBase64(symKeyBytes.slice(32, 64));
|
||||||
state.unlockPassword = '';
|
state.unlockPassword = '';
|
||||||
state.unlockError = '';
|
state.unlockError = '';
|
||||||
|
state.unlockShowPassword = false;
|
||||||
await loadVault();
|
await loadVault();
|
||||||
await loadAdminData();
|
await loadAdminData();
|
||||||
state.phase = 'app';
|
state.phase = 'app';
|
||||||
@@ -241,7 +325,7 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function logout(){
|
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){
|
async function authFetch(path, options){
|
||||||
@@ -600,7 +684,8 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
}
|
}
|
||||||
async function deleteSelectedCipher(){
|
async function deleteSelectedCipher(){
|
||||||
var c=selectedCipher(); if(!c) return;
|
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 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');
|
var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Delete failed.', 'err');
|
||||||
closeDetailEdit();
|
closeDetailEdit();
|
||||||
@@ -838,20 +923,20 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
return ''
|
return ''
|
||||||
+ '<div class="auth-page">'
|
+ '<div class="auth-page">'
|
||||||
+ ' <div class="lang-switch" data-action="toggle-lang">'+t('langSwitch')+'</div>'
|
+ ' <div class="lang-switch" data-action="toggle-lang">'+t('langSwitch')+'</div>'
|
||||||
+ ' <div class="auth-card">'
|
+ ' <div class="auth-card unlock-card">'
|
||||||
+ ' <div class="auth-header">'
|
+ ' <div class="auth-header" style="margin-bottom:20px;">'
|
||||||
+ ' <div class="auth-logo"></div>'
|
+ ' <div class="auth-title" style="margin-bottom:4px;">'+t('login')+'</div>'
|
||||||
+ ' <div class="auth-title">'+t('brand')+'</div>'
|
+ ' <div class="auth-subtitle">'+t('brand')+'</div>'
|
||||||
+ ' <div class="auth-subtitle">'+t('subtitle')+'</div>'
|
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ renderMsg()
|
+ renderMsg()
|
||||||
+ ' <form id="loginForm">'
|
+ ' <form id="loginForm">'
|
||||||
+ ' <div class="form-group"><label class="form-label">'+t('email')+'</label><input class="form-input" type="email" name="email" value="'+esc(state.loginEmail)+'" required autofocus /></div>'
|
+ ' <div class="form-group"><label class="form-label">'+t('email')+'</label><input class="form-input" type="email" name="email" value="'+esc(state.loginEmail)+'" required autofocus /></div>'
|
||||||
+ ' <div class="form-group"><label class="form-label">'+t('masterPwd')+'</label><input class="form-input" type="password" name="password" value="'+esc(state.loginPassword)+'" required /></div>'
|
+ ' <div class="form-group unlock-pwd-wrap"><label class="form-label">'+t('masterPwd')+'</label><input class="form-input unlock-pwd-input" type="'+(state.loginShowPassword?'text':'password')+'" name="password" value="'+esc(state.loginPassword)+'" required /><button class="unlock-eye-btn" type="button" data-action="login-toggle-password" aria-label="Toggle password visibility">👁</button></div>'
|
||||||
+ ' <button class="btn btn-primary" type="submit" style="width:100%; margin-top:16px;">'+t('loginBtn')+'</button>'
|
+ ' <button class="btn btn-primary unlock-main-btn" type="submit">'+t('loginBtn')+'</button>'
|
||||||
+ ' </form>'
|
+ ' </form>'
|
||||||
+ ' <div class="auth-footer">'
|
+ ' <div class="unlock-or">or</div>'
|
||||||
+ ' <a href="#" data-action="goto-register">'+t('registerBtn')+'</a>'
|
+ ' <div style="display:flex; gap:8px;">'
|
||||||
|
+ ' <button class="btn btn-secondary unlock-secondary-btn" style="flex:1;" data-action="goto-register">'+t('registerBtn')+'</button>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ (state.pendingLogin ? ''
|
+ (state.pendingLogin ? ''
|
||||||
+ '<div class="totp-mask"><div class="totp-box"><h3 style="margin-top:0;">'+t('totpVerify')+'</h3><div class="tiny" style="margin-bottom:16px;">'+t('totpVerifySub')+'</div>'
|
+ '<div class="totp-mask"><div class="totp-box"><h3 style="margin-top:0;">'+t('totpVerify')+'</h3><div class="tiny" style="margin-bottom:16px;">'+t('totpVerifySub')+'</div>'
|
||||||
@@ -867,23 +952,23 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
return ''
|
return ''
|
||||||
+ '<div class="auth-page">'
|
+ '<div class="auth-page">'
|
||||||
+ ' <div class="lang-switch" data-action="toggle-lang">'+t('langSwitch')+'</div>'
|
+ ' <div class="lang-switch" data-action="toggle-lang">'+t('langSwitch')+'</div>'
|
||||||
+ ' <div class="auth-card">'
|
+ ' <div class="auth-card unlock-card">'
|
||||||
+ ' <div class="auth-header">'
|
+ ' <div class="auth-header" style="margin-bottom:20px;">'
|
||||||
+ ' <div class="auth-logo"></div>'
|
+ ' <div class="auth-title" style="margin-bottom:4px;">'+t('register')+'</div>'
|
||||||
+ ' <div class="auth-title">'+t('register')+'</div>'
|
|
||||||
+ ' <div class="auth-subtitle">'+t('brand')+'</div>'
|
+ ' <div class="auth-subtitle">'+t('brand')+'</div>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ renderMsg()
|
+ renderMsg()
|
||||||
+ ' <form id="registerForm">'
|
+ ' <form id="registerForm">'
|
||||||
+ ' <div class="form-group"><label class="form-label">'+t('name')+'</label><input class="form-input" name="name" value="'+esc(state.registerName)+'" required autofocus /></div>'
|
+ ' <div class="form-group"><label class="form-label">'+t('name')+'</label><input class="form-input" name="name" value="'+esc(state.registerName)+'" required autofocus /></div>'
|
||||||
+ ' <div class="form-group"><label class="form-label">'+t('email')+'</label><input class="form-input" type="email" name="email" value="'+esc(state.registerEmail)+'" required /></div>'
|
+ ' <div class="form-group"><label class="form-label">'+t('email')+'</label><input class="form-input" type="email" name="email" value="'+esc(state.registerEmail)+'" required /></div>'
|
||||||
+ ' <div class="form-group"><label class="form-label">'+t('masterPwd')+'</label><input class="form-input" type="password" name="password" value="'+esc(state.registerPassword)+'" minlength="12" required /></div>'
|
+ ' <div class="form-group unlock-pwd-wrap"><label class="form-label">'+t('masterPwd')+'</label><input class="form-input unlock-pwd-input" type="'+(state.registerShowPassword?'text':'password')+'" name="password" value="'+esc(state.registerPassword)+'" minlength="12" required /><button class="unlock-eye-btn" type="button" data-action="register-toggle-password" aria-label="Toggle password visibility">👁</button></div>'
|
||||||
+ ' <div class="form-group"><label class="form-label">'+t('confirmPwd')+'</label><input class="form-input" type="password" name="password2" value="'+esc(state.registerPassword2)+'" minlength="12" required /></div>'
|
+ ' <div class="form-group unlock-pwd-wrap"><label class="form-label">'+t('confirmPwd')+'</label><input class="form-input unlock-pwd-input" type="'+(state.registerShowPassword2?'text':'password')+'" name="password2" value="'+esc(state.registerPassword2)+'" minlength="12" required /><button class="unlock-eye-btn" type="button" data-action="register2-toggle-password" aria-label="Toggle password visibility">👁</button></div>'
|
||||||
+ ' <div class="form-group"><label class="form-label">'+t('inviteCode')+'</label><input class="form-input" name="inviteCode" value="'+esc(state.inviteCode)+'" /></div>'
|
+ ' <div class="form-group"><label class="form-label">'+t('inviteCode')+'</label><input class="form-input" name="inviteCode" value="'+esc(state.inviteCode)+'" /></div>'
|
||||||
+ ' <button class="btn btn-primary" type="submit" style="width:100%; margin-top:16px;">'+t('registerBtn')+'</button>'
|
+ ' <button class="btn btn-primary unlock-main-btn" type="submit">'+t('registerBtn')+'</button>'
|
||||||
+ ' </form>'
|
+ ' </form>'
|
||||||
+ ' <div class="auth-footer">'
|
+ ' <div class="unlock-or">or</div>'
|
||||||
+ ' <a href="#" data-action="goto-login">'+t('backToLogin')+'</a>'
|
+ ' <div style="display:flex; gap:8px;">'
|
||||||
|
+ ' <button class="btn btn-secondary unlock-secondary-btn" style="flex:1;" data-action="goto-login">'+t('backToLogin')+'</button>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
@@ -894,20 +979,20 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
return ''
|
return ''
|
||||||
+ '<div class="auth-page">'
|
+ '<div class="auth-page">'
|
||||||
+ ' <div class="lang-switch" data-action="toggle-lang">'+t('langSwitch')+'</div>'
|
+ ' <div class="lang-switch" data-action="toggle-lang">'+t('langSwitch')+'</div>'
|
||||||
+ ' <div class="auth-card">'
|
+ ' <div class="auth-card unlock-card">'
|
||||||
+ ' <div class="auth-header">'
|
+ ' <div class="auth-header" style="margin-bottom:20px;">'
|
||||||
+ ' <div class="auth-logo"></div>'
|
+ ' <div class="auth-title" style="margin-bottom:4px;">Unlock Vault</div>'
|
||||||
+ ' <div class="auth-title">Unlock Vault</div>'
|
|
||||||
+ ' <div class="auth-subtitle">'+esc(email)+'</div>'
|
+ ' <div class="auth-subtitle">'+esc(email)+'</div>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ renderMsg()
|
+ renderMsg()
|
||||||
+ (state.unlockError?('<div class="alert alert-danger">'+esc(state.unlockError)+'</div>'):'')
|
+ (state.unlockError?('<div class="alert alert-danger">'+esc(state.unlockError)+'</div>'):'')
|
||||||
+ ' <form id="unlockForm">'
|
+ ' <form id="unlockForm">'
|
||||||
+ ' <div class="form-group"><label class="form-label">'+t('masterPwd')+'</label><input class="form-input" type="password" name="password" value="'+esc(state.unlockPassword)+'" required autofocus /></div>'
|
+ ' <div class="form-group unlock-pwd-wrap"><label class="form-label">'+t('masterPwd')+'</label><input class="form-input unlock-pwd-input" type="'+(state.unlockShowPassword?'text':'password')+'" name="password" value="'+esc(state.unlockPassword)+'" required autofocus /><button class="unlock-eye-btn" type="button" data-action="unlock-toggle-password" aria-label="Toggle password visibility">👁</button></div>'
|
||||||
+ ' <button class="btn btn-primary" type="submit" style="width:100%; margin-top:16px;">Unlock</button>'
|
+ ' <button class="btn btn-primary unlock-main-btn" type="submit">Unlock</button>'
|
||||||
+ ' </form>'
|
+ ' </form>'
|
||||||
+ ' <div style="display:flex; gap:8px; margin-top:16px;">'
|
+ ' <div class="unlock-or">or</div>'
|
||||||
+ ' <button class="btn btn-secondary" style="flex:1;" data-action="logout">Log Out</button>'
|
+ ' <div style="display:flex; gap:8px;">'
|
||||||
|
+ ' <button class="btn btn-secondary unlock-secondary-btn" style="flex:1;" data-action="logout">Log Out</button>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ ' </div>'
|
+ ' </div>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
@@ -991,9 +1076,9 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
+ '<div style="margin-top:10px;"><label><input type="checkbox" data-action="draft-change" data-field="reprompt" '+(de.reprompt?'checked':'')+' /> Master password reprompt</label></div>'
|
+ '<div style="margin-top:10px;"><label><input type="checkbox" data-action="draft-change" data-field="reprompt" '+(de.reprompt?'checked':'')+' /> Master password reprompt</label></div>'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<div class="card"><div class="card-title">Fields</div>'+efsHtml+'<button class="link-btn" data-action="draft-field-open">'+t('addField')+'</button></div>'
|
+ '<div class="card"><div class="card-title">Fields</div>'+efsHtml+'<button class="link-btn" data-action="draft-field-open">'+t('addField')+'</button></div>'
|
||||||
+ '<div class="detail-actions"><div><button class="btn btn-primary" data-action="detail-save">Confirm</button><button class="btn btn-secondary" data-action="detail-cancel">Cancel</button></div><button class="btn btn-danger" data-action="detail-delete">🗑</button></div>';
|
+ '<div class="detail-actions"><div><button class="btn btn-primary" data-action="detail-save">Confirm</button><button class="btn btn-secondary" data-action="detail-cancel">Cancel</button></div><button class="btn btn-danger btn-danger-icon" data-action="detail-delete" aria-label="Delete item">🗑</button></div>';
|
||||||
} else detail=renderReadOnlyTypeDetails(c0, folderLabel, created, updated)
|
} else detail=renderReadOnlyTypeDetails(c0, folderLabel, created, updated)
|
||||||
+ '<div class="detail-actions"><div><button class="btn btn-secondary" data-action="detail-edit">Edit</button></div><button class="btn btn-danger" data-action="detail-delete">🗑</button></div>';
|
+ '<div class="detail-actions"><div><button class="btn btn-secondary" data-action="detail-edit">Edit</button></div><button class="btn btn-danger btn-danger-icon" data-action="detail-delete" aria-label="Delete item">🗑</button></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
@@ -1016,9 +1101,9 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
function renderTotpDisableModal(){
|
function renderTotpDisableModal(){
|
||||||
if(!state.totpDisableOpen) return '';
|
if(!state.totpDisableOpen) return '';
|
||||||
return ''
|
return ''
|
||||||
+ '<div class="totp-mask"><div class="totp-box"><h3 style="margin-top:0;">'+t('disableTotp')+'</h3><div class="tiny" style="margin-bottom:16px;">'+t('totpDisableSub')+'</div>'
|
+ '<div class="dialog-mask"><div class="dialog-card form-dialog"><div class="dialog-icon">⚠</div><h3 class="dialog-title">'+t('disableTotp')+'</h3><div class="dialog-msg">'+t('totpDisableSub')+'</div>'
|
||||||
+ (state.totpDisableError?'<div class="alert alert-danger" style="margin-bottom:16px;">'+esc(state.totpDisableError)+'</div>':'')
|
+ (state.totpDisableError?'<div class="dialog-error">'+esc(state.totpDisableError)+'</div>':'')
|
||||||
+ '<form id="totpDisableForm"><div class="form-group"><label class="form-label">'+t('masterPwd')+'</label><input class="form-input" type="password" name="masterPassword" value="'+esc(state.totpDisablePassword)+'" required autofocus /></div><div style="display:flex; gap:8px; margin-top:16px;"><button class="btn btn-danger" type="submit" style="flex:1;">'+t('disableTotp')+'</button><button class="btn btn-secondary" type="button" data-action="totp-disable-cancel" style="flex:1;">'+t('cancel')+'</button></div></form>'
|
+ '<form id="totpDisableForm"><div class="form-group"><label class="form-label">'+t('masterPwd')+'</label><input class="form-input" type="password" name="masterPassword" value="'+esc(state.totpDisablePassword)+'" required autofocus /></div><button class="btn btn-danger dialog-btn" type="submit">'+t('disableTotp')+'</button><button class="btn btn-secondary dialog-btn" type="button" data-action="totp-disable-cancel">'+t('cancel')+'</button></form>'
|
||||||
+ '</div></div>';
|
+ '</div></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1096,7 +1181,7 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<div class="app-body">'
|
+ '<div class="app-body">'
|
||||||
+ (showFolders?(' <aside class="sidebar">'
|
+ (showFolders?(' <aside class="sidebar">'
|
||||||
+ '<div class="sidebar-block"><div class="sidebar-title">'+t('filter')+'</div><input class="search-input" data-action="vault-search" placeholder="'+t('searchVault')+'" value="'+esc(state.vaultQuery)+'" /></div>'
|
+ '<div class="sidebar-block"><div class="sidebar-title">'+t('searchVault')+'</div><input class="search-input" data-action="vault-search" placeholder="'+t('searchVault')+'" value="'+esc(state.vaultQuery)+'" /></div>'
|
||||||
+ '<div class="sidebar-block"><div class="sidebar-title">'+t('allItems')+'</div>'+typeTree+'</div>'
|
+ '<div class="sidebar-block"><div class="sidebar-title">'+t('allItems')+'</div>'+typeTree+'</div>'
|
||||||
+ '<div class="sidebar-block"><div class="sidebar-title">'+t('folders')+'</div>'+folders+'</div>'
|
+ '<div class="sidebar-block"><div class="sidebar-title">'+t('folders')+'</div>'+folders+'</div>'
|
||||||
+ '</aside>'):'')
|
+ '</aside>'):'')
|
||||||
@@ -1105,11 +1190,39 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function render(){
|
function render(){
|
||||||
if(state.phase==='loading'){ app.innerHTML='<div class="auth-page" style="align-items:center; justify-content:center;"><div style="display:flex; flex-direction:column; align-items:center; gap:16px;"><div class="auth-logo" style="margin:0;"></div><div style="color:var(--text-secondary); font-weight:500;">'+t('loading')+'</div></div></div>'; return; }
|
var active = document.activeElement;
|
||||||
if(state.phase==='register'){ app.innerHTML=renderRegisterScreen(); return; }
|
var keepSearchFocus = false;
|
||||||
if(state.phase==='login'){ app.innerHTML=renderLoginScreen(); return; }
|
var keepSearchSelStart = 0;
|
||||||
if(state.phase==='locked'){ app.innerHTML=renderLockedScreen(); return; }
|
var keepSearchSelEnd = 0;
|
||||||
app.innerHTML=renderApp();
|
if (active instanceof HTMLInputElement && active.getAttribute('data-action') === 'vault-search') {
|
||||||
|
keepSearchFocus = true;
|
||||||
|
keepSearchSelStart = active.selectionStart == null ? 0 : active.selectionStart;
|
||||||
|
keepSearchSelEnd = active.selectionEnd == null ? keepSearchSelStart : active.selectionEnd;
|
||||||
|
}
|
||||||
|
if(state.phase==='loading'){ app.innerHTML='<div class="auth-page" style="align-items:center; justify-content:center;"><div style="display:flex; flex-direction:column; align-items:center; gap:16px;"><div class="auth-logo" style="margin:0;"></div><div style="color:var(--text-secondary); font-weight:500;">'+t('loading')+'</div></div></div>' + renderToasts() + renderDialog(); return; }
|
||||||
|
if(state.phase==='register'){ app.innerHTML=renderRegisterScreen() + renderToasts() + renderDialog(); return; }
|
||||||
|
if(state.phase==='login'){ app.innerHTML=renderLoginScreen() + renderToasts() + renderDialog(); return; }
|
||||||
|
if(state.phase==='locked'){ app.innerHTML=renderLockedScreen() + renderToasts() + renderDialog(); return; }
|
||||||
|
var prevContent = app.querySelector('.content');
|
||||||
|
var prevSidebar = app.querySelector('.sidebar');
|
||||||
|
var prevVaultList = app.querySelector('.vault-list');
|
||||||
|
var contentTop = prevContent ? prevContent.scrollTop : 0;
|
||||||
|
var sidebarTop = prevSidebar ? prevSidebar.scrollTop : 0;
|
||||||
|
var vaultListTop = prevVaultList ? prevVaultList.scrollTop : 0;
|
||||||
|
app.innerHTML=renderApp() + renderToasts() + renderDialog();
|
||||||
|
var nextContent = app.querySelector('.content');
|
||||||
|
var nextSidebar = app.querySelector('.sidebar');
|
||||||
|
var nextVaultList = app.querySelector('.vault-list');
|
||||||
|
if(nextContent) nextContent.scrollTop = contentTop;
|
||||||
|
if(nextSidebar) nextSidebar.scrollTop = sidebarTop;
|
||||||
|
if(nextVaultList) nextVaultList.scrollTop = vaultListTop;
|
||||||
|
if (keepSearchFocus) {
|
||||||
|
var nextSearch = app.querySelector('input[data-action="vault-search"]');
|
||||||
|
if (nextSearch instanceof HTMLInputElement) {
|
||||||
|
nextSearch.focus();
|
||||||
|
try { nextSearch.setSelectionRange(keepSearchSelStart, keepSearchSelEnd); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
updateLiveTotpDisplay();
|
updateLiveTotpDisplay();
|
||||||
renderTotpSetupQr();
|
renderTotpSetupQr();
|
||||||
}
|
}
|
||||||
@@ -1245,12 +1358,12 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onBulkDelete(){ var ids=[]; for(var k in state.selectedMap){ if(state.selectedMap[k]) ids.push(k);} if(ids.length===0) return setMsg('Select items first.', 'err'); if(!window.confirm('Delete selected '+ids.length+' items?')) return; for(var i=0;i<ids.length;i++) await authFetch('/api/ciphers/'+encodeURIComponent(ids[i]),{method:'DELETE'}); state.selectedMap={}; await loadVault(); render(); setMsg('Deleted selected items.', 'ok'); }
|
async function onBulkDelete(){ var ids=[]; for(var k in state.selectedMap){ if(state.selectedMap[k]) ids.push(k);} if(ids.length===0) return setMsg('Select items first.', 'err'); var ok=await askConfirm({title:'Delete items',message:'Are you sure you want to delete '+ids.length+' selected items?',okText:'Yes',cancelText:'No',danger:true}); if(!ok) return; for(var i=0;i<ids.length;i++) await authFetch('/api/ciphers/'+encodeURIComponent(ids[i]),{method:'DELETE'}); state.selectedMap={}; await loadVault(); render(); setMsg('Deleted selected items.', 'ok'); }
|
||||||
async function onBulkMove(){ var ids=[]; for(var k in state.selectedMap){ if(state.selectedMap[k]) ids.push(k);} if(ids.length===0) return setMsg('Select items first.', 'err'); var opts=['0) No folder']; for(var i=0;i<state.folders.length;i++){ var f=state.folders[i]; var label=(f.decName||f.name||f.id); opts.push(String(i+1)+') '+String(label)); } var pick=window.prompt('Move selected items to:\n'+opts.join('\n')+'\n\nInput number (empty to cancel):','0'); if(pick===null) return; pick=String(pick).trim(); if(!pick) return; var idx=Number(pick); if(!Number.isInteger(idx)||idx<0||idx>state.folders.length) return setMsg('Invalid folder selection.', 'err'); var folderId=idx===0?null:state.folders[idx-1].id; var r=await authFetch('/api/ciphers/move',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:ids,folderId:folderId})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Bulk move failed.', 'err'); await loadVault(); render(); setMsg('Moved selected items.', 'ok'); }
|
async function onBulkMove(){ var ids=[]; for(var k in state.selectedMap){ if(state.selectedMap[k]) ids.push(k);} if(ids.length===0) return setMsg('Select items first.', 'err'); var folderId=await askMoveFolder(); if(folderId===undefined) return; var r=await authFetch('/api/ciphers/move',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:ids,folderId:folderId})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Bulk move failed.', 'err'); await loadVault(); render(); setMsg('Moved selected items.', 'ok'); }
|
||||||
|
|
||||||
async function onCreateInvite(form){ var fd=new FormData(form); var h=Number(fd.get('hours')||168); var r=await authFetch('/api/admin/invites',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({expiresInHours:h})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Create invite failed.', 'err'); await loadAdminData(); render(); setMsg('Invite created.', 'ok'); }
|
async function onCreateInvite(form){ var fd=new FormData(form); var h=Number(fd.get('hours')||168); var r=await authFetch('/api/admin/invites',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({expiresInHours:h})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Create invite failed.', 'err'); await loadAdminData(); render(); setMsg('Invite created.', 'ok'); }
|
||||||
async function onToggleUserStatus(id,status){ var n=status==='active'?'banned':'active'; var r=await authFetch('/api/admin/users/'+encodeURIComponent(id)+'/status',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status:n})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Update user status failed.', 'err'); await loadAdminData(); render(); setMsg('User status updated.', 'ok'); }
|
async function onToggleUserStatus(id,status){ var n=status==='active'?'banned':'active'; var r=await authFetch('/api/admin/users/'+encodeURIComponent(id)+'/status',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status:n})}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Update user status failed.', 'err'); await loadAdminData(); render(); setMsg('User status updated.', 'ok'); }
|
||||||
async function onDeleteUser(id){ if(!window.confirm('Delete this user and all user data?')) return; var r=await authFetch('/api/admin/users/'+encodeURIComponent(id),{method:'DELETE'}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Delete user failed.', 'err'); await loadAdminData(); render(); setMsg('User deleted.', 'ok'); }
|
async function onDeleteUser(id){ var ok=await askConfirm({title:'Delete user',message:'Delete this user and all user data?',okText:'Yes',cancelText:'No',danger:true}); if(!ok) return; var r=await authFetch('/api/admin/users/'+encodeURIComponent(id),{method:'DELETE'}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Delete user failed.', 'err'); await loadAdminData(); render(); setMsg('User deleted.', 'ok'); }
|
||||||
async function onRevokeInvite(code){ var r=await authFetch('/api/admin/invites/'+encodeURIComponent(code),{method:'DELETE'}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Revoke invite failed.', 'err'); await loadAdminData(); render(); setMsg('Invite revoked.', 'ok'); }
|
async function onRevokeInvite(code){ var r=await authFetch('/api/admin/invites/'+encodeURIComponent(code),{method:'DELETE'}); var j=await jsonOrNull(r); if(!r.ok) return setMsg((j&&(j.error||j.error_description))||'Revoke invite failed.', 'err'); await loadAdminData(); render(); setMsg('Invite revoked.', 'ok'); }
|
||||||
|
|
||||||
app.addEventListener('submit', function(ev){
|
app.addEventListener('submit', function(ev){
|
||||||
@@ -1267,12 +1380,36 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
if(form.id==='inviteForm') return void onCreateInvite(form);
|
if(form.id==='inviteForm') return void onCreateInvite(form);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.addEventListener('click', function(ev){
|
app.addEventListener('click', async function(ev){
|
||||||
var n=ev.target; while(n&&n!==app&&!n.getAttribute('data-action')) n=n.parentElement; if(!n||n===app) return; var a=n.getAttribute('data-action'); if(!a) return;
|
var n=ev.target; while(n&&n!==app&&!n.getAttribute('data-action')) n=n.parentElement; if(!n||n===app) return; var a=n.getAttribute('data-action'); if(!a) return;
|
||||||
|
ev.preventDefault();
|
||||||
|
if(a==='toast-close'){ dismissToast(n.getAttribute('data-id')||''); return; }
|
||||||
|
if(a==='dialog-cancel'){ closeDialog(false); return; }
|
||||||
|
if(a==='dialog-confirm'){
|
||||||
|
if(state.dialog&&state.dialog.type==='move'){
|
||||||
|
var sel=state.dialog.selectedFolderId;
|
||||||
|
closeDialog(sel==='__none__'?null:sel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(a==='toggle-lang'){ state.lang = state.lang === 'zh' ? 'en' : 'zh'; render(); return; }
|
if(a==='toggle-lang'){ state.lang = state.lang === 'zh' ? 'en' : 'zh'; render(); return; }
|
||||||
if(a==='goto-login'){ state.phase='login'; clearMsg(); render(); return; }
|
if(a==='goto-login'){ state.phase='login'; clearMsg(); render(); return; }
|
||||||
if(a==='goto-register'){ state.phase='register'; clearMsg(); render(); return; }
|
if(a==='goto-register'){ state.phase='register'; clearMsg(); render(); return; }
|
||||||
if(a==='logout'){ if(window.confirm('Log out now?')) logout(); return; }
|
if(a==='unlock-toggle-password' || a==='login-toggle-password' || a==='register-toggle-password' || a==='register2-toggle-password'){
|
||||||
|
var wrap = n.parentElement;
|
||||||
|
var input = wrap ? wrap.querySelector('input.unlock-pwd-input') : null;
|
||||||
|
if(!(input instanceof HTMLInputElement)) return;
|
||||||
|
var nextIsText = input.type !== 'text';
|
||||||
|
input.type = nextIsText ? 'text' : 'password';
|
||||||
|
if(a==='unlock-toggle-password') state.unlockShowPassword = nextIsText;
|
||||||
|
if(a==='login-toggle-password') state.loginShowPassword = nextIsText;
|
||||||
|
if(a==='register-toggle-password') state.registerShowPassword = nextIsText;
|
||||||
|
if(a==='register2-toggle-password') state.registerShowPassword2 = nextIsText;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(a==='logout'){ var okLogout=await askConfirm({title:'Log out',message:'Are you sure you want to log out?',okText:'Yes',cancelText:'No'}); if(okLogout) logout(); return; }
|
||||||
if(a==='lock'){ lockVault(true, true); return; }
|
if(a==='lock'){ lockVault(true, true); return; }
|
||||||
if(a==='totp-cancel'){ state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError=''; render(); return; }
|
if(a==='totp-cancel'){ state.pendingLogin=null; state.loginTotpToken=''; state.loginTotpError=''; render(); return; }
|
||||||
if(a==='totp-disable-cancel'){ state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError=''; render(); return; }
|
if(a==='totp-disable-cancel'){ state.totpDisableOpen=false; state.totpDisablePassword=''; state.totpDisableError=''; render(); return; }
|
||||||
@@ -1309,7 +1446,14 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
if(a==='bulk-delete') return void onBulkDelete();
|
if(a==='bulk-delete') return void onBulkDelete();
|
||||||
if(a==='bulk-move') return void onBulkMove();
|
if(a==='bulk-move') return void onBulkMove();
|
||||||
if(a==='vault-refresh'){ loadVault().then(function(){ render(); setMsg('Vault refreshed.', 'ok'); }).catch(function(e){ setMsg('Refresh failed: '+(e&&e.message?e.message:String(e)), 'err'); }); return; }
|
if(a==='vault-refresh'){ loadVault().then(function(){ render(); setMsg('Vault refreshed.', 'ok'); }).catch(function(e){ setMsg('Refresh failed: '+(e&&e.message?e.message:String(e)), 'err'); }); return; }
|
||||||
if(a==='totp-secret-refresh'){ state.totpSetupSecret=randomBase32Secret(32); render(); return; }
|
if(a==='totp-secret-refresh'){
|
||||||
|
state.totpSetupSecret=randomBase32Secret(32);
|
||||||
|
var f=document.getElementById('totpEnableForm');
|
||||||
|
var s=f?f.querySelector('input[name=\"secret\"]'):null;
|
||||||
|
if(s instanceof HTMLInputElement) s.value=state.totpSetupSecret;
|
||||||
|
renderTotpSetupQr();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(a==='totp-secret-copy'){ navigator.clipboard.writeText(currentTotpSecret()).then(function(){ setMsg('TOTP secret copied.', 'ok'); }).catch(function(){ setMsg('Copy failed.', 'err'); }); return; }
|
if(a==='totp-secret-copy'){ navigator.clipboard.writeText(currentTotpSecret()).then(function(){ setMsg('TOTP secret copied.', 'ok'); }).catch(function(){ setMsg('Copy failed.', 'err'); }); return; }
|
||||||
if(a==='totp-disable'){ onDisableTotp(); return; }
|
if(a==='totp-disable'){ onDisableTotp(); return; }
|
||||||
if(a==='admin-refresh'){ loadAdminData().then(function(){ render(); setMsg('Admin data refreshed.', 'ok'); }).catch(function(e){ setMsg('Refresh failed: '+(e&&e.message?e.message:String(e)), 'err'); }); return; }
|
if(a==='admin-refresh'){ loadAdminData().then(function(){ render(); setMsg('Admin data refreshed.', 'ok'); }).catch(function(e){ setMsg('Refresh failed: '+(e&&e.message?e.message:String(e)), 'err'); }); return; }
|
||||||
@@ -1327,7 +1471,7 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
state.vaultQuery=String(n.value||'');
|
state.vaultQuery=String(n.value||'');
|
||||||
if(ev.isComposing||state.vaultSearchComposing) return;
|
if(ev.isComposing||state.vaultSearchComposing) return;
|
||||||
if(state.vaultSearchTimer) clearTimeout(state.vaultSearchTimer);
|
if(state.vaultSearchTimer) clearTimeout(state.vaultSearchTimer);
|
||||||
state.vaultSearchTimer=setTimeout(function(){ syncSelectedWithFilter(); render(); }, 120);
|
state.vaultSearchTimer=setTimeout(function(){ state.vaultSearchTimer=0; syncSelectedWithFilter(); render(); }, 120);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(a==='draft-change'&&state.detailDraft){
|
if(a==='draft-change'&&state.detailDraft){
|
||||||
@@ -1353,6 +1497,7 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
if(!(n instanceof HTMLSelectElement)) return;
|
if(!(n instanceof HTMLSelectElement)) return;
|
||||||
var a=n.getAttribute('data-action');
|
var a=n.getAttribute('data-action');
|
||||||
if(a==='field-modal-type'){ state.fieldModalType=String(n.value||'text'); return; }
|
if(a==='field-modal-type'){ state.fieldModalType=String(n.value||'text'); return; }
|
||||||
|
if(a==='dialog-move-folder' && state.dialog && state.dialog.type==='move'){ state.dialog.selectedFolderId=String(n.value||'__none__'); return; }
|
||||||
});
|
});
|
||||||
|
|
||||||
app.addEventListener('compositionstart', function(ev){
|
app.addEventListener('compositionstart', function(ev){
|
||||||
@@ -1365,7 +1510,7 @@ export function startNodewardenApp(runtimeConfig) {
|
|||||||
state.vaultSearchComposing=false;
|
state.vaultSearchComposing=false;
|
||||||
state.vaultQuery=String(n.value||'');
|
state.vaultQuery=String(n.value||'');
|
||||||
if(state.vaultSearchTimer) clearTimeout(state.vaultSearchTimer);
|
if(state.vaultSearchTimer) clearTimeout(state.vaultSearchTimer);
|
||||||
state.vaultSearchTimer=setTimeout(function(){ syncSelectedWithFilter(); render(); }, 60);
|
state.vaultSearchTimer=setTimeout(function(){ state.vaultSearchTimer=0; syncSelectedWithFilter(); render(); }, 60);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+227
-1
@@ -166,6 +166,197 @@
|
|||||||
}
|
}
|
||||||
.alert-success { background: var(--success-bg); color: var(--success); border-color: #BADBCC; }
|
.alert-success { background: var(--success-bg); color: var(--success); border-color: #BADBCC; }
|
||||||
.alert-danger { background: var(--danger-bg); color: var(--danger); border-color: #F5C2C7; }
|
.alert-danger { background: var(--danger-bg); color: var(--danger); border-color: #F5C2C7; }
|
||||||
|
.toast-stack {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 1200;
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: min(420px, calc(100vw - 24px));
|
||||||
|
}
|
||||||
|
.toast-item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid #c9e9d6;
|
||||||
|
background: #dff4e5;
|
||||||
|
color: #0f5132;
|
||||||
|
padding: 14px 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.toast-item.error {
|
||||||
|
border-color: #f5c2c7;
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #842029;
|
||||||
|
}
|
||||||
|
.toast-item.warning {
|
||||||
|
border-color: #ffe69c;
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #664d03;
|
||||||
|
}
|
||||||
|
.toast-text {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
.toast-close {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.toast-close:hover { opacity: 1; }
|
||||||
|
.toast-bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 3px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0,0,0,0.12);
|
||||||
|
transform-origin: left center;
|
||||||
|
animation: toastBar 4.5s linear forwards;
|
||||||
|
}
|
||||||
|
@keyframes toastBar { from { transform: scaleX(1); } to { transform: scaleX(0); } }
|
||||||
|
.dialog-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(17, 24, 39, 0.45);
|
||||||
|
z-index: 1300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.dialog-card {
|
||||||
|
width: min(540px, 100%);
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
padding: 24px 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.dialog-icon {
|
||||||
|
font-size: 34px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #f4b400;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.dialog-title {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 34px;
|
||||||
|
line-height: 1.15;
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.dialog-msg {
|
||||||
|
margin: 0 auto 18px auto;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 20px;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
.dialog-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.form-dialog {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.form-dialog .dialog-title {
|
||||||
|
font-size: 30px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.form-dialog .dialog-msg {
|
||||||
|
font-size: 16px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.form-dialog .dialog-btn {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
.dialog-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c2c7;
|
||||||
|
color: #842029;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
.unlock-card {
|
||||||
|
max-width: 620px;
|
||||||
|
padding: 30px 34px;
|
||||||
|
}
|
||||||
|
.unlock-pwd-wrap {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.unlock-pwd-input {
|
||||||
|
padding-right: 88px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-color: #3f5b9e;
|
||||||
|
}
|
||||||
|
.auth-page .form-input {
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-color: #3f5b9e;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.auth-page .form-input:focus {
|
||||||
|
border-color: #3f5b9e;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.unlock-eye-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 42px;
|
||||||
|
bottom: 8px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #233a72;
|
||||||
|
font-size: 17px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.unlock-main-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.unlock-secondary-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.unlock-or {
|
||||||
|
text-align: center;
|
||||||
|
color: #1f2f4f;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 10px 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
.totp-qr-card {
|
.totp-qr-card {
|
||||||
background:#fff;
|
background:#fff;
|
||||||
padding:16px;
|
padding:16px;
|
||||||
@@ -323,6 +514,42 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: #F8FAFC;
|
background: #F8FAFC;
|
||||||
}
|
}
|
||||||
|
.content .btn {
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
.content .btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.content .btn-primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
|
||||||
|
.content .btn-secondary {
|
||||||
|
background: #fff;
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
.content .btn-secondary:hover { background: #edf3ff; }
|
||||||
|
.content .btn-danger {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #e11d48;
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
.content .btn-danger:hover { background: #fff1f2; }
|
||||||
|
.content .btn-danger-icon {
|
||||||
|
width: 42px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #e11d48;
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.content .btn-danger-icon:hover {
|
||||||
|
border: 1px solid #fecdd3;
|
||||||
|
background: #fff1f2;
|
||||||
|
}
|
||||||
|
|
||||||
/* Vault Grid */
|
/* Vault Grid */
|
||||||
.vault-grid {
|
.vault-grid {
|
||||||
@@ -583,4 +810,3 @@
|
|||||||
}
|
}
|
||||||
.vault-grid { grid-template-columns: 1fr; }
|
.vault-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user