making settings more readable and navigatable

This commit is contained in:
2026-05-23 20:10:57 +02:00
parent c6ff4fa703
commit 0f3b80f0c1
9 changed files with 148 additions and 112 deletions

View File

@@ -2539,7 +2539,7 @@ body.layout-legacy .comments-list {
body.layout-legacy .scroll-nav-wrapper { body.layout-legacy .scroll-nav-wrapper {
position: absolute; position: absolute;
top: 0; top: -30px;
bottom: 0; bottom: 0;
right: -50px; right: -50px;
width: 40px; width: 40px;
@@ -8017,12 +8017,6 @@ span.badge.badge-current {
margin-bottom: 20px; margin-bottom: 20px;
} }
@media (max-width: 900px) {
.avatar-settings-wrapper {
grid-template-columns: 1fr;
}
}
.avatar-preview-container { .avatar-preview-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -3223,9 +3223,9 @@ window.cancelAnimFrame = (function () {
const targetEl = document.querySelector(hash); const targetEl = document.querySelector(hash);
if (targetEl) { if (targetEl) {
targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Clear any previous highlight, then permanently mark this element
document.querySelectorAll('.comment-highlighted').forEach(el => el.classList.remove('comment-highlighted'));
targetEl.classList.add('comment-highlighted'); targetEl.classList.add('comment-highlighted');
// Remove highlight after some time to keep UI clean
setTimeout(() => targetEl.classList.remove('comment-highlighted'), 3000);
} }
} }

View File

@@ -711,9 +711,12 @@
const id = contextLink.dataset.id; const id = contextLink.dataset.id;
const target = commentsList.querySelector(`.comment-item[data-comment-id="${id}"]`); const target = commentsList.querySelector(`.comment-item[data-comment-id="${id}"]`);
if (target) { if (target) {
// Clear any previous persistent highlight
commentsList.querySelectorAll('.comment-linked').forEach(el => el.classList.remove('comment-linked'));
target.scrollIntoView({ behavior: 'smooth', block: 'center' }); target.scrollIntoView({ behavior: 'smooth', block: 'center' });
target.classList.add('highlight-comment'); // Flash the animation for attention, then keep the persistent highlight
setTimeout(() => target.classList.remove('highlight-comment'), 2000); target.classList.add('highlight-comment', 'comment-linked');
setTimeout(() => target.classList.remove('highlight-comment'), 2500);
} }
} }
}); });

View File

@@ -120,6 +120,7 @@
"switching": "Wird umgeschaltet...", "switching": "Wird umgeschaltet...",
"generating": "Wird generiert...", "generating": "Wird generiert...",
"title": "Einstellungen", "title": "Einstellungen",
"profile": "Profil",
"avatar": "Avatar", "avatar": "Avatar",
"current_avatar": "Aktueller Avatar", "current_avatar": "Aktueller Avatar",
"upload_custom_avatar": "Eigenen Avatar hochladen", "upload_custom_avatar": "Eigenen Avatar hochladen",

View File

@@ -120,6 +120,7 @@
"switching": "Switching...", "switching": "Switching...",
"generating": "Generating...", "generating": "Generating...",
"title": "Settings", "title": "Settings",
"profile": "Profile",
"avatar": "Avatar", "avatar": "Avatar",
"current_avatar": "Current Avatar", "current_avatar": "Current Avatar",
"upload_custom_avatar": "Upload Custom Avatar", "upload_custom_avatar": "Upload Custom Avatar",

View File

@@ -120,6 +120,7 @@
"switching": "Omschakelen...", "switching": "Omschakelen...",
"generating": "Genereren...", "generating": "Genereren...",
"title": "Instellingen", "title": "Instellingen",
"profile": "Profiel",
"avatar": "Avatar", "avatar": "Avatar",
"current_avatar": "Huidige Avatar", "current_avatar": "Huidige Avatar",
"upload_custom_avatar": "Aangepaste Avatar Uploaden", "upload_custom_avatar": "Aangepaste Avatar Uploaden",

View File

@@ -120,6 +120,7 @@
"switching": "Umschaltung wird vorgenommen...", "switching": "Umschaltung wird vorgenommen...",
"generating": "Generierung wird angestoßen...", "generating": "Generierung wird angestoßen...",
"title": "Einstellungen", "title": "Einstellungen",
"profile": "Profil",
"avatar": "Profilbild", "avatar": "Profilbild",
"current_avatar": "Aktuelles Profilbild", "current_avatar": "Aktuelles Profilbild",
"upload_custom_avatar": "Benutzerdefiniertes Profilbild aufladieren", "upload_custom_avatar": "Benutzerdefiniertes Profilbild aufladieren",

View File

@@ -630,6 +630,10 @@
100% { background: rgba(255,255,255,.03); border-color: rgba(255,255,255,.05); } 100% { background: rgba(255,255,255,.03); border-color: rgba(255,255,255,.05); }
} }
.highlight-comment { animation: comment-highlight 2.5s cubic-bezier(0.2, 0, 0, 1); } .highlight-comment { animation: comment-highlight 2.5s cubic-bezier(0.2, 0, 0, 1); }
.comment-linked {
border-left: 2px solid var(--accent) !important;
background: rgba(var(--accent-rgb, 233, 30, 140), 0.07) !important;
}
#reply-indicator { #reply-indicator {
display: flex; align-items: center; gap: 8px; display: flex; align-items: center; gap: 8px;
padding: 6px 12px; background: rgba(255,255,255,.05); padding: 6px 12px; background: rgba(255,255,255,.05);

View File

@@ -6,20 +6,20 @@
<!-- Quick navigation --> <!-- Quick navigation -->
<nav id="settings-quicknav" aria-label="Settings sections"> <nav id="settings-quicknav" aria-label="Settings sections">
<a href="/settings#sec-avatar">{{ t('settings.avatar') }}</a> <a href="#profile">{{ t('settings.profile') }}</a>
<a href="/settings#sec-preferences">{{ t('settings.preferences') }}</a> <a href="#preferences">{{ t('settings.preferences') }}</a>
@if(enable_data_export) @if(enable_data_export)
<a href="/settings#sec-export">{{ t('settings.export_data_title') }}</a> <a href="#export">{{ t('settings.export_data_title') }}</a>
@endif @endif
<a href="/settings#sec-account">{{ t('settings.account') }}</a> <a href="#account">{{ t('settings.account') }}</a>
@if(matrix_enabled) @if(matrix_enabled)
<a href="/settings#sec-linked">{{ t('settings.linked_accounts') }}</a> <a href="#linked">{{ t('settings.linked_accounts') }}</a>
@endif @endif
@if(enable_user_api_keys) @if(enable_user_api_keys)
<a href="/settings#sec-apikey">API Key</a> <a href="#apikey">API Key</a>
@endif @endif
@if(enable_user_invites) @if(enable_user_invites)
<a href="/settings#sec-invites">{{ t('invites.section_title') }}</a> <a href="#invites">{{ t('invites.section_title') }}</a>
@endif @endif
</nav> </nav>
<style> <style>
@@ -27,16 +27,19 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
margin-bottom: 28px; padding: 8px 14px;
padding: 10px 14px; background: rgba(20,20,20,0.92);
background: rgba(255,255,255,0.04); border-bottom: 1px solid var(--nav-border-color);
border: 1px solid var(--nav-border-color); position: fixed;
border-radius: 8px; top: var(--navbar-h, 50px); /* sit just below the site navbar */
position: sticky; left: 0;
top: 0; right: 0;
z-index: 50; z-index: 200;
backdrop-filter: blur(8px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
} }
/* Push content down so the fixed nav doesn't overlap headings */
.settings { padding-top: 56px; }
#settings-quicknav a { #settings-quicknav a {
font-size: 0.82em; font-size: 0.82em;
font-weight: 600; font-weight: 600;
@@ -57,22 +60,38 @@
</style> </style>
<script> <script>
(function(){ (function(){
const nav = document.getElementById('settings-quicknav'); var nav = document.getElementById('settings-quicknav');
if (!nav) return; if (!nav) return;
const links = Array.from(nav.querySelectorAll('a[href^="#"]'));
const targets = links.map(l => document.getElementById(l.getAttribute('href').slice(1))).filter(Boolean); function setActive(href) {
if (!targets.length) return; nav.querySelectorAll('a').forEach(function(a) {
const obs = new IntersectionObserver(entries => { a.classList.toggle('active', a.getAttribute('href') === href);
entries.forEach(e => { });
const link = nav.querySelector('a[href="#' + e.target.id + '"]'); }
if (link) link.classList.toggle('active', e.isIntersecting);
// Set initial active from hash or default to first
var initHash = location.hash;
var links = nav.querySelectorAll('a[href^="#"]');
var firstLink = links[0];
if (initHash && nav.querySelector('a[href="' + initHash + '"]')) {
setActive(initHash);
} else if (firstLink) {
setActive(firstLink.getAttribute('href'));
}
// Delegated click on the nav container — fires before document-level handlers
nav.addEventListener('click', function(e) {
var link = e.target.closest('a[href^="#"]');
if (!link) return;
setActive(link.getAttribute('href'));
}); });
}, { rootMargin: '-10% 0px -80% 0px', threshold: 0 });
targets.forEach(t => obs.observe(t));
})(); })();
</script> </script>
<h2 id="sec-avatar">{{ t('settings.avatar') }}</h2> <!-- ═══════════════════════════════ PROFILE ═══════════════════════════════ -->
<h2 id="profile">{{ t('settings.profile') }}</h2>
<fieldset style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.avatar') }}</legend>
<div class="avatar-settings-wrapper"> <div class="avatar-settings-wrapper">
<div class="avatar-preview-container"> <div class="avatar-preview-container">
<div class="avatar-preview-label">{{ t('settings.current_avatar') }}</div> <div class="avatar-preview-label">{{ t('settings.current_avatar') }}</div>
@@ -90,7 +109,6 @@
</div> </div>
<div class="avatar-upload-section"> <div class="avatar-upload-section">
<h4>{{ t('settings.upload_custom_avatar') }}</h4>
<p class="avatar-hint">{{ t('settings.avatar_hint') }}</p> <p class="avatar-hint">{{ t('settings.avatar_hint') }}</p>
<div class="avatar-upload-wrapper"> <div class="avatar-upload-wrapper">
@@ -115,10 +133,12 @@
<div id="avatar-upload-status" class="avatar-status"></div> <div id="avatar-upload-status" class="avatar-status"></div>
</div> </div>
</div> </div>
</fieldset>
@if(enable_profile_description) @if(enable_profile_description)
<div class="profile-settings-wrapper"> <fieldset style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.custom_description') }}</legend>
<div class="setting-item"> <div class="setting-item">
<label for="profile_description">{{ t('settings.custom_description') }}</label>
<textarea id="profile_description" class="input" placeholder="{{ t('settings.description_placeholder') }}" <textarea id="profile_description" class="input" placeholder="{{ t('settings.description_placeholder') }}"
maxlength="255">{!! session.description || '' !!}</textarea> maxlength="255">{!! session.description || '' !!}</textarea>
<div class="profile-settings-actions"> <div class="profile-settings-actions">
@@ -127,10 +147,12 @@
</div> </div>
<div id="description-status" class="avatar-status"></div> <div id="description-status" class="avatar-status"></div>
</div> </div>
</div> </fieldset>
@endif @endif
<div class="setting-item" style="margin-top: 20px;">
<label for="username_color_picker" style="display: block; margin-bottom: 5px;">{{ t('settings.username_color') }}</label> <fieldset style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.username_color') }}</legend>
<div class="setting-item">
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;"> <div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<input type="color" id="username_color_picker" value="{{ session.username_color || '#ffffff' }}" <input type="color" id="username_color_picker" value="{{ session.username_color || '#ffffff' }}"
style="width: 50px; height: 30px; padding: 0; border: 1px solid var(--nav-border-color); cursor: pointer; background: none;"> style="width: 50px; height: 30px; padding: 0; border: 1px solid var(--nav-border-color); cursor: pointer; background: none;">
@@ -144,7 +166,24 @@
</div> </div>
<small class="text-muted">{{ t('settings.username_color_hint') }}</small> <small class="text-muted">{{ t('settings.username_color_hint') }}</small>
</div> </div>
<h2 id="sec-preferences">{{ t('settings.preferences') }}</h2> </fieldset>
<fieldset style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.display_name') }}</legend>
<div class="setting-item">
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<input type="text" id="display_name_input" class="input"
placeholder="{{ t('settings.display_name_placeholder') }}" value="{!! session.display_name || '' !!}"
maxlength="32" style="max-width: 200px; height: 30px; font-size: 0.9em;">
<button type="button" id="btn-update-display-name" class="button"
style="padding: 5px 10px; font-size: 0.85em;">{{ t('settings.save') }}</button>
</div>
<div id="display-name-status" class="avatar-status"></div>
</div>
</fieldset>
<!-- ═══════════════════════════════ PREFERENCES ═══════════════════════════════ -->
<h2 id="preferences">{{ t('settings.preferences') }}</h2>
<div class="preferences-settings-wrapper"> <div class="preferences-settings-wrapper">
<fieldset <fieldset
style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;"> style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
@@ -367,7 +406,8 @@
</div> </div>
@if(enable_data_export) @if(enable_data_export)
<h2 id="sec-export">{{ t('settings.export_data_title') || 'Export Data' }}</h2> <!-- ═══════════════════════════════ EXPORT ═══════════════════════════════ -->
<h2 id="export">{{ t('settings.export_data_title') || 'Export Data' }}</h2>
<div class="export-settings-wrapper" style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;"> <div class="export-settings-wrapper" style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
<p>{{ t('settings.export_data_desc') || 'Download a copy of your data. This process happens entirely in your browser to protect your privacy and save server resources.' }}</p> <p>{{ t('settings.export_data_desc') || 'Download a copy of your data. This process happens entirely in your browser to protect your privacy and save server resources.' }}</p>
@@ -407,8 +447,8 @@
</button> </button>
</div> </div>
@endif @endif
<!-- ═══════════════════════════════ ACCOUNT ═══════════════════════════════ -->
<h2 id="sec-account">{{ t('settings.account') }}</h2> <h2 id="account">{{ t('settings.account') }}</h2>
<div class="account-settings-wrapper" <div class="account-settings-wrapper"
style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;"> style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
<table class="table account-info-table" <table class="table account-info-table"
@@ -422,17 +462,6 @@
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.username') }}</td> <td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.username') }}</td>
<td style="border: none;">{!! session.user !!}</td> <td style="border: none;">{!! session.user !!}</td>
</tr> </tr>
<tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.display_name') }}
</td>
<td style="border: none; display: flex; align-items: center; gap: 10px;">
<input type="text" id="display_name_input" class="input"
placeholder="{{ t('settings.display_name_placeholder') }}" value="{!! session.display_name || '' !!}"
maxlength="32" style="max-width: 200px; height: 30px; font-size: 0.9em;">
<button type="button" id="btn-update-display-name" class="button"
style="padding: 2px 10px; font-size: 0.8em; height: 30px;">{{ t('settings.save') }}</button>
</td>
</tr>
<tr> <tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.email') }}</td> <td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.email') }}</td>
<td id="display-email" style="border: none;">{{ email || t('settings.email_not_set') }}</td> <td id="display-email" style="border: none;">{{ email || t('settings.email_not_set') }}</td>
@@ -443,7 +472,6 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div id="display-name-status" class="avatar-status" style="margin-bottom: 20px;"></div>
<div class="account-actions-grid" <div class="account-actions-grid"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px;"> style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px;">
@@ -479,7 +507,8 @@
</div> </div>
</div> </div>
@if(matrix_enabled) @if(matrix_enabled)
<h2 id="sec-linked">{{ t('settings.linked_accounts') }}</h2> <!-- ═══════════════════════════════ LINKED ACCOUNTS ═══════════════════════════════ -->
<h2 id="linked">{{ t('settings.linked_accounts') }}</h2>
<div class="linked-accounts-wrapper"> <div class="linked-accounts-wrapper">
<p>{{ t('settings.matrix_link_desc') }}</p> <p>{{ t('settings.matrix_link_desc') }}</p>
@@ -510,7 +539,8 @@
@endif @endif
@if(enable_user_api_keys) @if(enable_user_api_keys)
<h2 id="sec-apikey">Upload API Key</h2> <!-- ═══════════════════════════════ API KEY ═══════════════════════════════ -->
<h2 id="apikey">Upload API Key</h2>
<div id="api-key-section" class="account-settings-wrapper" <div id="api-key-section" class="account-settings-wrapper"
style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;"> style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
<p style="color: var(--text-muted); margin-bottom: 15px;"> <p style="color: var(--text-muted); margin-bottom: 15px;">
@@ -544,7 +574,8 @@
@endif @endif
@if(enable_user_invites) @if(enable_user_invites)
<h2 id="sec-invites">{{ t('invites.section_title') }}</h2> <!-- ═══════════════════════════════ INVITES ═══════════════════════════════ -->
<h2 id="invites">{{ t('invites.section_title') }}</h2>
<div id="invite-section" class="account-settings-wrapper" <div id="invite-section" class="account-settings-wrapper"
style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;"> style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">