experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(pagestream): full screen height, center-active carousel scrolling

Pagestream window now uses full screen height instead of 80%.
Cards carousel always keeps the active card centered vertically.
Navigation (arrow/j/k) moves the carousel, not the selection indicator.
Top/bottom spacers allow first and last cards to reach center.
Scrollbar hidden for cleaner carousel appearance.

+52 -36
+1 -1
extensions/pagestream/background.js
··· 62 62 screenH = window.screen.availHeight || window.screen.height || 900; 63 63 } catch {} 64 64 const width = screenW; 65 - const height = Math.round(screenH * 0.8); 65 + const height = screenH; 66 66 67 67 const params = { 68 68 // IZUI role
+14 -1
extensions/pagestream/home.css
··· 27 27 .stream-container { 28 28 flex: 1; 29 29 overflow-y: auto; 30 - padding: 20px 16px; 30 + overflow-x: hidden; 31 + padding: 16px; 31 32 display: flex; 32 33 flex-direction: column; 33 34 gap: 20px; 35 + /* Hide scrollbar for cleaner carousel look */ 36 + scrollbar-width: none; 37 + -ms-overflow-style: none; 38 + } 39 + 40 + .stream-container::-webkit-scrollbar { 41 + display: none; 42 + } 43 + 44 + /* Spacers at top/bottom allow first and last cards to reach center */ 45 + .stream-spacer { 46 + flex-shrink: 0; 34 47 } 35 48 36 49 /* Cards — opaque backgrounds, no borders */
+37 -34
extensions/pagestream/home.js
··· 59 59 filterTagId: null, 60 60 filterTagName: null, 61 61 isLoading: true, 62 - wasAtBottom: true, 63 62 animating: false, 64 63 openWindowId: null, 65 64 openCardIndex: -1, ··· 135 134 return Array.from(document.querySelectorAll('#stream peek-card')); 136 135 }; 137 136 138 - const updateSelection = () => { 137 + const updateSelection = (animate = true) => { 139 138 const cards = getCards(); 140 139 cards.forEach((card, i) => { 141 140 const isActive = (i === state.selectedIndex); ··· 176 175 177 176 const selected = cards[state.selectedIndex]; 178 177 if (selected) { 179 - selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); 178 + scrollCardToCenter(selected, animate); 180 179 } 181 180 }; 182 181 183 - const render = () => { 182 + /** Scroll so that the given card is vertically centered in the stream */ 183 + const scrollCardToCenter = (card, animate = true) => { 184 + const container = document.getElementById('stream'); 185 + const cardRect = card.getBoundingClientRect(); 186 + const containerRect = container.getBoundingClientRect(); 187 + 188 + // Card center relative to container's scroll position 189 + const cardCenterInContainer = (card.offsetTop + cardRect.height / 2); 190 + const targetScrollTop = cardCenterInContainer - container.clientHeight / 2; 191 + 192 + container.scrollTo({ 193 + top: targetScrollTop, 194 + behavior: animate ? 'smooth' : 'instant' 195 + }); 196 + }; 197 + 198 + const render = (initialLoad = false) => { 184 199 const container = document.getElementById('stream'); 185 200 const emptyState = document.getElementById('empty-state'); 186 201 const filtered = getFilteredVisits(); 187 202 188 - const existingCards = container.querySelectorAll('peek-card'); 189 - existingCards.forEach(card => card.remove()); 203 + // Remove old cards and spacers 204 + container.querySelectorAll('peek-card, .stream-spacer').forEach(el => el.remove()); 190 205 191 206 if (state.isLoading) { 192 207 emptyState.textContent = 'Loading history...'; ··· 202 217 203 218 emptyState.style.display = 'none'; 204 219 220 + // Add top spacer — half viewport height so first card can be centered 221 + const spacerHeight = Math.floor(container.clientHeight / 2 - 60); 222 + const topSpacer = document.createElement('div'); 223 + topSpacer.className = 'stream-spacer'; 224 + topSpacer.style.height = spacerHeight + 'px'; 225 + container.appendChild(topSpacer); 226 + 205 227 filtered.forEach(({ visit, item }, index) => { 206 228 const card = createVisitCard(visit, item, index); 207 229 container.appendChild(card); 208 230 }); 209 231 232 + // Add bottom spacer 233 + const bottomSpacer = document.createElement('div'); 234 + bottomSpacer.className = 'stream-spacer'; 235 + bottomSpacer.style.height = spacerHeight + 'px'; 236 + container.appendChild(bottomSpacer); 237 + 210 238 if (state.selectedIndex < 0 || state.selectedIndex >= filtered.length) { 211 239 state.selectedIndex = filtered.length - 1; 212 240 } 213 - updateSelection(); 214 - 215 - if (state.wasAtBottom) { 216 - scrollToBottom(); 217 - } 218 - }; 219 - 220 - const scrollToBottom = () => { 221 - const container = document.getElementById('stream'); 222 - container.scrollTop = container.scrollHeight; 223 - }; 224 - 225 - const isAtBottom = () => { 226 - const container = document.getElementById('stream'); 227 - const threshold = 50; 228 - return container.scrollHeight - container.scrollTop - container.clientHeight < threshold; 241 + // On initial load, snap to center instantly (no smooth scroll) 242 + updateSelection(!initialLoad); 229 243 }; 230 244 231 245 // ===== Open URL in Page Host (with animation) ===== ··· 609 623 610 624 api.escape.onEscape(handleEscape); 611 625 612 - const streamContainer = document.getElementById('stream'); 613 - streamContainer.addEventListener('scroll', () => { 614 - state.wasAtBottom = isAtBottom(); 615 - }); 616 - 617 626 document.addEventListener('keydown', handleKeydown); 618 627 619 628 await loadVisits(); 620 - render(); 621 - 622 - scrollToBottom(); 623 - requestAnimationFrame(() => { 624 - scrollToBottom(); 625 - }); 629 + render(true); 626 630 627 631 const debouncedRefresh = debounce(async () => { 628 632 debug && console.log('[pagestream] debounced refresh triggered'); 629 - state.wasAtBottom = isAtBottom(); 630 633 await loadVisits(); 631 634 render(); 632 635 }, 150);