Skip to content

Commit 4685603

Browse files
raifdmuellerclaude
andcommitted
feat: add copy-to-clipboard button on contracts page
- Redesign action bar: count badge + Download + Copy buttons - Extract buildContractsMarkdown() shared by download and copy - Copy shows "Copied!" feedback for 2 seconds - Both buttons disabled when no contracts selected - Bilingual labels (EN + DE) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 81f2bad commit 4685603

3 files changed

Lines changed: 66 additions & 10 deletions

File tree

website/src/components/contracts-page.js

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,31 @@ export function renderContractsPage() {
5050
<a href="https://www.linkedin.com/feed/update/urn:li:activity:7438137401019105281/" target="_blank" rel="noopener noreferrer" class="text-blue-600 dark:text-blue-400 hover:underline" data-i18n="contracts.linkedinLink">${i18n.t('contracts.linkedinLink')}</a>
5151
</p>
5252
53-
<div class="flex items-center gap-4 mb-6">
53+
<div class="flex items-center gap-3 mb-6">
54+
<span id="contracts-count" class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/30 px-3 py-1 text-sm font-medium text-blue-700 dark:text-blue-300">${selectedCount} <span data-i18n="contracts.selected" class="ml-1">${i18n.t('contracts.selected')}</span></span>
5455
<button
5556
id="contracts-download"
56-
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
57+
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
58+
title="${i18n.t('contracts.download')}"
5759
>
5860
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
59-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
61+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
6062
</svg>
6163
<span data-i18n="contracts.download">${i18n.t('contracts.download')}</span>
62-
<span id="contracts-count" class="rounded-full bg-blue-500 px-2 py-0.5 text-xs">${selectedCount}</span>
64+
</button>
65+
<button
66+
id="contracts-copy"
67+
class="inline-flex items-center gap-2 rounded-lg border border-blue-600 dark:border-blue-400 px-3 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
68+
title="${i18n.t('contracts.copy')}"
69+
>
70+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
71+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/>
72+
</svg>
73+
<span data-i18n="contracts.copy">${i18n.t('contracts.copy')}</span>
6374
</button>
6475
<button
6576
id="contracts-select-all"
66-
class="text-sm text-blue-600 dark:text-blue-400 hover:underline"
77+
class="text-sm text-blue-600 dark:text-blue-400 hover:underline ml-1"
6778
data-i18n="contracts.selectAll"
6879
>${i18n.t('contracts.selectAll')}</button>
6980
<button
@@ -176,6 +187,12 @@ export function initContractsPage(contracts) {
176187
downloadBtn.addEventListener('click', () => downloadContracts(contracts))
177188
}
178189

190+
// Copy to clipboard button
191+
const copyBtn = document.getElementById('contracts-copy')
192+
if (copyBtn) {
193+
copyBtn.addEventListener('click', () => copyContracts(contracts))
194+
}
195+
179196
// Select all
180197
const selectAllBtn = document.getElementById('contracts-select-all')
181198
if (selectAllBtn) {
@@ -200,11 +217,18 @@ export function initContractsPage(contracts) {
200217
function updateUI() {
201218
const selected = getSelectedContracts()
202219
const countEl = document.getElementById('contracts-count')
203-
if (countEl) countEl.textContent = selected.length
220+
if (countEl) {
221+
countEl.querySelector('span:not([data-i18n])') ||
222+
(countEl.childNodes[0].textContent = selected.length + ' ')
223+
countEl.firstChild.textContent = selected.length + ' '
224+
}
204225

205226
const downloadBtn = document.getElementById('contracts-download')
206227
if (downloadBtn) downloadBtn.disabled = selected.length === 0
207228

229+
const copyBtn = document.getElementById('contracts-copy')
230+
if (copyBtn) copyBtn.disabled = selected.length === 0
231+
208232
// Update card styles
209233
document.querySelectorAll('.contract-card').forEach((card) => {
210234
const id = card.dataset.contractId
@@ -217,12 +241,12 @@ function updateUI() {
217241
})
218242
}
219243

220-
function downloadContracts(contracts) {
244+
function buildContractsMarkdown(contracts) {
221245
const selected = getSelectedContracts()
222246
const lang = i18n.currentLanguage || 'en'
223247
const filtered = contracts.filter((c) => selected.includes(c.id))
224248

225-
if (filtered.length === 0) return
249+
if (filtered.length === 0) return null
226250

227251
let md = '# Semantic Contracts\n\n'
228252
md +=
@@ -242,6 +266,13 @@ function downloadContracts(contracts) {
242266
? 'Generiert von https://llm-coding.github.io/Semantic-Anchors/#/contracts\n'
243267
: 'Generated from https://llm-coding.github.io/Semantic-Anchors/#/contracts\n'
244268

269+
return md
270+
}
271+
272+
function downloadContracts(contracts) {
273+
const md = buildContractsMarkdown(contracts)
274+
if (!md) return
275+
245276
const blob = new Blob([md], { type: 'text/markdown' })
246277
const url = URL.createObjectURL(blob)
247278
const a = document.createElement('a')
@@ -250,3 +281,22 @@ function downloadContracts(contracts) {
250281
a.click()
251282
URL.revokeObjectURL(url)
252283
}
284+
285+
async function copyContracts(contracts) {
286+
const md = buildContractsMarkdown(contracts)
287+
if (!md) return
288+
289+
try {
290+
await navigator.clipboard.writeText(md)
291+
const copyBtn = document.getElementById('contracts-copy')
292+
if (copyBtn) {
293+
const original = copyBtn.querySelector('span[data-i18n]').textContent
294+
copyBtn.querySelector('span[data-i18n]').textContent = i18n.t('contracts.copied')
295+
setTimeout(() => {
296+
copyBtn.querySelector('span[data-i18n]').textContent = original
297+
}, 2000)
298+
}
299+
} catch {
300+
// fallback ignored
301+
}
302+
}

website/src/translations/de.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
"contracts.title": "Semantic Contracts",
1818
"contracts.explanation": "Semantic Anchors referenzieren öffentliches Wissen, das LLMs bereits kennen. Aber die Konventionen, Templates und Definitionen deines Teams? Dafür braucht es Semantic Contracts. Ein Contract definiert, was ein Begriff in deinem Projekt bedeutet — entweder durch Komposition etablierter Anker oder durch eigene Definitionen, die nur in deinem Team existieren. Wähle die passenden aus und lade sie für deine AGENTS.md oder CLAUDE.md herunter.",
1919
"contracts.linkedinLink": "Lies die ganze Geschichte hinter Semantic Contracts auf LinkedIn \u2192",
20-
"contracts.download": "semantic-contracts.md herunterladen",
20+
"contracts.download": "Download",
21+
"contracts.copy": "Kopieren",
22+
"contracts.copied": "Kopiert!",
23+
"contracts.selected": "ausgewählt",
2124
"contracts.selectAll": "Alle auswählen",
2225
"contracts.deselectAll": "Alle abwählen",
2326
"main.heading": "Semantic Anchors erkunden",

website/src/translations/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
"contracts.title": "Semantic Contracts",
1818
"contracts.explanation": "Semantic Anchors reference public knowledge that LLMs already understand. But your team's conventions, templates, and definitions? Those need Semantic Contracts. A contract defines what a term means in your project — either by composing established anchors or by providing custom definitions that only exist within your team. Select the ones that fit and download them for your AGENTS.md or CLAUDE.md.",
1919
"contracts.linkedinLink": "Read the full story behind Semantic Contracts on LinkedIn \u2192",
20-
"contracts.download": "Download semantic-contracts.md",
20+
"contracts.download": "Download",
21+
"contracts.copy": "Copy",
22+
"contracts.copied": "Copied!",
23+
"contracts.selected": "selected",
2124
"contracts.selectAll": "Select all",
2225
"contracts.deselectAll": "Deselect all",
2326
"main.heading": "Explore Semantic Anchors",

0 commit comments

Comments
 (0)