experiments in a post-browser web
10
fork

Configure Feed

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

feat(page): add group mode widget to page host top-right

+137 -126
+32 -55
app/page/index.html
··· 166 166 cursor: grabbing; 167 167 } 168 168 169 - /* Mode indicator - subtle corner badge */ 170 - .mode-indicator { 171 - position: absolute; 172 - top: 8px; 173 - right: 8px; 174 - display: none; 169 + /* --- Group mode widget --- */ 170 + 171 + .group-mode-widget-body { 172 + display: flex; 175 173 align-items: center; 176 174 gap: 6px; 177 - padding: 4px 10px; 178 - border-radius: 12px; 179 - font-size: 11px; 180 - font-weight: 500; 181 - font-family: var(--theme-font-sans, system-ui, -apple-system, sans-serif); 182 - z-index: 200; 183 - pointer-events: none; 184 - transition: opacity 0.2s ease; 185 175 } 186 176 187 - .mode-indicator.visible { 188 - display: flex; 189 - } 190 - 191 - .mode-indicator .mode-label { 192 - text-transform: capitalize; 193 - } 194 - 195 - .mode-indicator .group-name { 196 - display: none; 197 - opacity: 0.9; 198 - } 199 - 200 - /* Mode-specific styles */ 201 - .mode-indicator[data-mode="page"] { 202 - display: none; /* Hide in page mode - it's the default */ 203 - } 204 - 205 - .mode-indicator[data-mode="group"] { 206 - display: flex; 207 - background: rgba(153, 102, 204, 0.85); 208 - color: white; 209 - border: 1px solid rgba(153, 102, 204, 0.9); 177 + .group-mode-dot { 178 + width: 8px; 179 + height: 8px; 180 + border-radius: 50%; 181 + flex-shrink: 0; 210 182 } 211 183 212 - .mode-indicator[data-mode="group"] .group-name { 213 - display: inline; 184 + .group-mode-name { 185 + font-size: 11px; 186 + font-weight: 500; 187 + color: var(--theme-text-secondary, #aaa); 188 + overflow: hidden; 189 + text-overflow: ellipsis; 190 + white-space: nowrap; 214 191 } 215 192 216 193 /* --- Panel / Widget collapse toggle --- */ ··· 264 241 display: flex; 265 242 flex-direction: column; 266 243 gap: 8px; 267 - z-index: 150; 244 + z-index: 140; 268 245 pointer-events: none; /* Container itself is transparent to clicks */ 246 + opacity: 0; 247 + transition: opacity 0.5s ease; 248 + } 249 + 250 + .widget-container.visible { 251 + opacity: 1; 269 252 } 270 253 271 254 .widget { 272 255 pointer-events: auto; 273 - background: color-mix(in srgb, var(--base01, var(--theme-bg-secondary)) 92%, transparent); 274 - backdrop-filter: blur(12px); 275 - -webkit-backdrop-filter: blur(12px); 256 + background: color-mix(in srgb, var(--base00) 85%, transparent); 257 + backdrop-filter: blur(20px); 258 + -webkit-backdrop-filter: blur(20px); 259 + border: var(--theme-surface-border, 1px solid rgba(255, 255, 255, 0.08)); 276 260 color: var(--theme-text, #e0e0e0); 277 261 font-family: var(--theme-font-sans, system-ui, -apple-system, BlinkMacSystemFont, sans-serif); 278 - font-size: 13px; 262 + font-size: 12px; 279 263 border-radius: 10px; 280 264 overflow: hidden; 281 265 opacity: 0; 282 266 transition: opacity 0.5s ease; 283 - max-width: 280px; 284 - min-width: 200px; 285 267 } 286 268 287 269 .widget.visible { ··· 296 278 display: flex; 297 279 align-items: center; 298 280 gap: 8px; 299 - padding: 8px 10px; 281 + padding: 8px 10px 4px 10px; 300 282 user-select: none; 301 283 -webkit-user-select: none; 302 284 } 303 285 304 286 .widget-title { 305 287 flex: 1; 306 - font-size: 11px; 288 + font-size: 10px; 307 289 font-weight: 600; 308 290 text-transform: uppercase; 309 - letter-spacing: 0.04em; 291 + letter-spacing: 0.05em; 310 292 color: var(--theme-text-secondary, #aaa); 311 293 overflow: hidden; 312 294 text-overflow: ellipsis; ··· 337 319 } 338 320 339 321 .widget-body { 340 - padding: 0 10px 10px 10px; 322 + padding: 4px 10px 10px 10px; 341 323 } 342 324 343 325 /* OpenSearch discovery widget specific styles */ ··· 1199 1181 <div class="page-layout"> 1200 1182 <div class="left-gutter"></div> 1201 1183 <div class="center-column" id="center-column"> 1202 - <!-- Mode indicator - shown when in group mode --> 1203 - <div id="mode-indicator" class="mode-indicator"> 1204 - <span class="mode-label">page</span> 1205 - <span class="group-name"></span> 1206 - </div> 1207 1184 <div class="trigger-zone" id="trigger-zone"></div> 1208 1185 <script type="module"> 1209 1186 import 'peek://app/components/peek-navbar.js';
+105 -71
app/page/page.js
··· 129 129 navNe: document.getElementById('resize-nav-ne'), 130 130 navNw: document.getElementById('resize-nav-nw'), 131 131 }; 132 - const modeIndicator = document.getElementById('mode-indicator'); 133 132 const widgetContainer = document.getElementById('widget-container'); 134 133 const centerColumn = document.getElementById('center-column'); 135 134 const pageInfoPanel = document.getElementById('page-info-panel'); ··· 282 281 resizeHandles.navNw.style.top = `0px`; 283 282 } 284 283 285 - // Mode indicator — top-right corner of the webview 286 - if (modeIndicator) { 287 - modeIndicator.style.left = `${webviewLeft + webviewWidth - 120}px`; 288 - modeIndicator.style.top = `${webviewTop + 8}px`; 289 - } 290 - 291 - // Widget container — right side of the webview with a gap 292 - if (widgetContainer) { 293 - const widgetGap = 12; 294 - widgetContainer.style.left = `${webviewLeft + webviewWidth + widgetGap}px`; 295 - widgetContainer.style.top = `${webviewTop}px`; 296 - } 297 - 298 284 // Panel top offset — enough clearance below the top edge of the web content area 299 285 const panelTopOffset = webviewTop + 40; 300 286 ··· 314 300 extensionsPanel.style.width = `${PANEL_WIDTH}px`; 315 301 } 316 302 317 - // Tags Panel — right side, top of right stack 303 + // Right-side stack: widgets first (top), then tags, notes, entities 304 + const rightLeft = webviewLeft + webviewWidth - PANEL_OVERLAP; 305 + let rightStackTop = panelTopOffset; 306 + 307 + // Widget container — top of right stack 308 + if (widgetContainer) { 309 + widgetContainer.style.left = `${rightLeft}px`; 310 + widgetContainer.style.top = `${rightStackTop}px`; 311 + widgetContainer.style.width = `${PANEL_WIDTH}px`; 312 + const widgetH = (widgetContainer.classList.contains('visible') && widgetContainer.offsetHeight > 0) ? widgetContainer.offsetHeight : 0; 313 + if (widgetH > 0) rightStackTop += widgetH + 12; 314 + } 315 + 316 + // Tags Panel — right side, below widgets 318 317 if (tagsPanel) { 319 - tagsPanel.style.left = `${webviewLeft + webviewWidth - PANEL_OVERLAP}px`; 320 - tagsPanel.style.top = `${panelTopOffset}px`; 318 + tagsPanel.style.left = `${rightLeft}px`; 319 + tagsPanel.style.top = `${rightStackTop}px`; 321 320 tagsPanel.style.width = `${PANEL_WIDTH}px`; 321 + const tagsH = (tagsPanel.classList.contains('visible')) ? tagsPanel.offsetHeight : 0; 322 + if (tagsH > 0) rightStackTop += tagsH + 12; 322 323 } 323 324 324 - // Notes Panel — right side, below tags panel 325 + // Notes Panel — right side, below tags 325 326 if (notesPanel) { 326 - notesPanel.style.left = `${webviewLeft + webviewWidth - PANEL_OVERLAP}px`; 327 - const tagsHeight = (tagsPanel && tagsPanel.classList.contains('visible')) ? tagsPanel.offsetHeight : 0; 328 - const notesPanelTop = panelTopOffset + (tagsHeight > 0 ? tagsHeight + 12 : 0); 329 - notesPanel.style.top = `${notesPanelTop}px`; 327 + notesPanel.style.left = `${rightLeft}px`; 328 + notesPanel.style.top = `${rightStackTop}px`; 330 329 notesPanel.style.width = `${PANEL_WIDTH}px`; 330 + const notesH = (notesPanel.classList.contains('visible')) ? notesPanel.offsetHeight : 0; 331 + if (notesH > 0) rightStackTop += notesH + 12; 331 332 } 332 333 333 - // Entities Panel — right side, below notes panel 334 + // Entities Panel — right side, below notes 334 335 if (entitiesPanel) { 335 - entitiesPanel.style.left = `${webviewLeft + webviewWidth - PANEL_OVERLAP}px`; 336 - const tagsHeight = (tagsPanel && tagsPanel.classList.contains('visible')) ? tagsPanel.offsetHeight : 0; 337 - const notesHeight = (notesPanel && notesPanel.classList.contains('visible')) ? notesPanel.offsetHeight : 0; 338 - let entitiesPanelTop = panelTopOffset; 339 - if (tagsHeight > 0) entitiesPanelTop += tagsHeight + 12; 340 - if (notesHeight > 0) entitiesPanelTop += notesHeight + 12; 341 - entitiesPanel.style.top = `${entitiesPanelTop}px`; 336 + entitiesPanel.style.left = `${rightLeft}px`; 337 + entitiesPanel.style.top = `${rightStackTop}px`; 342 338 entitiesPanel.style.width = `${PANEL_WIDTH}px`; 343 339 } 344 340 } ··· 1000 996 if (entitiesPanel) entitiesPanel.classList.add('visible'); 1001 997 if (extensionsPanel) extensionsPanel.classList.add('visible'); 1002 998 if (notesPanel) notesPanel.classList.add('visible'); 999 + if (widgetContainer) widgetContainer.classList.add('visible'); 1003 1000 // Re-run positions now that panels are visible (offsetHeight is accurate) 1004 1001 updatePositions(); 1005 1002 await setWindowPadding(PANEL_OVERHANG * 2); ··· 1030 1027 if (entitiesPanel) entitiesPanel.classList.remove('visible'); 1031 1028 if (extensionsPanel) extensionsPanel.classList.remove('visible'); 1032 1029 if (notesPanel) notesPanel.classList.remove('visible'); 1030 + if (widgetContainer) widgetContainer.classList.remove('visible'); 1033 1031 updatePositions(); 1034 1032 // Restore visibility now that the window has been resized 1035 1033 centerColumn.style.opacity = '1'; ··· 1045 1043 && !(tagsPanel && tagsPanel.contains(e.target)) 1046 1044 && !(entitiesPanel && entitiesPanel.contains(e.target)) 1047 1045 && !(extensionsPanel && extensionsPanel.contains(e.target)) 1048 - && !(notesPanel && notesPanel.contains(e.target))) { 1046 + && !(notesPanel && notesPanel.contains(e.target)) 1047 + && !(widgetContainer && widgetContainer.contains(e.target))) { 1049 1048 hide(); 1050 1049 } 1051 1050 }); ··· 1138 1137 if (notesPanel) { 1139 1138 notesPanel.addEventListener('mouseenter', () => { cancelHide(); }); 1140 1139 notesPanel.addEventListener('mouseleave', () => { 1140 + if (showSource === 'hover' || showSource === 'shortcut') scheduleHide(); 1141 + }); 1142 + } 1143 + if (widgetContainer) { 1144 + widgetContainer.addEventListener('mouseenter', () => { cancelHide(); }); 1145 + widgetContainer.addEventListener('mouseleave', () => { 1141 1146 if (showSource === 'hover' || showSource === 'shortcut') scheduleHide(); 1142 1147 }); 1143 1148 } ··· 1413 1418 execute: () => webview.reload() 1414 1419 }); 1415 1420 1416 - // --- Mode indicator --- 1421 + // --- Group mode widget --- 1422 + // Shows as a widget in the top-right widget container when in group mode. 1423 + // Uses the standard widget system (addWidget/removeWidget/updateWidget). 1417 1424 1425 + const GROUP_MODE_WIDGET_ID = '__group-mode__'; 1418 1426 let currentMode = 'page'; 1419 1427 let currentModeMetadata = {}; 1420 1428 1421 - function updateModeIndicator() { 1422 - if (!modeIndicator) return; 1429 + function buildGroupModeContent(metadata) { 1430 + const container = document.createElement('div'); 1431 + container.className = 'group-mode-widget-body'; 1423 1432 1424 - const label = modeIndicator.querySelector('.mode-label'); 1425 - const groupName = modeIndicator.querySelector('.group-name'); 1433 + const dot = document.createElement('span'); 1434 + dot.className = 'group-mode-dot'; 1435 + dot.style.background = metadata.color || '#9966cc'; 1436 + container.appendChild(dot); 1426 1437 1427 - modeIndicator.setAttribute('data-mode', currentMode); 1438 + const name = document.createElement('span'); 1439 + name.className = 'group-mode-name'; 1440 + name.textContent = metadata.groupName || 'Group'; 1441 + container.appendChild(name); 1428 1442 1429 - if (label) { 1430 - label.textContent = currentMode; 1431 - } 1443 + return container; 1444 + } 1432 1445 1433 - if (groupName) { 1434 - if (currentMode === 'group' && currentModeMetadata.groupName) { 1435 - groupName.textContent = currentModeMetadata.groupName; 1436 - groupName.style.display = 'inline'; 1446 + function updateGroupModeWidget() { 1447 + if (currentMode === 'group' && currentModeMetadata.groupName) { 1448 + // Show or update the group mode widget 1449 + if (widgets.has(GROUP_MODE_WIDGET_ID)) { 1450 + updateWidget(GROUP_MODE_WIDGET_ID, { 1451 + title: 'Group', 1452 + content: buildGroupModeContent(currentModeMetadata), 1453 + }); 1437 1454 } else { 1438 - groupName.style.display = 'none'; 1455 + addWidget(GROUP_MODE_WIDGET_ID, { 1456 + title: 'Group', 1457 + content: buildGroupModeContent(currentModeMetadata), 1458 + closable: false, 1459 + collapsible: false, 1460 + }); 1461 + } 1462 + DEBUG && console.log('[page] Group mode widget shown:', currentModeMetadata); 1463 + } else { 1464 + // Remove the group mode widget when not in group mode 1465 + if (widgets.has(GROUP_MODE_WIDGET_ID)) { 1466 + removeWidget(GROUP_MODE_WIDGET_ID); 1467 + DEBUG && console.log('[page] Group mode widget removed'); 1439 1468 } 1440 1469 } 1441 - 1442 - DEBUG && console.log('[page] Mode indicator updated:', currentMode, currentModeMetadata); 1443 1470 } 1444 1471 1445 1472 async function initModeContext() { ··· 1468 1495 if (modeResult.success && modeResult.data) { 1469 1496 currentMode = modeResult.data.value || 'page'; 1470 1497 currentModeMetadata = modeResult.data.metadata || {}; 1471 - updateModeIndicator(); 1498 + updateGroupModeWidget(); 1472 1499 } 1473 1500 1474 1501 api.context.watchMode((mode, entry) => { 1475 1502 if (mode) { 1476 1503 currentMode = mode; 1477 1504 currentModeMetadata = entry?.metadata || {}; 1478 - updateModeIndicator(); 1505 + updateGroupModeWidget(); 1479 1506 } 1480 1507 }); 1481 1508 ··· 1490 1517 1491 1518 const widgets = new Map(); 1492 1519 1493 - function addWidget(id, { title, content, autoDismiss = 0, onClose } = {}) { 1520 + function addWidget(id, { title, content, autoDismiss = 0, onClose, closable = true, collapsible = true } = {}) { 1494 1521 if (widgets.has(id)) { 1495 1522 removeWidget(id); 1496 1523 } ··· 1501 1528 1502 1529 const header = document.createElement('div'); 1503 1530 header.className = 'widget-header'; 1504 - 1505 - const toggle = document.createElement('button'); 1506 - toggle.className = 'widget-toggle'; 1507 - toggle.textContent = '\u25BC'; 1508 - toggle.title = 'Collapse'; 1509 1531 1510 1532 const titleEl = document.createElement('span'); 1511 1533 titleEl.className = 'widget-title'; 1512 1534 titleEl.textContent = title || ''; 1513 1535 1514 - const closeBtn = document.createElement('button'); 1515 - closeBtn.className = 'widget-close'; 1516 - closeBtn.textContent = '\u00D7'; 1517 - closeBtn.title = 'Dismiss'; 1518 - closeBtn.addEventListener('click', (e) => { 1519 - e.stopPropagation(); 1520 - removeWidget(id); 1521 - }); 1536 + let toggle = null; 1537 + if (collapsible) { 1538 + toggle = document.createElement('button'); 1539 + toggle.className = 'widget-toggle'; 1540 + toggle.textContent = '\u25BC'; 1541 + toggle.title = 'Collapse'; 1542 + header.appendChild(toggle); 1543 + } 1522 1544 1523 - header.appendChild(toggle); 1524 1545 header.appendChild(titleEl); 1525 - header.appendChild(closeBtn); 1546 + 1547 + if (closable) { 1548 + const closeBtn = document.createElement('button'); 1549 + closeBtn.className = 'widget-close'; 1550 + closeBtn.textContent = '\u00D7'; 1551 + closeBtn.title = 'Dismiss'; 1552 + closeBtn.addEventListener('click', (e) => { 1553 + e.stopPropagation(); 1554 + removeWidget(id); 1555 + }); 1556 + header.appendChild(closeBtn); 1557 + } 1526 1558 el.appendChild(header); 1527 1559 1528 1560 const bodyWrap = document.createElement('div'); ··· 1538 1570 bodyWrap.appendChild(body); 1539 1571 el.appendChild(bodyWrap); 1540 1572 1541 - toggle.addEventListener('click', (e) => { 1542 - e.stopPropagation(); 1543 - const collapsed = toggle.classList.toggle('collapsed'); 1544 - bodyWrap.classList.toggle('collapsed', collapsed); 1545 - toggle.title = collapsed ? 'Expand' : 'Collapse'; 1546 - }); 1573 + if (toggle) { 1574 + toggle.addEventListener('click', (e) => { 1575 + e.stopPropagation(); 1576 + const collapsed = toggle.classList.toggle('collapsed'); 1577 + bodyWrap.classList.toggle('collapsed', collapsed); 1578 + toggle.title = collapsed ? 'Expand' : 'Collapse'; 1579 + }); 1580 + } 1547 1581 1548 1582 widgetContainer.appendChild(el); 1549 1583