experiments in a post-browser web
10
fork

Configure Feed

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

feat(pagestream): transparent bg, card sizing, shared navbar component

- Extract page host navbar into reusable peek-navbar web component
(app/components/peek-navbar.js) with URL input, back/forward/reload
buttons, keyboard shortcuts (Cmd+L, Cmd+R, Cmd+[/]), and events
- Update page host (app/page/) to use peek-navbar component, preserving
existing drag/resize/hover trigger zone architecture
- Add peek-navbar to pagestream replacing the old peek-input URL bar
- Pagestream window now opens with transparent: true background
- CSS updated for translucent card backgrounds with backdrop-filter blur
- Active/focused card displayed at ~2x size (scale 1.15 + larger text/icons),
non-active cards scaled down to 0.88 with reduced opacity
- Card gap increased from 8px to 20px for better visual separation
- Navbar URL input auto-focuses when pagestream window opens
- peek-navbar shortcuts property allows host to control keyboard handling

+453 -210
+1
WIP.md
··· 11 11 Today 12 12 - [x] integrate app versioning changes, local end to end testing, deployment to server/mobile/desktop and test 13 13 - [x][desktop] the items in the History section under the Addressability header 14 + - [~] pagestream improvements: transparent bg, card spacing, active card 2x, navbar component, auto-focus
+297
app/components/peek-navbar.js
··· 1 + /** 2 + * Peek Navbar Component 3 + * 4 + * A floating navigation bar with URL input, back/forward/reload buttons, 5 + * and keyboard shortcuts (Cmd+L to focus, Escape to blur, Enter to navigate). 6 + * 7 + * @element peek-navbar 8 + * 9 + * @prop {string} url - Current URL to display 10 + * @prop {boolean} canGoBack - Whether back navigation is available 11 + * @prop {boolean} canGoForward - Whether forward navigation is available 12 + * 13 + * @fires navigate - When user enters a URL and presses Enter. Detail: { url } 14 + * @fires go-back - When back button is clicked or Cmd+[ pressed 15 + * @fires go-forward - When forward button is clicked or Cmd+] pressed 16 + * @fires reload - When reload button is clicked or Cmd+R pressed 17 + * @fires show - When navbar becomes visible 18 + * @fires hide - When navbar is dismissed 19 + * 20 + * @cssprop --peek-navbar-bg - Navbar background color 21 + * @cssprop --peek-navbar-height - Navbar height (default: 36px) 22 + * 23 + * @example 24 + * <peek-navbar 25 + * url="https://example.com" 26 + * .canGoBack=${true} 27 + * @navigate=${(e) => webview.loadURL(e.detail.url)} 28 + * @go-back=${() => webview.goBack()} 29 + * @go-forward=${() => webview.goForward()} 30 + * @reload=${() => webview.reload()} 31 + * ></peek-navbar> 32 + */ 33 + 34 + import { html, css } from 'lit'; 35 + import { PeekElement, sharedStyles } from './base.js'; 36 + 37 + export class PeekNavbar extends PeekElement { 38 + static properties = { 39 + url: { type: String }, 40 + canGoBack: { type: Boolean, attribute: 'can-go-back' }, 41 + canGoForward: { type: Boolean, attribute: 'can-go-forward' }, 42 + visible: { type: Boolean, reflect: true }, 43 + /** When true, component registers its own document-level keyboard shortcuts 44 + * (Cmd+L, Cmd+R, Cmd+[, Cmd+]). Set to false if the host manages these. */ 45 + shortcuts: { type: Boolean }, 46 + }; 47 + 48 + static styles = [ 49 + sharedStyles, 50 + css` 51 + :host { 52 + display: block; 53 + height: var(--peek-navbar-height, 36px); 54 + } 55 + 56 + .navbar { 57 + height: var(--peek-navbar-height, 36px); 58 + display: flex; 59 + align-items: center; 60 + gap: 8px; 61 + padding: 0 12px; 62 + background: var(--peek-navbar-bg, var(--theme-bg-secondary, rgba(30, 30, 30, 0.92))); 63 + backdrop-filter: blur(12px); 64 + -webkit-backdrop-filter: blur(12px); 65 + color: var(--theme-text, #e0e0e0); 66 + font-family: var(--theme-font-sans, system-ui, -apple-system, BlinkMacSystemFont, sans-serif); 67 + font-size: 15px; 68 + border: none; 69 + border-radius: 10px; 70 + user-select: none; 71 + -webkit-user-select: none; 72 + } 73 + 74 + .nav-btn { 75 + background: none; 76 + border: none; 77 + color: var(--theme-text-secondary, #bbb); 78 + font-size: 20px; 79 + width: 36px; 80 + height: 36px; 81 + border-radius: 8px; 82 + cursor: pointer; 83 + display: flex; 84 + align-items: center; 85 + justify-content: center; 86 + padding: 0; 87 + flex-shrink: 0; 88 + transition: background 0.1s, color 0.1s; 89 + } 90 + 91 + .nav-btn:hover:not(:disabled) { 92 + background: var(--theme-bg-tertiary, rgba(255, 255, 255, 0.12)); 93 + color: var(--theme-text, #fff); 94 + } 95 + 96 + .nav-btn:active:not(:disabled) { 97 + background: var(--theme-bg-tertiary, rgba(255, 255, 255, 0.2)); 98 + } 99 + 100 + .nav-btn:disabled { 101 + color: var(--theme-text-muted, #555); 102 + cursor: default; 103 + } 104 + 105 + .url-text { 106 + flex: 1; 107 + overflow: hidden; 108 + text-overflow: ellipsis; 109 + white-space: nowrap; 110 + color: var(--theme-text-secondary, #aaa); 111 + font-size: 14px; 112 + font-family: inherit; 113 + padding: 6px 10px; 114 + border-radius: 6px; 115 + background: var(--theme-bg-tertiary, rgba(128, 128, 128, 0.1)); 116 + border: none; 117 + outline: none; 118 + cursor: text; 119 + min-width: 0; 120 + } 121 + 122 + .url-text:hover { 123 + background: var(--theme-bg, rgba(128, 128, 128, 0.18)); 124 + } 125 + 126 + .url-text:focus { 127 + background: var(--theme-bg, rgba(128, 128, 128, 0.22)); 128 + color: var(--theme-text, #e0e0e0); 129 + } 130 + ` 131 + ]; 132 + 133 + constructor() { 134 + super(); 135 + this.url = ''; 136 + this.canGoBack = false; 137 + this.canGoForward = false; 138 + this.visible = false; 139 + this.shortcuts = true; 140 + this._handleKeydown = this._handleKeydown.bind(this); 141 + } 142 + 143 + connectedCallback() { 144 + super.connectedCallback(); 145 + document.addEventListener('keydown', this._handleKeydown); 146 + } 147 + 148 + disconnectedCallback() { 149 + super.disconnectedCallback(); 150 + document.removeEventListener('keydown', this._handleKeydown); 151 + } 152 + 153 + get inputElement() { 154 + return this.shadowRoot?.querySelector('.url-text'); 155 + } 156 + 157 + /** 158 + * Normalize a URL input, adding https:// if needed 159 + */ 160 + _normalizeUrl(input) { 161 + const trimmed = input.trim(); 162 + if (!trimmed) return null; 163 + if (/^https?:\/\//i.test(trimmed)) return trimmed; 164 + if (trimmed.includes('.') && !trimmed.includes(' ')) return 'https://' + trimmed; 165 + return 'https://' + trimmed; 166 + } 167 + 168 + /** 169 + * Handle global keyboard shortcuts (only when shortcuts property is true) 170 + */ 171 + _handleKeydown(e) { 172 + if (!this.shortcuts) return; 173 + 174 + // Cmd+L to focus URL input 175 + if (e.key === 'l' && (e.metaKey || e.ctrlKey)) { 176 + e.preventDefault(); 177 + this.focusUrl(); 178 + return; 179 + } 180 + 181 + // Cmd+R to reload 182 + if (e.key === 'r' && (e.metaKey || e.ctrlKey)) { 183 + e.preventDefault(); 184 + this.emit('reload'); 185 + return; 186 + } 187 + 188 + // Cmd+[ or Cmd+Left to go back 189 + if ((e.key === '[' || e.key === 'ArrowLeft') && (e.metaKey || e.ctrlKey)) { 190 + e.preventDefault(); 191 + if (this.canGoBack) this.emit('go-back'); 192 + return; 193 + } 194 + 195 + // Cmd+] or Cmd+Right to go forward 196 + if ((e.key === ']' || e.key === 'ArrowRight') && (e.metaKey || e.ctrlKey)) { 197 + e.preventDefault(); 198 + if (this.canGoForward) this.emit('go-forward'); 199 + return; 200 + } 201 + } 202 + 203 + /** 204 + * Handle keydown in the URL input field 205 + */ 206 + _onUrlKeydown(e) { 207 + if (e.key === 'Enter') { 208 + e.preventDefault(); 209 + const input = this.inputElement; 210 + const url = this._normalizeUrl(input.value); 211 + if (url) { 212 + this.emit('navigate', { url }); 213 + input.value = url; 214 + input.blur(); 215 + } 216 + } 217 + if (e.key === 'Escape') { 218 + const input = this.inputElement; 219 + input.value = this.url || ''; 220 + input.blur(); 221 + this.emit('dismiss'); 222 + e.stopPropagation(); 223 + } 224 + } 225 + 226 + /** 227 + * Focus the URL input and select its text 228 + */ 229 + focusUrl() { 230 + requestAnimationFrame(() => { 231 + const input = this.inputElement; 232 + if (input) { 233 + input.focus(); 234 + input.select(); 235 + } 236 + }); 237 + } 238 + 239 + /** 240 + * Update the displayed URL 241 + */ 242 + setUrl(url) { 243 + this.url = url; 244 + const input = this.inputElement; 245 + // Don't overwrite the input if the user is actively typing in it 246 + const isFocused = this.shadowRoot?.activeElement === input; 247 + if (input && !isFocused) { 248 + input.value = url; 249 + } 250 + } 251 + 252 + _onUrlFocus() { 253 + requestAnimationFrame(() => { 254 + this.inputElement?.select(); 255 + }); 256 + } 257 + 258 + _onUrlMousedown(e) { 259 + e.stopPropagation(); 260 + } 261 + 262 + render() { 263 + return html` 264 + <div class="navbar"> 265 + <button 266 + class="nav-btn" 267 + ?disabled=${!this.canGoBack} 268 + title="Back (Cmd+[)" 269 + @click=${(e) => { e.stopPropagation(); this.emit('go-back'); }} 270 + >&#8592;</button> 271 + <button 272 + class="nav-btn" 273 + ?disabled=${!this.canGoForward} 274 + title="Forward (Cmd+])" 275 + @click=${(e) => { e.stopPropagation(); this.emit('go-forward'); }} 276 + >&#8594;</button> 277 + <button 278 + class="nav-btn" 279 + title="Reload (Cmd+R)" 280 + @click=${(e) => { e.stopPropagation(); this.emit('reload'); }} 281 + >&#8635;</button> 282 + <input 283 + type="text" 284 + class="url-text" 285 + .value=${this.url || ''} 286 + spellcheck="false" 287 + autocomplete="off" 288 + @keydown=${this._onUrlKeydown} 289 + @focus=${this._onUrlFocus} 290 + @mousedown=${this._onUrlMousedown} 291 + > 292 + </div> 293 + `; 294 + } 295 + } 296 + 297 + customElements.define('peek-navbar', PeekNavbar);
+23 -81
app/page/index.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 <title>Loading...</title> 7 7 <link rel="stylesheet" href="peek://theme/variables.css"> 8 + <!-- Import map for Lit (required by peek-navbar component) --> 9 + <script type="importmap"> 10 + { 11 + "imports": { 12 + "lit": "peek://node_modules/lit/index.js", 13 + "lit/": "peek://node_modules/lit/", 14 + "lit-html": "peek://node_modules/lit-html/lit-html.js", 15 + "lit-html/": "peek://node_modules/lit-html/", 16 + "lit-element": "peek://node_modules/lit-element/lit-element.js", 17 + "lit-element/": "peek://node_modules/lit-element/", 18 + "@lit/reactive-element": "peek://node_modules/@lit/reactive-element/reactive-element.js", 19 + "@lit/reactive-element/": "peek://node_modules/@lit/reactive-element/" 20 + } 21 + } 22 + </script> 8 23 <style> 9 24 * { 10 25 margin: 0; ··· 36 51 * Window expands upward via setBounds() to accommodate it. 37 52 * Dragging is handled by custom JS (moves window), not -webkit-app-region. 38 53 */ 39 - .navbar { 54 + peek-navbar { 40 55 position: absolute; 41 - height: 36px; 42 56 display: none; 43 - align-items: center; 44 - gap: 8px; 45 - padding: 0 12px; 46 - background: var(--theme-bg-secondary, rgba(30, 30, 30, 0.92)); 47 - backdrop-filter: blur(12px); 48 - -webkit-backdrop-filter: blur(12px); 49 - color: var(--theme-text, #e0e0e0); 50 - font-family: var(--theme-font-sans, system-ui, -apple-system, BlinkMacSystemFont, sans-serif); 51 - font-size: 15px; 52 - border: none; 53 - border-radius: 10px; 54 - cursor: grab; 55 - user-select: none; 56 - -webkit-user-select: none; 57 57 z-index: 100; 58 - } 59 - 60 - .navbar.visible { 61 - display: flex; 62 - } 63 - 64 - .nav-btn { 65 - background: none; 66 - border: none; 67 - color: var(--theme-text-secondary, #bbb); 68 - font-size: 20px; 69 - width: 36px; 70 - height: 36px; 71 - border-radius: 8px; 72 - cursor: pointer; 73 - display: flex; 74 - align-items: center; 75 - justify-content: center; 76 - padding: 0; 77 - flex-shrink: 0; 78 - transition: background 0.1s, color 0.1s; 79 - } 80 - 81 - .nav-btn:hover:not(:disabled) { 82 - background: var(--theme-bg-tertiary, rgba(255, 255, 255, 0.12)); 83 - color: var(--theme-text, #fff); 84 - } 85 - 86 - .nav-btn:active:not(:disabled) { 87 - background: var(--theme-bg-tertiary, rgba(255, 255, 255, 0.2)); 88 - } 89 - 90 - .nav-btn:disabled { 91 - color: var(--theme-text-muted, #555); 92 - cursor: default; 58 + cursor: grab; 93 59 } 94 60 95 - .url-text { 96 - flex: 1; 97 - overflow: hidden; 98 - text-overflow: ellipsis; 99 - white-space: nowrap; 100 - color: var(--theme-text-secondary, #aaa); 101 - font-size: 14px; 102 - font-family: inherit; 103 - padding: 6px 10px; 104 - border-radius: 6px; 105 - background: var(--theme-bg-tertiary, rgba(128, 128, 128, 0.1)); 106 - border: none; 107 - outline: none; 108 - cursor: text; 109 - min-width: 0; 110 - } 111 - 112 - .url-text:hover { 113 - background: var(--theme-bg, rgba(128, 128, 128, 0.18)); 114 - } 115 - 116 - .url-text:focus { 117 - background: var(--theme-bg, rgba(128, 128, 128, 0.22)); 118 - color: var(--theme-text, #e0e0e0); 61 + peek-navbar.visible { 62 + display: block; 119 63 } 120 64 121 65 /* Trigger zone — transparent strip above webview, always part of window. ··· 349 293 <span class="group-name"></span> 350 294 </div> 351 295 <div class="trigger-zone" id="trigger-zone"></div> 352 - <div class="navbar" id="navbar"> 353 - <button class="nav-btn" id="btn-back" title="Back (Cmd+[)">&#8592;</button> 354 - <button class="nav-btn" id="btn-forward" title="Forward (Cmd+])">&#8594;</button> 355 - <button class="nav-btn" id="btn-reload" title="Reload (Cmd+R)">&#8635;</button> 356 - <input type="text" class="url-text" id="url-text" spellcheck="false" autocomplete="off"> 357 - </div> 296 + <script type="module"> 297 + import 'peek://app/components/peek-navbar.js'; 298 + </script> 299 + <peek-navbar id="navbar"></peek-navbar> 358 300 <webview id="content" allowpopups></webview> 359 301 <div class="drag-overlay" id="drag-overlay"></div> 360 302 <div class="resize-handle" id="resize-se" data-dir="se"></div>
+46 -76
app/page/page.js
··· 122 122 navNe: document.getElementById('resize-nav-ne'), 123 123 navNw: document.getElementById('resize-nav-nw'), 124 124 }; 125 - const btnBack = document.getElementById('btn-back'); 126 - const btnForward = document.getElementById('btn-forward'); 127 - const btnReload = document.getElementById('btn-reload'); 128 - const urlText = document.getElementById('url-text'); 129 125 const modeIndicator = document.getElementById('mode-indicator'); 130 126 const widgetContainer = document.getElementById('widget-container'); 131 127 128 + // Disable component-level keyboard shortcuts — page.js handles them via pubsub 129 + navbar.shortcuts = false; 130 + 131 + // --- peek-navbar component event wiring --- 132 + navbar.addEventListener('navigate', (e) => { 133 + const url = e.detail?.url; 134 + if (url) { 135 + DEBUG && console.log('[page] Navigating to:', url); 136 + webview.loadURL(url); 137 + navbar.setUrl(url); 138 + } 139 + }); 140 + navbar.addEventListener('go-back', () => { 141 + if (webview.canGoBack()) { 142 + DEBUG && console.log('[page] navbar go-back'); 143 + webview.goBack(); 144 + updateState(); 145 + } 146 + }); 147 + navbar.addEventListener('go-forward', () => { 148 + if (webview.canGoForward()) { 149 + DEBUG && console.log('[page] navbar go-forward'); 150 + webview.goForward(); 151 + updateState(); 152 + } 153 + }); 154 + navbar.addEventListener('reload', () => { 155 + DEBUG && console.log('[page] navbar reload'); 156 + webview.reload(); 157 + }); 158 + 132 159 // --- Window bounds computation --- 133 160 // Computes the BrowserWindow bounds from the webview screen position 134 161 ··· 272 299 } 273 300 274 301 webview.src = targetUrl; 275 - urlText.value = targetUrl; 302 + navbar.setUrl(targetUrl); 276 303 } 277 304 278 305 initWebview(); ··· 315 342 dragOverlay.classList.remove('active'); 316 343 } 317 344 318 - // Instant drag on navbar background (excluding buttons and URL text) 345 + // Instant drag on navbar background (excluding buttons and URL text inside shadow DOM) 319 346 navbar.addEventListener('mousedown', (e) => { 320 - if (e.target.closest('.nav-btn') || e.target.closest('.url-text')) return; 347 + // The peek-navbar component handles its own button/input mousedowns with stopPropagation, 348 + // so if the event reaches us, it's on the navbar background — start drag. 321 349 pageMouseButtonDown = true; 322 350 startDrag(e.screenX, e.screenY); 323 351 navbar.style.cursor = 'grabbing'; ··· 674 702 // --- State display --- 675 703 676 704 function updateState() { 677 - btnBack.disabled = !webview.canGoBack(); 678 - btnForward.disabled = !webview.canGoForward(); 705 + navbar.canGoBack = webview.canGoBack(); 706 + navbar.canGoForward = webview.canGoForward(); 679 707 } 680 708 681 709 // --- Show / Hide navbar --- ··· 701 729 DEBUG && console.log('[page] Navbar shown, source:', showSource); 702 730 } 703 731 if (opts?.focusUrl) { 704 - requestAnimationFrame(() => { 705 - urlText.focus(); 706 - urlText.select(); 707 - }); 732 + navbar.focusUrl(); 708 733 } 709 734 } 710 735 ··· 718 743 // Window stays the same size (always includes navbar space). 719 744 // Just update trigger zone and resize handle visibility. 720 745 updatePositions(); 721 - urlText.blur(); 746 + navbar.inputElement?.blur(); 722 747 showSource = null; 723 748 DEBUG && console.log('[page] Navbar hidden'); 724 749 } ··· 822 847 } 823 848 }, api.scopes.GLOBAL); 824 849 825 - // --- Nav button actions --- 826 - 827 - btnBack.addEventListener('click', (e) => { 828 - e.stopPropagation(); 829 - if (webview.canGoBack()) { 830 - webview.goBack(); 831 - updateState(); 832 - } 833 - }); 834 - 835 - btnForward.addEventListener('click', (e) => { 836 - e.stopPropagation(); 837 - if (webview.canGoForward()) { 838 - webview.goForward(); 839 - updateState(); 840 - } 841 - }); 842 - 843 - btnReload.addEventListener('click', (e) => { 844 - e.stopPropagation(); 845 - webview.reload(); 846 - }); 847 - 848 - // --- URL field: navigate on Enter --- 849 - 850 - function normalizeUrl(input) { 851 - const trimmed = input.trim(); 852 - if (!trimmed) return null; 853 - if (/^https?:\/\//i.test(trimmed)) return trimmed; 854 - if (trimmed.includes('.') && !trimmed.includes(' ')) return 'https://' + trimmed; 855 - return 'https://' + trimmed; 856 - } 857 - 858 - urlText.addEventListener('keydown', (e) => { 859 - if (e.key === 'Enter') { 860 - e.preventDefault(); 861 - const url = normalizeUrl(urlText.value); 862 - if (url) { 863 - DEBUG && console.log('[page] Navigating to:', url); 864 - webview.loadURL(url); 865 - urlText.value = url; 866 - urlText.blur(); 867 - hide(); 868 - } 869 - } 870 - if (e.key === 'Escape') { 871 - urlText.value = webview.getURL() || targetUrl; 872 - urlText.blur(); 873 - hide(); 874 - e.stopPropagation(); 875 - } 876 - }); 877 - 878 - urlText.addEventListener('mousedown', (e) => { 879 - e.stopPropagation(); 880 - }); 881 - 882 - urlText.addEventListener('focus', () => { 883 - requestAnimationFrame(() => urlText.select()); 884 - }); 850 + // --- Nav button actions and URL navigation are handled by peek-navbar component --- 851 + // Event listeners wired up in the "peek-navbar component event wiring" section above. 852 + // On navigate or dismiss (Escape), also hide the navbar (page-specific behavior). 853 + navbar.addEventListener('navigate', () => { hide(); }); 854 + navbar.addEventListener('dismiss', () => { hide(); }); 885 855 886 856 // --- Escape handling --- 887 857 ··· 909 879 910 880 webview.addEventListener('did-navigate', async (e) => { 911 881 DEBUG && console.log('[page] did-navigate:', e.url); 912 - urlText.value = e.url; 882 + navbar.setUrl(e.url); 913 883 updateState(); 914 884 915 885 // Update OAuth state (Level 2) ··· 933 903 934 904 webview.addEventListener('did-navigate-in-page', (e) => { 935 905 if (e.isMainFrame) { 936 - urlText.value = e.url; 906 + navbar.setUrl(e.url); 937 907 updateState(); 938 908 } 939 909 });
+1
extensions/pagestream/background.js
··· 66 66 key: address, 67 67 height, 68 68 width, 69 + transparent: true, 69 70 trackingSource: 'cmd', 70 71 trackingSourceId: 'pagestream' 71 72 };
+55 -28
extensions/pagestream/home.css
··· 15 15 } 16 16 17 17 body { 18 - background: var(--base00); 18 + /* Fully transparent - window itself handles transparency */ 19 + background: transparent; 19 20 color: var(--base05); 20 21 height: 100vh; 21 22 display: flex; ··· 32 33 peek-input.search-input { 33 34 display: block; 34 35 width: 100%; 35 - --peek-input-bg: var(--base01); 36 - --peek-input-border: var(--base02); 36 + --peek-input-bg: rgba(var(--base01-rgb, 30, 30, 46), 0.8); 37 + --peek-input-border: rgba(var(--base02-rgb, 45, 45, 65), 0.6); 37 38 --peek-input-height: 36px; 39 + backdrop-filter: blur(8px); 40 + -webkit-backdrop-filter: blur(8px); 41 + border-radius: 8px; 38 42 } 39 43 40 44 peek-input.search-input::part(input) { ··· 46 50 .stream-container { 47 51 flex: 1; 48 52 overflow-y: auto; 49 - padding: 12px 16px; 53 + padding: 16px 16px; 50 54 display: flex; 51 55 flex-direction: column; 52 - gap: 8px; 56 + gap: 20px; 53 57 scroll-behavior: smooth; 54 58 } 55 59 56 60 /* Cards in the stream */ 57 61 .stream-container peek-card { 58 - --peek-card-bg: var(--base01); 59 - --peek-card-hover-bg: var(--base02); 62 + --peek-card-bg: rgba(var(--base01-rgb, 30, 30, 46), 0.85); 63 + --peek-card-hover-bg: rgba(var(--base02-rgb, 45, 45, 65), 0.9); 60 64 --peek-card-border: transparent; 61 65 --peek-card-radius: 8px; 62 66 --peek-card-padding: 10px; 63 67 --peek-card-gap: 4px; 64 68 flex-shrink: 0; 65 - transition: transform 0.15s ease, box-shadow 0.15s ease; 69 + transition: transform 0.25s ease, box-shadow 0.25s ease, padding 0.25s ease, font-size 0.25s ease; 70 + backdrop-filter: blur(8px); 71 + -webkit-backdrop-filter: blur(8px); 66 72 } 67 73 68 74 .stream-container peek-card[selected] { 69 - --peek-card-bg: var(--base02); 70 - transform: scale(1.02); 75 + --peek-card-bg: rgba(var(--base02-rgb, 45, 45, 65), 0.9); 71 76 } 72 77 73 78 .stream-container peek-card[selected]:hover { 74 - --peek-card-bg: var(--base03); 79 + --peek-card-bg: rgba(var(--base03-rgb, 60, 60, 80), 0.92); 75 80 } 76 81 82 + /* Active/focused card is displayed at 2x size */ 77 83 .stream-container peek-card.active-card { 78 - transform: scale(1.03); 79 - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); 84 + transform: scale(1.15); 85 + transform-origin: center; 86 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35); 87 + --peek-card-padding: 16px; 88 + --peek-card-gap: 8px; 89 + z-index: 10; 90 + } 91 + 92 + .stream-container peek-card.active-card .card-title { 93 + font-size: 18px; 94 + } 95 + 96 + .stream-container peek-card.active-card .card-url { 97 + font-size: 13px; 98 + } 99 + 100 + .stream-container peek-card.active-card .card-favicon { 101 + width: 24px; 102 + height: 24px; 103 + } 104 + 105 + .stream-container peek-card.active-card .card-time, 106 + .stream-container peek-card.active-card .card-domain { 107 + font-size: 13px; 108 + } 109 + 110 + /* Non-active cards are smaller */ 111 + .stream-container peek-card:not(.active-card) { 112 + transform: scale(0.88); 113 + opacity: 0.75; 80 114 } 81 115 82 116 /* Card slotted content */ ··· 156 190 color: var(--base05); 157 191 } 158 192 159 - /* URL input at bottom */ 160 - .url-input-container { 193 + /* Navbar at bottom */ 194 + .navbar-container { 161 195 padding: 8px 16px 12px 16px; 162 196 flex-shrink: 0; 163 - border-top: 1px solid var(--base02); 164 197 } 165 198 166 - peek-input.url-input { 167 - display: block; 168 - width: 100%; 169 - --peek-input-bg: var(--base01); 170 - --peek-input-border: var(--base02); 171 - --peek-input-height: 36px; 172 - } 173 - 174 - peek-input.url-input::part(input) { 175 - color: var(--base05); 176 - font-size: 13px; 199 + peek-navbar { 200 + --peek-navbar-bg: rgba(var(--base01-rgb, 30, 30, 46), 0.85); 201 + backdrop-filter: blur(12px); 202 + -webkit-backdrop-filter: blur(12px); 203 + border-radius: 10px; 177 204 } 178 205 179 206 /* Empty state */ 180 207 .empty-state { 181 208 text-align: center; 182 209 padding: 48px 24px; 183 - color: var(--base03); 210 + color: var(--base04); 184 211 font-size: 14px; 185 212 flex: 1; 186 213 display: flex;
+3 -7
extensions/pagestream/home.html
··· 27 27 <script type="module"> 28 28 import 'peek://app/components/peek-card.js'; 29 29 import 'peek://app/components/peek-input.js'; 30 + import 'peek://app/components/peek-navbar.js'; 30 31 </script> 31 32 </head> 32 33 <body> ··· 44 45 </div> 45 46 </div> 46 47 47 - <div class="url-input-container"> 48 - <peek-input 49 - class="url-input" 50 - id="url-input" 51 - placeholder="Enter URL... (Cmd+L)" 52 - type="url" 53 - ></peek-input> 48 + <div class="navbar-container"> 49 + <peek-navbar id="navbar"></peek-navbar> 54 50 </div> 55 51 56 52 <script type="module" src="home.js"></script>
+27 -18
extensions/pagestream/home.js
··· 479 479 */ 480 480 const handleKeydown = (e) => { 481 481 const searchInput = document.querySelector('peek-input.search-input'); 482 - const urlInput = document.getElementById('url-input'); 482 + const navbarEl = document.getElementById('navbar'); 483 483 484 484 // Check if an input is focused 485 485 const isSearchFocused = document.activeElement === searchInput || 486 486 (searchInput && searchInput.shadowRoot?.activeElement); 487 - const isUrlFocused = document.activeElement === urlInput || 488 - (urlInput && urlInput.shadowRoot?.activeElement); 487 + const isUrlFocused = document.activeElement === navbarEl || 488 + (navbarEl && navbarEl.shadowRoot?.activeElement); 489 489 const isInputFocused = isSearchFocused || isUrlFocused; 490 490 491 - // Cmd+L or / to focus URL input (when not in an input) 491 + // Cmd+L or / to focus navbar URL input (when not in an input) 492 492 if ((e.key === 'l' && (e.metaKey || e.ctrlKey)) || (e.key === '/' && !isInputFocused)) { 493 493 e.preventDefault(); 494 - urlInput.focus(); 494 + navbarEl.focusUrl(); 495 495 return; 496 496 } 497 497 ··· 507 507 return; 508 508 } 509 509 510 - // Handle Enter in URL input 510 + // Enter in navbar URL is handled by the peek-navbar component's navigate event. 511 + // Skip keyboard navigation when navbar URL input is focused. 511 512 if (e.key === 'Enter' && isUrlFocused) { 512 - e.preventDefault(); 513 - const value = urlInput.value; 514 - const url = getValidURL(value); 515 - if (url) { 516 - urlInput.value = ''; 517 - api.window.open(url, { 518 - role: 'content', 519 - key: url, 520 - width: 800, 521 - height: 600 522 - }); 523 - } 524 513 return; 525 514 } 526 515 ··· 588 577 // Register escape handler 589 578 api.escape.onEscape(handleEscape); 590 579 580 + // Set up navbar component for URL navigation 581 + const navbarEl = document.getElementById('navbar'); 582 + navbarEl.addEventListener('navigate', (e) => { 583 + const url = e.detail?.url; 584 + if (url) { 585 + debug && console.log('[pagestream] Navigating to:', url); 586 + api.window.open(url, { 587 + role: 'content', 588 + key: url, 589 + width: 800, 590 + height: 600 591 + }); 592 + } 593 + }); 594 + 591 595 // Set up search input 592 596 const searchInput = document.querySelector('peek-input.search-input'); 593 597 searchInput.addEventListener('input', (e) => { ··· 610 614 611 615 // Scroll to bottom (newest) on initial load 612 616 scrollToBottom(); 617 + 618 + // Auto-focus the navbar URL input on open 619 + requestAnimationFrame(() => { 620 + navbarEl.focusUrl(); 621 + }); 613 622 614 623 // Debounced refresh for reactive updates 615 624 const debouncedRefresh = debounce(async () => {