ui: replace empty-state emoji with monochrome SVG icons
The empty states used emoji icons (egg, refresh, magnifier, bookmark, and a
checkmark), violating the no-emoji brand rule. Ported GO's inline-SVG pattern:
BB.ui.emptyStateIcon(key) returns a 24x24 stroke="currentColor" icon (feed /
refresh / inbox / search / bookmark), theme-aware and monochrome. The
"all caught up" state now uses the inbox icon (was a checkmark). Wrappers
(.empty-icon / .source-item-empty-icon) size and center the SVG.
Covers items.js (4), bookmarks.js (1), sources.js (1). 55 JS tests pass;
lint clean; CSS parses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 files changed,
+43 insertions,
-10 deletions
| 380 |
380 |
|
}
|
| 381 |
381 |
|
.row--article.is-pinned { border-left-color: var(--action); }
|
| 382 |
382 |
|
.empty-icon {
|
| 383 |
|
- |
font-size: 2.5rem;
|
|
383 |
+ |
display: flex;
|
|
384 |
+ |
justify-content: center;
|
| 384 |
385 |
|
margin-bottom: 0.75rem;
|
| 385 |
|
- |
opacity: 0.6;
|
|
386 |
+ |
color: var(--content-muted);
|
| 386 |
387 |
|
}
|
|
388 |
+ |
.empty-icon svg { width: 2.5rem; height: 2.5rem; }
|
| 387 |
389 |
|
|
| 388 |
390 |
|
/* Standalone empty-state block. The items/bookmarks empty rows use
|
| 389 |
391 |
|
`<li class="empty-state">`; new surfaces use BB.ui.renderEmptyState().
|
| 997 |
999 |
|
}
|
| 998 |
1000 |
|
.source-item--empty:hover { background-color: transparent; }
|
| 999 |
1001 |
|
.source-item-empty-icon {
|
| 1000 |
|
- |
font-size: 1.5rem;
|
|
1002 |
+ |
display: flex;
|
|
1003 |
+ |
justify-content: center;
|
| 1001 |
1004 |
|
margin-bottom: 0.5rem;
|
| 1002 |
|
- |
opacity: 0.5;
|
|
1005 |
+ |
color: var(--content-muted);
|
| 1003 |
1006 |
|
}
|
|
1007 |
+ |
.source-item-empty-icon svg { width: 1.5rem; height: 1.5rem; }
|
| 1004 |
1008 |
|
.source-item-empty-strong { color: var(--category-four); font-weight: 700; }
|
| 1005 |
1009 |
|
.source-item-empty-link { color: var(--category-four); }
|
| 1006 |
1010 |
|
|
| 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="empty-state"><div class="empty-icon">🔖</div>' + msg + '</li>';
|
|
45 |
+ |
list.innerHTML = '<li class="empty-state"><div class="empty-icon">' + BB.ui.emptyStateIcon('bookmark') + '</div>' + msg + '</li>';
|
| 46 |
46 |
|
renderTagBar([]);
|
| 47 |
47 |
|
return;
|
| 48 |
48 |
|
}
|
| 253 |
253 |
|
* @param {function} [opts.onClick] - Click handler for the CTA button.
|
| 254 |
254 |
|
* @param {boolean} [opts.compact=false] - Use the compact size variant.
|
| 255 |
255 |
|
*/
|
|
256 |
+ |
/**
|
|
257 |
+ |
* Inline monochrome SVG icons for empty states (theme-aware via
|
|
258 |
+ |
* currentColor; no emoji, per the brand rule). 24x24 stroke paths.
|
|
259 |
+ |
*/
|
|
260 |
+ |
const EMPTY_STATE_ICONS = {
|
|
261 |
+ |
feed: '<path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1.5" fill="currentColor" stroke="none"/>',
|
|
262 |
+ |
refresh: '<path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 4v5h-5"/>',
|
|
263 |
+ |
inbox: '<path d="M3 13l3-8h12l3 8"/><path d="M3 13h5l1.5 3h5L16 13h5v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
264 |
+ |
search: '<circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>',
|
|
265 |
+ |
bookmark: '<path d="M6 3h12a1 1 0 0 1 1 1v17l-7-4-7 4V4a1 1 0 0 1 1-1z"/>',
|
|
266 |
+ |
};
|
|
267 |
+ |
|
|
268 |
+ |
/**
|
|
269 |
+ |
* Return an inline SVG string for an empty-state icon, or '' for an unknown
|
|
270 |
+ |
* key. Wrap the result in a sizing element (`.empty-icon` /
|
|
271 |
+ |
* `.source-item-empty-icon`) at the call site.
|
|
272 |
+ |
* @param {string} key - One of the EMPTY_STATE_ICONS keys.
|
|
273 |
+ |
* @returns {string}
|
|
274 |
+ |
*/
|
|
275 |
+ |
function emptyStateIcon(key) {
|
|
276 |
+ |
const paths = EMPTY_STATE_ICONS[key];
|
|
277 |
+ |
if (!paths) return '';
|
|
278 |
+ |
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" '
|
|
279 |
+ |
+ 'stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" '
|
|
280 |
+ |
+ 'aria-hidden="true">' + paths + '</svg>';
|
|
281 |
+ |
}
|
|
282 |
+ |
|
| 256 |
283 |
|
function renderEmptyState(container, message, opts) {
|
| 257 |
284 |
|
opts = opts || {};
|
| 258 |
285 |
|
container.innerHTML = '';
|
| 645 |
672 |
|
confirmAction, // legacy — prefer showConfirmDialog for new code
|
| 646 |
673 |
|
showConfirmDialog,
|
| 647 |
674 |
|
renderEmptyState,
|
|
675 |
+ |
emptyStateIcon,
|
| 648 |
676 |
|
showContextMenu,
|
| 649 |
677 |
|
showUndoToast,
|
| 650 |
678 |
|
bulkActionWithUndo,
|
| 100 |
100 |
|
const hasFeeds = BB.state.sources && BB.state.sources.length > 0;
|
| 101 |
101 |
|
const totalItems = hasFeeds ? BB.state.sources.reduce((n, s) => n + s.totalCount, 0) : 0;
|
| 102 |
102 |
|
let message;
|
|
103 |
+ |
const icon = (key) => '<div class="empty-icon">' + BB.ui.emptyStateIcon(key) + '</div>';
|
| 103 |
104 |
|
if (!hasFeeds) {
|
| 104 |
|
- |
message = '<div class="empty-icon">🍳</div>No feeds yet.<br>Click <strong>+ Add Feed</strong> to get started, or import an OPML file.<br><kbd>?</kbd> for keyboard shortcuts.';
|
|
105 |
+ |
message = icon('feed') + 'No feeds yet.<br>Click <strong>+ Add Feed</strong> to get started, or import an OPML file.<br><kbd>?</kbd> for keyboard shortcuts.';
|
| 105 |
106 |
|
} else if (totalItems === 0) {
|
| 106 |
|
- |
message = '<div class="empty-icon">🔄</div>Feeds added but no articles yet.<br>Click <strong>Refresh</strong> to fetch posts.';
|
|
107 |
+ |
message = icon('refresh') + 'Feeds added but no articles yet.<br>Click <strong>Refresh</strong> to fetch posts.';
|
| 107 |
108 |
|
} else if (BB.state.unreadOnly) {
|
| 108 |
|
- |
message = '<div class="empty-icon">✓</div>All caught up! No unread items.';
|
|
109 |
+ |
message = icon('inbox') + 'All caught up! No unread items.';
|
| 109 |
110 |
|
} else {
|
| 110 |
|
- |
message = '<div class="empty-icon">🔍</div>No items match the current filter.<br>Try switching to "All" or a different source.';
|
|
111 |
+ |
message = icon('search') + 'No items match the current filter.<br>Try switching to "All" or a different source.';
|
| 111 |
112 |
|
}
|
| 112 |
113 |
|
list.innerHTML = '<li class="empty-state">' + message + '</li>';
|
| 113 |
114 |
|
document.getElementById('load-more').style.display = 'none';
|
| 165 |
165 |
|
if (sources.length === 0) {
|
| 166 |
166 |
|
const emptyLi = document.createElement('li');
|
| 167 |
167 |
|
emptyLi.className = 'source-item source-item--empty';
|
| 168 |
|
- |
emptyLi.innerHTML = '<div class="source-item-empty-icon">\uD83C\uDF73</div>Add your first feed to get started.<br>Click <strong class="source-item-empty-strong">+ Add Feed</strong> above,<br>or <a href="#" onclick="event.preventDefault(); BB.feeds.importOpml();" class="source-item-empty-link">import an OPML file</a>.';
|
|
168 |
+ |
emptyLi.innerHTML = '<div class="source-item-empty-icon">' + BB.ui.emptyStateIcon('feed') + '</div>Add your first feed to get started.<br>Click <strong class="source-item-empty-strong">+ Add Feed</strong> above,<br>or <a href="#" onclick="event.preventDefault(); BB.feeds.importOpml();" class="source-item-empty-link">import an OPML file</a>.';
|
| 169 |
169 |
|
frag.appendChild(emptyLi);
|
| 170 |
170 |
|
}
|
| 171 |
171 |
|
|