experiments in a post-browser web
10
fork

Configure Feed

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

fix(tags): replace modal with inline detail, fix ESC navigation

- Remove peek-dialog modal entirely, replacing with inline detail view
that appears in the main content area when an item card is clicked
- Fix ESC handler using groups pattern: extracted handleEscape function
with view state (VIEW_LIST / VIEW_DETAIL) instead of checking dialog
open state, which was broken because preload closes dialogs before
the extension's onEscape callback fires
- ESC flow: detail view -> list view -> clear search -> clear tag
filter -> clear type filter -> close window
- Add getItemDisplayInfo() to share display logic between cards and
detail view (removes duplicated title/subtitle/favicon computation)
- Detail view shows: item title/URL, content for text items (scrollable),
Open Page button, Edit in Editor button, tag management
- Remove all dialog/overlay/blur CSS; add inline detail view styles
- Remove peek-dialog import from HTML

+280 -317
+77 -83
extensions/tags/home.css
··· 350 350 font-size: 14px; 351 351 } 352 352 353 - /* Modal overlay */ 354 - /* Modal - peek-dialog handles overlay and structure */ 355 - peek-dialog#editModal { 356 - --peek-dialog-bg: var(--base00); 357 - --peek-dialog-overlay-bg: rgba(0, 0, 0, 0.5); 353 + /* Card open button - opens URL in webview */ 354 + .card-open-btn { 355 + display: flex; 356 + align-items: center; 357 + justify-content: center; 358 + width: 28px; 359 + height: 28px; 360 + background: var(--base02); 361 + border: 1px solid var(--base03); 362 + border-radius: 6px; 363 + cursor: pointer; 364 + color: var(--base04); 365 + flex-shrink: 0; 366 + transition: all 0.15s; 367 + padding: 0; 358 368 } 359 369 360 - .modal-body { 361 - padding: 20px; 362 - overflow-y: auto; 363 - flex: 1; 370 + .card-open-btn:hover { 371 + background: var(--base0D); 372 + border-color: var(--base0D); 373 + color: var(--base00); 374 + } 375 + 376 + /* ==================== Inline Detail View ==================== */ 377 + 378 + .detail-view { 379 + padding: 0; 364 380 } 365 381 366 - /* Modal item info */ 367 - .modal-item-info { 382 + .detail-header { 368 383 display: flex; 369 - align-items: center; 370 - gap: 12px; 371 - padding: 12px; 372 - background: var(--base01); 373 - border-radius: 8px; 384 + align-items: flex-start; 385 + gap: 14px; 386 + padding: 16px 0; 387 + border-bottom: 1px solid var(--base02); 374 388 margin-bottom: 20px; 375 389 } 376 390 377 - .modal-favicon { 378 - width: 32px; 379 - height: 32px; 380 - border-radius: 4px; 391 + .detail-favicon { 392 + width: 36px; 393 + height: 36px; 394 + border-radius: 6px; 381 395 background: var(--base02); 382 396 object-fit: contain; 397 + flex-shrink: 0; 383 398 } 384 399 385 - .modal-item-details { 400 + .detail-header-info { 386 401 flex: 1; 387 402 min-width: 0; 388 403 } 389 404 390 - .modal-item-title { 391 - font-size: 14px; 392 - font-weight: 500; 405 + .detail-title { 406 + font-size: 16px; 407 + font-weight: 600; 393 408 color: var(--base05); 394 - white-space: nowrap; 395 - overflow: hidden; 396 - text-overflow: ellipsis; 409 + word-break: break-word; 410 + line-height: 1.4; 397 411 } 398 412 399 - .modal-item-url { 400 - font-size: 12px; 413 + .detail-url { 414 + font-size: 13px; 401 415 color: var(--base04); 402 416 white-space: nowrap; 403 417 overflow: hidden; 404 418 text-overflow: ellipsis; 405 - margin-top: 2px; 419 + margin-top: 4px; 420 + } 421 + 422 + /* Detail actions row */ 423 + .detail-actions { 424 + display: flex; 425 + gap: 8px; 426 + margin-bottom: 20px; 427 + } 428 + 429 + .detail-actions:empty { 430 + display: none; 431 + } 432 + 433 + /* Detail content section (for text items) */ 434 + .detail-content-section { 435 + margin-bottom: 20px; 436 + } 437 + 438 + .detail-content-text { 439 + background: var(--base01); 440 + border: 1px solid var(--base02); 441 + border-radius: 8px; 442 + padding: 14px; 443 + font-size: 14px; 444 + color: var(--base05); 445 + white-space: pre-wrap; 446 + word-break: break-word; 447 + max-height: 300px; 448 + overflow-y: auto; 449 + line-height: 1.6; 406 450 } 407 451 408 - /* Edit sections */ 409 - .edit-section { 452 + /* Detail sections (tags) */ 453 + .detail-section { 410 454 margin-bottom: 16px; 411 455 } 412 456 413 - .edit-label { 457 + .detail-label { 414 458 display: block; 415 459 font-size: 12px; 416 460 font-weight: 600; ··· 529 573 opacity: 0.5; 530 574 cursor: default; 531 575 } 532 - 533 - 534 - /* Card open button - opens URL in webview */ 535 - .card-open-btn { 536 - display: flex; 537 - align-items: center; 538 - justify-content: center; 539 - width: 28px; 540 - height: 28px; 541 - background: var(--base02); 542 - border: 1px solid var(--base03); 543 - border-radius: 6px; 544 - cursor: pointer; 545 - color: var(--base04); 546 - flex-shrink: 0; 547 - transition: all 0.15s; 548 - padding: 0; 549 - } 550 - 551 - .card-open-btn:hover { 552 - background: var(--base0D); 553 - border-color: var(--base0D); 554 - color: var(--base00); 555 - } 556 - 557 - /* Modal open page button */ 558 - .modal-open-page-btn { 559 - display: flex; 560 - align-items: center; 561 - gap: 6px; 562 - padding: 6px 12px; 563 - background: var(--base0D); 564 - border: none; 565 - border-radius: 6px; 566 - font-size: 12px; 567 - font-weight: 500; 568 - color: var(--base00); 569 - cursor: pointer; 570 - white-space: nowrap; 571 - flex-shrink: 0; 572 - transition: all 0.15s; 573 - } 574 - 575 - .modal-open-page-btn:hover { 576 - filter: brightness(1.1); 577 - } 578 - 579 - .modal-open-page-btn svg { 580 - flex-shrink: 0; 581 - }
+32 -36
extensions/tags/home.html
··· 30 30 import 'peek://app/components/peek-input.js'; 31 31 import 'peek://app/components/peek-button.js'; 32 32 import 'peek://app/components/peek-button-group.js'; 33 - import 'peek://app/components/peek-dialog.js'; 34 33 </script> 35 34 </head> 36 35 <body> ··· 96 95 </aside> 97 96 98 97 <main class="items-container"> 98 + <!-- Card grid view (list) --> 99 99 <peek-grid class="cards" min-item-width="280" gap="12"></peek-grid> 100 - </main> 101 - </div> 102 100 103 - <!-- Edit Modal --> 104 - <peek-dialog id="editModal" size="md" close-on-backdrop close-on-escape> 105 - <span slot="header">Edit Tags</span> 106 - <div class="modal-body"> 107 - <div class="modal-item-info"> 108 - <img class="modal-favicon" src="" alt=""> 109 - <div class="modal-item-details"> 110 - <div class="modal-item-title"></div> 111 - <div class="modal-item-url"></div> 101 + <!-- Inline detail view (replaces card grid when an item is selected) --> 102 + <div class="detail-view" style="display: none;"> 103 + <div class="detail-header"> 104 + <img class="detail-favicon" src="" alt=""> 105 + <div class="detail-header-info"> 106 + <div class="detail-title"></div> 107 + <div class="detail-url"></div> 108 + </div> 112 109 </div> 113 - <peek-button class="modal-open-page-btn" variant="ghost" size="sm" style="display: none;" title="Open page"> 114 - <svg slot="prefix" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 115 - <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> 116 - <polyline points="15 3 21 3 21 9"></polyline> 117 - <line x1="10" y1="14" x2="21" y2="3"></line> 118 - </svg> 119 - Open Page 120 - </peek-button> 121 - </div> 110 + 111 + <div class="detail-actions"></div> 112 + 113 + <div class="detail-content-section" style="display: none;"> 114 + <label class="detail-label">Content</label> 115 + <div class="detail-content-text"></div> 116 + </div> 122 117 123 - <div class="edit-section"> 124 - <label class="edit-label">Current Tags</label> 125 - <div class="current-tags"></div> 126 - </div> 118 + <div class="detail-section"> 119 + <label class="detail-label">Current Tags</label> 120 + <div class="current-tags"></div> 121 + </div> 127 122 128 - <div class="edit-section"> 129 - <label class="edit-label">Add Tag</label> 130 - <div class="new-tag-row"> 131 - <peek-input class="new-tag-input" placeholder="Enter new tag..."></peek-input> 132 - <peek-button class="add-tag-btn" variant="primary" size="sm">Add</peek-button> 123 + <div class="detail-section"> 124 + <label class="detail-label">Add Tag</label> 125 + <div class="new-tag-row"> 126 + <peek-input class="new-tag-input" placeholder="Enter new tag..."></peek-input> 127 + <peek-button class="add-tag-btn" variant="primary" size="sm">Add</peek-button> 128 + </div> 133 129 </div> 134 - </div> 135 130 136 - <div class="edit-section"> 137 - <label class="edit-label">Available Tags</label> 138 - <div class="available-tags"></div> 131 + <div class="detail-section"> 132 + <label class="detail-label">Available Tags</label> 133 + <div class="available-tags"></div> 134 + </div> 139 135 </div> 140 - </div> 141 - </peek-dialog> 136 + </main> 137 + </div> 142 138 143 139 <script type="module" src="home.js"></script> 144 140 </body>
+171 -198
extensions/tags/home.js
··· 7 7 * - Tag editing on items (add/remove tags) 8 8 * - Content search across items (URLs, titles, text) with tag combo filtering 9 9 * - Selected tags pinned to top of sidebar with visual distinction 10 + * - Inline detail view (no modal/dialog) with proper ESC navigation 10 11 */ 11 12 12 13 const api = window.app; 13 14 const debug = api?.debug; 15 + 16 + // View states (following groups pattern) 17 + const VIEW_LIST = 'list'; 18 + const VIEW_DETAIL = 'detail'; 14 19 15 20 /** 16 21 * Extract the first URL from a text string. ··· 67 72 68 73 // State 69 74 let state = { 70 - activeFilter: 'all', // 'all' | 'page' | 'text' | 'tagset' | 'image' 71 - activeTags: [], // Array of tag objects for filtering (AND logic) 72 - items: [], // All addresses 73 - tags: [], // All tags sorted by frecency 74 - itemTags: new Map(), // Map of addressId -> [tags] 75 + currentView: VIEW_LIST, // 'list' | 'detail' 76 + activeFilter: 'all', // 'all' | 'page' | 'text' | 'tagset' | 'image' 77 + activeTags: [], // Array of tag objects for filtering (AND logic) 78 + items: [], // All addresses 79 + tags: [], // All tags sorted by frecency 80 + itemTags: new Map(), // Map of addressId -> [tags] 75 81 selectedIndex: 0, 76 82 searchQuery: '', 77 - editingItem: null // Item being edited in modal 83 + editingItem: null // Item being viewed in detail 78 84 }; 79 85 80 86 // Expose state for debugging ··· 84 90 let searchInput; 85 91 let cardsContainer; 86 92 let tagList; 87 - let modalOverlay; 93 + let detailView; 88 94 89 95 /** 90 96 * Initialize the UI ··· 96 102 searchInput = document.querySelector('.search-input'); 97 103 cardsContainer = document.querySelector('.cards'); 98 104 tagList = document.querySelector('.tag-list'); 99 - modalOverlay = document.getElementById('editModal'); 105 + detailView = document.querySelector('.detail-view'); 100 106 101 107 // Load data 102 108 await loadData(); ··· 168 174 }; 169 175 170 176 /** 177 + * Internal ESC handler for tags navigation (following groups pattern). 178 + * Returns { handled: true } if we navigated internally. 179 + * Returns { handled: false } if at root (list view) and window should close. 180 + */ 181 + const handleEscape = () => { 182 + console.log('[tags:esc] handleEscape called, view:', state.currentView, 'searchQuery:', state.searchQuery); 183 + 184 + // If in detail view, navigate back to list 185 + if (state.currentView === VIEW_DETAIL) { 186 + // Use setTimeout to ensure handler returns before DOM work starts 187 + setTimeout(() => { 188 + showList(); 189 + }, 0); 190 + console.log('[tags:esc] VIEW_DETAIL -> navigating to list, returning handled: true'); 191 + return { handled: true }; 192 + } 193 + 194 + // If search has content, clear it 195 + if (state.searchQuery) { 196 + state.searchQuery = ''; 197 + searchInput.value = ''; 198 + render(); 199 + console.log('[tags:esc] Cleared search, returning handled: true'); 200 + return { handled: true }; 201 + } 202 + 203 + // If tag filter is active, clear it 204 + if (state.activeTags.length > 0) { 205 + clearTagFilter(); 206 + console.log('[tags:esc] Cleared tag filter, returning handled: true'); 207 + return { handled: true }; 208 + } 209 + 210 + // If type filter is active, clear it 211 + if (state.activeFilter !== 'all') { 212 + state.activeFilter = 'all'; 213 + state.selectedIndex = 0; 214 + render(); 215 + console.log('[tags:esc] Cleared type filter, returning handled: true'); 216 + return { handled: true }; 217 + } 218 + 219 + // Nothing to clear, let window close 220 + console.log('[tags:esc] At root (VIEW_LIST), returning handled: false'); 221 + return { handled: false }; 222 + }; 223 + 224 + /** 171 225 * Set up all event listeners 172 226 */ 173 227 const setupEventListeners = () => { ··· 175 229 const debouncedRefresh = debounce(async () => { 176 230 debug && console.log('[tags] debounced refresh triggered'); 177 231 await loadData(); 178 - render(); 232 + if (state.currentView === VIEW_LIST) { 233 + render(); 234 + } else if (state.currentView === VIEW_DETAIL && state.editingItem) { 235 + // Refresh detail view with updated data 236 + const updatedItem = state.items.find(i => i.id === state.editingItem.id); 237 + if (updatedItem) { 238 + state.editingItem = updatedItem; 239 + showDetailView(updatedItem); 240 + } else { 241 + // Item was deleted, go back to list 242 + showList(); 243 + } 244 + } 179 245 }, 150); 180 246 181 247 // Subscribe to tag events for reactive updates ··· 242 308 }); 243 309 }); 244 310 245 - // Modal close - peek-dialog handles close-on-backdrop; ESC is handled by the preload escape system 246 - modalOverlay.addEventListener('close', closeModal); 247 - 248 - // New tag input 311 + // New tag input in detail view 249 312 const newTagInput = document.querySelector('.new-tag-input'); 250 313 const addTagBtn = document.querySelector('.add-tag-btn'); 251 314 ··· 264 327 // Keyboard navigation 265 328 document.addEventListener('keydown', handleKeydown); 266 329 267 - // Escape handling 330 + // Register escape handler (following groups pattern - extracted function, not inline) 268 331 if (api.escape) { 269 - api.escape.onEscape(() => { 270 - // If the detail modal is open (or was just closed by the preload escape system), 271 - // treat ESC as "go back to tags list" rather than closing the window. 272 - // The preload closes peek-dialog elements before this callback fires, 273 - // so we check state.editingItem to know a modal was just dismissed. 274 - if (state.editingItem) { 275 - state.editingItem = null; 276 - // Ensure the dialog is closed (preload may have already closed it) 277 - if (modalOverlay.open) { 278 - modalOverlay.close(); 279 - } 280 - return { handled: true }; 281 - } 282 - 283 - // If search has content, clear it 284 - if (state.searchQuery) { 285 - state.searchQuery = ''; 286 - searchInput.value = ''; 287 - render(); 288 - return { handled: true }; 289 - } 290 - 291 - // If tag filter is active, clear it 292 - if (state.activeTags.length > 0) { 293 - clearTagFilter(); 294 - return { handled: true }; 295 - } 296 - 297 - // If type filter is active, clear it 298 - if (state.activeFilter !== 'all') { 299 - state.activeFilter = 'all'; 300 - state.selectedIndex = 0; 301 - render(); 302 - return { handled: true }; 303 - } 304 - 305 - // Nothing to clear, let window close 306 - return { handled: false }; 307 - }); 332 + api.escape.onEscape(handleEscape); 308 333 } 309 334 }; 310 335 ··· 312 337 * Handle keyboard navigation 313 338 */ 314 339 const handleKeydown = (e) => { 315 - // If modal is open, let the keydown be absorbed (don't process navigation keys). 316 - // ESC closing of dialogs is handled by the preload escape system which intercepts 317 - // ESC on keyDown via before-input-event before DOM dispatch reaches here. 318 - if (modalOverlay.open) { 340 + // If in detail view, don't process grid navigation 341 + if (state.currentView === VIEW_DETAIL) { 319 342 return; 320 343 } 321 344 ··· 429 452 selected.click(); 430 453 } 431 454 }; 455 + 456 + /** 457 + * Show the list view (card grid), hide detail view 458 + */ 459 + const showList = () => { 460 + state.currentView = VIEW_LIST; 461 + state.editingItem = null; 462 + 463 + cardsContainer.style.display = ''; 464 + detailView.style.display = 'none'; 465 + 466 + render(); 467 + }; 468 + 469 + // Expose for testing (Playwright escape doesn't trigger Electron before-input-event) 470 + window.showList = showList; 432 471 433 472 /** 434 473 * Update filter button counts ··· 699 738 }; 700 739 701 740 /** 702 - * Create a card element for an item 741 + * Compute display properties for an item (shared by card and detail view) 703 742 */ 704 - const createItemCard = (item) => { 705 - const card = document.createElement('peek-card'); 706 - card.interactive = true; 707 - card.dataset.itemId = item.id; 708 - 743 + const getItemDisplayInfo = (item) => { 709 744 const tags = state.itemTags.get(item.id) || []; 710 - 711 - // Handle both old address schema and new item schema 712 745 const isAddress = !!item.uri; 713 746 const itemType = item.type || 'url'; 714 - 715 - // For text items, check if content contains a URL 716 747 const noteUrl = (itemType === 'text') ? extractUrl(item.content) : null; 717 748 718 749 let title, subtitle, faviconUrl; 719 750 720 751 if (isAddress) { 721 - // Old address schema 722 752 title = item.title || item.uri; 723 753 subtitle = item.uri; 724 754 faviconUrl = item.favicon || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🌐</text></svg>'; 725 755 } else { 726 - // New item schema 727 756 if (itemType === 'url') { 728 757 title = item.content; 729 758 subtitle = item.content; ··· 731 760 } else if (itemType === 'text') { 732 761 title = item.content.substring(0, 100) + (item.content.length > 100 ? '...' : ''); 733 762 if (noteUrl) { 734 - // Note contains a URL - show the URL as subtitle and use a link icon 735 763 subtitle = noteUrl; 736 764 faviconUrl = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🔗</text></svg>'; 737 765 } else { ··· 749 777 } 750 778 } 751 779 752 - // Build the open button for items with URLs (both url type and notes with URLs) 753 780 const itemUrl = isAddress ? item.uri : (itemType === 'url') ? item.content : noteUrl; 781 + 782 + return { tags, isAddress, itemType, noteUrl, title, subtitle, faviconUrl, itemUrl }; 783 + }; 784 + 785 + /** 786 + * Create a card element for an item 787 + */ 788 + const createItemCard = (item) => { 789 + const card = document.createElement('peek-card'); 790 + card.interactive = true; 791 + card.dataset.itemId = item.id; 792 + 793 + const { tags, itemUrl, title, subtitle, faviconUrl } = getItemDisplayInfo(item); 794 + 795 + // Build the open button for items with URLs (both url type and notes with URLs) 754 796 const openBtnHtml = itemUrl 755 797 ? `<button class="card-open-btn" data-url="${escapeHtml(itemUrl)}" title="Open page">` + 756 798 `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">` + ··· 784 826 }); 785 827 } 786 828 787 - // Click on card to open edit modal 829 + // Click on card to open inline detail view 788 830 card.addEventListener('click', (e) => { 789 831 // If clicking a tag, add it to filter (toggle behavior) 790 832 if (e.target.classList.contains('card-tag')) { ··· 803 845 return; 804 846 } 805 847 806 - // Open edit modal 807 - openEditModal(item); 848 + // Show inline detail view 849 + showDetailView(item); 808 850 }); 809 851 810 852 return card; 811 853 }; 812 854 813 855 /** 814 - * Open the edit modal for an item 856 + * Show the inline detail view for an item (replaces card grid) 815 857 */ 816 - const openEditModal = (item) => { 858 + const showDetailView = (item) => { 859 + state.currentView = VIEW_DETAIL; 817 860 state.editingItem = item; 818 861 819 - const modal = modalOverlay; 820 - const tags = state.itemTags.get(item.id) || []; 862 + const { tags, itemType, itemUrl, title, subtitle, faviconUrl } = getItemDisplayInfo(item); 821 863 822 - // Handle both old address schema and new item schema 823 - const isAddress = !!item.uri; 824 - const itemType = item.type || 'url'; 825 - 826 - // For text items, check if content contains a URL 827 - const noteUrl = (itemType === 'text') ? extractUrl(item.content) : null; 828 - 829 - let title, subtitle, faviconUrl; 830 - 831 - if (isAddress) { 832 - // Old address schema 833 - title = item.title || item.uri; 834 - subtitle = item.uri; 835 - faviconUrl = item.favicon || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🌐</text></svg>'; 836 - } else { 837 - // New item schema 838 - if (itemType === 'url') { 839 - title = item.content; 840 - subtitle = item.content; 841 - faviconUrl = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🌐</text></svg>'; 842 - } else if (itemType === 'text') { 843 - title = item.content || ''; 844 - if (noteUrl) { 845 - subtitle = noteUrl; 846 - faviconUrl = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🔗</text></svg>'; 847 - } else { 848 - subtitle = 'Text'; 849 - faviconUrl = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">📝</text></svg>'; 850 - } 851 - } else if (itemType === 'tagset') { 852 - title = 'Tag Set'; 853 - subtitle = tags.length > 0 ? tags.map(t => t.name).join(', ') : 'Empty tagset'; 854 - faviconUrl = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🏷️</text></svg>'; 855 - } else if (itemType === 'image') { 856 - title = item.content || 'Image'; 857 - subtitle = 'Image'; 858 - faviconUrl = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🖼️</text></svg>'; 859 - } 860 - } 864 + // Hide card grid, show detail view 865 + cardsContainer.style.display = 'none'; 866 + detailView.style.display = ''; 861 867 862 - // Set item info 863 - modal.querySelector('.modal-favicon').src = faviconUrl; 864 - const titleEl = modal.querySelector('.modal-item-title'); 868 + // Set header info 869 + detailView.querySelector('.detail-favicon').src = faviconUrl; 870 + const titleEl = detailView.querySelector('.detail-title'); 865 871 titleEl.textContent = title; 866 - // For text items, make the title scrollable to show full content 867 - if (itemType === 'text') { 868 - titleEl.style.maxHeight = '200px'; 869 - titleEl.style.overflowY = 'auto'; 870 - titleEl.style.whiteSpace = 'pre-wrap'; 871 - titleEl.style.wordBreak = 'break-word'; 872 - } else { 873 - titleEl.style.maxHeight = ''; 874 - titleEl.style.overflowY = ''; 875 - titleEl.style.whiteSpace = ''; 876 - titleEl.style.wordBreak = ''; 877 - } 878 - modal.querySelector('.modal-item-url').textContent = subtitle; 872 + detailView.querySelector('.detail-url').textContent = subtitle; 879 873 880 - // Show or hide the "Open Page" button based on whether item has a URL 881 - const openPageBtn = modal.querySelector('.modal-open-page-btn'); 882 - const itemUrl = isAddress ? item.uri : (itemType === 'url' ? item.content : noteUrl); 883 - if (openPageBtn) { 884 - if (itemUrl) { 885 - openPageBtn.style.display = 'flex'; 886 - openPageBtn.dataset.url = itemUrl; 887 - // Clone to remove old listeners, then add fresh listener 888 - const newBtn = openPageBtn.cloneNode(true); 889 - openPageBtn.parentNode.replaceChild(newBtn, openPageBtn); 890 - newBtn.addEventListener('click', () => { 891 - openItemUrl(newBtn.dataset.url); 892 - closeModal(); 893 - }); 894 - } else { 895 - openPageBtn.style.display = 'none'; 896 - } 874 + // Build actions row 875 + const actionsEl = detailView.querySelector('.detail-actions'); 876 + actionsEl.innerHTML = ''; 877 + 878 + if (itemUrl) { 879 + const openPageBtn = document.createElement('peek-button'); 880 + openPageBtn.variant = 'primary'; 881 + openPageBtn.size = 'sm'; 882 + openPageBtn.innerHTML = ` 883 + <svg slot="prefix" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 884 + <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> 885 + <polyline points="15 3 21 3 21 9"></polyline> 886 + <line x1="10" y1="14" x2="21" y2="3"></line> 887 + </svg> 888 + Open Page 889 + `; 890 + openPageBtn.addEventListener('click', () => openItemUrl(itemUrl)); 891 + actionsEl.appendChild(openPageBtn); 897 892 } 898 893 899 - // Show or hide "Edit in Editor" button for text items 900 - let editInEditorBtn = modal.querySelector('.modal-edit-editor-btn'); 901 894 if (itemType === 'text') { 902 - if (!editInEditorBtn) { 903 - // Create the button on first use 904 - editInEditorBtn = document.createElement('peek-button'); 905 - editInEditorBtn.className = 'modal-edit-editor-btn'; 906 - editInEditorBtn.variant = 'ghost'; 907 - editInEditorBtn.size = 'sm'; 908 - editInEditorBtn.title = 'Edit in Editor'; 909 - editInEditorBtn.innerHTML = ` 910 - <svg slot="prefix" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 911 - <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 912 - <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 913 - </svg> 914 - Edit in Editor 915 - `; 916 - // Insert after the Open Page button's parent area 917 - const itemInfoEl = modal.querySelector('.modal-item-info'); 918 - itemInfoEl.appendChild(editInEditorBtn); 919 - } 920 - editInEditorBtn.style.display = 'flex'; 921 - // Clone to remove old listeners 922 - const newEditBtn = editInEditorBtn.cloneNode(true); 923 - editInEditorBtn.parentNode.replaceChild(newEditBtn, editInEditorBtn); 924 - newEditBtn.addEventListener('click', () => { 895 + const editBtn = document.createElement('peek-button'); 896 + editBtn.variant = 'ghost'; 897 + editBtn.size = 'sm'; 898 + editBtn.innerHTML = ` 899 + <svg slot="prefix" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 900 + <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 901 + <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 902 + </svg> 903 + Edit in Editor 904 + `; 905 + editBtn.addEventListener('click', () => { 925 906 api.publish('editor:open', { itemId: item.id }, api.scopes.GLOBAL); 926 - closeModal(); 927 907 }); 928 - } else if (editInEditorBtn) { 929 - editInEditorBtn.style.display = 'none'; 908 + actionsEl.appendChild(editBtn); 909 + } 910 + 911 + // Show content section for text items 912 + const contentSection = detailView.querySelector('.detail-content-section'); 913 + if (itemType === 'text' && item.content) { 914 + contentSection.style.display = ''; 915 + detailView.querySelector('.detail-content-text').textContent = item.content; 916 + } else { 917 + contentSection.style.display = 'none'; 930 918 } 931 919 932 920 // Render current tags ··· 937 925 938 926 // Clear new tag input 939 927 document.querySelector('.new-tag-input').value = ''; 940 - 941 - // Show modal 942 - modalOverlay.showModal(); 943 928 }; 944 929 945 930 /** 946 - * Close the edit modal 947 - */ 948 - const closeModal = () => { 949 - modalOverlay.close(); 950 - state.editingItem = null; 951 - }; 952 - 953 - /** 954 - * Render current tags in modal 931 + * Render current tags in detail view 955 932 */ 956 933 const renderCurrentTags = (tags) => { 957 934 const container = document.querySelector('.current-tags'); ··· 978 955 }; 979 956 980 957 /** 981 - * Render available tags in modal 958 + * Render available tags in detail view 982 959 */ 983 960 const renderAvailableTags = (currentTags) => { 984 961 const container = document.querySelector('.available-tags'); ··· 1021 998 state.itemTags.set(state.editingItem.id, tags); 1022 999 } 1023 1000 1024 - // Re-render modal 1001 + // Re-render detail view tags 1025 1002 renderCurrentTags(tags); 1026 1003 renderAvailableTags(tags); 1027 - 1028 - // Re-render cards to show updated tags 1029 - renderCards(); 1030 1004 } else { 1031 1005 console.error('[tags] Failed to add tag:', result.error); 1032 1006 } ··· 1045 1019 tags = tags.filter(t => t.id !== tag.id); 1046 1020 state.itemTags.set(state.editingItem.id, tags); 1047 1021 1048 - // Re-render modal 1022 + // Re-render detail view tags 1049 1023 renderCurrentTags(tags); 1050 1024 renderAvailableTags(tags); 1051 1025 1052 - // Re-render cards and sidebar 1053 - renderCards(); 1026 + // Re-render sidebar 1054 1027 renderTagSidebar(); 1055 1028 } else { 1056 1029 console.error('[tags] Failed to remove tag:', result.error);