Improve posts UI in three iterative passes

This commit is contained in:
2026-02-24 23:02:36 +01:00
parent 2c54c96cc7
commit ad6e156268
3 changed files with 309 additions and 22 deletions

View File

@@ -357,6 +357,8 @@ const bookmarkSortSelect = document.getElementById('bookmarkSortSelect');
const bookmarkSortDirectionToggle = document.getElementById('bookmarkSortDirectionToggle');
const profileSelectElement = document.getElementById('profileSelect');
const includeExpiredToggle = document.getElementById('includeExpiredToggle');
const searchInput = document.getElementById('searchInput');
const searchClearBtn = document.getElementById('searchClearBtn');
const mergeControls = document.getElementById('mergeControls');
const mergeModeToggle = document.getElementById('mergeModeToggle');
const mergeSubmitBtn = document.getElementById('mergeSubmitBtn');
@@ -540,6 +542,8 @@ let pendingAutoOpenTimerId = null;
let pendingAutoOpenCountdownIntervalId = null;
let pendingProcessingBatch = false;
let pendingOpenCooldownMap = loadPendingOpenCooldownMap(currentProfile);
let pendingBulkStatusResetTimerId = null;
let lastPendingBulkHint = { openableCount: 0, cooldownBlocked: 0 };
function updateIncludeExpiredToggleVisibility() {
if (!includeExpiredToggle) {
@@ -583,27 +587,89 @@ function updateMergeControlsUI() {
}
}
function updatePendingBulkControls(filteredCount = 0) {
function clearPendingBulkStatusResetTimer() {
if (pendingBulkStatusResetTimerId) {
clearTimeout(pendingBulkStatusResetTimerId);
pendingBulkStatusResetTimerId = null;
}
}
function buildPendingBulkHintText({ openableCount = 0, cooldownBlocked = 0 } = {}) {
const openable = Math.max(0, openableCount || 0);
const blocked = Math.max(0, cooldownBlocked || 0);
if (blocked > 0) {
return `Öffnungsbereit: ${openable} · Cooldown: ${blocked}`;
}
return `Öffnungsbereit: ${openable}`;
}
function applyPendingBulkHintStatus() {
if (!pendingBulkStatus) {
return;
}
if (currentTab !== 'pending') {
pendingBulkStatus.textContent = '';
pendingBulkStatus.classList.remove('bulk-status--hint', 'bulk-status--error');
return;
}
pendingBulkStatus.textContent = buildPendingBulkHintText(lastPendingBulkHint);
pendingBulkStatus.classList.add('bulk-status--hint');
pendingBulkStatus.classList.remove('bulk-status--error');
}
function updatePendingBulkControls(filteredCount = 0, stats = null) {
if (!pendingBulkControls) {
return;
}
const isPendingTab = currentTab === 'pending';
pendingBulkControls.hidden = !isPendingTab;
pendingBulkControls.style.display = isPendingTab ? 'flex' : 'none';
if (!isPendingTab) {
clearPendingBulkStatusResetTimer();
if (pendingBulkStatus) {
pendingBulkStatus.textContent = '';
pendingBulkStatus.classList.remove('bulk-status--hint', 'bulk-status--error');
}
}
const normalizedStats = stats && typeof stats === 'object'
? {
openableCount: Math.max(0, parseInt(stats.openableCount, 10) || 0),
cooldownBlocked: Math.max(0, parseInt(stats.cooldownBlocked, 10) || 0)
}
: { openableCount: Math.max(0, filteredCount || 0), cooldownBlocked: 0 };
lastPendingBulkHint = normalizedStats;
if (pendingBulkOpenBtn) {
pendingBulkOpenBtn.disabled = !isPendingTab || pendingProcessingBatch || filteredCount === 0;
pendingBulkOpenBtn.disabled = !isPendingTab || pendingProcessingBatch || normalizedStats.openableCount === 0;
}
if (isPendingTab) {
applyPendingBulkHintStatus();
}
}
function setPendingBulkStatus(message = '', isError = false) {
clearPendingBulkStatusResetTimer();
if (!pendingBulkStatus) {
if (message) {
showToast(message, isError ? 'error' : 'info');
}
return;
}
pendingBulkStatus.textContent = '';
pendingBulkStatus.classList.remove('bulk-status--error');
if (message) {
showToast(message, isError ? 'error' : 'info');
if (!message) {
applyPendingBulkHintStatus();
return;
}
pendingBulkStatus.textContent = message;
pendingBulkStatus.classList.toggle('bulk-status--error', !!isError);
pendingBulkStatus.classList.toggle('bulk-status--hint', !isError);
showToast(message, isError ? 'error' : 'info');
pendingBulkStatusResetTimerId = setTimeout(() => {
pendingBulkStatusResetTimerId = null;
applyPendingBulkHintStatus();
}, 4000);
}
function initializeFocusParams() {
@@ -1949,15 +2015,65 @@ function normalizeRequiredProfiles(post) {
return Array.from({ length: count }, (_, index) => index + 1);
}
function getTabButtons() {
return Array.from(document.querySelectorAll('.tab-btn[data-tab]'));
}
function updateTabButtons() {
document.querySelectorAll('.tab-btn').forEach((button) => {
if (!button.dataset.tab) {
return;
}
button.classList.toggle('active', button.dataset.tab === currentTab);
getTabButtons().forEach((button) => {
const isActive = button.dataset.tab === currentTab;
button.classList.toggle('active', isActive);
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
button.setAttribute('tabindex', isActive ? '0' : '-1');
});
}
function handleTabKeydown(event) {
if (!event || (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight' && event.key !== 'Home' && event.key !== 'End')) {
return;
}
const tabs = getTabButtons();
if (!tabs.length) {
return;
}
const currentIndex = tabs.findIndex((tab) => tab === event.currentTarget);
if (currentIndex === -1) {
return;
}
event.preventDefault();
let nextIndex = currentIndex;
if (event.key === 'Home') {
nextIndex = 0;
} else if (event.key === 'End') {
nextIndex = tabs.length - 1;
} else if (event.key === 'ArrowRight') {
nextIndex = (currentIndex + 1) % tabs.length;
} else if (event.key === 'ArrowLeft') {
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
}
const nextTab = tabs[nextIndex];
if (!nextTab || !nextTab.dataset.tab) {
return;
}
setTab(nextTab.dataset.tab, { updateUrl: true });
nextTab.focus();
}
function updateSearchClearButtonVisibility() {
if (!searchInput || !searchClearBtn) {
return;
}
const hasValue = typeof searchInput.value === 'string' && searchInput.value.trim().length > 0;
searchClearBtn.hidden = !hasValue;
searchClearBtn.disabled = !hasValue;
}
function isPostsViewActive() {
const postsView = document.querySelector('[data-view="posts"]');
return Boolean(postsView && postsView.classList.contains('app-view--active'));
@@ -2026,7 +2142,6 @@ function getPostListState() {
const tabTotalCount = filteredItems.length;
const searchInput = document.getElementById('searchInput');
const searchValue = searchInput && typeof searchInput.value === 'string' ? searchInput.value.trim() : '';
const searchActive = Boolean(searchValue);
@@ -3651,10 +3766,11 @@ if (pendingAutoOpenToggle) {
}
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
getTabButtons().forEach((btn) => {
btn.addEventListener('click', () => {
setTab(btn.dataset.tab, { updateUrl: true });
});
btn.addEventListener('keydown', handleTabKeydown);
});
window.addEventListener('app:view-change', (event) => {
@@ -3715,14 +3831,44 @@ if (manualRefreshBtn) {
});
}
const searchInput = document.getElementById('searchInput');
if (searchInput) {
searchInput.addEventListener('input', () => {
updateSearchClearButtonVisibility();
resetVisibleCount();
renderPosts();
});
document.addEventListener('keydown', (event) => {
if (!event || event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey) {
return;
}
const target = event.target;
const tagName = target && target.tagName ? target.tagName.toLowerCase() : '';
const isEditable = !!(target && (target.isContentEditable || tagName === 'input' || tagName === 'textarea' || tagName === 'select'));
if (isEditable || !isPostsViewActive()) {
return;
}
event.preventDefault();
searchInput.focus();
searchInput.select();
});
}
if (searchClearBtn) {
searchClearBtn.addEventListener('click', () => {
if (!searchInput) {
return;
}
searchInput.value = '';
updateSearchClearButtonVisibility();
resetVisibleCount();
renderPosts();
searchInput.focus();
});
}
updateSearchClearButtonVisibility();
if (mergeModeToggle) {
mergeModeToggle.addEventListener('click', () => {
if (currentTab !== 'all') {
@@ -4129,7 +4275,22 @@ function renderPosts() {
}
updateFilteredCount(currentTab, filteredItems.length);
updatePendingBulkControls(filteredItems.length);
let pendingBulkStats = null;
if (currentTab === 'pending') {
pendingBulkStats = filteredItems.reduce((stats, item) => {
const post = item && item.post ? item.post : null;
if (!post || !post.url) {
return stats;
}
if (isPendingOpenCooldownActive(post.id)) {
stats.cooldownBlocked += 1;
} else {
stats.openableCount += 1;
}
return stats;
}, { openableCount: 0, cooldownBlocked: 0 });
}
updatePendingBulkControls(filteredItems.length, pendingBulkStats);
const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab));
const visibleItems = filteredItems.slice(0, visibleCount);

View File

@@ -214,9 +214,9 @@
</div>
<div class="tabs-section">
<div class="tabs">
<button class="tab-btn" data-tab="all">Alle Beiträge</button>
<button class="tab-btn active" data-tab="pending">Offene Beiträge</button>
<div class="tabs" role="tablist" aria-label="Beitragsansicht filtern">
<button class="tab-btn" id="postsTabAll" type="button" role="tab" aria-selected="false" aria-controls="postsContainer" tabindex="-1" data-tab="all">Alle Beiträge</button>
<button class="tab-btn active" id="postsTabPending" type="button" role="tab" aria-selected="true" aria-controls="postsContainer" tabindex="0" data-tab="pending">Offene Beiträge</button>
</div>
<div class="merge-controls" id="mergeControls" hidden>
<div class="merge-actions">
@@ -229,7 +229,13 @@
<input type="checkbox" id="includeExpiredToggle">
<span>Abgelaufene/abgeschlossene anzeigen</span>
</label>
<input type="text" id="searchInput" class="search-input" placeholder="Beiträge durchsuchen...">
<div class="search-field">
<label for="searchInput" class="search-field__label">Suche</label>
<div class="search-field__input-wrap">
<input type="search" id="searchInput" class="search-input" placeholder="Beiträge durchsuchen..." autocomplete="off" enterkeyhint="search">
<button type="button" id="searchClearBtn" class="search-clear-btn" aria-label="Suche leeren" title="Suche leeren" hidden>×</button>
</div>
</div>
</div>
<div class="posts-bulk-controls" id="pendingBulkControls" hidden>
<div class="bulk-actions">

View File

@@ -269,11 +269,12 @@ header {
}
.search-input {
padding: 6px 12px;
padding: 8px 38px 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 13px;
min-width: 200px;
min-width: 0;
width: 100%;
}
.search-input:focus {
@@ -282,6 +283,50 @@ header {
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.search-field {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 240px;
}
.search-field__label {
font-size: 12px;
font-weight: 700;
color: #475569;
}
.search-field__input-wrap {
position: relative;
display: flex;
align-items: center;
}
.search-clear-btn {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
width: 26px;
height: 26px;
border: none;
border-radius: 6px;
background: transparent;
color: #6b7280;
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.search-clear-btn:hover {
background: #e5e7eb;
color: #1f2937;
}
.search-clear-btn:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 1px;
}
.switch {
display: inline-flex;
align-items: center;
@@ -530,6 +575,11 @@ h1 {
background: #f8f9fa;
}
.tab-btn:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 2px;
}
.tab-btn.active {
background: #1877f2;
color: white;
@@ -637,10 +687,27 @@ h1 {
.bulk-status {
font-size: 13px;
color: #6b7280;
min-height: 32px;
display: inline-flex;
align-items: center;
}
.bulk-status--hint {
padding: 6px 12px;
border-radius: 999px;
border: 1px solid #c7d2fe;
background: #eef2ff;
color: #1e3a8a;
font-weight: 600;
}
.bulk-status--error {
color: #dc2626;
color: #991b1b;
border: 1px solid #fecaca;
background: #fee2e2;
border-radius: 999px;
padding: 6px 12px;
font-weight: 600;
}
.auto-open-overlay {
@@ -2189,6 +2256,19 @@ h1 {
max-height: 75vh;
}
.search-field {
min-width: 100%;
}
.bulk-actions {
flex-direction: column;
align-items: stretch;
}
.bulk-actions .btn {
width: 100%;
}
.bookmark-section__list {
gap: 5px;
}
@@ -2354,6 +2434,46 @@ h1 {
align-items: flex-start;
}
.tabs-section {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.tabs {
width: 100%;
}
.tab-btn {
flex: 1 1 0;
text-align: center;
}
.search-container {
margin-left: 0;
width: 100%;
justify-content: flex-start;
}
.search-field {
flex: 1 1 260px;
}
.posts-bulk-controls {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.bulk-actions {
width: 100%;
justify-content: flex-start;
}
.bulk-status {
width: 100%;
}
.form-actions {
flex-direction: column;
}