experiments in a post-browser web
10
fork

Configure Feed

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

fix(cards): standardize card UI, buttons, and grid sizing

+489 -249
+2 -2
app/components/peek-grid.js
··· 7 7 * @element peek-grid 8 8 * 9 9 * @prop {number} minItemWidth - Minimum item width in pixels (default: 250) 10 - * @prop {number} gap - Gap between items in pixels (default: 16) 10 + * @prop {number} gap - Gap between items in pixels (default: 10) 11 11 * @prop {number} columns - Fixed column count (overrides auto-fit if set) 12 12 * @prop {string} align - Item alignment: 'start' | 'center' | 'end' | 'stretch' 13 13 * @prop {boolean} dense - Enable dense packing algorithm ··· 129 129 constructor() { 130 130 super(); 131 131 this.minItemWidth = 250; 132 - this.gap = 16; 132 + this.gap = 10; 133 133 this.columns = null; 134 134 this.align = 'stretch'; 135 135 this.dense = false;
+36 -27
app/lib/search-result-card.js
··· 69 69 70 70 const { title, subtitle, faviconUrl, itemUrl, itemType } = getItemDisplayInfo(item, displayOpts); 71 71 72 - // ── Header slot: favicon + title + optional open button ────────── 72 + // ── Header slot: favicon + title + button group ────────── 73 73 const favicon = createFaviconEl(faviconUrl); 74 74 const header = createHeaderSlot(favicon, title); 75 75 76 - if (opts.showOpenButton && itemUrl) { 77 - const openBtn = document.createElement('button'); 78 - openBtn.className = 'card-open-btn'; 79 - openBtn.title = 'Open page'; 80 - openBtn.innerHTML = OPEN_BUTTON_SVG; 81 - openBtn.style.flexShrink = '0'; 82 - openBtn.addEventListener('click', (e) => { 83 - e.stopPropagation(); 84 - if (opts.onOpen) { 85 - opts.onOpen(itemUrl); 86 - } 87 - }); 88 - header.appendChild(openBtn); 89 - } 76 + // Collect action buttons into a grouped container 77 + const hasOpenBtn = opts.showOpenButton && itemUrl; 78 + const hasDeleteBtn = !!opts.onDelete; 79 + 80 + if (hasOpenBtn || hasDeleteBtn) { 81 + const btnGroup = document.createElement('span'); 82 + btnGroup.className = 'card-btn-group'; 83 + 84 + if (hasOpenBtn) { 85 + const openBtn = document.createElement('button'); 86 + openBtn.className = 'card-open-btn'; 87 + openBtn.title = 'Open page'; 88 + openBtn.innerHTML = OPEN_BUTTON_SVG; 89 + openBtn.addEventListener('click', (e) => { 90 + e.stopPropagation(); 91 + if (opts.onOpen) { 92 + opts.onOpen(itemUrl); 93 + } 94 + }); 95 + btnGroup.appendChild(openBtn); 96 + } 97 + 98 + if (hasDeleteBtn) { 99 + const deleteBtn = document.createElement('button'); 100 + deleteBtn.className = 'card-delete-btn'; 101 + deleteBtn.title = 'Delete item'; 102 + deleteBtn.innerHTML = DELETE_BUTTON_SVG; 103 + deleteBtn.addEventListener('click', (e) => { 104 + e.stopPropagation(); 105 + opts.onDelete(item); 106 + }); 107 + btnGroup.appendChild(deleteBtn); 108 + } 90 109 91 - if (opts.onDelete) { 92 - const deleteBtn = document.createElement('button'); 93 - deleteBtn.className = 'card-delete-btn'; 94 - deleteBtn.title = 'Delete item'; 95 - deleteBtn.innerHTML = DELETE_BUTTON_SVG; 96 - deleteBtn.style.flexShrink = '0'; 97 - deleteBtn.addEventListener('click', (e) => { 98 - e.stopPropagation(); 99 - opts.onDelete(item); 100 - }); 101 - header.appendChild(deleteBtn); 110 + header.appendChild(btnGroup); 102 111 } 103 112 104 113 card.appendChild(header); ··· 124 133 if (tags.length > 0) { 125 134 const tagsContainer = document.createElement('span'); 126 135 tagsContainer.className = 'card-tags'; 127 - tagsContainer.style.cssText = 'display:flex;gap:4px;flex-wrap:wrap;flex-shrink:0;'; 136 + tagsContainer.style.cssText = 'display:flex;gap:3px;flex-wrap:wrap;flex-shrink:0;'; 128 137 tags.forEach(tag => { 129 138 const chip = document.createElement('span'); 130 139 chip.className = 'card-tag';
+28 -30
extensions/entities/home.css
··· 27 27 display: flex; 28 28 align-items: center; 29 29 justify-content: space-between; 30 - padding: 16px 24px; 30 + padding: 10px 16px; 31 31 border-bottom: 1px solid var(--base02); 32 32 background: var(--base00); 33 33 } ··· 35 35 .header-left { 36 36 display: flex; 37 37 align-items: center; 38 - gap: 12px; 38 + gap: 8px; 39 39 } 40 40 41 41 .header-title { 42 - font-size: 18px; 42 + font-size: 15px; 43 43 font-weight: 600; 44 44 color: var(--base05); 45 45 } ··· 54 54 .filters { 55 55 display: flex; 56 56 gap: 4px; 57 - padding: 12px 24px; 57 + padding: 8px 16px; 58 58 border-bottom: 1px solid var(--base02); 59 59 flex-wrap: wrap; 60 60 align-items: center; ··· 63 63 .filter-btn { 64 64 display: flex; 65 65 align-items: center; 66 - gap: 4px; 67 - padding: 6px 10px; 66 + gap: 3px; 67 + padding: 4px 7px; 68 68 background: transparent; 69 69 border: none; 70 - border-radius: 6px; 70 + border-radius: 5px; 71 71 cursor: pointer; 72 72 color: var(--base04); 73 73 font-size: 12px; ··· 86 86 87 87 /* Search */ 88 88 .search-container { 89 - padding: 16px 24px; 89 + padding: 8px 16px; 90 90 } 91 91 92 92 peek-input.search-input { 93 93 width: 100%; 94 94 --peek-input-bg: var(--base01); 95 95 --peek-input-border: var(--base02); 96 - --peek-input-height: 40px; 96 + --peek-input-height: 32px; 97 97 } 98 98 99 99 /* Grid toolbar */ ··· 111 111 .content { 112 112 flex: 1; 113 113 overflow-y: auto; 114 - padding: 0 24px 24px 24px; 115 - } 116 - 117 - /* Cards grid - peek-grid handles layout */ 118 - peek-grid { 119 - --peek-grid-min-item-width: 280px; 120 - --peek-grid-gap: 12px; 114 + padding: 0 16px 16px 16px; 121 115 } 122 116 123 117 /* Card customization - peek-card custom properties */ ··· 125 119 --peek-card-bg: var(--base01); 126 120 --peek-card-border: transparent; 127 121 --peek-card-radius: 6px; 128 - --peek-card-padding: 10px; 122 + --peek-card-padding: 8px; 129 123 --peek-card-gap: 4px; 130 124 cursor: pointer; 131 125 position: relative; ··· 142 136 display: inline-flex; 143 137 align-items: center; 144 138 justify-content: center; 145 - width: 20px; 146 - height: 20px; 139 + width: 16px; 140 + height: 16px; 147 141 border-radius: 50%; 148 - font-size: 11px; 142 + font-size: 9px; 149 143 flex-shrink: 0; 150 144 } 151 145 ··· 165 159 .card-header { 166 160 display: flex; 167 161 align-items: center; 168 - gap: 6px; 162 + gap: 8px; 169 163 min-width: 0; 170 164 } 171 165 ··· 175 169 overflow: hidden; 176 170 text-overflow: ellipsis; 177 171 white-space: nowrap; 178 - font-size: 13px; 172 + font-size: 12px; 179 173 font-weight: 500; 180 174 color: var(--base05); 175 + line-height: 1.3; 181 176 } 182 177 183 178 .entity-type-prefix { ··· 186 181 } 187 182 188 183 .entity-name { 189 - font-size: 13px; 184 + font-size: 12px; 190 185 font-weight: 500; 191 186 color: var(--base05); 192 187 white-space: nowrap; 193 188 overflow: hidden; 194 189 text-overflow: ellipsis; 190 + line-height: 1.3; 195 191 } 196 192 197 193 .entity-confidence { ··· 228 224 color: var(--base03); 229 225 } 230 226 227 + /* Card button group - standardized icon button container in card header */ 228 + .card-btn-group { 229 + display: flex; 230 + align-items: center; 231 + gap: 4px; 232 + flex-shrink: 0; 233 + } 234 + 231 235 /* Open link button - inline in header */ 232 236 .card-open-btn { 233 237 display: flex; ··· 236 240 width: 22px; 237 241 height: 22px; 238 242 padding: 0; 239 - background: var(--base02); 243 + background: transparent; 240 244 border: none; 241 245 border-radius: 4px; 242 246 cursor: pointer; ··· 246 250 } 247 251 248 252 .card-open-btn:hover { 249 - background: var(--base03); 250 253 color: var(--base05); 251 254 } 252 255 ··· 265 268 color: var(--base03); 266 269 flex-shrink: 0; 267 270 transition: all 0.15s; 268 - opacity: 0; 269 - } 270 - 271 - peek-card:hover .card-delete-btn { 272 - opacity: 1; 273 271 } 274 272 275 273 .card-delete-btn:hover {
+1 -1
extensions/entities/home.html
··· 64 64 65 65 <div class="content"> 66 66 <peek-grid-toolbar class="grid-toolbar" id="gridToolbar" view-mode="columns"></peek-grid-toolbar> 67 - <peek-grid id="entityGrid" min-item-width="280" gap="12"></peek-grid> 67 + <peek-grid id="entityGrid" min-item-width="250" gap="10"></peek-grid> 68 68 69 69 <!-- Inline detail view (replaces card grid when an entity is selected) --> 70 70 <div class="detail-view" id="detailView" style="display: none;">
+18 -1
extensions/entities/home.js
··· 442 442 <line x1="10" y1="14" x2="21" y2="3"></line> 443 443 </svg></button>`; 444 444 } 445 + const deleteBtnHtml = `<button class="card-delete-btn" data-entity-id="${escapeHtml(entity.id)}" title="Delete entity"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 446 + <polyline points="3 6 5 6 21 6"></polyline> 447 + <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> 448 + </svg></button>`; 445 449 headerDiv.innerHTML = ` 446 450 <div class="card-header"> 447 451 <span class="entity-type-icon type-${entityType}">${icon}</span> 448 452 <span class="card-header-label"><span class="entity-type-prefix">${escapeHtml(entityTypeLabel)}:</span> ${entityName}</span> 449 - ${openBtnHtml} 453 + <span class="card-btn-group">${openBtnHtml}${deleteBtnHtml}</span> 450 454 </div> 451 455 `; 452 456 card.appendChild(headerDiv); ··· 510 514 if (openBtn) { 511 515 e.stopPropagation(); 512 516 openSourceUrl(openBtn.dataset.url); 517 + return; 518 + } 519 + 520 + const deleteBtn = e.target.closest('.card-delete-btn[data-entity-id]'); 521 + if (deleteBtn) { 522 + e.stopPropagation(); 523 + if (!confirm(`Delete entity "${entity.content || entity.name || 'this entity'}"?`)) return; 524 + try { 525 + await api.datastore.deleteItem(entity.id); 526 + renderEntities(); 527 + } catch (err) { 528 + console.error('[entities:ui] Failed to delete entity:', err); 529 + } 513 530 return; 514 531 } 515 532 // Otherwise open detail view
+26 -15
extensions/groups/home.css
··· 114 114 --peek-card-radius: 6px; 115 115 --peek-card-padding: 8px; 116 116 --peek-card-gap: 4px; 117 - max-width: 260px; 118 117 } 119 118 120 119 peek-card[selected] { ··· 133 132 height: 10px; 134 133 border-radius: 50%; 135 134 flex-shrink: 0; 136 - margin-top: 2px; 135 + margin-top: 1px; 137 136 } 138 137 139 138 /* Address card slotted content */ 140 139 .address-card .card-favicon { 141 - width: 12px; 142 - height: 12px; 140 + width: 16px; 141 + height: 16px; 143 142 border-radius: 3px; 144 143 flex-shrink: 0; 145 144 background: var(--base02); 146 145 object-fit: contain; 147 - margin-top: 2px; 146 + margin-top: 1px; 147 + } 148 + 149 + /* Card button group - standardized icon button container in card header */ 150 + .card-btn-group { 151 + display: flex; 152 + align-items: center; 153 + gap: 4px; 154 + flex-shrink: 0; 148 155 } 149 156 150 157 /* Card open button */ ··· 154 161 justify-content: center; 155 162 width: 22px; 156 163 height: 22px; 157 - background: var(--base02); 164 + background: transparent; 158 165 border: none; 159 166 border-radius: 4px; 160 167 cursor: pointer; ··· 165 172 } 166 173 167 174 .card-open-btn:hover { 168 - background: var(--base0D); 169 - color: var(--base00); 175 + color: var(--base05); 170 176 } 171 177 172 178 /* Card delete button */ ··· 184 190 flex-shrink: 0; 185 191 transition: all 0.15s; 186 192 padding: 0; 187 - opacity: 0; 188 - } 189 - 190 - peek-card:hover .card-delete-btn { 191 - opacity: 1; 192 193 } 193 194 194 195 .card-delete-btn:hover { ··· 237 238 238 239 /* Shared slotted content styles */ 239 240 .card-title { 240 - font-size: 13px; 241 - font-weight: 600; 241 + font-size: 12px; 242 + font-weight: 500; 242 243 color: var(--base05); 243 244 white-space: nowrap; 244 245 overflow: hidden; 245 246 text-overflow: ellipsis; 247 + line-height: 1.3; 248 + } 249 + 250 + .card-url { 251 + font-size: 11px; 252 + color: var(--base04); 253 + white-space: nowrap; 254 + overflow: hidden; 255 + text-overflow: ellipsis; 256 + margin-top: 1px; 246 257 } 247 258 248 259 .card-meta {
+1 -1
extensions/groups/home.html
··· 60 60 </div> 61 61 62 62 <peek-grid-toolbar class="grid-toolbar" view-mode="columns"></peek-grid-toolbar> 63 - <peek-grid class="cards" min-item-width="180" gap="8"></peek-grid> 63 + <peek-grid class="cards" min-item-width="250" gap="10"></peek-grid> 64 64 65 65 <script type="module" src="home.js"></script> 66 66 </body>
+86 -18
extensions/groups/home.js
··· 794 794 const card = document.createElement('peek-card'); 795 795 card.className = 'group-card'; 796 796 card.interactive = true; 797 - card.elevated = true; 798 797 if (tag.isSpecial) { 799 798 card.classList.add('special-group'); 800 799 } ··· 865 864 866 865 const card = createSearchResultCard(address, { 867 866 className: 'address-card', 868 - elevated: true, 869 867 visitCount: address.visitCount, 868 + showOpenButton: isWebUrl(addressUrl), 869 + onOpen: (url) => { 870 + api.window.open(url, { 871 + role: 'content', 872 + key: url, 873 + width: 800, 874 + height: 600 875 + }); 876 + }, 877 + onDelete: async (item) => { 878 + if (!confirm(`Delete "${item.title || item.content || 'this item'}"?`)) return; 879 + try { 880 + await api.datastore.deleteItem(item.id); 881 + api.publish('item:deleted', { id: item.id }, api.scopes.GLOBAL); 882 + } catch (err) { 883 + console.error('[groups] Failed to delete item:', err); 884 + } 885 + }, 886 + onTagRemove: async (item, tag) => { 887 + try { 888 + await api.datastore.untagItem(item.id, tag.id); 889 + api.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 890 + } catch (err) { 891 + console.error('[groups] Failed to remove tag:', err); 892 + } 893 + }, 894 + onTagClick: (tag) => { 895 + // Navigate to addresses view for the clicked tag 896 + const matchingTag = state.tags.find(t => t.name === tag.name || t.id === tag.id); 897 + if (matchingTag) { 898 + showAddresses(matchingTag); 899 + } 900 + }, 870 901 onClick: async () => { 871 902 debug && console.log('Opening address:', addressUrl); 872 903 const openOptions = { ··· 901 932 } 902 933 } 903 934 904 - // Async: load item tags and render affordances if any rules match 905 - if (rules.length > 0) { 906 - api.datastore.getItemTags(address.id).then(tagsResult => { 907 - if (!tagsResult.success || !tagsResult.data.length) return; 908 - const tagNames = tagsResult.data.map(t => t.name); 935 + // Async: load item tags and render tag chips + affordances 936 + api.datastore.getItemTags(address.id).then(tagsResult => { 937 + if (!tagsResult.success || !tagsResult.data.length) return; 938 + const tags = tagsResult.data; 939 + const footer = card.querySelector('[slot="footer"]'); 940 + if (!footer) return; 941 + 942 + // Build right-side container for tags and affordances 943 + let footerRight = footer.querySelector('span:last-child'); 944 + if (!footerRight || footerRight.className === 'card-url') { 945 + footerRight = document.createElement('span'); 946 + footerRight.style.cssText = 'display:flex;align-items:center;gap:6px;flex-shrink:0;'; 947 + footer.appendChild(footerRight); 948 + } 949 + 950 + // Tag action affordances 951 + if (rules.length > 0) { 952 + const tagNames = tags.map(t => t.name); 909 953 const el = createAffordanceElements(address.id, tagNames, rules, api, { 910 954 onToggle: () => { 911 955 if (state.currentTag) loadAddressesForTag(state.currentTag.id).then(() => renderAddresses()); 912 956 } 913 957 }); 914 - if (el) { 915 - const footer = card.querySelector('[slot="footer"]'); 916 - if (footer) { 917 - const lastChild = footer.lastElementChild; 918 - if (lastChild) { 919 - footer.insertBefore(el, lastChild); 920 - } else { 921 - footer.appendChild(el); 922 - } 958 + if (el) footerRight.appendChild(el); 959 + } 960 + 961 + // Tag chips with remove buttons 962 + const tagsContainer = document.createElement('span'); 963 + tagsContainer.className = 'card-tags'; 964 + tagsContainer.style.cssText = 'display:flex;gap:3px;flex-wrap:wrap;flex-shrink:0;'; 965 + tags.forEach(tag => { 966 + const chip = document.createElement('span'); 967 + chip.className = 'card-tag'; 968 + chip.dataset.tagId = tag.id; 969 + chip.textContent = tag.name; 970 + chip.addEventListener('click', (e) => { 971 + e.stopPropagation(); 972 + const matchingTag = state.tags.find(t => t.name === tag.name || t.id === tag.id); 973 + if (matchingTag) showAddresses(matchingTag); 974 + }); 975 + 976 + const removeBtn = document.createElement('span'); 977 + removeBtn.className = 'card-tag-remove'; 978 + removeBtn.textContent = '\u00D7'; 979 + removeBtn.title = `Remove ${tag.name}`; 980 + removeBtn.addEventListener('click', async (e) => { 981 + e.stopPropagation(); 982 + try { 983 + await api.datastore.untagItem(address.id, tag.id); 984 + api.publish('tag:item-removed', { itemId: address.id, tagId: tag.id }, api.scopes.GLOBAL); 985 + } catch (err) { 986 + console.error('[groups] Failed to remove tag:', err); 923 987 } 924 - } 988 + }); 989 + chip.appendChild(removeBtn); 990 + 991 + tagsContainer.appendChild(chip); 925 992 }); 926 - } 993 + footerRight.appendChild(tagsContainer); 994 + }); 927 995 928 996 return card; 929 997 };
+51 -87
extensions/lists/home.css
··· 22 22 23 23 /* Search */ 24 24 .search-container { 25 - padding: 24px 24px 0 24px; 25 + padding: 12px 16px 0 16px; 26 26 display: flex; 27 27 align-items: center; 28 - gap: 12px; 28 + gap: 8px; 29 29 } 30 30 31 31 /* Component customization for peek-input */ ··· 34 34 flex: 1; 35 35 --peek-input-bg: var(--base01); 36 36 --peek-input-border: var(--base02); 37 - --peek-input-height: 44px; 37 + --peek-input-height: 32px; 38 38 } 39 39 40 40 peek-input.search-input::part(input) { 41 41 color: var(--base05); 42 - font-size: 15px; 42 + font-size: 13px; 43 43 } 44 44 45 45 .result-count { ··· 49 49 flex-shrink: 0; 50 50 } 51 51 52 + /* Component customization for peek-card */ 53 + peek-card { 54 + --peek-card-bg: var(--base01); 55 + --peek-card-border: transparent; 56 + --peek-card-radius: 6px; 57 + --peek-card-padding: 8px; 58 + --peek-card-gap: 4px; 59 + } 60 + 61 + peek-card[selected] { 62 + --peek-card-bg: var(--base02); 63 + } 64 + 52 65 /* Results container */ 53 66 .results { 54 - padding: 16px 24px; 67 + padding: 10px 16px; 55 68 overflow-y: auto; 56 - max-height: calc(100vh - 100px); 69 + max-height: calc(100vh - 80px); 57 70 } 58 71 59 72 /* Result group (URLs, Notes, Tags, etc.) */ 60 73 .result-group { 61 - margin-bottom: 20px; 74 + margin-bottom: 12px; 62 75 } 63 76 64 77 .result-group-header { ··· 69 82 color: var(--base03); 70 83 padding: 0 4px 6px 4px; 71 84 border-bottom: 1px solid var(--base02); 72 - margin-bottom: 8px; 85 + margin-bottom: 6px; 73 86 } 74 87 75 - /* Result item (card-like row) */ 76 - .result-item { 77 - display: flex; 78 - align-items: flex-start; 79 - gap: 10px; 80 - padding: 8px 10px; 81 - border-radius: 6px; 82 - cursor: pointer; 83 - transition: background-color 0.1s; 84 - } 85 - 86 - .result-item:hover { 87 - background: var(--base01); 88 - } 89 - 90 - .result-item.selected { 91 - background: var(--base02); 92 - } 93 - 94 - .result-item.selected:hover { 95 - background: var(--base03); 96 - } 97 - 98 - /* Type icon */ 99 - .result-icon { 100 - width: 20px; 101 - height: 20px; 102 - flex-shrink: 0; 103 - display: flex; 104 - align-items: center; 105 - justify-content: center; 106 - font-size: 14px; 107 - margin-top: 2px; 108 - color: var(--base03); 109 - } 110 - 111 - .result-icon img { 88 + /* Card favicon */ 89 + .card-favicon { 112 90 width: 16px; 113 91 height: 16px; 114 92 border-radius: 3px; 93 + flex-shrink: 0; 94 + background: var(--base02); 115 95 object-fit: contain; 96 + margin-top: 1px; 116 97 } 117 98 118 - /* Result content */ 119 - .result-content { 120 - flex: 1; 121 - min-width: 0; 122 - } 123 - 124 - .result-title { 125 - font-size: 14px; 99 + /* Card title */ 100 + .card-title { 101 + font-size: 12px; 126 102 font-weight: 500; 127 103 color: var(--base05); 128 104 white-space: nowrap; 129 105 overflow: hidden; 130 106 text-overflow: ellipsis; 107 + line-height: 1.3; 131 108 } 132 109 133 - .result-preview { 134 - font-size: 12px; 135 - color: var(--base03); 110 + .card-url { 111 + font-size: 11px; 112 + color: var(--base04); 136 113 white-space: nowrap; 137 114 overflow: hidden; 138 115 text-overflow: ellipsis; 139 - margin-top: 2px; 116 + margin-top: 1px; 140 117 } 141 118 142 - .result-meta { 143 - display: flex; 144 - align-items: center; 145 - gap: 8px; 146 - margin-top: 4px; 147 - } 148 - 149 - /* Legacy result-tag (unused after shared card migration) */ 150 - .result-tag { 151 - font-size: 10px; 152 - padding: 1px 6px; 153 - border-radius: 3px; 154 - background: var(--base02); 155 - color: var(--base04); 156 - } 157 - 158 - /* Shared search-result-card tag chips */ 119 + /* Card tags */ 159 120 .card-tags { 160 121 display: flex; 161 122 flex-wrap: wrap; ··· 168 129 border-radius: 8px; 169 130 font-size: 10px; 170 131 color: var(--base04); 132 + cursor: pointer; 171 133 transition: all 0.15s; 172 134 } 173 135 ··· 176 138 color: var(--base05); 177 139 } 178 140 141 + /* Card button group - standardized icon button container in card header */ 142 + .card-btn-group { 143 + display: flex; 144 + align-items: center; 145 + gap: 4px; 146 + flex-shrink: 0; 147 + } 148 + 179 149 /* Card open button */ 180 150 .card-open-btn { 181 151 display: flex; ··· 183 153 justify-content: center; 184 154 width: 22px; 185 155 height: 22px; 186 - background: var(--base02); 156 + background: transparent; 187 157 border: none; 188 158 border-radius: 4px; 189 159 cursor: pointer; ··· 194 164 } 195 165 196 166 .card-open-btn:hover { 197 - background: var(--base0D); 198 - color: var(--base00); 167 + color: var(--base05); 199 168 } 200 169 201 170 /* Card delete button */ ··· 213 182 flex-shrink: 0; 214 183 transition: all 0.15s; 215 184 padding: 0; 216 - opacity: 0; 217 - } 218 - 219 - peek-card:hover .card-delete-btn { 220 - opacity: 1; 221 185 } 222 186 223 187 .card-delete-btn:hover { ··· 242 206 color: var(--base08); 243 207 } 244 208 245 - .result-date { 209 + .card-meta { 246 210 font-size: 11px; 247 211 color: var(--base03); 248 212 } ··· 258 222 /* Empty state */ 259 223 .empty-state { 260 224 text-align: center; 261 - padding: 48px 24px; 225 + padding: 32px 16px; 262 226 color: var(--base03); 263 - font-size: 15px; 227 + font-size: 13px; 264 228 } 265 229 266 230 /* No results state */ 267 231 .no-results { 268 232 text-align: center; 269 - padding: 32px 24px; 233 + padding: 32px 16px; 270 234 color: var(--base03); 271 - font-size: 14px; 235 + font-size: 13px; 272 236 }
+11
extensions/lists/home.js
··· 322 322 // Convert tag name strings to tag-like objects for the shared card builder 323 323 const tagObjects = (item.tags || []).map(name => ({ name, id: name })); 324 324 325 + const itemUrl = item.content; 325 326 const card = createSearchResultCard(item, { 326 327 className: 'result-item', 327 328 tags: tagObjects, 328 329 visitCount: item.visitCount, 330 + showOpenButton: item.type === 'url' && isWebUrl(itemUrl), 331 + onOpen: (url) => { 332 + api.window.open(url, { 333 + role: 'content', 334 + key: url, 335 + width: 1024, 336 + height: 768 337 + }); 338 + }, 329 339 onDelete: async (item) => { 330 340 if (!confirm(`Delete "${item.title || item.content || 'this item'}"?`)) return; 331 341 await api.datastore.deleteItem(item.id); ··· 333 343 }, 334 344 onTagRemove: async (item, tag) => { 335 345 await api.datastore.untagItem(item.id, tag.id); 346 + api.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 336 347 }, 337 348 onClick: async (item) => { 338 349 state.selectedIndex = index;
+22 -19
extensions/pagestream/home.css
··· 52 52 --peek-card-hover-bg: var(--base01, #2c2c2e); 53 53 --peek-card-border: transparent; 54 54 --peek-card-radius: 8px; 55 - --peek-card-padding: 10px; 55 + --peek-card-padding: 8px; 56 56 --peek-card-gap: 4px; 57 57 --peek-focus-ring: none; 58 58 --theme-accent: transparent; ··· 74 74 } 75 75 76 76 .stream-container peek-card.active-card .card-title { 77 - font-size: 16px; 77 + font-size: 14px; 78 78 } 79 79 80 80 .stream-container peek-card.active-card .card-url { ··· 82 82 } 83 83 84 84 .stream-container peek-card.active-card .card-favicon { 85 - width: 24px; 86 - height: 24px; 85 + width: 20px; 86 + height: 20px; 87 87 } 88 88 89 89 .stream-container peek-card.active-card .card-time, ··· 108 108 } 109 109 110 110 .card-title { 111 - font-size: 13px; 112 - font-weight: 600; 111 + font-size: 12px; 112 + font-weight: 500; 113 113 color: var(--base05); 114 114 white-space: nowrap; 115 115 overflow: hidden; ··· 117 117 flex: 1; 118 118 min-width: 0; 119 119 margin: 0; 120 + line-height: 1.3; 120 121 } 121 122 122 123 .card-url { ··· 148 149 149 150 .card-tags { 150 151 display: flex; 151 - gap: 4px; 152 + gap: 3px; 152 153 flex-wrap: wrap; 153 154 margin-top: 4px; 154 155 } 155 156 156 - .tag-chip { 157 + .card-tag { 157 158 font-size: 10px; 158 159 color: var(--base04); 159 160 background: var(--base02); 160 161 padding: 1px 6px; 161 - border-radius: 3px; 162 + border-radius: 8px; 162 163 cursor: pointer; 163 - transition: background 0.1s ease; 164 + transition: all 0.15s; 164 165 } 165 166 166 - .tag-chip:hover { 167 + .card-tag:hover { 167 168 background: var(--base03); 168 169 color: var(--base05); 170 + } 171 + 172 + /* Card button group - standardized icon button container in card header */ 173 + .card-btn-group { 174 + display: flex; 175 + align-items: center; 176 + gap: 4px; 177 + flex-shrink: 0; 169 178 } 170 179 171 180 /* Card open button */ ··· 175 184 justify-content: center; 176 185 width: 22px; 177 186 height: 22px; 178 - background: var(--base02); 187 + background: transparent; 179 188 border: none; 180 189 border-radius: 4px; 181 190 cursor: pointer; ··· 186 195 } 187 196 188 197 .card-open-btn:hover { 189 - background: var(--base0D); 190 - color: var(--base00); 198 + color: var(--base05); 191 199 } 192 200 193 201 /* Card delete button */ ··· 205 213 flex-shrink: 0; 206 214 transition: all 0.15s; 207 215 padding: 0; 208 - opacity: 0; 209 - } 210 - 211 - peek-card:hover .card-delete-btn { 212 - opacity: 1; 213 216 } 214 217 215 218 .card-delete-btn:hover {
+44 -1
extensions/pagestream/home.js
··· 464 464 const card = createSearchResultCard(item, { 465 465 className: 'visit-card', 466 466 favicon: item.favicon || `https://www.google.com/s2/favicons?domain=${domain}&sz=32`, 467 + showOpenButton: isWebUrl(url), 468 + onOpen: (openUrl) => { 469 + api.window.open(openUrl, { 470 + role: 'content', 471 + key: openUrl, 472 + width: 1024, 473 + height: 768 474 + }); 475 + }, 476 + onDelete: async (item) => { 477 + if (!confirm(`Delete "${item.title || item.content || 'this item'}"?`)) return; 478 + try { 479 + await api.datastore.deleteItem(item.id); 480 + api.publish('item:deleted', { id: item.id }, api.scopes.GLOBAL); 481 + } catch (err) { 482 + console.error('[pagestream] Failed to delete item:', err); 483 + } 484 + }, 485 + onTagRemove: async (item, tag) => { 486 + try { 487 + await api.datastore.untagItem(item.id, tag.id); 488 + api.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 489 + } catch (err) { 490 + console.error('[pagestream] Failed to remove tag:', err); 491 + } 492 + }, 467 493 onClick: async () => { 468 494 state.selectedIndex = index; 469 495 updateSelection(); ··· 528 554 tagsContainer.slot = 'footer'; 529 555 tags.forEach(tag => { 530 556 const chip = document.createElement('span'); 531 - chip.className = 'tag-chip'; 557 + chip.className = 'card-tag'; 558 + chip.dataset.tagId = tag.id; 532 559 chip.textContent = tag.name; 533 560 chip.addEventListener('click', (e) => { 534 561 e.stopPropagation(); 535 562 filterByTag(tag.id, tag.name); 536 563 }); 564 + 565 + const removeBtn = document.createElement('span'); 566 + removeBtn.className = 'card-tag-remove'; 567 + removeBtn.textContent = '\u00D7'; 568 + removeBtn.title = `Remove ${tag.name}`; 569 + removeBtn.addEventListener('click', async (e) => { 570 + e.stopPropagation(); 571 + try { 572 + await api.datastore.untagItem(item.id, tag.id); 573 + api.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 574 + } catch (err) { 575 + console.error('[pagestream] Failed to remove tag:', err); 576 + } 577 + }); 578 + chip.appendChild(removeBtn); 579 + 537 580 tagsContainer.appendChild(chip); 538 581 }); 539 582 card.appendChild(tagsContainer);
+83 -11
extensions/search/home.css
··· 67 67 --peek-card-radius: 6px; 68 68 --peek-card-padding: 8px; 69 69 --peek-card-gap: 4px; 70 - max-width: 260px; 71 70 } 72 71 73 72 peek-card[selected] { ··· 80 79 81 80 /* Result card slotted content */ 82 81 .result-card .card-favicon { 83 - width: 12px; 84 - height: 12px; 82 + width: 16px; 83 + height: 16px; 85 84 border-radius: 3px; 86 85 flex-shrink: 0; 87 86 background: var(--base02); 88 87 object-fit: contain; 89 - margin-top: 2px; 88 + margin-top: 1px; 90 89 } 91 90 92 91 .result-card .card-type-icon { ··· 99 98 100 99 /* Shared slotted content styles */ 101 100 .card-title { 102 - font-size: 13px; 103 - font-weight: 600; 101 + font-size: 12px; 102 + font-weight: 500; 104 103 color: var(--base05); 105 104 white-space: nowrap; 106 105 overflow: hidden; 107 106 text-overflow: ellipsis; 107 + line-height: 1.3; 108 108 } 109 109 110 110 .card-meta { ··· 122 122 max-width: none; 123 123 } 124 124 125 + /* Card button group - standardized icon button container in card header */ 126 + .card-btn-group { 127 + display: flex; 128 + align-items: center; 129 + gap: 4px; 130 + flex-shrink: 0; 131 + } 132 + 133 + /* Card open button */ 134 + .card-open-btn { 135 + display: flex; 136 + align-items: center; 137 + justify-content: center; 138 + width: 22px; 139 + height: 22px; 140 + background: transparent; 141 + border: none; 142 + border-radius: 4px; 143 + cursor: pointer; 144 + color: var(--base04); 145 + flex-shrink: 0; 146 + transition: all 0.15s; 147 + padding: 0; 148 + } 149 + 150 + .card-open-btn:hover { 151 + color: var(--base05); 152 + } 153 + 125 154 /* Card delete button */ 126 155 .card-delete-btn { 127 156 display: flex; ··· 137 166 flex-shrink: 0; 138 167 transition: all 0.15s; 139 168 padding: 0; 140 - opacity: 0; 141 - } 142 - 143 - peek-card:hover .card-delete-btn { 144 - opacity: 1; 145 169 } 146 170 147 171 .card-delete-btn:hover { 148 172 background: var(--base08); 149 173 color: var(--base00); 174 + } 175 + 176 + /* Card tags */ 177 + .card-tags { 178 + display: flex; 179 + flex-wrap: wrap; 180 + gap: 3px; 181 + } 182 + 183 + .card-tag { 184 + padding: 1px 6px; 185 + background: var(--base02); 186 + border-radius: 8px; 187 + font-size: 10px; 188 + color: var(--base04); 189 + cursor: pointer; 190 + transition: all 0.15s; 191 + } 192 + 193 + .card-tag:hover { 194 + background: var(--base03); 195 + color: var(--base05); 196 + } 197 + 198 + /* Tag remove button */ 199 + .card-tag-remove { 200 + display: inline-flex; 201 + align-items: center; 202 + justify-content: center; 203 + margin-left: 3px; 204 + font-size: 11px; 205 + line-height: 1; 206 + cursor: pointer; 207 + color: var(--base03); 208 + transition: color 0.15s; 209 + } 210 + 211 + .card-tag-remove:hover { 212 + color: var(--base08); 213 + } 214 + 215 + .card-url { 216 + font-size: 11px; 217 + color: var(--base04); 218 + white-space: nowrap; 219 + overflow: hidden; 220 + text-overflow: ellipsis; 221 + margin-top: 1px; 150 222 } 151 223 152 224 /* Empty state */
+1 -1
extensions/search/home.html
··· 36 36 </div> 37 37 38 38 <peek-grid-toolbar class="grid-toolbar" view-mode="list"></peek-grid-toolbar> 39 - <peek-grid class="cards" min-item-width="180" gap="8"></peek-grid> 39 + <peek-grid class="cards" min-item-width="250" gap="10"></peek-grid> 40 40 41 41 <script type="module" src="home.js"></script> 42 42 </body>
+65 -17
extensions/search/home.js
··· 269 269 270 270 const card = createSearchResultCard(item, { 271 271 className: 'result-card', 272 - elevated: true, 273 272 visitCount: item.visitCount, 273 + showOpenButton: itemUrl && isWebUrl(itemUrl), 274 + onOpen: (url) => { 275 + api.window.open(url, { 276 + role: 'content', 277 + key: url, 278 + width: 800, 279 + height: 600 280 + }); 281 + }, 274 282 onDelete: async (item) => { 275 283 if (!confirm(`Delete "${item.title || item.content || 'this item'}"?`)) return; 276 284 await api.datastore.deleteItem(item.id); 285 + api.publish('item:deleted', { id: item.id }, api.scopes.GLOBAL); 277 286 state.results = state.results.filter(r => r.id !== item.id); 278 287 render(); 279 288 }, 289 + onTagRemove: async (item, tag) => { 290 + try { 291 + await api.datastore.untagItem(item.id, tag.id); 292 + api.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 293 + } catch (err) { 294 + console.error('[search] Failed to remove tag:', err); 295 + } 296 + }, 280 297 affordances: rules.length > 0 ? { 281 298 rules, 282 299 api, ··· 296 313 } 297 314 }); 298 315 299 - // Async: load item tags and render tag chips + affordances 300 - if (rules.length > 0) { 301 - api.datastore.getItemTags(item.id).then(tagsResult => { 302 - if (!tagsResult.success || !tagsResult.data.length) return; 303 - const tagNames = tagsResult.data.map(t => t.name); 316 + // Async: load item tags and render tag chips + affordances in footer 317 + api.datastore.getItemTags(item.id).then(tagsResult => { 318 + if (!tagsResult.success || !tagsResult.data.length) return; 319 + const tags = tagsResult.data; 320 + const footer = card.querySelector('[slot="footer"]'); 321 + if (!footer) return; 322 + 323 + // Build right-side container for tags and affordances 324 + let footerRight = footer.querySelector('span:last-child'); 325 + if (!footerRight || footerRight.className === 'card-url') { 326 + footerRight = document.createElement('span'); 327 + footerRight.style.cssText = 'display:flex;align-items:center;gap:6px;flex-shrink:0;'; 328 + footer.appendChild(footerRight); 329 + } 330 + 331 + // Tag action affordances 332 + if (rules.length > 0) { 333 + const tagNames = tags.map(t => t.name); 304 334 const el = createAffordanceElements(item.id, tagNames, rules, api, { 305 335 onToggle: () => { render(); } 306 336 }); 307 - if (el) { 308 - const footer = card.querySelector('[slot="footer"]'); 309 - if (footer) { 310 - const lastChild = footer.lastElementChild; 311 - if (lastChild) { 312 - footer.insertBefore(el, lastChild); 313 - } else { 314 - footer.appendChild(el); 315 - } 337 + if (el) footerRight.appendChild(el); 338 + } 339 + 340 + // Tag chips with remove buttons 341 + const tagsContainer = document.createElement('span'); 342 + tagsContainer.className = 'card-tags'; 343 + tagsContainer.style.cssText = 'display:flex;gap:3px;flex-wrap:wrap;flex-shrink:0;'; 344 + tags.forEach(tag => { 345 + const chip = document.createElement('span'); 346 + chip.className = 'card-tag'; 347 + chip.dataset.tagId = tag.id; 348 + chip.textContent = tag.name; 349 + 350 + const removeBtn = document.createElement('span'); 351 + removeBtn.className = 'card-tag-remove'; 352 + removeBtn.textContent = '\u00D7'; 353 + removeBtn.title = `Remove ${tag.name}`; 354 + removeBtn.addEventListener('click', async (e) => { 355 + e.stopPropagation(); 356 + try { 357 + await api.datastore.untagItem(item.id, tag.id); 358 + api.publish('tag:item-removed', { itemId: item.id, tagId: tag.id }, api.scopes.GLOBAL); 359 + } catch (err) { 360 + console.error('[search] Failed to remove tag:', err); 316 361 } 317 - } 362 + }); 363 + chip.appendChild(removeBtn); 364 + tagsContainer.appendChild(chip); 318 365 }); 319 - } 366 + footerRight.appendChild(tagsContainer); 367 + }); 320 368 321 369 return card; 322 370 };
+10 -14
extensions/tags/home.css
··· 437 437 --theme-accent: var(--base0D); 438 438 } 439 439 440 - /* Cards grid - peek-grid custom properties */ 441 - peek-grid.cards { 442 - --peek-grid-min-item-width: 220px; 443 - --peek-grid-gap: 6px; 444 - } 445 - 446 440 /* Card customization - peek-card custom properties */ 447 441 peek-card { 448 442 --peek-card-bg: var(--base01); ··· 547 541 font-size: 13px; 548 542 } 549 543 544 + /* Card button group - standardized icon button container in card header */ 545 + .card-btn-group { 546 + display: flex; 547 + align-items: center; 548 + gap: 4px; 549 + flex-shrink: 0; 550 + } 551 + 550 552 /* Card open button - opens URL in webview */ 551 553 .card-open-btn { 552 554 display: flex; ··· 554 556 justify-content: center; 555 557 width: 22px; 556 558 height: 22px; 557 - background: var(--base02); 559 + background: transparent; 558 560 border: none; 559 561 border-radius: 4px; 560 562 cursor: pointer; ··· 565 567 } 566 568 567 569 .card-open-btn:hover { 568 - background: var(--base0D); 569 - color: var(--base00); 570 + color: var(--base05); 570 571 } 571 572 572 573 /* Card delete button */ ··· 584 585 flex-shrink: 0; 585 586 transition: all 0.15s; 586 587 padding: 0; 587 - opacity: 0; 588 - } 589 - 590 - peek-card:hover .card-delete-btn { 591 - opacity: 1; 592 588 } 593 589 594 590 .card-delete-btn:hover {
+1 -1
extensions/tags/home.html
··· 131 131 <peek-grid-toolbar class="grid-toolbar" view-mode="columns"></peek-grid-toolbar> 132 132 133 133 <!-- Card grid view (list) --> 134 - <peek-grid class="cards" min-item-width="240" gap="8"></peek-grid> 134 + <peek-grid class="cards" min-item-width="250" gap="10"></peek-grid> 135 135 136 136 <!-- Inline detail view (replaces card grid when an item is selected) --> 137 137 <div class="detail-view" style="display: none;">
+2 -2
extensions/timers/home.html
··· 38 38 39 39 <div class="section active-section" id="active-section"> 40 40 <div class="section-label">Active</div> 41 - <peek-grid class="cards active-cards" min-item-width="280" gap="8"></peek-grid> 41 + <peek-grid class="cards active-cards" min-item-width="250" gap="10"></peek-grid> 42 42 </div> 43 43 44 44 <div class="section" id="completed-section"> 45 45 <div class="section-label">History</div> 46 - <peek-grid class="cards completed-cards" min-item-width="280" gap="8"></peek-grid> 46 + <peek-grid class="cards completed-cards" min-item-width="250" gap="10"></peek-grid> 47 47 </div> 48 48 49 49 <script type="module" src="home.js"></script>
+1 -1
extensions/windows/windows.html
··· 41 41 </div> 42 42 43 43 <peek-grid-toolbar class="grid-toolbar" view-mode="columns"></peek-grid-toolbar> 44 - <peek-grid class="cards" min-item-width="200" gap="12"></peek-grid> 44 + <peek-grid class="cards" min-item-width="250" gap="10"></peek-grid> 45 45 46 46 <script type="module" src="windows.js"></script> 47 47 </body>