fix: ctrl+click, shared pill rendering, header layout — all in one

Three recurring issues fixed together:

1. Ctrl+click: All protocol and collection pills now use <a href>
   instead of <div onclick>, so ctrl/cmd+click opens in new tab.
   text-decoration:none and color:inherit prevent link styling.

2. Consistent modules: Shared renderProtocolPill() and
   renderCollectionPill() functions used everywhere — library,
   collections list, collection detail, user profile. All produce
   identical HTML with .protocol-grid wrapper for consistent spacing.
   Profile collections no longer have .icon divs.

3. Header layout: Logo (32px) on left with crumb-text wrapper.
   $ protocol-droid on first line, crumb path below it.
   Favicon links and PWA manifest restored.
   Relative paths (frontend/ not /frontend/) for subdirectory support.
This commit is contained in:
Protocolbot
2026-07-09 09:46:31 -06:00
parent 10ee5fe1df
commit b5ce383147
3 changed files with 43 additions and 41 deletions
+30 -36
View File
@@ -297,16 +297,28 @@ function renderProtocolGrid(protocols) {
grid.innerHTML = '<div class="empty">No protocols yet. <a href="#create" onclick="navigate(\'create\');return false">Create one</a></div>';
return;
}
grid.innerHTML = protocols.map(function(p) {
var sourceLabel = p.source || (p.author ? '@' + p.author : '');
var tags = (p.tags || []).map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('');
return '<div class="protocol-pill" onclick="showDetail(\'' + p.slug + '\')">' +
'<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' +
'<p>' + escapeHtml(p.description || '') + '</p>' +
(tags ? '<div class="tag-row">' + tags + '</div>' : '') + '</div>' +
'<div class="meta"><div class="steps">' + (p.steps || []).length + ' steps</div>' +
'<div>' + escapeHtml(sourceLabel) + '</div></div></div>';
}).join('');
grid.innerHTML = protocols.map(renderProtocolPill).join('');
}
// --- Shared rendering (use everywhere for consistency) ---
function renderProtocolPill(p) {
var sourceLabel = p.source || (p.author ? '@' + p.author : '');
var tags = (p.tags || []).map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('');
return '<a class="protocol-pill" href="#protocols/' + p.slug + '">' +
'<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' +
'<p>' + escapeHtml(p.description || '') + '</p>' +
(tags ? '<div class="tag-row">' + tags + '</div>' : '') + '</div>' +
'<div class="meta"><div class="steps">' + (p.steps || []).length + ' steps</div>' +
(sourceLabel ? '<div>' + escapeHtml(sourceLabel) + '</div>' : '') + '</div></a>';
}
function renderCollectionPill(c) {
var sourceLabel = c.source || (c.author ? '@' + c.author : '');
return '<a class="protocol-pill collection" href="#collections/' + c.slug + '">' +
'<div class="info"><h3>' + escapeHtml(c.title) + '</h3>' +
'<p>' + escapeHtml(c.description || '') + '</p></div>' +
'<div class="meta"><div class="steps">' + (c.item_count || 0) + ' protocols</div>' +
(sourceLabel ? '<div>' + escapeHtml(sourceLabel) + '</div>' : '') + '</div></a>';
}
// (iconFor removed — icons are no longer used)
@@ -563,12 +575,7 @@ async function loadCollections() {
grid.innerHTML = '<div class="empty">No collections yet. <a href="#" onclick="navigate(\'create-collection\');return false">Create one</a></div>';
return;
}
grid.innerHTML = collections.map(function(c) {
var sourceLabel = c.author ? '@' + c.author : (c.source || '');
return '<div class="protocol-pill collection" onclick="showCollectionDetail(\'' + c.slug + '\')">' +
'<div class="info"><h3>' + escapeHtml(c.title) + '</h3><p>' + escapeHtml(c.description || '') + '</p></div>' +
'<div class="meta"><div class="steps">' + (c.item_count || 0) + ' protocols</div><div>' + escapeHtml(sourceLabel) + '</div></div></div>';
}).join('');
grid.innerHTML = collections.map(renderCollectionPill).join('');
} catch (e) {
document.getElementById('collectionsGrid').innerHTML = '<div class="empty">' + escapeHtml(e.message) + '</div>';
}
@@ -729,11 +736,7 @@ async function showCollectionDetail(slug) {
if (item.type === 'protocol') {
try {
var p = await api('/protocols/' + item.slug);
itemsHtml += '<div class="protocol-pill" onclick="showDetail(\'' + p.slug + '\')">' +
'<div class="info"><h3>' + escapeHtml(p.title) + '</h3>' +
'<p>' + escapeHtml(p.description || '') + '</p>' +
(p.tags && p.tags.length ? '<div class="tag-row">' + p.tags.map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('') + '</div>' : '') + '</div>' +
'<div class="meta"><div class="steps">' + (p.steps || []).length + ' steps</div></div></div>';
itemsHtml += renderProtocolPill(p);
} catch (e2) {
itemsHtml += '<div class="protocol-pill"><div class="info"><h3>' + escapeHtml(item.slug) + '</h3><p>Protocol not found</p></div></div>';
}
@@ -800,21 +803,12 @@ async function showUser(username) {
'<div class="profile-card"><h2>@' + escapeHtml(data.user.username) + '</h2>' +
(data.user.display_name ? '<div class="bio">' + escapeHtml(data.user.display_name) + '</div>' : '') +
(data.user.bio ? '<div class="bio">' + escapeHtml(data.user.bio) + '</div>' : '') + '</div>' +
(p.length ? '<div class="detail-section"><h2>Protocols (' + p.length + ')</h2>' +
p.map(function(proto) {
return '<div class="protocol-pill" onclick="showDetail(\'' + proto.slug + '\')">' +
'<div class="info"><h3>' + escapeHtml(proto.title) + '</h3>' +
'<p>' + escapeHtml(proto.description||'') + '</p>' +
(proto.tags && proto.tags.length ? '<div class="tag-row">' + proto.tags.map(function(t) { return '<span class="tag">' + escapeHtml(t) + '</span>'; }).join('') + '</div>' : '') + '</div>' +
'<div class="meta"><div class="steps">' + (proto.steps || []).length + ' steps</div></div></div>';
}).join('') +
'</div>' : '') +
(c.length ? '<div class="detail-section"><h2>Collections (' + c.length + ')</h2>' +
c.map(function(col) {
return '<div class="protocol-pill collection" onclick="showCollectionDetail(\'' + col.slug + '\')"><div class="icon">[+]</div>' +
'<div class="info"><h3>' + escapeHtml(col.title) + '</h3><p>' + escapeHtml(col.description||'') + '</p></div></div>';
}).join('') +
'</div>' : '');
(p.length ? '<div class="detail-section"><h2>Protocols (' + p.length + ')</h2><div class="protocol-grid">' +
p.map(renderProtocolPill).join('') +
'</div></div>' : '') +
(c.length ? '<div class="detail-section"><h2>Collections (' + c.length + ')</h2><div class="protocol-grid">' +
c.map(renderCollectionPill).join('') +
'</div></div>' : '');
// Add actions if viewing own profile
var actionsHtml = '';
if (currentUser && currentUser.username === username) {