max / balanced_breakfast
4 files changed,
+168 insertions,
-128 deletions
| @@ -686,46 +686,39 @@ body { | |||
| 686 | 686 | ||
| 687 | 687 | .items-list { list-style: none; } | |
| 688 | 688 | ||
| 689 | - | .item { | |
| 690 | - | display: flex; | |
| 689 | + | /* Article rows: feed items + reading-list bookmarks. Uses the renderRow .row | |
| 690 | + | layout (icon / content / actions, flex + gap) plus article appearance + | |
| 691 | + | read/unread/selected/pinned state. Replaces the legacy .item-* layout. */ | |
| 692 | + | .row--article { | |
| 691 | 693 | padding: 0.75rem 1.25rem; | |
| 692 | 694 | border-bottom: 1px solid var(--border); | |
| 693 | 695 | border-left: 3px solid transparent; | |
| 694 | 696 | cursor: pointer; | |
| 695 | 697 | transition: background-color 0.15s; | |
| 696 | 698 | } | |
| 697 | - | .item:hover { background-color: var(--surface-overlay); } | |
| 698 | - | .item.unread { | |
| 699 | + | .row--article:hover { background-color: var(--surface-overlay); } | |
| 700 | + | .row--article.is-unread { | |
| 699 | 701 | border-left-color: var(--danger); | |
| 700 | 702 | background-color: color-mix(in srgb, var(--danger) 6%, var(--surface-page)); | |
| 701 | 703 | } | |
| 702 | - | .item.read { color: var(--content-secondary); } | |
| 703 | - | .item.selected { | |
| 704 | + | .row--article.is-read { color: var(--content-secondary); } | |
| 705 | + | .row--article.is-selected { | |
| 704 | 706 | background-color: var(--surface-sunken); | |
| 705 | 707 | border-left-color: var(--category-four); | |
| 706 | 708 | } | |
| 707 | - | .item.unread.selected { | |
| 709 | + | .row--article.is-unread.is-selected { | |
| 708 | 710 | border-left-color: var(--danger); | |
| 709 | 711 | background-color: color-mix(in srgb, var(--danger) 12%, var(--surface-page)); | |
| 710 | 712 | } | |
| 711 | - | .item.empty-state { | |
| 712 | - | text-align: center; | |
| 713 | - | color: var(--content-muted); | |
| 714 | - | padding: 3rem 2rem; | |
| 715 | - | cursor: default; | |
| 716 | - | line-height: 1.6; | |
| 717 | - | flex-direction: column; | |
| 718 | - | align-items: center; | |
| 719 | - | } | |
| 713 | + | .row--article.is-pinned { border-left-color: var(--action); } | |
| 720 | 714 | .empty-icon { | |
| 721 | 715 | font-size: 2.5rem; | |
| 722 | 716 | margin-bottom: 0.75rem; | |
| 723 | 717 | opacity: 0.6; | |
| 724 | 718 | } | |
| 725 | 719 | ||
| 726 | - | /* F2 addition (2026-06-02) — .empty-state as its own block. Today's | |
| 727 | - | .item.empty-state (modifier-on-row) stays as legacy until F5 retires | |
| 728 | - | call sites. Use this block + BB.ui.renderEmptyState() for new surfaces. | |
| 720 | + | /* Standalone empty-state block. The items/bookmarks empty rows use | |
| 721 | + | `<li class="empty-state">`; new surfaces use BB.ui.renderEmptyState(). | |
| 729 | 722 | <div class="empty-state"> | |
| 730 | 723 | <div class="empty-state-icon">...</div> | |
| 731 | 724 | <p class="empty-state-text">No items</p> | |
| @@ -758,11 +751,12 @@ body { | |||
| 758 | 751 | } | |
| 759 | 752 | .empty-state--compact .empty-state-icon { font-size: 1.5rem; } | |
| 760 | 753 | ||
| 761 | - | .item-indicators { | |
| 754 | + | /* Control stack (star / pin + read dot) passed into renderRow's icon slot. | |
| 755 | + | The .row layout provides the gap to the content; no margin needed here. */ | |
| 756 | + | .article-controls { | |
| 762 | 757 | display: flex; | |
| 763 | 758 | flex-direction: column; | |
| 764 | 759 | align-items: center; | |
| 765 | - | margin-right: 0.75rem; | |
| 766 | 760 | min-width: 24px; | |
| 767 | 761 | } | |
| 768 | 762 | ||
| @@ -788,38 +782,39 @@ body { | |||
| 788 | 782 | } | |
| 789 | 783 | .read-indicator.unread { background-color: var(--danger); } | |
| 790 | 784 | ||
| 791 | - | .item-content { flex: 1; min-width: 0; } | |
| 785 | + | /* Content column is renderRow's .row-content (flex:1; min-width:0). */ | |
| 792 | 786 | ||
| 793 | - | .item-header { | |
| 787 | + | /* Header slot holds the author/time meta line (+ mobile source badge). */ | |
| 788 | + | .article-meta { | |
| 794 | 789 | display: flex; | |
| 795 | 790 | justify-content: space-between; | |
| 796 | 791 | align-items: flex-start; | |
| 797 | 792 | margin-bottom: 0.15rem; | |
| 798 | 793 | } | |
| 799 | - | ||
| 800 | - | .item-author { | |
| 794 | + | .article-author { | |
| 801 | 795 | font-weight: 700; | |
| 802 | 796 | color: var(--danger); | |
| 803 | - | font-size: 0.8rem; | |
| 797 | + | font-size: var(--font-size-md); | |
| 804 | 798 | } | |
| 799 | + | .article-time { color: var(--content-muted); font-size: var(--font-size-sm); } | |
| 805 | 800 | ||
| 806 | - | .item-time { color: var(--content-muted); font-size: 0.7rem; } | |
| 807 | - | ||
| 808 | - | .item-text { | |
| 809 | - | color: var(--content); | |
| 801 | + | /* Title: two-line clamp (overrides .row-primary's single flex line). */ | |
| 802 | + | .row--article .row-primary { | |
| 810 | 803 | display: -webkit-box; | |
| 811 | 804 | -webkit-line-clamp: 2; | |
| 812 | 805 | -webkit-box-orient: vertical; | |
| 813 | 806 | overflow: hidden; | |
| 807 | + | font-weight: 400; | |
| 808 | + | color: var(--content); | |
| 814 | 809 | line-height: 1.4; | |
| 815 | - | font-size: 0.875rem; | |
| 810 | + | font-size: var(--font-size-base); | |
| 816 | 811 | } | |
| 817 | - | ||
| 818 | - | .item-secondary { | |
| 812 | + | .row--article .row-secondary { | |
| 819 | 813 | color: var(--content-secondary); | |
| 820 | - | font-size: 0.8rem; | |
| 814 | + | font-size: var(--font-size-md); | |
| 821 | 815 | margin-top: 0.15rem; | |
| 822 | 816 | } | |
| 817 | + | /* .article-source-badge (mobile-only) lives in section 38. */ | |
| 823 | 818 | ||
| 824 | 819 | .load-more { padding: 1rem; text-align: center; } | |
| 825 | 820 | ||
| @@ -1430,7 +1425,7 @@ body { | |||
| 1430 | 1425 | .source-item:focus-visible, | |
| 1431 | 1426 | .star-btn:focus-visible, | |
| 1432 | 1427 | .source-delete:focus-visible, | |
| 1433 | - | .item:focus-visible, | |
| 1428 | + | .row--article:focus-visible, | |
| 1434 | 1429 | .tab:focus-visible, | |
| 1435 | 1430 | .plugin-item:focus-visible, | |
| 1436 | 1431 | .sort-select:focus-visible, | |
| @@ -1779,7 +1774,7 @@ body { | |||
| 1779 | 1774 | /* === 38. Source-badge on items (mobile-only display) ================= | |
| 1780 | 1775 | Hidden on desktop; shown inside the @media (max-width: 768px) block below. | |
| 1781 | 1776 | ========================================================================== */ | |
| 1782 | - | .item-source-badge { display: none; } | |
| 1777 | + | .article-source-badge { display: none; } | |
| 1783 | 1778 | ||
| 1784 | 1779 | /* === 39. @media (max-width: 768px) — mobile layout ==================== */ | |
| 1785 | 1780 | @media (max-width: 768px) { | |
| @@ -1834,7 +1829,7 @@ body { | |||
| 1834 | 1829 | .mobile-search-bar { display: flex; } | |
| 1835 | 1830 | ||
| 1836 | 1831 | /* Larger tap targets */ | |
| 1837 | - | .item { padding: 0.875rem 1rem; } | |
| 1832 | + | .row--article { padding: 0.875rem 1rem; } | |
| 1838 | 1833 | .star-btn { font-size: 1.3rem; padding: 0.4rem 0.5rem; margin: -0.4rem -0.5rem; } | |
| 1839 | 1834 | .source-item { min-height: 3rem; } | |
| 1840 | 1835 | ||
| @@ -1870,7 +1865,7 @@ body { | |||
| 1870 | 1865 | } | |
| 1871 | 1866 | ||
| 1872 | 1867 | /* Source badge on items */ | |
| 1873 | - | .item-source-badge { | |
| 1868 | + | .article-source-badge { | |
| 1874 | 1869 | display: inline-block; | |
| 1875 | 1870 | background-color: var(--surface-sunken); | |
| 1876 | 1871 | color: var(--content-secondary); | |
| @@ -1882,7 +1877,7 @@ body { | |||
| 1882 | 1877 | } | |
| 1883 | 1878 | ||
| 1884 | 1879 | /* Bold unread item titles */ | |
| 1885 | - | .item.unread .item-text { font-weight: 700; } | |
| 1880 | + | .row--article.is-unread .row-primary { font-weight: 700; } | |
| 1886 | 1881 | ||
| 1887 | 1882 | /* Sticky detail header */ | |
| 1888 | 1883 | .detail-header { | |
| @@ -1909,10 +1904,10 @@ body { | |||
| 1909 | 1904 | ||
| 1910 | 1905 | /* === 40. @media (hover: none) — touch overrides ====================== */ | |
| 1911 | 1906 | @media (hover: none) { | |
| 1912 | - | .item:hover { background-color: transparent; } | |
| 1913 | - | .item.unread:hover { background-color: color-mix(in srgb, var(--danger) 6%, var(--surface-page)); } | |
| 1914 | - | .item.selected:hover { background-color: var(--surface-sunken); } | |
| 1915 | - | .item.unread.selected:hover { background-color: color-mix(in srgb, var(--danger) 12%, var(--surface-page)); } | |
| 1907 | + | .row--article:hover { background-color: transparent; } | |
| 1908 | + | .row--article.is-unread:hover { background-color: color-mix(in srgb, var(--danger) 6%, var(--surface-page)); } | |
| 1909 | + | .row--article.is-selected:hover { background-color: var(--surface-sunken); } | |
| 1910 | + | .row--article.is-unread.is-selected:hover { background-color: color-mix(in srgb, var(--danger) 12%, var(--surface-page)); } | |
| 1916 | 1911 | .source-item:hover { background-color: transparent; } | |
| 1917 | 1912 | .source-item.active:hover { background-color: var(--surface-sunken); } | |
| 1918 | 1913 | .sidebar-saved:hover { background-color: transparent; } | |
| @@ -2133,22 +2128,8 @@ body { | |||
| 2133 | 2128 | color: white; | |
| 2134 | 2129 | border-color: var(--action); | |
| 2135 | 2130 | } | |
| 2136 | - | .bookmark-item .bookmark-actions { | |
| 2137 | - | display: flex; | |
| 2138 | - | gap: 0.25rem; | |
| 2139 | - | align-items: center; | |
| 2140 | - | margin-left: auto; | |
| 2141 | - | flex-shrink: 0; | |
| 2142 | - | opacity: 0; | |
| 2143 | - | transition: opacity 0.15s; | |
| 2144 | - | } | |
| 2145 | - | .bookmark-item:hover .bookmark-actions, | |
| 2146 | - | .bookmark-item:focus-within .bookmark-actions { | |
| 2147 | - | opacity: 1; | |
| 2148 | - | } | |
| 2149 | - | .bookmark-item.pinned { | |
| 2150 | - | border-left: 3px solid var(--action); | |
| 2151 | - | } | |
| 2131 | + | /* Bookmark Open/more actions use renderRow's .row-actions (hover-revealed); | |
| 2132 | + | pinned state uses .row--article.is-pinned. */ | |
| 2152 | 2133 | .bookmark-tags { | |
| 2153 | 2134 | display: flex; | |
| 2154 | 2135 | gap: 0.25rem; |
| @@ -42,7 +42,7 @@ | |||
| 42 | 42 | const msg = currentTag | |
| 43 | 43 | ? 'No bookmarks with tag "' + escapeHtml(currentTag) + '".' | |
| 44 | 44 | : 'Your reading list is empty.<br>Bookmark articles from the detail panel, or add a URL.'; | |
| 45 | - | list.innerHTML = '<li class="item empty-state"><div class="empty-icon">🔖</div>' + msg + '</li>'; | |
| 45 | + | list.innerHTML = '<li class="empty-state"><div class="empty-icon">🔖</div>' + msg + '</li>'; | |
| 46 | 46 | renderTagBar([]); | |
| 47 | 47 | return; | |
| 48 | 48 | } | |
| @@ -53,43 +53,75 @@ | |||
| 53 | 53 | const frag = document.createDocumentFragment(); | |
| 54 | 54 | ||
| 55 | 55 | for (const bm of bookmarks) { | |
| 56 | - | const li = document.createElement('li'); | |
| 57 | - | li.className = 'item bookmark-item' + (bm.isPinned ? ' pinned' : ''); | |
| 58 | - | li.dataset.id = bm.id; | |
| 59 | - | li.setAttribute('role', 'option'); | |
| 60 | - | li.setAttribute('tabindex', '0'); | |
| 61 | - | ||
| 62 | - | const tags = bm.tags.length > 0 | |
| 63 | - | ? '<div class="bookmark-tags">' + bm.tags.map(t => | |
| 64 | - | '<span class="tag bookmark-tag" onclick="event.stopPropagation(); BB.bookmarks.load(\'' + escapeAttr(t) + '\')">' | |
| 65 | - | + escapeHtml(t) + '</span>' | |
| 66 | - | ).join('') + '</div>' | |
| 67 | - | : ''; | |
| 68 | - | ||
| 69 | - | li.innerHTML = ` | |
| 70 | - | <div class="item-indicators"> | |
| 71 | - | <button class="star-btn${bm.isPinned ? ' starred' : ''}" | |
| 72 | - | aria-label="Toggle pin" | |
| 73 | - | onclick="event.stopPropagation(); BB.bookmarks.togglePin('${escapeAttr(bm.id)}', ${bm.isPinned})"> | |
| 74 | - | ${bm.isPinned ? '\u{1F4CC}' : '\u{1F4CC}'} | |
| 75 | - | </button> | |
| 76 | - | </div> | |
| 77 | - | <div class="item-content"> | |
| 78 | - | <div class="item-header"> | |
| 79 | - | ${bm.author ? '<span class="item-author">' + escapeHtml(bm.author) + '</span>' : ''} | |
| 80 | - | <span class="item-time">${escapeHtml(bm.timeAgo)}</span> | |
| 81 | - | </div> | |
| 82 | - | ${bm.sourceName ? '<span class="item-source-badge">' + escapeHtml(bm.sourceName) + '</span>' : ''} | |
| 83 | - | <div class="item-text">${escapeHtml(bm.title || bm.url)}</div> | |
| 84 | - | ${tags} | |
| 85 | - | </div> | |
| 86 | - | <div class="bookmark-actions"> | |
| 87 | - | <button class="btn btn-small" onclick="event.stopPropagation(); BB.bookmarks.openUrl('${escapeAttr(bm.url)}')" title="Open URL">Open</button> | |
| 88 | - | <button class="btn btn-small" onclick="event.stopPropagation(); BB.bookmarks.showContextMenu(event, '${escapeAttr(bm.id)}')" title="More actions">\u22EF</button> | |
| 89 | - | </div> | |
| 90 | - | `; | |
| 56 | + | // Control stack: pin toggle (renderRow icon slot). | |
| 57 | + | const controls = document.createElement('div'); | |
| 58 | + | controls.className = 'article-controls'; | |
| 59 | + | const pin = document.createElement('button'); | |
| 60 | + | pin.className = 'star-btn' + (bm.isPinned ? ' starred' : ''); | |
| 61 | + | pin.setAttribute('aria-label', 'Toggle pin'); | |
| 62 | + | pin.textContent = '\u{1F4CC}'; | |
| 63 | + | pin.onclick = (e) => { e.stopPropagation(); togglePin(bm.id, bm.isPinned); }; | |
| 64 | + | controls.appendChild(pin); | |
| 65 | + | ||
| 66 | + | // Header slot: optional author + time meta line + mobile source badge. | |
| 67 | + | const header = document.createDocumentFragment(); | |
| 68 | + | const meta = document.createElement('div'); | |
| 69 | + | meta.className = 'article-meta'; | |
| 70 | + | if (bm.author) { | |
| 71 | + | const author = document.createElement('span'); | |
| 72 | + | author.className = 'article-author'; | |
| 73 | + | author.textContent = bm.author; | |
| 74 | + | meta.appendChild(author); | |
| 75 | + | } | |
| 76 | + | const time = document.createElement('span'); | |
| 77 | + | time.className = 'article-time'; | |
| 78 | + | time.textContent = bm.timeAgo; | |
| 79 | + | meta.appendChild(time); | |
| 80 | + | header.appendChild(meta); | |
| 81 | + | if (bm.sourceName) { | |
| 82 | + | const badge = document.createElement('span'); | |
| 83 | + | badge.className = 'article-source-badge'; | |
| 84 | + | badge.textContent = bm.sourceName; | |
| 85 | + | header.appendChild(badge); | |
| 86 | + | } | |
| 91 | 87 | ||
| 92 | - | li.onclick = () => openUrl(bm.url); | |
| 88 | + | // Secondary slot: tag chips. | |
| 89 | + | let secondary = null; | |
| 90 | + | if (bm.tags.length > 0) { | |
| 91 | + | secondary = document.createElement('div'); | |
| 92 | + | secondary.className = 'bookmark-tags'; | |
| 93 | + | for (const t of bm.tags) { | |
| 94 | + | const chip = document.createElement('span'); | |
| 95 | + | chip.className = 'tag bookmark-tag'; | |
| 96 | + | chip.textContent = t; | |
| 97 | + | chip.onclick = (e) => { e.stopPropagation(); load(t); }; | |
| 98 | + | secondary.appendChild(chip); | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // Actions slot: Open + more (hover-revealed via .row-actions). | |
| 103 | + | const openBtn = document.createElement('button'); | |
| 104 | + | openBtn.className = 'btn btn-small'; | |
| 105 | + | openBtn.title = 'Open URL'; | |
| 106 | + | openBtn.textContent = 'Open'; | |
| 107 | + | openBtn.onclick = (e) => { e.stopPropagation(); openUrl(bm.url); }; | |
| 108 | + | const moreBtn = document.createElement('button'); | |
| 109 | + | moreBtn.className = 'btn btn-small'; | |
| 110 | + | moreBtn.title = 'More actions'; | |
| 111 | + | moreBtn.textContent = '\u22EF'; | |
| 112 | + | moreBtn.onclick = (e) => { e.stopPropagation(); showContextMenu(e, bm.id); }; | |
| 113 | + | ||
| 114 | + | const li = BB.ui.renderRow({ | |
| 115 | + | tag: 'li', | |
| 116 | + | className: 'row--article' + (bm.isPinned ? ' is-pinned' : ''), | |
| 117 | + | icon: controls, | |
| 118 | + | header: header, | |
| 119 | + | primary: bm.title || bm.url, | |
| 120 | + | secondary: secondary, | |
| 121 | + | actions: [openBtn, moreBtn], | |
| 122 | + | attrs: { role: 'option', tabindex: '0', 'data-id': bm.id }, | |
| 123 | + | onClick: () => openUrl(bm.url), | |
| 124 | + | }); | |
| 93 | 125 | li.onkeydown = (e) => { | |
| 94 | 126 | if (e.key === 'Enter' || e.key === ' ') { | |
| 95 | 127 | e.preventDefault(); |
| @@ -7,7 +7,7 @@ | |||
| 7 | 7 | (function() { | |
| 8 | 8 | 'use strict'; | |
| 9 | 9 | ||
| 10 | - | const { escapeHtml, escapeAttr, getErrorMessage } = BB.utils; | |
| 10 | + | const { getErrorMessage } = BB.utils; | |
| 11 | 11 | ||
| 12 | 12 | /** Set of item IDs with in-flight star/read toggles to prevent double-clicks. */ | |
| 13 | 13 | const inFlight = new Set(); | |
| @@ -109,7 +109,7 @@ | |||
| 109 | 109 | } else { | |
| 110 | 110 | message = '<div class="empty-icon">🔍</div>No items match the current filter.<br>Try switching to "All" or a different source.'; | |
| 111 | 111 | } | |
| 112 | - | list.innerHTML = '<li class="item empty-state">' + message + '</li>'; | |
| 112 | + | list.innerHTML = '<li class="empty-state">' + message + '</li>'; | |
| 113 | 113 | document.getElementById('load-more').style.display = 'none'; | |
| 114 | 114 | return; | |
| 115 | 115 | } | |
| @@ -118,36 +118,63 @@ | |||
| 118 | 118 | const frag = document.createDocumentFragment(); | |
| 119 | 119 | ||
| 120 | 120 | items.forEach(item => { | |
| 121 | - | const li = document.createElement('li'); | |
| 122 | - | li.className = 'item' | |
| 123 | - | + (item.isRead ? ' read' : ' unread') | |
| 124 | - | + (selected === item.id ? ' selected' : ''); | |
| 125 | - | li.dataset.id = item.id; | |
| 126 | - | li.setAttribute('role', 'option'); | |
| 127 | - | li.setAttribute('aria-selected', selected === item.id ? 'true' : 'false'); | |
| 121 | + | const isSelected = selected === item.id; | |
| 128 | 122 | ||
| 129 | - | li.innerHTML = ` | |
| 130 | - | <div class="item-indicators"> | |
| 131 | - | <button class="star-btn${item.isStarred ? ' starred' : ''}" | |
| 132 | - | aria-label="Toggle star" | |
| 133 | - | onclick="event.stopPropagation(); BB.items.toggleStar('${escapeAttr(item.id)}', ${item.isStarred})"> | |
| 134 | - | ${item.isStarred ? '\u2605' : '\u2606'} | |
| 135 | - | </button> | |
| 136 | - | <div class="read-indicator${item.isRead ? '' : ' unread'}"></div> | |
| 137 | - | </div> | |
| 138 | - | <div class="item-content"> | |
| 139 | - | <div class="item-header"> | |
| 140 | - | <span class="item-author">${escapeHtml(item.author)}</span> | |
| 141 | - | <span class="item-time">${escapeHtml(item.timeAgo)}</span> | |
| 142 | - | </div> | |
| 143 | - | <span class="item-source-badge">${escapeHtml(item.sourceName)}</span> | |
| 144 | - | <div class="item-text">${escapeHtml(item.title || item.text)}</div> | |
| 145 | - | ${item.secondary ? `<div class="item-secondary">${escapeHtml(item.secondary)}</div>` : ''} | |
| 146 | - | </div> | |
| 147 | - | `; | |
| 123 | + | // Control stack: star + read-indicator (renderRow icon slot). | |
| 124 | + | const controls = document.createElement('div'); | |
| 125 | + | controls.className = 'article-controls'; | |
| 126 | + | const star = document.createElement('button'); | |
| 127 | + | star.className = 'star-btn' + (item.isStarred ? ' starred' : ''); | |
| 128 | + | star.setAttribute('aria-label', 'Toggle star'); | |
| 129 | + | star.textContent = item.isStarred ? '\u2605' : '\u2606'; | |
| 130 | + | star.onclick = (e) => { e.stopPropagation(); toggleStar(item.id, item.isStarred); }; | |
| 131 | + | const readDot = document.createElement('div'); | |
| 132 | + | readDot.className = 'read-indicator' + (item.isRead ? '' : ' unread'); | |
| 133 | + | controls.appendChild(star); | |
| 134 | + | controls.appendChild(readDot); | |
| 148 | 135 | ||
| 149 | - | li.setAttribute('tabindex', '0'); | |
| 150 | - | li.onclick = () => selectItem(item.id); | |
| 136 | + | // Header slot: author/time meta line + mobile source badge. | |
| 137 | + | const header = document.createDocumentFragment(); | |
| 138 | + | const meta = document.createElement('div'); | |
| 139 | + | meta.className = 'article-meta'; | |
| 140 | + | const author = document.createElement('span'); | |
| 141 | + | author.className = 'article-author'; | |
| 142 | + | author.textContent = item.author; | |
| 143 | + | const time = document.createElement('span'); | |
| 144 | + | time.className = 'article-time'; | |
| 145 | + | time.textContent = item.timeAgo; | |
| 146 | + | meta.appendChild(author); | |
| 147 | + | meta.appendChild(time); | |
| 148 | + | header.appendChild(meta); | |
| 149 | + | if (item.sourceName) { | |
| 150 | + | const badge = document.createElement('span'); | |
| 151 | + | badge.className = 'article-source-badge'; | |
| 152 | + | badge.textContent = item.sourceName; | |
| 153 | + | header.appendChild(badge); | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | let secondary = null; | |
| 157 | + | if (item.secondary) { | |
| 158 | + | secondary = document.createElement('div'); | |
| 159 | + | secondary.textContent = item.secondary; | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | const li = BB.ui.renderRow({ | |
| 163 | + | tag: 'li', | |
| 164 | + | className: 'row--article ' + (item.isRead ? 'is-read' : 'is-unread') | |
| 165 | + | + (isSelected ? ' is-selected' : ''), | |
| 166 | + | icon: controls, | |
| 167 | + | header: header, | |
| 168 | + | primary: item.title || item.text, | |
| 169 | + | secondary: secondary, | |
| 170 | + | attrs: { | |
| 171 | + | role: 'option', | |
| 172 | + | 'aria-selected': isSelected ? 'true' : 'false', | |
| 173 | + | tabindex: '0', | |
| 174 | + | 'data-id': item.id, | |
| 175 | + | }, | |
| 176 | + | onClick: () => selectItem(item.id), | |
| 177 | + | }); | |
| 151 | 178 | li.ondblclick = (e) => { | |
| 152 | 179 | e.preventDefault(); | |
| 153 | 180 | expandReaderView(item.id); |
| @@ -187,7 +187,7 @@ | |||
| 187 | 187 | if (!container) return; | |
| 188 | 188 | ||
| 189 | 189 | addSwipeDelegate(container, { | |
| 190 | - | rowSelector: '.item', | |
| 190 | + | rowSelector: '.row--article', | |
| 191 | 191 | getActions: (row) => { | |
| 192 | 192 | const id = row.dataset.id; | |
| 193 | 193 | if (!id) return null; | |
| @@ -216,7 +216,7 @@ | |||
| 216 | 216 | const container = document.getElementById('items-list'); | |
| 217 | 217 | if (!container) return; | |
| 218 | 218 | ||
| 219 | - | addLongPressDelegate(container, '.item', (row) => { | |
| 219 | + | addLongPressDelegate(container, '.row--article', (row) => { | |
| 220 | 220 | const id = row.dataset.id; | |
| 221 | 221 | const item = BB.state.items.find(i => i.id === id); | |
| 222 | 222 | if (!item) return; |