aboutsummaryrefslogtreecommitdiffstats
path: root/frontend-vanilla
diff options
context:
space:
mode:
Diffstat (limited to 'frontend-vanilla')
-rw-r--r--frontend-vanilla/public/themes/codex.css15
-rw-r--r--frontend-vanilla/public/themes/refined.css12
-rw-r--r--frontend-vanilla/public/themes/sakura.css19
-rw-r--r--frontend-vanilla/public/themes/terminal.css26
-rw-r--r--frontend-vanilla/src/main.ts39
-rw-r--r--frontend-vanilla/src/style.css12
6 files changed, 87 insertions, 36 deletions
diff --git a/frontend-vanilla/public/themes/codex.css b/frontend-vanilla/public/themes/codex.css
index 50942e6..fcf4e33 100644
--- a/frontend-vanilla/public/themes/codex.css
+++ b/frontend-vanilla/public/themes/codex.css
@@ -170,7 +170,7 @@ body {
padding: 2.5rem 2rem;
}
-.main-content > * {
+.main-content>* {
max-width: 33em;
}
@@ -182,10 +182,19 @@ body {
border-radius: 0;
padding-bottom: 2.5rem;
border-bottom: none;
+ transition: opacity 0.4s ease;
+}
+
+.feed-item.read {
+ opacity: 0.5;
+}
+
+.feed-item.read .item-title {
+ color: var(--codex-muted);
}
/* Decorative separator between items -- a subtle fleuron */
-.feed-item + .feed-item::before {
+.feed-item+.feed-item::before {
content: '\2766';
display: block;
text-align: center;
@@ -451,4 +460,4 @@ select:focus {
.theme-dark .sidebar-backdrop {
background: rgba(28, 26, 23, 0.6);
-}
+} \ No newline at end of file
diff --git a/frontend-vanilla/public/themes/refined.css b/frontend-vanilla/public/themes/refined.css
index fab2b96..191a5a2 100644
--- a/frontend-vanilla/public/themes/refined.css
+++ b/frontend-vanilla/public/themes/refined.css
@@ -86,6 +86,16 @@ body {
.feed-item {
padding: var(--spacing-md) var(--spacing-sm);
margin-top: var(--spacing-xl);
+ transition: opacity 0.3s ease;
+}
+
+.feed-item.read {
+ opacity: 0.45;
+}
+
+.feed-item.read .item-title {
+ font-weight: 500;
+ opacity: 0.8;
}
.item-title {
@@ -269,4 +279,4 @@ select:focus {
padding: 2px 8px;
border-radius: 4px;
letter-spacing: 0.04em;
-}
+} \ No newline at end of file
diff --git a/frontend-vanilla/public/themes/sakura.css b/frontend-vanilla/public/themes/sakura.css
index 48a1c0a..948e22e 100644
--- a/frontend-vanilla/public/themes/sakura.css
+++ b/frontend-vanilla/public/themes/sakura.css
@@ -184,7 +184,7 @@ body {
padding: 2.5rem 2rem;
}
-.main-content > * {
+.main-content>* {
max-width: 34em;
}
@@ -195,10 +195,20 @@ body {
margin-top: 2.5rem;
border-radius: 0;
border-bottom: none;
+ transition: opacity 0.4s ease;
+}
+
+.feed-item.read {
+ opacity: 0.4;
+}
+
+.feed-item.read .item-title {
+ color: var(--sakura-stone);
+ font-weight: 400;
}
/* Subtle separator -- a single thin rule, Japanese-style restraint */
-.feed-item + .feed-item {
+.feed-item+.feed-item {
border-top: 1px solid var(--sakura-shadow);
padding-top: 2.5rem;
}
@@ -462,6 +472,7 @@ select:focus {
}
/* ---- Loading/Empty ---- */
-.loading, .empty {
+.loading,
+.empty {
color: var(--sakura-stone);
-}
+} \ No newline at end of file
diff --git a/frontend-vanilla/public/themes/terminal.css b/frontend-vanilla/public/themes/terminal.css
index 48164c9..484d3ff 100644
--- a/frontend-vanilla/public/themes/terminal.css
+++ b/frontend-vanilla/public/themes/terminal.css
@@ -68,13 +68,11 @@ body {
height: 100%;
pointer-events: none;
z-index: 9999;
- background: repeating-linear-gradient(
- to bottom,
- transparent,
- transparent 2px,
- rgba(0, 0, 0, 0.03) 2px,
- rgba(0, 0, 0, 0.03) 4px
- );
+ background: repeating-linear-gradient(to bottom,
+ transparent,
+ transparent 2px,
+ rgba(0, 0, 0, 0.03) 2px,
+ rgba(0, 0, 0, 0.03) 4px);
will-change: transform;
}
}
@@ -217,6 +215,15 @@ body {
padding: 1.25rem 0.5rem;
margin-top: 1.5rem;
border-bottom: 1px solid var(--border-color);
+ transition: opacity 0.4s ease;
+}
+
+.feed-item.read {
+ opacity: 0.35;
+}
+
+.feed-item.read .item-title {
+ text-decoration: none;
}
.feed-item.selected {
@@ -482,8 +489,9 @@ select:focus {
}
/* ---- Loading/Empty States ---- */
-.loading, .empty {
+.loading,
+.empty {
color: var(--text-color);
opacity: 0.4;
font-family: inherit;
-}
+} \ No newline at end of file
diff --git a/frontend-vanilla/src/main.ts b/frontend-vanilla/src/main.ts
index 0a67dfe..b59d185 100644
--- a/frontend-vanilla/src/main.ts
+++ b/frontend-vanilla/src/main.ts
@@ -325,8 +325,8 @@ export function renderItems() {
const scrollRoot = document.getElementById('main-content');
if (scrollRoot) {
let readTimeoutId: number | null = null;
- scrollRoot.onscroll = () => {
- // Infinite scroll check (container only)
+ const scrollHandler = () => {
+ // Infinite scroll check
if (!store.loading && store.hasMore && scrollRoot.scrollHeight > scrollRoot.clientHeight) {
if (scrollRoot.scrollHeight - scrollRoot.scrollTop - scrollRoot.clientHeight < 200) {
loadMore();
@@ -336,40 +336,40 @@ export function renderItems() {
// Mark-as-read: debounced
if (readTimeoutId === null) {
readTimeoutId = window.setTimeout(() => {
- debugLog('onscroll trigger checkReadItems');
checkReadItems(scrollRoot);
readTimeoutId = null;
}, 250);
}
};
+
+ scrollRoot.onscroll = scrollHandler;
+ // Fallback for cases where main-content doesn't capture the scroll
+ window.onscroll = scrollHandler;
}
}
-function checkReadItems(scrollRoot: HTMLElement) {
- const containerRect = scrollRoot.getBoundingClientRect();
+function checkReadItems(scrollRoot?: HTMLElement) {
+ const root = scrollRoot || document.getElementById('main-content') || document.documentElement;
+ const containerRect = root.getBoundingClientRect();
debugLog('checkReadItems start', { containerTop: containerRect.top });
- // Batch DOM query: select all feed items at once instead of O(n) individual
- // querySelector calls with attribute selectors per scroll tick.
- const allItems = scrollRoot.querySelectorAll('.feed-item');
- for (const el of allItems) {
+ // Use faster DOM query for only unread items
+ const unreadItems = document.querySelectorAll('.feed-item.unread');
+ for (const el of unreadItems) {
const idAttr = el.getAttribute('data-id');
if (!idAttr) continue;
const id = parseInt(idAttr);
+
+ // Safety check: skip if store already says it's read (though unread class implies not)
const item = store.items.find(i => i._id === id);
- if (!item || item.read) continue;
+ if (item?.read) continue;
const rect = el.getBoundingClientRect();
- // Use a small buffer (5px) to be more robust
+ // Mark as read if the bottom of the item is above the top of the container (with 5px buffer)
const isPast = rect.bottom < (containerRect.top + 5);
if (DEBUG) {
- debugLog(`Item ${id} check`, {
- rectTop: rect.top,
- rectBottom: rect.bottom,
- containerTop: containerRect.top,
- isPast
- });
+ debugLog(`Item ${id} check`, { rectBottom: rect.bottom, containerTop: containerRect.top, isPast });
}
if (isPast) {
@@ -714,7 +714,10 @@ export async function updateItem(id: number | string, updates: Partial<Item>) {
// Selective DOM update to avoid full re-render
const el = document.querySelector(`.feed-item[data-id="${id}"]`);
if (el) {
- if (updates.read !== undefined) el.classList.toggle('read', updates.read);
+ if (updates.read !== undefined) {
+ el.classList.toggle('read', updates.read);
+ el.classList.toggle('unread', !updates.read);
+ }
if (updates.starred !== undefined) {
const starBtn = el.querySelector('.star-btn');
if (starBtn) {
diff --git a/frontend-vanilla/src/style.css b/frontend-vanilla/src/style.css
index fd51eff..f6e7f2f 100644
--- a/frontend-vanilla/src/style.css
+++ b/frontend-vanilla/src/style.css
@@ -450,7 +450,17 @@ select:focus {
margin-top: 2rem;
border-bottom: none;
border-radius: 8px;
- transition: background-color 0.2s ease;
+ transition: background-color 0.2s ease, opacity 0.3s ease;
+}
+
+.feed-item.read {
+ opacity: 0.5;
+}
+
+.feed-item.read .item-title {
+ font-weight: normal;
+ color: var(--text-color);
+ opacity: 0.8;
}