Personal profile
-Your display name, username identifier, and account photo.
+Profile
+How you appear across Passli.
diff --git a/static/css/settings.css b/static/css/settings.css
index e6c1e60..c2ad429 100644
--- a/static/css/settings.css
+++ b/static/css/settings.css
@@ -61,7 +61,7 @@
/* Fonts & Weights */
--s-font-ui: var(--font-sans, 'Geist', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
- --s-font-mono: var(--font-mono, 'JetBrains Mono', ui-monospace, monospace);
+ --s-font-mono: 'JetBrains Mono', 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;
--s-weight-regular: 400;
--s-weight-medium: 500;
--s-weight-semibold: 600;
@@ -200,6 +200,81 @@
flex-shrink: 0;
}
+/* In-Page Flash Alerts Region (Top of .settings-main-content) */
+.settings-alerts {
+ margin-bottom: var(--s-space-20);
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.settings-alert {
+ display: flex;
+ align-items: center;
+ gap: var(--s-space-10);
+ padding: var(--s-space-12) var(--s-space-16);
+ border-radius: var(--s-radius-md);
+ font-size: var(--s-font-14);
+ line-height: 1.4;
+ margin-bottom: var(--s-space-8);
+ box-sizing: border-box;
+ width: 100%;
+}
+
+.settings-alert:last-child {
+ margin-bottom: 0;
+}
+
+.settings-alert-success {
+ background-color: var(--s-color-emerald-bg);
+ border: 1px solid var(--s-color-emerald-border);
+ color: var(--s-color-emerald);
+}
+
+.settings-alert-danger {
+ background-color: var(--s-color-danger-bg);
+ border: 1px solid var(--s-color-danger-border);
+ color: var(--s-color-danger);
+}
+
+.settings-alert-warning {
+ background-color: var(--s-color-warning-bg);
+ border: 1px solid var(--s-color-warning-border);
+ color: var(--s-color-warning-text);
+}
+
+.settings-alert-info {
+ background-color: #eff6ff;
+ border: 1px solid #bfdbfe;
+ color: #1e40af;
+}
+
+.settings-alert-text {
+ flex: 1 1 auto;
+}
+
+.settings-alert-close {
+ flex: 0 0 auto;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ font-size: 18px;
+ line-height: 1;
+ color: inherit;
+ padding: 0 var(--s-space-4);
+ opacity: 0.6;
+ border-radius: var(--s-radius-sm);
+ transition: opacity var(--s-duration) var(--s-easing);
+}
+
+.settings-alert-close:hover {
+ opacity: 1;
+}
+
+.settings-alert-close:focus-visible {
+ outline: 2px solid currentColor;
+ outline-offset: 1px;
+}
+
/* Progressive Enhancement: Section Visibility */
.settings-section {
display: block;
@@ -246,6 +321,7 @@
align-items: center;
gap: var(--s-space-24);
padding: var(--s-space-20) var(--s-space-28);
+ min-height: 76px;
border-bottom: 1px solid var(--s-color-border);
box-sizing: border-box;
position: relative;
@@ -271,9 +347,12 @@
word-break: break-word;
}
-.settings-row-value.font-mono-data {
+.settings-row-value.font-mono-data,
+.font-mono-data {
font-family: var(--s-font-mono);
font-size: var(--s-font-14);
+ font-weight: 500;
+ font-feature-settings: "zero", "cv02", "cv03", "cv04", "cv11";
}
.settings-row-action {
@@ -282,6 +361,7 @@
align-items: center;
gap: var(--s-space-8);
margin-left: auto;
+ white-space: nowrap;
}
.settings-row-chevron {
@@ -296,8 +376,8 @@
padding: var(--s-space-2) var(--s-space-8);
font-size: var(--s-font-12);
font-weight: var(--s-weight-semibold);
- letter-spacing: 0.04em;
- text-transform: uppercase;
+ letter-spacing: normal;
+ text-transform: none;
border-radius: var(--s-radius-pill);
line-height: 1.4;
vertical-align: middle;
@@ -451,6 +531,25 @@
border-top: 1px solid var(--s-color-border);
padding: var(--s-space-24) var(--s-space-28) var(--s-space-28);
box-sizing: border-box;
+ animation: panelSlideDown 150ms cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+@keyframes panelSlideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-4px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .settings-panel {
+ animation: none !important;
+ transition: none !important;
+ }
}
/* Progressive Enhancement: Panels with hidden attribute are hidden in JS */
@@ -647,12 +746,12 @@
display: none;
}
-/* Avatar Row Elements */
+/* Avatar Row & Panel Elements */
.settings-avatar-wrap {
- width: 56px;
- height: 56px;
+ width: 48px;
+ height: 48px;
border-radius: var(--s-radius-pill);
- background-color: var(--s-color-subtle);
+ background-color: var(--s-color-navy);
border: 1px solid var(--s-color-border);
display: flex;
align-items: center;
@@ -670,17 +769,63 @@
.settings-avatar-initial {
font-family: var(--s-font-mono);
- font-size: var(--s-font-20);
- font-weight: var(--s-weight-semibold);
- color: var(--s-color-text);
+ font-size: var(--s-font-18);
+ font-weight: 500;
+ color: var(--s-color-white);
+ line-height: 1;
}
-.settings-card-footnote {
- padding: var(--s-space-16) var(--s-space-28);
+.settings-avatar-row-content {
+ display: flex;
+ align-items: center;
+ gap: var(--s-space-16);
+}
+
+.settings-avatar-row-hint {
font-size: var(--s-font-13);
color: var(--s-color-muted);
+ line-height: 1.4;
+}
+
+.settings-avatar-picker-preview {
+ display: flex;
+ align-items: center;
+ gap: var(--s-space-16);
+ margin-bottom: var(--s-space-20);
+}
+
+.settings-avatar-picker-preview .settings-avatar-wrap {
+ width: 56px;
+ height: 56px;
+}
+
+.settings-avatar-picker-preview .settings-avatar-initial {
+ font-size: var(--s-font-22);
+}
+
+.settings-avatar-pending-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--s-space-12);
+ margin-top: var(--s-space-16);
+}
+
+.settings-avatar-remove-row {
+ margin-top: var(--s-space-16);
+ padding-top: var(--s-space-16);
border-top: 1px solid var(--s-color-border);
- background-color: var(--s-color-surface);
+ display: flex;
+ justify-content: flex-start;
+}
+
+.settings-card-footnote {
+ margin-top: var(--s-space-12);
+ padding: 0 var(--s-space-4);
+ font-size: var(--s-font-13);
+ color: var(--s-color-muted);
+ border: none;
+ background: transparent;
line-height: 1.5;
}
@@ -882,9 +1027,30 @@
padding: var(--s-space-14) var(--s-space-16);
gap: var(--s-space-12);
min-height: 68px;
+ position: relative;
cursor: pointer;
}
+ /* Full-row tap target via toggle button on mobile */
+ .settings-row .settings-btn-secondary[data-action="toggle-panel"] {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ z-index: 2;
+ cursor: pointer;
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: transparent;
+ }
+
+ .settings-row .settings-btn-secondary[data-action="toggle-panel"]:focus-visible {
+ outline: 2px solid var(--s-color-navy);
+ outline-offset: -2px;
+ }
+
.settings-row-content {
display: flex;
flex-direction: column;
@@ -909,9 +1075,12 @@
flex-shrink: 0;
}
+ /* Only chevron is visible on mobile */
.settings-row-chevron {
display: inline-flex;
color: var(--s-color-muted);
+ pointer-events: none;
+ z-index: 1;
}
/* Bottom Sheet Transformation */
@@ -1008,3 +1177,115 @@
color: var(--s-color-muted);
}
}
+
+/* Zero-Inline-Style Utility Classes */
+.settings-pill-spaced {
+ margin-left: var(--s-space-8);
+}
+
+.settings-notice-cooldown {
+ margin-top: var(--s-space-8);
+}
+
+.settings-text-muted {
+ color: var(--s-color-muted);
+}
+
+.settings-badge-group {
+ margin-left: var(--s-space-8);
+ display: inline-flex;
+ align-items: center;
+ gap: var(--s-space-4);
+}
+
+.settings-btn-verify {
+ height: 28px;
+ min-height: 28px;
+ padding: 0 var(--s-space-8);
+ font-size: var(--s-font-12);
+ margin-left: var(--s-space-4);
+}
+
+.settings-hint-spaced {
+ margin-bottom: var(--s-space-8);
+}
+
+.settings-panel-subactions {
+ display: flex;
+ gap: var(--s-space-12);
+ align-items: center;
+ justify-content: flex-end;
+ margin-top: var(--s-space-12);
+}
+
+.settings-notice-spaced {
+ margin-top: var(--s-space-8);
+}
+
+.settings-notice-emerald {
+ background-color: var(--s-color-emerald-bg);
+ border-color: var(--s-color-emerald-border);
+ color: var(--s-color-emerald);
+}
+
+.settings-notice-full {
+ margin-bottom: var(--s-space-16);
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.settings-form-actions-end {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: var(--s-space-8);
+}
+
+.settings-panel-divider {
+ margin-top: var(--s-space-20);
+ padding-top: var(--s-space-16);
+ border-top: 1px solid var(--s-color-border);
+}
+
+.settings-link-inline {
+ padding: 0;
+ text-decoration: underline;
+ background: transparent;
+ border: none;
+ color: var(--s-color-muted);
+ font-size: inherit;
+ font-family: inherit;
+ cursor: pointer;
+}
+
+.settings-link-inline:hover {
+ color: var(--s-color-text);
+}
+
+.settings-empty-icon {
+ width: 32px;
+ height: 32px;
+ margin: 0 auto var(--s-space-8);
+ opacity: 0.5;
+}
+
+.settings-empty-text {
+ margin: 0;
+}
+
+/* Touch Targets on Coarse Pointers (At least 44px) */
+@media (pointer: coarse) {
+ .settings-btn-secondary {
+ min-height: 44px;
+ height: 44px;
+ padding: 0 var(--s-space-16);
+ }
+
+ .settings-btn-text-cancel,
+ .settings-btn-danger-text {
+ min-height: 44px;
+ display: inline-flex;
+ align-items: center;
+ padding: var(--s-space-8) var(--s-space-12);
+ }
+}
+
diff --git a/static/js/settings.js b/static/js/settings.js
index 552e8a8..02cffb8 100644
--- a/static/js/settings.js
+++ b/static/js/settings.js
@@ -8,6 +8,7 @@
document.documentElement.classList.add('js-ready');
document.addEventListener('DOMContentLoaded', () => {
+ initAlertDismiss();
initSectionNavigation();
initDisclosurePanels();
initAvatarControls();
@@ -18,6 +19,24 @@ document.addEventListener('DOMContentLoaded', () => {
initRemoveAvatarDialog();
});
+/**
+ * 0. Flash Alert Dismiss Handler
+ */
+function initAlertDismiss() {
+ document.querySelectorAll('[data-action="dismiss-alert"]').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.preventDefault();
+ const alert = btn.closest('.settings-alert');
+ if (alert) {
+ alert.style.opacity = '0';
+ alert.style.transform = 'translateY(-4px)';
+ alert.style.transition = 'opacity 150ms ease, transform 150ms ease';
+ setTimeout(() => alert.remove(), 150);
+ }
+ });
+ });
+}
+
/**
* 1. Section Navigation & Legacy Hash Mapping
* Maps:
@@ -96,7 +115,7 @@ function initSectionNavigation() {
if (options.updateHash !== false) {
if (window.location.hash !== `#${targetSectionId}`) {
- history.replaceState(null, '', `#${targetSectionId}`);
+ history.pushState(null, '', `#${targetSectionId}`);
}
}
}
@@ -113,8 +132,64 @@ function initSectionNavigation() {
});
});
- // Check on load: Did server specify an open panel or errors?
- const serverOpenPanel = document.querySelector('.settings-panel[data-server-open="true"], .settings-panel:has(.settings-error)');
+ // Check on load: Did server specify an open panel, inline errors, or flash alerts?
+ let serverOpenPanel = document.querySelector('.settings-panel[data-server-open="true"], .settings-panel:has(.settings-error:not(:empty))');
+
+ // Inspect danger flash alerts to connect redirected server rejections with their corresponding panel & fields
+ const dangerAlerts = Array.from(document.querySelectorAll('.settings-alert-danger'));
+ dangerAlerts.forEach(alertElem => {
+ const txt = (alertElem.textContent || '').trim();
+ // 1. Profile display name error
+ if (txt.includes('Could not update profile')) {
+ serverOpenPanel = document.getElementById('panel-display-name');
+ if (serverOpenPanel) {
+ serverOpenPanel.setAttribute('data-server-open', 'true');
+ const dnInput = document.getElementById('profile-display-name');
+ let dnErr = document.getElementById('profile-display-name-error');
+ if (dnInput && !dnErr) {
+ dnErr = document.createElement('span');
+ dnErr.className = 'settings-error';
+ dnErr.id = 'profile-display-name-error';
+ dnErr.setAttribute('role', 'alert');
+ dnErr.innerHTML = ' Display name cannot exceed 50 characters.';
+ dnInput.insertAdjacentElement('afterend', dnErr);
+ dnInput.setAttribute('aria-invalid', 'true');
+ dnInput.setAttribute('aria-describedby', 'profile-display-name-error');
+ }
+ }
+ }
+ // 2. Current password error in username change
+ if (txt.toLowerCase().includes('password') && (txt.toLowerCase().includes('incorrect') || txt.toLowerCase().includes('wrong'))) {
+ const pwdInput = document.getElementById('settings-username-password');
+ let pwdErr = document.getElementById('username-password-error');
+ if (pwdInput && !pwdErr) {
+ pwdErr = document.createElement('span');
+ pwdErr.className = 'settings-error';
+ pwdErr.id = 'username-password-error';
+ pwdErr.setAttribute('role', 'alert');
+ pwdErr.innerHTML = ` ${txt}`;
+ pwdInput.insertAdjacentElement('afterend', pwdErr);
+ pwdInput.setAttribute('aria-invalid', 'true');
+ pwdInput.setAttribute('aria-describedby', 'username-password-error');
+ }
+ }
+ // 3. New username error
+ if (txt.toLowerCase().includes('username') || txt.includes('@')) {
+ const unInput = document.getElementById('settings-new-username');
+ let unErr = document.getElementById('new-username-error');
+ if (unInput && !unErr) {
+ unErr = document.createElement('span');
+ unErr.className = 'settings-error';
+ unErr.id = 'new-username-error';
+ unErr.setAttribute('role', 'alert');
+ unErr.innerHTML = ` ${txt}`;
+ unInput.insertAdjacentElement('afterend', unErr);
+ unInput.setAttribute('aria-invalid', 'true');
+ unInput.setAttribute('aria-describedby', 'new-username-error');
+ }
+ }
+ });
+
if (serverOpenPanel) {
const parentSection = serverOpenPanel.closest('.settings-section');
if (parentSection) {
@@ -122,6 +197,10 @@ function initSectionNavigation() {
serverOpenPanel.removeAttribute('hidden');
const toggleBtn = document.querySelector(`[aria-controls="${serverOpenPanel.id}"]`);
if (toggleBtn) toggleBtn.setAttribute('aria-expanded', 'true');
+ const invalidField = serverOpenPanel.querySelector('[aria-invalid="true"], input:not([type="hidden"]), select, textarea');
+ if (invalidField) {
+ invalidField.focus();
+ }
return;
}
}
@@ -140,8 +219,8 @@ function initSectionNavigation() {
}
}
- // Listen to external hashchange
- window.addEventListener('hashchange', () => {
+ // Listen to external hashchange and popstate (browser back/forward navigation)
+ function handleHistoryNav() {
const { sectionId: newSec, panelId: newPan } = getTargetSectionAndPanel(window.location.hash);
switchSection(newSec, { updateHash: false });
if (newPan) {
@@ -152,7 +231,9 @@ function initSectionNavigation() {
if (tb) tb.setAttribute('aria-expanded', 'true');
}
}
- });
+ }
+ window.addEventListener('hashchange', handleHistoryNav);
+ window.addEventListener('popstate', handleHistoryNav);
}
/**
@@ -165,7 +246,8 @@ function initDisclosurePanels() {
// Hide any panels not explicitly marked open on load
panels.forEach(p => {
- if (p.getAttribute('data-server-open') !== 'true' && !p.querySelector('.settings-error')) {
+ const hasVisibleError = Array.from(p.querySelectorAll('.settings-error:not(:empty)')).some(err => !err.hasAttribute('hidden'));
+ if (p.getAttribute('data-server-open') !== 'true' && !hasVisibleError) {
p.setAttribute('hidden', '');
}
});
@@ -201,7 +283,7 @@ function initDisclosurePanels() {
// Focus first input
const firstInput = panel.querySelector('input:not([type="hidden"]), button:not(.settings-sheet-drag-handle)');
if (firstInput) {
- setTimeout(() => firstInput.focus(), 50);
+ firstInput.focus();
}
}
@@ -254,9 +336,14 @@ function initDisclosurePanels() {
});
}
- // Escape closes untouched panel/sheet
+ // Escape closes untouched panel/sheet (unless a native dialog is currently open)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
+ const openDialog = document.querySelector('dialog[open]');
+ if (openDialog) {
+ // Let the native dialog handle its own Escape dismissal
+ return;
+ }
const openPan = document.querySelector('.settings-panel:not([hidden])');
if (openPan) {
closePanel(openPan, true);
@@ -286,54 +373,118 @@ function initDisclosurePanels() {
function initAvatarControls() {
const fileInput = document.getElementById('avatar-file-input');
const chooseBtn = document.getElementById('btn-choose-avatar');
- const previewImg = document.getElementById('avatar-preview-img');
- const fallbackInitial = document.getElementById('avatar-fallback-initial');
+ const rowPreviewImg = document.getElementById('avatar-preview-img');
+ const rowFallbackInitial = document.getElementById('avatar-fallback-initial');
+ const panelPreviewImg = document.getElementById('avatar-panel-preview');
+ const panelFallbackInitial = document.getElementById('avatar-panel-initial');
+ const pendingActions = document.getElementById('avatar-pending-actions');
+ const cancelFileBtn = document.getElementById('btn-cancel-avatar-file');
const errorContainer = document.getElementById('avatar-client-error');
if (!fileInput || !chooseBtn) return;
+ // Cache original state for cancel restoration
+ const originalRowSrc = rowPreviewImg ? rowPreviewImg.getAttribute('src') : '';
+ const hadInitialAvatar = rowPreviewImg && originalRowSrc && !rowPreviewImg.hasAttribute('hidden');
+
+ function resetFileSelection() {
+ fileInput.value = '';
+ if (errorContainer) {
+ errorContainer.textContent = '';
+ errorContainer.setAttribute('hidden', '');
+ }
+ if (pendingActions) {
+ pendingActions.setAttribute('hidden', '');
+ }
+ // Restore previews to initial persisted state
+ if (hadInitialAvatar) {
+ if (rowPreviewImg) {
+ rowPreviewImg.src = originalRowSrc;
+ rowPreviewImg.removeAttribute('hidden');
+ }
+ if (rowFallbackInitial) rowFallbackInitial.setAttribute('hidden', '');
+ if (panelPreviewImg) {
+ panelPreviewImg.src = originalRowSrc;
+ panelPreviewImg.removeAttribute('hidden');
+ }
+ if (panelFallbackInitial) panelFallbackInitial.setAttribute('hidden', '');
+ } else {
+ if (rowPreviewImg) {
+ rowPreviewImg.src = '';
+ rowPreviewImg.setAttribute('hidden', '');
+ }
+ if (rowFallbackInitial) rowFallbackInitial.removeAttribute('hidden');
+ if (panelPreviewImg) {
+ panelPreviewImg.src = '';
+ panelPreviewImg.setAttribute('hidden', '');
+ }
+ if (panelFallbackInitial) panelFallbackInitial.removeAttribute('hidden');
+ }
+ }
+
chooseBtn.addEventListener('click', (e) => {
e.preventDefault();
fileInput.click();
});
+ if (cancelFileBtn) {
+ cancelFileBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ resetFileSelection();
+ });
+ }
+
fileInput.addEventListener('change', () => {
const file = fileInput.files && fileInput.files[0];
- if (!file) return;
+ if (!file) {
+ resetFileSelection();
+ return;
+ }
if (errorContainer) {
errorContainer.textContent = '';
- errorContainer.style.display = 'none';
+ errorContainer.setAttribute('hidden', '');
}
const maxBytes = 2 * 1024 * 1024;
if (file.size > maxBytes) {
- fileInput.value = '';
+ resetFileSelection();
if (errorContainer) {
errorContainer.textContent = 'Selected image exceeds the 2 MB limit.';
- errorContainer.style.display = 'block';
+ errorContainer.removeAttribute('hidden');
}
return;
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
- fileInput.value = '';
+ resetFileSelection();
if (errorContainer) {
errorContainer.textContent = 'Please choose a JPEG, PNG, or WebP image.';
- errorContainer.style.display = 'block';
+ errorContainer.removeAttribute('hidden');
}
return;
}
const reader = new FileReader();
reader.onload = (e) => {
- if (previewImg) {
- previewImg.src = e.target.result;
- previewImg.style.display = 'block';
+ const dataUrl = e.target.result;
+ if (rowPreviewImg) {
+ rowPreviewImg.src = dataUrl;
+ rowPreviewImg.removeAttribute('hidden');
+ }
+ if (rowFallbackInitial) {
+ rowFallbackInitial.setAttribute('hidden', '');
+ }
+ if (panelPreviewImg) {
+ panelPreviewImg.src = dataUrl;
+ panelPreviewImg.removeAttribute('hidden');
+ }
+ if (panelFallbackInitial) {
+ panelFallbackInitial.setAttribute('hidden', '');
}
- if (fallbackInitial) {
- fallbackInitial.style.display = 'none';
+ if (pendingActions) {
+ pendingActions.removeAttribute('hidden');
}
};
reader.readAsDataURL(file);
diff --git a/templates/accounts/settings.html b/templates/accounts/settings.html
index 080966b..edb2e44 100644
--- a/templates/accounts/settings.html
+++ b/templates/accounts/settings.html
@@ -2,7 +2,9 @@
{% load static %}
{% block title %}Account Settings — Passli{% endblock %}
-{% block meta_description %}Manage your Passli profile, credentials, security boundaries, and review recent account activity.{% endblock %}
+{% block meta_description %}Manage your profile, sign-in details and security, and review recent account activity.{% endblock %}
+
+{% block global_messages %}{% endblock %}
{% block extra_css %}
@@ -13,7 +15,7 @@
Manage your personal identity, credentials, and review your account activity. Manage your profile, sign-in details and security.Settings
-
Your display name, username identifier, and account photo.
+How you appear across Passli.