feat: Add copy buttons for FMRI and install command on detail page

Replace the cramped FMRI badge and install-cmd div with full-width
copyable blocks that have labeled headers, single-line horizontal
scrolling (no wrapping), and a copy-to-clipboard button. The button
shows a checkmark on success. The install command copy strips the
leading $ prompt.
This commit is contained in:
Till Wegmueller 2026-03-15 23:22:32 +01:00
parent 195863f6d8
commit 7d5ddb626f
No known key found for this signature in database
3 changed files with 94 additions and 32 deletions

View file

@ -516,40 +516,70 @@ main.container {
word-break: break-all; word-break: break-all;
} }
/* Install command */ /* Copyable blocks (FMRI, install command) */
.install-cmd { .copyable-block { margin-bottom: 0.75rem; }
.copyable-label {
display: block;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--d-text-muted);
font-weight: 600;
margin-bottom: 0.3rem;
}
.copyable-row {
background: var(--d-bg-surface); background: var(--d-bg-surface);
border: 1px solid var(--d-border); border: 1px solid var(--d-border);
border-radius: var(--d-radius); border-radius: var(--d-radius);
padding: 0.65rem 1rem; padding: 0.55rem 0.75rem;
font-family: var(--d-mono);
font-size: 0.84rem;
color: var(--d-heading);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.6rem; justify-content: space-between;
margin-bottom: 2rem; gap: 0.75rem;
overflow-x: auto;
white-space: nowrap;
} }
.install-cmd .prompt { .copyable-text {
font-family: var(--d-mono);
font-size: 0.82rem;
color: var(--d-heading);
white-space: nowrap;
overflow-x: auto;
flex: 1;
scrollbar-width: thin;
}
.copyable-text .prompt {
color: var(--d-accent); color: var(--d-accent);
user-select: none; user-select: none;
font-weight: 600; font-weight: 600;
} }
/* FMRI badge */ .copy-btn {
.fmri-badge { flex-shrink: 0;
display: inline-block; display: inline-flex;
background: var(--d-bg-surface); align-items: center;
border: 1px solid var(--d-border); justify-content: center;
width: 30px;
height: 30px;
border-radius: 4px; border-radius: 4px;
padding: 0.15rem 0.5rem; background: transparent;
font-family: var(--d-mono); border: 1px solid var(--d-border);
font-size: 0.78rem; color: var(--d-text-muted);
color: var(--d-text); cursor: pointer;
word-break: break-all; transition: all var(--d-transition);
}
.copy-btn:hover {
background: var(--d-bg-hover);
color: var(--d-heading);
border-color: var(--d-accent-dim);
}
.copy-btn.copied {
color: var(--d-success);
border-color: var(--d-success);
} }
/* ========================================================================== /* ==========================================================================

View file

@ -82,9 +82,14 @@
.info-row{display:flex;justify-content:space-between;align-items:baseline;gap:1rem} .info-row{display:flex;justify-content:space-between;align-items:baseline;gap:1rem}
.info-row .label{color:var(--d-text-muted);font-size:0.84rem;white-space:nowrap} .info-row .label{color:var(--d-text-muted);font-size:0.84rem;white-space:nowrap}
.info-row .value{font-family:var(--d-mono);font-size:0.84rem;color:var(--d-heading);text-align:right;word-break:break-all} .info-row .value{font-family:var(--d-mono);font-size:0.84rem;color:var(--d-heading);text-align:right;word-break:break-all}
.install-cmd{background:var(--d-bg-surface);border:1px solid var(--d-border);border-radius:var(--d-radius);padding:0.65rem 1rem;font-family:var(--d-mono);font-size:0.84rem;color:var(--d-heading);display:flex;align-items:center;gap:0.6rem;margin-bottom:2rem;overflow-x:auto;white-space:nowrap} .copyable-block{margin-bottom:0.75rem}
.install-cmd .prompt{color:var(--d-accent);user-select:none;font-weight:600} .copyable-label{display:block;font-size:0.72rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--d-text-muted);font-weight:600;margin-bottom:0.3rem}
.fmri-badge{display:inline-block;background:var(--d-bg-surface);border:1px solid var(--d-border);border-radius:4px;padding:0.15rem 0.5rem;font-family:var(--d-mono);font-size:0.78rem;color:var(--d-text);word-break:break-all} .copyable-row{background:var(--d-bg-surface);border:1px solid var(--d-border);border-radius:var(--d-radius);padding:0.55rem 0.75rem;display:flex;align-items:center;justify-content:space-between;gap:0.75rem}
.copyable-text{font-family:var(--d-mono);font-size:0.82rem;color:var(--d-heading);white-space:nowrap;overflow-x:auto;flex:1;scrollbar-width:thin}
.copyable-text .prompt{color:var(--d-accent);user-select:none;font-weight:600}
.copy-btn{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:4px;background:transparent;border:1px solid var(--d-border);color:var(--d-text-muted);cursor:pointer;transition:all var(--d-transition)}
.copy-btn:hover{background:var(--d-bg-hover);color:var(--d-heading);border-color:var(--d-accent-dim)}
.copy-btn.copied{color:var(--d-success);border-color:var(--d-success)}
.section-heading{font-size:1.05rem;margin:2rem 0 0.75rem;padding-bottom:0.4rem;border-bottom:1px solid var(--d-border-subtle)} .section-heading{font-size:1.05rem;margin:2rem 0 0.75rem;padding-bottom:0.4rem;border-bottom:1px solid var(--d-border-subtle)}
.dep-list li{display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0;border-bottom:1px solid var(--d-border-subtle);font-family:var(--d-mono);font-size:0.82rem} .dep-list li{display:flex;align-items:center;gap:0.6rem;padding:0.4rem 0;border-bottom:1px solid var(--d-border-subtle);font-family:var(--d-mono);font-size:0.82rem}
.dep-list li:last-child{border-bottom:none} .dep-list li:last-child{border-bottom:none}

View file

@ -56,18 +56,28 @@
<span class="label">Compressed</span> <span class="label">Compressed</span>
<span class="value">{{ csize }}</span> <span class="value">{{ csize }}</span>
</div> </div>
<div class="info-row">
<span class="label">FMRI</span>
<span class="value">
<span class="fmri-badge">{{ fmri_str }}</span>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="install-cmd"> <div class="copyable-block">
<span class="prompt">$</span> pkg install {{ fmri_str }} <span class="copyable-label">FMRI</span>
<div class="copyable-row">
<code class="copyable-text" id="fmri-text">{{ fmri_str }}</code>
<button class="copy-btn" onclick="copyText('fmri-text')" title="Copy FMRI">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
</div>
<div class="copyable-block">
<span class="copyable-label">Install</span>
<div class="copyable-row">
<code class="copyable-text" id="install-text"><span class="prompt">$</span> pkg install {{ fmri_str }}</code>
<button class="copy-btn" onclick="copyText('install-text', true)" title="Copy install command">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
</div> </div>
{% if !dependencies.is_empty() %} {% if !dependencies.is_empty() %}
@ -106,4 +116,21 @@
<span class="spinner"></span> Loading manifest... <span class="spinner"></span> Loading manifest...
</span> </span>
<div id="manifest-content"></div> <div id="manifest-content"></div>
<script>
function copyText(id, stripPrompt) {
const el = document.getElementById(id);
let text = el.textContent;
if (stripPrompt) text = text.replace(/^\$\s*/, '');
navigator.clipboard.writeText(text.trim()).then(() => {
const btn = el.parentElement.querySelector('.copy-btn');
btn.classList.add('copied');
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
}, 1500);
});
}
</script>
{% endblock %} {% endblock %}