Apply ten iterative posts UI improvements
This commit is contained in:
179
web/app.js
179
web/app.js
@@ -370,6 +370,7 @@ const pendingAutoOpenOverlay = document.getElementById('pendingAutoOpenOverlay')
|
|||||||
const pendingAutoOpenOverlayPanel = document.getElementById('pendingAutoOpenOverlayPanel');
|
const pendingAutoOpenOverlayPanel = document.getElementById('pendingAutoOpenOverlayPanel');
|
||||||
const pendingAutoOpenCountdown = document.getElementById('pendingAutoOpenCountdown');
|
const pendingAutoOpenCountdown = document.getElementById('pendingAutoOpenCountdown');
|
||||||
const pendingBulkStatus = document.getElementById('pendingBulkStatus');
|
const pendingBulkStatus = document.getElementById('pendingBulkStatus');
|
||||||
|
const postsScrollTopBtn = document.getElementById('postsScrollTopBtn');
|
||||||
|
|
||||||
const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
|
const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
|
||||||
const SORT_SETTINGS_KEY = 'trackerSortSettings';
|
const SORT_SETTINGS_KEY = 'trackerSortSettings';
|
||||||
@@ -384,12 +385,24 @@ const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen'];
|
|||||||
const DEFAULT_BOOKMARK_LABEL = 'Gewinnspiel / gewinnen / verlosen';
|
const DEFAULT_BOOKMARK_LABEL = 'Gewinnspiel / gewinnen / verlosen';
|
||||||
const BOOKMARK_PREFS_KEY = 'trackerBookmarkPreferences';
|
const BOOKMARK_PREFS_KEY = 'trackerBookmarkPreferences';
|
||||||
const INCLUDE_EXPIRED_STORAGE_KEY = 'trackerIncludeExpired';
|
const INCLUDE_EXPIRED_STORAGE_KEY = 'trackerIncludeExpired';
|
||||||
|
const POSTS_SEARCH_STORAGE_KEY = 'trackerPostsSearchTerm';
|
||||||
const PENDING_BULK_COUNT_STORAGE_KEY = 'trackerPendingBulkCount';
|
const PENDING_BULK_COUNT_STORAGE_KEY = 'trackerPendingBulkCount';
|
||||||
const PENDING_AUTO_OPEN_STORAGE_KEY = 'trackerPendingAutoOpen';
|
const PENDING_AUTO_OPEN_STORAGE_KEY = 'trackerPendingAutoOpen';
|
||||||
const DEFAULT_PENDING_BULK_COUNT = 5;
|
const DEFAULT_PENDING_BULK_COUNT = 5;
|
||||||
const PENDING_AUTO_OPEN_DELAY_MS = 1500;
|
const PENDING_AUTO_OPEN_DELAY_MS = 1500;
|
||||||
const PENDING_OPEN_COOLDOWN_STORAGE_KEY = 'trackerPendingOpenCooldown';
|
const PENDING_OPEN_COOLDOWN_STORAGE_KEY = 'trackerPendingOpenCooldown';
|
||||||
const PENDING_OPEN_COOLDOWN_MS = 40 * 60 * 1000;
|
const PENDING_OPEN_COOLDOWN_MS = 40 * 60 * 1000;
|
||||||
|
const TAB_LABELS = {
|
||||||
|
all: 'Alle Beiträge',
|
||||||
|
pending: 'Offene Beiträge'
|
||||||
|
};
|
||||||
|
const SORT_MODE_LABELS = {
|
||||||
|
created: 'Erstelldatum',
|
||||||
|
deadline: 'Deadline',
|
||||||
|
lastCheck: 'Letzte Teilnahme',
|
||||||
|
lastChange: 'Letzte Änderung',
|
||||||
|
smart: 'Smart (Dringlichkeit)'
|
||||||
|
};
|
||||||
|
|
||||||
function loadIncludeExpiredPreference() {
|
function loadIncludeExpiredPreference() {
|
||||||
try {
|
try {
|
||||||
@@ -527,6 +540,31 @@ function persistPendingAutoOpenEnabled(enabled) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadPostsSearchTerm() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(POSTS_SEARCH_STORAGE_KEY);
|
||||||
|
if (typeof stored === 'string') {
|
||||||
|
return stored.slice(0, 240);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Konnte Suchbegriff der Posts-Ansicht nicht laden:', error);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistPostsSearchTerm(value) {
|
||||||
|
try {
|
||||||
|
const normalized = typeof value === 'string' ? value : '';
|
||||||
|
if (!normalized) {
|
||||||
|
localStorage.removeItem(POSTS_SEARCH_STORAGE_KEY);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem(POSTS_SEARCH_STORAGE_KEY, normalized.slice(0, 240));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Konnte Suchbegriff der Posts-Ansicht nicht speichern:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateIncludeExpiredToggleUI() {
|
function updateIncludeExpiredToggleUI() {
|
||||||
if (!includeExpiredToggle) {
|
if (!includeExpiredToggle) {
|
||||||
return;
|
return;
|
||||||
@@ -535,6 +573,7 @@ function updateIncludeExpiredToggleUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
includeExpiredPosts = loadIncludeExpiredPreference();
|
includeExpiredPosts = loadIncludeExpiredPreference();
|
||||||
|
let postsSearchTerm = loadPostsSearchTerm();
|
||||||
let pendingBulkCount = loadPendingBulkCount();
|
let pendingBulkCount = loadPendingBulkCount();
|
||||||
let pendingAutoOpenEnabled = loadPendingAutoOpenEnabled();
|
let pendingAutoOpenEnabled = loadPendingAutoOpenEnabled();
|
||||||
let pendingAutoOpenTriggered = false;
|
let pendingAutoOpenTriggered = false;
|
||||||
@@ -764,6 +803,10 @@ const tabFilteredCounts = {
|
|||||||
pending: 0,
|
pending: 0,
|
||||||
all: 0
|
all: 0
|
||||||
};
|
};
|
||||||
|
const tabDisplayCounts = {
|
||||||
|
pending: 0,
|
||||||
|
all: 0
|
||||||
|
};
|
||||||
let loadMoreObserver = null;
|
let loadMoreObserver = null;
|
||||||
let observedLoadMoreElement = null;
|
let observedLoadMoreElement = null;
|
||||||
|
|
||||||
@@ -2019,7 +2062,28 @@ function getTabButtons() {
|
|||||||
return Array.from(document.querySelectorAll('.tab-btn[data-tab]'));
|
return Array.from(document.querySelectorAll('.tab-btn[data-tab]'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateTabCountLabels(counts = {}) {
|
||||||
|
getTabButtons().forEach((button) => {
|
||||||
|
const tabKey = button.dataset.tab === 'all' ? 'all' : 'pending';
|
||||||
|
const fallbackLabel = TAB_LABELS[tabKey] || button.textContent.trim();
|
||||||
|
if (!button.dataset.baseLabel) {
|
||||||
|
button.dataset.baseLabel = fallbackLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
const incomingCount = counts[tabKey];
|
||||||
|
if (Number.isFinite(incomingCount)) {
|
||||||
|
tabDisplayCounts[tabKey] = Math.max(0, incomingCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = tabDisplayCounts[tabKey];
|
||||||
|
const label = button.dataset.baseLabel || fallbackLabel;
|
||||||
|
button.textContent = `${label} (${count})`;
|
||||||
|
button.setAttribute('aria-label', `${label} (${count})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function updateTabButtons() {
|
function updateTabButtons() {
|
||||||
|
updateTabCountLabels();
|
||||||
getTabButtons().forEach((button) => {
|
getTabButtons().forEach((button) => {
|
||||||
const isActive = button.dataset.tab === currentTab;
|
const isActive = button.dataset.tab === currentTab;
|
||||||
button.classList.toggle('active', isActive);
|
button.classList.toggle('active', isActive);
|
||||||
@@ -2061,7 +2125,7 @@ function handleTabKeydown(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTab(nextTab.dataset.tab, { updateUrl: true });
|
setTab(nextTab.dataset.tab, { updateUrl: true, announceChange: true });
|
||||||
nextTab.focus();
|
nextTab.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2072,6 +2136,15 @@ function updateSearchClearButtonVisibility() {
|
|||||||
const hasValue = typeof searchInput.value === 'string' && searchInput.value.trim().length > 0;
|
const hasValue = typeof searchInput.value === 'string' && searchInput.value.trim().length > 0;
|
||||||
searchClearBtn.hidden = !hasValue;
|
searchClearBtn.hidden = !hasValue;
|
||||||
searchClearBtn.disabled = !hasValue;
|
searchClearBtn.disabled = !hasValue;
|
||||||
|
searchClearBtn.setAttribute('aria-hidden', hasValue ? 'false' : 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSortModeLabel(mode = sortMode) {
|
||||||
|
return SORT_MODE_LABELS[mode] || SORT_MODE_LABELS.created;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSortDirectionLabel(direction = sortDirection) {
|
||||||
|
return direction === 'asc' ? 'aufsteigend' : 'absteigend';
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPostsViewActive() {
|
function isPostsViewActive() {
|
||||||
@@ -2079,6 +2152,27 @@ function isPostsViewActive() {
|
|||||||
return Boolean(postsView && postsView.classList.contains('app-view--active'));
|
return Boolean(postsView && postsView.classList.contains('app-view--active'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prefersReducedMotion() {
|
||||||
|
try {
|
||||||
|
return Boolean(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
||||||
|
} catch (_error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScrollBehavior() {
|
||||||
|
return prefersReducedMotion() ? 'auto' : 'smooth';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePostsScrollTopButtonVisibility() {
|
||||||
|
if (!postsScrollTopBtn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shouldShow = isPostsViewActive() && window.scrollY > 520;
|
||||||
|
postsScrollTopBtn.hidden = !shouldShow;
|
||||||
|
postsScrollTopBtn.setAttribute('aria-hidden', shouldShow ? 'false' : 'true');
|
||||||
|
}
|
||||||
|
|
||||||
function updateTabInUrl() {
|
function updateTabInUrl() {
|
||||||
if (!isPostsViewActive()) {
|
if (!isPostsViewActive()) {
|
||||||
return;
|
return;
|
||||||
@@ -2142,7 +2236,10 @@ function getPostListState() {
|
|||||||
|
|
||||||
const tabTotalCount = filteredItems.length;
|
const tabTotalCount = filteredItems.length;
|
||||||
|
|
||||||
const searchValue = searchInput && typeof searchInput.value === 'string' ? searchInput.value.trim() : '';
|
const searchValueRaw = searchInput && typeof searchInput.value === 'string'
|
||||||
|
? searchInput.value
|
||||||
|
: postsSearchTerm;
|
||||||
|
const searchValue = typeof searchValueRaw === 'string' ? searchValueRaw.trim() : '';
|
||||||
const searchActive = Boolean(searchValue);
|
const searchActive = Boolean(searchValue);
|
||||||
|
|
||||||
if (searchActive) {
|
if (searchActive) {
|
||||||
@@ -2349,9 +2446,9 @@ function cleanupLoadMoreObserver() {
|
|||||||
|
|
||||||
function getTabDisplayLabel(tab = currentTab) {
|
function getTabDisplayLabel(tab = currentTab) {
|
||||||
if (tab === 'all') {
|
if (tab === 'all') {
|
||||||
return 'Alle Beiträge';
|
return TAB_LABELS.all;
|
||||||
}
|
}
|
||||||
return 'Offene Beiträge';
|
return TAB_LABELS.pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPostsSummary({
|
function buildPostsSummary({
|
||||||
@@ -2372,6 +2469,7 @@ function buildPostsSummary({
|
|||||||
|
|
||||||
segments.push(`<span class="posts-summary__item">Tab gesamt: <strong>${tabTotalCount}</strong></span>`);
|
segments.push(`<span class="posts-summary__item">Tab gesamt: <strong>${tabTotalCount}</strong></span>`);
|
||||||
segments.push(`<span class="posts-summary__item">Alle Beiträge: <strong>${totalCountAll}</strong></span>`);
|
segments.push(`<span class="posts-summary__item">Alle Beiträge: <strong>${totalCountAll}</strong></span>`);
|
||||||
|
segments.push(`<span class="posts-summary__item">Sortierung: <strong>${getSortModeLabel()} · ${getSortDirectionLabel()}</strong></span>`);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="posts-summary" role="status" aria-live="polite">
|
<div class="posts-summary" role="status" aria-live="polite">
|
||||||
@@ -2437,7 +2535,8 @@ function loadMorePosts(tab = currentTab, { triggeredByScroll = false } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTab(tab, { updateUrl = true } = {}) {
|
function setTab(tab, { updateUrl = true, announceChange = false } = {}) {
|
||||||
|
const previousTab = currentTab;
|
||||||
if (tab === 'all') {
|
if (tab === 'all') {
|
||||||
currentTab = 'all';
|
currentTab = 'all';
|
||||||
} else {
|
} else {
|
||||||
@@ -2455,6 +2554,9 @@ function setTab(tab, { updateUrl = true } = {}) {
|
|||||||
}
|
}
|
||||||
renderPosts();
|
renderPosts();
|
||||||
maybeAutoOpenPending('tab');
|
maybeAutoOpenPending('tab');
|
||||||
|
if (announceChange && previousTab !== currentTab) {
|
||||||
|
showToast(`Tab: ${getTabDisplayLabel(currentTab)}`, 'info');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeTabFromUrl() {
|
function initializeTabFromUrl() {
|
||||||
@@ -3768,7 +3870,7 @@ if (pendingAutoOpenToggle) {
|
|||||||
// Tab switching
|
// Tab switching
|
||||||
getTabButtons().forEach((btn) => {
|
getTabButtons().forEach((btn) => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
setTab(btn.dataset.tab, { updateUrl: true });
|
setTab(btn.dataset.tab, { updateUrl: true, announceChange: true });
|
||||||
});
|
});
|
||||||
btn.addEventListener('keydown', handleTabKeydown);
|
btn.addEventListener('keydown', handleTabKeydown);
|
||||||
});
|
});
|
||||||
@@ -3777,6 +3879,7 @@ window.addEventListener('app:view-change', (event) => {
|
|||||||
if (event && event.detail && event.detail.view === 'posts') {
|
if (event && event.detail && event.detail.view === 'posts') {
|
||||||
updateTabInUrl();
|
updateTabInUrl();
|
||||||
}
|
}
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (manualPostForm) {
|
if (manualPostForm) {
|
||||||
@@ -3832,12 +3935,35 @@ if (manualRefreshBtn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchInput) {
|
if (searchInput) {
|
||||||
|
if (postsSearchTerm) {
|
||||||
|
searchInput.value = postsSearchTerm;
|
||||||
|
}
|
||||||
|
|
||||||
searchInput.addEventListener('input', () => {
|
searchInput.addEventListener('input', () => {
|
||||||
|
postsSearchTerm = searchInput.value || '';
|
||||||
|
persistPostsSearchTerm(postsSearchTerm);
|
||||||
updateSearchClearButtonVisibility();
|
updateSearchClearButtonVisibility();
|
||||||
resetVisibleCount();
|
resetVisibleCount();
|
||||||
renderPosts();
|
renderPosts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('keydown', (event) => {
|
||||||
|
if (!event || event.key !== 'Escape') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof searchInput.value === 'string' && searchInput.value.length > 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
searchInput.value = '';
|
||||||
|
postsSearchTerm = '';
|
||||||
|
persistPostsSearchTerm(postsSearchTerm);
|
||||||
|
updateSearchClearButtonVisibility();
|
||||||
|
resetVisibleCount();
|
||||||
|
renderPosts();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
searchInput.blur();
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
if (!event || event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey) {
|
if (!event || event.key !== '/' || event.metaKey || event.ctrlKey || event.altKey) {
|
||||||
return;
|
return;
|
||||||
@@ -3860,6 +3986,8 @@ if (searchClearBtn) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
searchInput.value = '';
|
searchInput.value = '';
|
||||||
|
postsSearchTerm = '';
|
||||||
|
persistPostsSearchTerm(postsSearchTerm);
|
||||||
updateSearchClearButtonVisibility();
|
updateSearchClearButtonVisibility();
|
||||||
resetVisibleCount();
|
resetVisibleCount();
|
||||||
renderPosts();
|
renderPosts();
|
||||||
@@ -3869,6 +3997,16 @@ if (searchClearBtn) {
|
|||||||
|
|
||||||
updateSearchClearButtonVisibility();
|
updateSearchClearButtonVisibility();
|
||||||
|
|
||||||
|
if (postsScrollTopBtn) {
|
||||||
|
postsScrollTopBtn.addEventListener('click', () => {
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: getScrollBehavior()
|
||||||
|
});
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (mergeModeToggle) {
|
if (mergeModeToggle) {
|
||||||
mergeModeToggle.addEventListener('click', () => {
|
mergeModeToggle.addEventListener('click', () => {
|
||||||
if (currentTab !== 'all') {
|
if (currentTab !== 'all') {
|
||||||
@@ -4087,7 +4225,7 @@ function highlightPostCard(post) {
|
|||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
try {
|
try {
|
||||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
card.scrollIntoView({ behavior: getScrollBehavior(), block: 'center' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Konnte Karte nicht scrollen:', error);
|
console.warn('Konnte Karte nicht scrollen:', error);
|
||||||
}
|
}
|
||||||
@@ -4220,12 +4358,14 @@ function renderPosts() {
|
|||||||
if (container) {
|
if (container) {
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hideError();
|
hideError();
|
||||||
|
|
||||||
const container = document.getElementById('postsContainer');
|
const container = document.getElementById('postsContainer');
|
||||||
if (!container) {
|
if (!container) {
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4242,6 +4382,22 @@ function renderPosts() {
|
|||||||
searchActive
|
searchActive
|
||||||
} = getPostListState();
|
} = getPostListState();
|
||||||
|
|
||||||
|
const tabCounts = sortedItems.reduce((acc, item) => {
|
||||||
|
if (!item || !item.status) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
const status = item.status;
|
||||||
|
if (!status.isExpired && status.canCurrentProfileCheck && !status.isComplete) {
|
||||||
|
acc.pending += 1;
|
||||||
|
}
|
||||||
|
if (includeExpiredPosts || (!status.isExpired && !status.isComplete)) {
|
||||||
|
acc.all += 1;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, { pending: 0, all: 0 });
|
||||||
|
updateTabCountLabels(tabCounts);
|
||||||
|
updateTabButtons();
|
||||||
|
|
||||||
let filteredItems = filteredItemsResult;
|
let filteredItems = filteredItemsResult;
|
||||||
const focusCandidateEntry = (!focusHandled && (focusPostIdParam || focusNormalizedUrl))
|
const focusCandidateEntry = (!focusHandled && (focusPostIdParam || focusNormalizedUrl))
|
||||||
? sortedItems.find((item) => doesPostMatchFocus(item.post))
|
? sortedItems.find((item) => doesPostMatchFocus(item.post))
|
||||||
@@ -4326,6 +4482,7 @@ function renderPosts() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4365,6 +4522,7 @@ function renderPosts() {
|
|||||||
if (!focusHandled && focusTargetInfo.index !== -1 && focusTargetInfo.post) {
|
if (!focusHandled && focusTargetInfo.index !== -1 && focusTargetInfo.post) {
|
||||||
requestAnimationFrame(() => highlightPostCard(focusTargetInfo.post));
|
requestAnimationFrame(() => highlightPostCard(focusTargetInfo.post));
|
||||||
}
|
}
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachPostEventHandlers(post, status) {
|
function attachPostEventHandlers(post, status) {
|
||||||
@@ -5337,8 +5495,13 @@ window.addEventListener('resize', () => {
|
|||||||
if (screenshotModal && screenshotModal.classList.contains('open') && !screenshotModalZoomed) {
|
if (screenshotModal && screenshotModal.classList.contains('open') && !screenshotModalZoomed) {
|
||||||
applyScreenshotModalSize();
|
applyScreenshotModalSize();
|
||||||
}
|
}
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('scroll', () => {
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
window.addEventListener('app:view-change', (event) => {
|
window.addEventListener('app:view-change', (event) => {
|
||||||
const view = event && event.detail ? event.detail.view : null;
|
const view = event && event.detail ? event.detail.view : null;
|
||||||
if (view === 'posts') {
|
if (view === 'posts') {
|
||||||
@@ -5346,6 +5509,7 @@ window.addEventListener('app:view-change', (event) => {
|
|||||||
} else {
|
} else {
|
||||||
cancelPendingAutoOpen(false);
|
cancelPendingAutoOpen(false);
|
||||||
}
|
}
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
@@ -5381,6 +5545,7 @@ async function bootstrapApp() {
|
|||||||
checkAutoCheck();
|
checkAutoCheck();
|
||||||
startUpdatesStream();
|
startUpdatesStream();
|
||||||
applyAutoRefreshSettings();
|
applyAutoRefreshSettings();
|
||||||
|
updatePostsScrollTopButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrapApp();
|
bootstrapApp();
|
||||||
|
|||||||
@@ -232,9 +232,10 @@
|
|||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="searchInput" class="search-field__label">Suche</label>
|
<label for="searchInput" class="search-field__label">Suche</label>
|
||||||
<div class="search-field__input-wrap">
|
<div class="search-field__input-wrap">
|
||||||
<input type="search" id="searchInput" class="search-input" placeholder="Beiträge durchsuchen..." autocomplete="off" enterkeyhint="search">
|
<input type="search" id="searchInput" class="search-input" placeholder="Beiträge durchsuchen..." autocomplete="off" enterkeyhint="search" aria-describedby="searchInputHint">
|
||||||
<button type="button" id="searchClearBtn" class="search-clear-btn" aria-label="Suche leeren" title="Suche leeren" hidden>×</button>
|
<button type="button" id="searchClearBtn" class="search-clear-btn" aria-label="Suche leeren" title="Suche leeren" hidden>×</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p id="searchInputHint" class="search-field__hint">Tipp: <kbd>/</kbd> fokussiert die Suche, <kbd>Esc</kbd> leert sie.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="posts-bulk-controls" id="pendingBulkControls" hidden>
|
<div class="posts-bulk-controls" id="pendingBulkControls" hidden>
|
||||||
@@ -275,6 +276,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="postsContainer" class="posts-container"></div>
|
<div id="postsContainer" class="posts-container"></div>
|
||||||
|
<button type="button" id="postsScrollTopBtn" class="posts-scroll-top" aria-label="Nach oben scrollen" title="Nach oben" hidden>↑</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="screenshotModal" class="screenshot-modal" hidden>
|
<div id="screenshotModal" class="screenshot-modal" hidden>
|
||||||
|
|||||||
@@ -4,6 +4,22 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.tab-btn,
|
||||||
|
.search-clear-btn,
|
||||||
|
.posts-scroll-top,
|
||||||
|
.post-card,
|
||||||
|
.auto-open-overlay,
|
||||||
|
.auto-open-overlay__panel {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card--highlight {
|
||||||
|
animation: none;
|
||||||
|
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--content-max-width: 1300px;
|
--content-max-width: 1300px;
|
||||||
--top-gap: 12px;
|
--top-gap: 12px;
|
||||||
@@ -302,6 +318,27 @@ header {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-field__hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field__hint kbd {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 18px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #0f172a;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.search-clear-btn {
|
.search-clear-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 6px;
|
right: 6px;
|
||||||
@@ -806,6 +843,34 @@ h1 {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.posts-scroll-top {
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.28);
|
||||||
|
z-index: 45;
|
||||||
|
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-scroll-top:hover {
|
||||||
|
background: #1d4ed8;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-scroll-top:focus-visible {
|
||||||
|
outline: 2px solid #1d4ed8;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.post-card {
|
.post-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: white;
|
background: white;
|
||||||
@@ -2260,6 +2325,10 @@ h1 {
|
|||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-field__hint {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.bulk-actions {
|
.bulk-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@@ -2455,7 +2524,15 @@ h1 {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-filter-toggle {
|
||||||
|
order: 2;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.search-field {
|
.search-field {
|
||||||
|
order: 1;
|
||||||
|
width: 100%;
|
||||||
flex: 1 1 260px;
|
flex: 1 1 260px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2472,6 +2549,12 @@ h1 {
|
|||||||
|
|
||||||
.bulk-status {
|
.bulk-status {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-scroll-top {
|
||||||
|
right: 14px;
|
||||||
|
bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
||||||
|
|||||||
Reference in New Issue
Block a user