experiments in a post-browser web
10
fork

Configure Feed

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

feat(tags): migrate tags UI to shared peek-components

Phase 2 complete - migrated tags extension to use peek-components:
- Replace search input with peek-input
- Replace cards grid with peek-grid
- Replace card elements with peek-card
- Replace modal with peek-dialog
- Replace buttons with peek-button
- Update CSS to use component custom properties
- Update JS to work with peek-component APIs

All tags UI now consistent with groups extension (Phase 1).

+118 -167
+22 -101
extensions/tags/home.css
··· 175 175 padding: 16px 24px; 176 176 } 177 177 178 - .search-input { 178 + peek-input.search-input { 179 179 width: 100%; 180 - padding: 10px 14px; 181 - font-size: 14px; 182 - font-family: var(--theme-font-sans); 183 - background: var(--base01); 184 - border: 1px solid var(--base02); 185 - border-radius: 8px; 186 - color: var(--base05); 187 - outline: none; 188 - transition: all 0.15s ease; 189 - } 190 - 191 - .search-input:focus { 192 - border-color: var(--base0D); 193 - background: var(--base00); 194 - } 195 - 196 - .search-input::placeholder { 197 - color: var(--base03); 180 + --peek-input-bg: var(--base01); 181 + --peek-input-border: var(--base02); 182 + --peek-input-height: 40px; 198 183 } 199 184 200 185 /* Content wrapper - sidebar + main */ ··· 269 254 padding: 16px 24px; 270 255 } 271 256 272 - /* Cards grid */ 273 - .cards { 274 - display: grid; 275 - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); 276 - gap: 12px; 257 + /* Cards grid - peek-grid custom properties */ 258 + peek-grid.cards { 259 + --peek-grid-min-item-width: 280px; 260 + --peek-grid-gap: 12px; 277 261 } 278 262 279 - /* Card base */ 280 - .card { 281 - background: var(--base01); 282 - border-radius: 8px; 283 - padding: 14px; 284 - cursor: pointer; 285 - transition: all 0.15s ease; 286 - display: flex; 287 - flex-direction: column; 288 - gap: 10px; 263 + /* Card customization - peek-card custom properties */ 264 + peek-card { 265 + --peek-card-bg: var(--base01); 266 + --peek-card-border: transparent; 267 + --peek-card-radius: 8px; 268 + --peek-card-padding: 14px; 269 + --peek-card-gap: 10px; 289 270 } 290 271 291 - .card:hover { 292 - background: var(--base02); 293 - transform: translateY(-1px); 294 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 295 - } 296 - 297 - .card.selected { 298 - background: var(--base02); 272 + peek-card[selected] { 273 + --peek-card-bg: var(--base02); 299 274 outline: 2px solid var(--base0D); 300 275 outline-offset: -2px; 301 276 } 302 277 303 - .card.selected:hover { 304 - background: var(--base03); 305 - } 306 - 307 - /* Card header (favicon + content) */ 278 + /* Card slotted content - styles for elements inside peek-card slots */ 308 279 .card-header { 309 280 display: flex; 310 281 align-items: flex-start; ··· 380 351 } 381 352 382 353 /* Modal overlay */ 383 - .modal-overlay { 384 - display: none; 385 - position: fixed; 386 - top: 0; 387 - left: 0; 388 - right: 0; 389 - bottom: 0; 390 - background: rgba(0, 0, 0, 0.5); 391 - align-items: center; 392 - justify-content: center; 393 - z-index: 1000; 394 - } 395 - 396 - .modal-overlay.visible { 397 - display: flex; 398 - } 399 - 400 - .modal { 401 - background: var(--base00); 402 - border-radius: 12px; 403 - width: 90%; 404 - max-width: 500px; 405 - max-height: 80vh; 406 - display: flex; 407 - flex-direction: column; 408 - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); 409 - } 410 - 411 - .modal-header { 412 - display: flex; 413 - align-items: center; 414 - justify-content: space-between; 415 - padding: 16px 20px; 416 - border-bottom: 1px solid var(--base02); 417 - } 418 - 419 - .modal-title { 420 - font-size: 16px; 421 - font-weight: 600; 422 - color: var(--base05); 423 - } 424 - 425 - .modal-close { 426 - background: none; 427 - border: none; 428 - font-size: 24px; 429 - color: var(--base04); 430 - cursor: pointer; 431 - padding: 0; 432 - line-height: 1; 433 - } 434 - 435 - .modal-close:hover { 436 - color: var(--base05); 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); 437 358 } 438 359 439 360 .modal-body {
+63 -38
extensions/tags/home.html
··· 6 6 <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1"> 7 7 <title>Tags</title> 8 8 <link rel="stylesheet" type="text/css" href="home.css"> 9 + 10 + <!-- Import map for resolving bare module specifiers --> 11 + <script type="importmap"> 12 + { 13 + "imports": { 14 + "lit": "peek://node_modules/lit/index.js", 15 + "lit/": "peek://node_modules/lit/", 16 + "lit-html": "peek://node_modules/lit-html/lit-html.js", 17 + "lit-html/": "peek://node_modules/lit-html/", 18 + "lit-element": "peek://node_modules/lit-element/lit-element.js", 19 + "lit-element/": "peek://node_modules/lit-element/", 20 + "@lit/reactive-element": "peek://node_modules/@lit/reactive-element/reactive-element.js", 21 + "@lit/reactive-element/": "peek://node_modules/@lit/reactive-element/" 22 + } 23 + } 24 + </script> 25 + 26 + <!-- Import peek-components --> 27 + <script type="module"> 28 + import 'peek://app/components/peek-card.js'; 29 + import 'peek://app/components/peek-grid.js'; 30 + import 'peek://app/components/peek-input.js'; 31 + import 'peek://app/components/peek-button.js'; 32 + import 'peek://app/components/peek-button-group.js'; 33 + import 'peek://app/components/peek-dialog.js'; 34 + </script> 9 35 </head> 10 36 <body> 11 37 <header class="header"> ··· 56 82 </header> 57 83 58 84 <div class="search-container"> 59 - <input type="text" class="search-input" placeholder="Search items and tags..."> 85 + <peek-input 86 + class="search-input" 87 + placeholder="Search items and tags..." 88 + type="search" 89 + ></peek-input> 60 90 </div> 61 91 62 92 <div class="content-wrapper"> ··· 66 96 </aside> 67 97 68 98 <main class="items-container"> 69 - <div class="cards"></div> 99 + <peek-grid class="cards" min-item-width="280" gap="12"></peek-grid> 70 100 </main> 71 101 </div> 72 102 73 103 <!-- Edit Modal --> 74 - <div class="modal-overlay" id="editModal"> 75 - <div class="modal"> 76 - <div class="modal-header"> 77 - <h2 class="modal-title">Edit Tags</h2> 78 - <button class="modal-close">&times;</button> 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> 112 + </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> 79 121 </div> 80 - <div class="modal-body"> 81 - <div class="modal-item-info"> 82 - <img class="modal-favicon" src="" alt=""> 83 - <div class="modal-item-details"> 84 - <div class="modal-item-title"></div> 85 - <div class="modal-item-url"></div> 86 - </div> 87 - <button class="modal-open-page-btn" style="display: none;" title="Open page"> 88 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 89 - <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> 90 - <polyline points="15 3 21 3 21 9"></polyline> 91 - <line x1="10" y1="14" x2="21" y2="3"></line> 92 - </svg> 93 - <span>Open Page</span> 94 - </button> 95 - </div> 96 122 97 - <div class="edit-section"> 98 - <label class="edit-label">Current Tags</label> 99 - <div class="current-tags"></div> 100 - </div> 123 + <div class="edit-section"> 124 + <label class="edit-label">Current Tags</label> 125 + <div class="current-tags"></div> 126 + </div> 101 127 102 - <div class="edit-section"> 103 - <label class="edit-label">Add Tag</label> 104 - <div class="new-tag-row"> 105 - <input type="text" class="new-tag-input" placeholder="Enter new tag..."> 106 - <button class="add-tag-btn">Add</button> 107 - </div> 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> 108 133 </div> 134 + </div> 109 135 110 - <div class="edit-section"> 111 - <label class="edit-label">Available Tags</label> 112 - <div class="available-tags"></div> 113 - </div> 136 + <div class="edit-section"> 137 + <label class="edit-label">Available Tags</label> 138 + <div class="available-tags"></div> 114 139 </div> 115 140 </div> 116 - </div> 141 + </peek-dialog> 117 142 118 143 <script type="module" src="home.js"></script> 119 144 </body>
+33 -28
extensions/tags/home.js
··· 241 241 }); 242 242 243 243 // Modal close 244 - document.querySelector('.modal-close').addEventListener('click', closeModal); 245 - modalOverlay.addEventListener('click', (e) => { 246 - if (e.target === modalOverlay) closeModal(); 247 - }); 244 + // Modal close - peek-dialog handles close-on-backdrop and close-on-escape automatically 245 + modalOverlay.addEventListener('close', closeModal); 248 246 249 247 // New tag input 250 248 const newTagInput = document.querySelector('.new-tag-input'); ··· 269 267 if (api.escape) { 270 268 api.escape.onEscape(() => { 271 269 // If modal is open, close it 272 - if (modalOverlay.classList.contains('visible')) { 270 + if (modalOverlay.open) { 273 271 closeModal(); 274 272 return { handled: true }; 275 273 } ··· 307 305 */ 308 306 const handleKeydown = (e) => { 309 307 // Ignore if modal is open and not in an input 310 - if (modalOverlay.classList.contains('visible')) { 308 + if (modalOverlay.open) { 311 309 if (e.key === 'Escape') { 312 310 closeModal(); 313 311 } ··· 383 381 /** 384 382 * Get all cards in the current view 385 383 */ 386 - const getCards = () => Array.from(document.querySelectorAll('.cards .card')); 384 + const getCards = () => Array.from(document.querySelectorAll('.cards peek-card')); 387 385 388 386 /** 389 387 * Get number of columns in the grid ··· 405 403 const updateSelection = () => { 406 404 const cards = getCards(); 407 405 cards.forEach((card, i) => { 408 - card.classList.toggle('selected', i === state.selectedIndex); 406 + card.selected = (i === state.selectedIndex); 409 407 }); 410 408 411 409 const selected = cards[state.selectedIndex]; ··· 525 523 const indicator = document.querySelector('.active-tag-indicator'); 526 524 527 525 if (state.activeTags.length > 0) { 528 - indicator.innerHTML = state.activeTags.map(tag => ` 529 - <span class="active-tag-chip" data-tag-id="${tag.id}"> 526 + indicator.innerHTML = ''; 527 + 528 + state.activeTags.forEach(tag => { 529 + const chip = document.createElement('peek-button'); 530 + chip.variant = 'primary'; 531 + chip.size = 'sm'; 532 + chip.className = 'active-tag-chip'; 533 + chip.dataset.tagId = tag.id; 534 + chip.innerHTML = ` 530 535 ${escapeHtml(tag.name)} 531 - <button class="clear-tag" title="Remove tag">&times;</button> 532 - </span> 533 - `).join('') + ` 534 - <button class="clear-all-tags" title="Clear all filters">&times; All</button> 535 - `; 536 - indicator.classList.add('visible'); 536 + <span slot="suffix" class="clear-tag" title="Remove tag">&times;</span> 537 + `; 538 + chip.addEventListener('click', () => removeActiveTag(tag.id)); 539 + indicator.appendChild(chip); 540 + }); 537 541 538 - indicator.querySelectorAll('.active-tag-chip .clear-tag').forEach(btn => { 539 - btn.addEventListener('click', (e) => { 540 - e.stopPropagation(); 541 - const tagId = parseInt(btn.parentElement.dataset.tagId, 10); 542 - removeActiveTag(tagId); 543 - }); 544 - }); 542 + const clearAll = document.createElement('peek-button'); 543 + clearAll.variant = 'ghost'; 544 + clearAll.size = 'sm'; 545 + clearAll.className = 'clear-all-tags'; 546 + clearAll.textContent = '× All'; 547 + clearAll.title = 'Clear all filters'; 548 + clearAll.addEventListener('click', clearTagFilter); 549 + indicator.appendChild(clearAll); 545 550 546 - indicator.querySelector('.clear-all-tags').addEventListener('click', clearTagFilter); 551 + indicator.classList.add('visible'); 547 552 } else { 548 553 indicator.classList.remove('visible'); 549 554 indicator.innerHTML = ''; ··· 646 651 * Create a card element for an item 647 652 */ 648 653 const createItemCard = (item) => { 649 - const card = document.createElement('div'); 650 - card.className = 'card'; 654 + const card = document.createElement('peek-card'); 655 + card.interactive = true; 651 656 card.dataset.itemId = item.id; 652 657 653 658 const tags = state.itemTags.get(item.id) || []; ··· 837 842 document.querySelector('.new-tag-input').value = ''; 838 843 839 844 // Show modal 840 - modalOverlay.classList.add('visible'); 845 + modalOverlay.showModal(); 841 846 }; 842 847 843 848 /** 844 849 * Close the edit modal 845 850 */ 846 851 const closeModal = () => { 847 - modalOverlay.classList.remove('visible'); 852 + modalOverlay.close(); 848 853 state.editingItem = null; 849 854 }; 850 855