experiments in a post-browser web
10
fork

Configure Feed

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

feat(pagestream): inline card browsing with iframe expand/collapse animations

- Cmd+L now scrolls stream to bottom and selects the latest card
- Enter on active card expands it to fill viewport with an iframe loading the URL
- Esc collapses the expanded card back to normal size, restoring original content
- Added backdrop overlay, CSS transitions, and CSP frame-src for iframe support

+127 -20
+51 -1
extensions/pagestream/home.css
··· 273 273 color: var(--base05); 274 274 } 275 275 276 - /* Browse overlay styles removed - pages now open in proper page windows */ 276 + /* ===== Inline Browse Mode ===== */ 277 + 278 + /* Overlay backdrop when a card is expanded */ 279 + .browse-backdrop { 280 + position: fixed; 281 + inset: 0; 282 + background: rgba(0, 0, 0, 0.5); 283 + z-index: 99; 284 + opacity: 0; 285 + transition: opacity 0.3s ease; 286 + pointer-events: none; 287 + } 288 + 289 + .browse-backdrop.active { 290 + opacity: 1; 291 + pointer-events: auto; 292 + } 293 + 294 + /* Expanded card fills the viewport */ 295 + .stream-container peek-card.card-expanded { 296 + position: fixed; 297 + top: 8px; 298 + left: 8px; 299 + right: 8px; 300 + bottom: 60px; /* leave room for navbar */ 301 + z-index: 100; 302 + opacity: 1; 303 + --peek-card-bg: var(--base01); 304 + --peek-card-padding: 0px; 305 + --peek-card-gap: 0px; 306 + --peek-card-radius: 12px; 307 + overflow: hidden; 308 + } 309 + 310 + .stream-container peek-card.card-expanded::part(card) { 311 + height: 100%; 312 + display: flex; 313 + flex-direction: column; 314 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5) !important; 315 + border-color: transparent !important; 316 + outline: none !important; 317 + } 318 + 319 + /* iframe inside expanded card */ 320 + .browse-iframe { 321 + width: 100%; 322 + flex: 1; 323 + border: none; 324 + background: white; 325 + border-radius: 0 0 12px 12px; 326 + }
+2 -1
extensions/pagestream/home.html
··· 2 2 <html> 3 3 <head> 4 4 <meta charset="utf-8"> 5 - <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"> 5 + <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'; frame-src https: http:;"> 6 6 <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"> 7 7 <title>Pagestream</title> 8 8 <link rel="stylesheet" type="text/css" href="home.css"> ··· 31 31 </script> 32 32 </head> 33 33 <body> 34 + <div class="browse-backdrop" id="browse-backdrop"></div> 34 35 <div class="search-container"> 35 36 <peek-input 36 37 class="search-input"
+74 -18
extensions/pagestream/home.js
··· 98 98 filterTagName: null, 99 99 isLoading: true, 100 100 wasAtBottom: true, // Track if user was scrolled to bottom 101 - browsing: false, // Whether we're in inline browse mode 102 - browseUrl: null // URL currently being browsed inline 101 + browsing: false, // Whether we're in inline browse mode 102 + browseUrl: null, // URL currently being browsed inline 103 + browseCardIndex: -1, // Index of the card being browsed 104 + browseOriginalContent: null // Saved original card innerHTML 103 105 }; 104 106 105 107 // Expose state for debugging in tests ··· 396 398 // ===== Inline Browsing ===== 397 399 398 400 /** 399 - * Open a URL in a proper page window via api.window.open(). 400 - * Extension windows don't support <webview> tags (webviewTag is not enabled), 401 - * so we open the URL in a full page window which has canvas + webview support. 401 + * Open a URL inline by expanding the active card and loading an iframe. 402 + * Extension windows don't support <webview> tags, so we use iframes. 402 403 */ 403 - const openInline = async (url) => { 404 + const openInline = (url) => { 404 405 if (state.browsing) return; 406 + 407 + const cards = getCards(); 408 + const card = cards[state.selectedIndex]; 409 + if (!card) return; 410 + 411 + debug && console.log('[pagestream] Opening inline:', url); 412 + 405 413 state.browsing = true; 406 414 state.browseUrl = url; 415 + state.browseCardIndex = state.selectedIndex; 416 + // Save original card content so we can restore on close 417 + state.browseOriginalContent = card.innerHTML; 407 418 408 - debug && console.log('[pagestream] Opening in page window:', url); 419 + // Clear card content and insert iframe 420 + card.innerHTML = ''; 409 421 410 - try { 411 - await api.window.open(url, { 412 - trackingSource: 'pagestream', 413 - }); 414 - } catch (err) { 415 - console.error('[pagestream] Failed to open page window:', err); 422 + const iframe = document.createElement('iframe'); 423 + iframe.className = 'browse-iframe'; 424 + iframe.src = url; 425 + iframe.sandbox = 'allow-scripts allow-same-origin allow-forms allow-popups'; 426 + card.appendChild(iframe); 427 + 428 + // Add expanded class to trigger CSS transition 429 + card.classList.add('card-expanded'); 430 + 431 + // Show backdrop 432 + const backdrop = document.getElementById('browse-backdrop'); 433 + backdrop.classList.add('active'); 434 + 435 + // Update navbar to show the current URL 436 + const navbarEl = document.getElementById('navbar'); 437 + if (navbarEl && navbarEl.setUrl) { 438 + navbarEl.setUrl(url); 416 439 } 417 - 418 - // Reset browsing state immediately so the card stream stays usable 419 - state.browsing = false; 420 - state.browseUrl = null; 421 440 }; 422 441 423 442 /** 424 - * Close browse mode and return to card view 443 + * Close browse mode: remove iframe, animate card back down, restore content 425 444 */ 426 445 const closeBrowse = () => { 446 + if (!state.browsing) return; 447 + 448 + const cards = getCards(); 449 + const card = cards[state.browseCardIndex]; 450 + 451 + if (card) { 452 + // Remove expanded class (triggers reverse CSS transition) 453 + card.classList.remove('card-expanded'); 454 + 455 + // Restore original card content 456 + if (state.browseOriginalContent) { 457 + card.innerHTML = state.browseOriginalContent; 458 + } 459 + } 460 + 461 + // Hide backdrop 462 + const backdrop = document.getElementById('browse-backdrop'); 463 + backdrop.classList.remove('active'); 464 + 465 + // Clear navbar URL 466 + const navbarEl = document.getElementById('navbar'); 467 + if (navbarEl && navbarEl.setUrl) { 468 + navbarEl.setUrl(''); 469 + } 470 + 427 471 state.browsing = false; 428 472 state.browseUrl = null; 473 + state.browseCardIndex = -1; 474 + state.browseOriginalContent = null; 475 + 476 + // Re-apply selection styling 477 + updateSelection(); 429 478 }; 430 479 431 480 // ===== Filtering ===== ··· 532 581 if ((e.key === 'l' && (e.metaKey || e.ctrlKey)) || (e.key === '/' && !isInputFocused)) { 533 582 e.preventDefault(); 534 583 navbarEl.focusUrl(); 584 + scrollToBottom(); 585 + // Also select the last card (most recent) 586 + const cards = getCards(); 587 + if (cards.length > 0) { 588 + state.selectedIndex = cards.length - 1; 589 + updateSelection(); 590 + } 535 591 return; 536 592 } 537 593