1
0

feat: per-page copy-link button for compact URLs + shareable force-language links
Deploy / deploy (push) Successful in 0s

- Copy-link pill in each page's top bar copies its compact URL (SEC-01/AGT-01, /, /privacy)
- ?lang= / #lang= forces the page language over saved/browser default (EN/DE, FA); hash form survives the compact-URL 302
- add .gitignore (keeps local CLAUDE.md out of the public repo)
This commit is contained in:
Pouya
2026-06-05 11:33:51 +02:00
parent 3677993690
commit eedceada29
5 changed files with 282 additions and 6 deletions
+7
View File
@@ -0,0 +1,7 @@
# Local-only assistant notes — this repo is PUBLIC, keep these out of git.
CLAUDE.md
.claude/
# Editor / OS cruft
.DS_Store
*.swp
+74 -2
View File
@@ -197,7 +197,7 @@ code{font-family:var(--mono);font-size:.88em;background:var(--surface-2);
/* ---- language selector (segmented pill, matches mono label system) ----- */
.langbar{
position:absolute;top:clamp(1rem,3vw,1.8rem);right:clamp(1.1rem,4vw,3rem);
display:flex;align-items:center;gap:.55rem;z-index:5;
display:flex;align-items:center;gap:.55rem;z-index:5;flex-wrap:wrap;justify-content:flex-end;
}
.langbar .lglabel{
font-family:var(--mono);font-size:.6rem;letter-spacing:.18em;text-transform:uppercase;
@@ -228,6 +228,24 @@ code{font-family:var(--mono);font-size:.88em;background:var(--surface-2);
.langbar{top:.7rem;right:.9rem}
}
/* ---- copy-link control (shares the segmented-pill language) ------- */
.copylink{
appearance:none;cursor:pointer;display:inline-flex;align-items:center;
font-family:var(--mono);font-size:.72rem;font-weight:700;letter-spacing:.1em;
text-transform:uppercase;line-height:1;color:var(--muted);background:var(--surface);
border:1px solid var(--border-strong);border-radius:99px;padding:.42em .85em;
box-shadow:var(--shadow);
transition:color .16s ease, border-color .16s ease, background-color .16s ease;
}
.copylink:hover{color:var(--accent-ink);border-color:var(--accent)}
.copylink:focus-visible{outline:2.5px solid var(--accent);outline-offset:2px}
.copylink svg{width:13px;height:13px;flex:none}
.copylink .cl-idle,.copylink .cl-done{display:inline-flex;align-items:center;gap:.45em}
.copylink .cl-done{display:none}
.copylink.copied{color:var(--good);border-color:var(--good);background:var(--accent-wash)}
.copylink.copied .cl-idle{display:none}
.copylink.copied .cl-done{display:inline-flex}
/* =========================================================================
SECTION SHELL
========================================================================= */
@@ -695,7 +713,19 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
<header class="masthead wrap">
<div class="langbar" role="group"
data-en-aria="Language" data-de-aria="Sprache" aria-label="Language">
data-en-aria="Language and link" data-de-aria="Sprache und Link" aria-label="Language and link">
<button type="button" class="copylink" data-copy-url="https://tutorials.pouyalab.de/AGT-01"
data-en-aria="Copy link to this guide" data-de-aria="Link zu dieser Anleitung kopieren"
aria-label="Copy link to this guide">
<span class="cl-idle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 15l6-6M10.5 6.5l1-1a4.95 4.95 0 0 1 7 7l-1 1M13.5 17.5l-1 1a4.95 4.95 0 0 1-7-7l1-1"/></svg>
<span data-i18n data-en="Copy link" data-de="Link kopieren">Copy link</span>
</span>
<span class="cl-done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg>
<span data-i18n data-en="Copied" data-de="Kopiert">Copied</span>
</span>
</button>
<span class="lglabel" data-i18n data-en="Language" data-de="Sprache">Language</span>
<div class="langseg">
<button type="button" id="lang-en" data-lang="en" aria-pressed="true"
@@ -1420,7 +1450,21 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
var ariaNodes = document.querySelectorAll('[data-en-aria],[data-de-aria]');
var buttons = document.querySelectorAll('.langseg button[data-lang]');
// A shareable ?lang= / #lang= in the URL forces the language, beating any
// saved choice — so a link can open straight in DE without defaulting to EN.
// The hash form survives the compact-URL 302 redirect (the browser reattaches
// the fragment), so /AGT-01#lang=de works with no server change.
function urlLang(){
try{
var m = (location.search + location.hash).match(/[?&#]lang=([a-z]{2})\b/i);
if(m){ var l = m[1].toLowerCase(); if(l === 'en' || l === 'de') return l; }
}catch(e){}
return null;
}
function decideInitial(){
var forced = urlLang();
if(forced) return forced;
try{
var saved = localStorage.getItem(STORE);
if(saved === 'en' || saved === 'de') return saved;
@@ -1469,6 +1513,34 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
}, {rootMargin:'0px 0px -8% 0px', threshold:.08});
els.forEach(function(e){io.observe(e)});
})();
/* Copy-link — copies the page's compact URL, with a brief "Copied" confirm.
Falls back to execCommand on non-secure contexts / older browsers. */
(function(){
function fallback(text, cb){
try{
var ta = document.createElement('textarea');
ta.value = text; ta.setAttribute('readonly','');
ta.style.position = 'fixed'; ta.style.top = '-9999px';
document.body.appendChild(ta); ta.focus(); ta.select();
document.execCommand('copy'); document.body.removeChild(ta); cb();
}catch(e){}
}
document.querySelectorAll('.copylink').forEach(function(b){
var t = null;
b.addEventListener('click', function(){
var url = b.getAttribute('data-copy-url'); if(!url) return;
var done = function(){
b.classList.add('copied');
if(t) clearTimeout(t);
t = setTimeout(function(){ b.classList.remove('copied'); }, 1700);
};
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(url).then(done).catch(function(){ fallback(url, done); });
} else { fallback(url, done); }
});
});
})();
</script>
</body>
@@ -279,6 +279,24 @@ code{font-family:var(--mono);font-size:.88em;background:var(--surface-2);
.themebar{top:.7rem;right:.9rem}
}
/* ---- copy-link control (shares the segmented-pill language) ------- */
.copylink{
appearance:none;cursor:pointer;display:inline-flex;align-items:center;
font-family:var(--mono);font-size:.72rem;font-weight:700;letter-spacing:.1em;
text-transform:uppercase;line-height:1;color:var(--muted);background:var(--surface);
border:1px solid var(--border-strong);border-radius:99px;padding:.42em .85em;
box-shadow:var(--shadow);
transition:color .16s ease, border-color .16s ease, background-color .16s ease;
}
.copylink:hover{color:var(--accent-ink);border-color:var(--accent)}
.copylink:focus-visible{outline:2.5px solid var(--accent);outline-offset:2px}
.copylink svg{width:13px;height:13px;flex:none}
.copylink .cl-idle,.copylink .cl-done{display:inline-flex;align-items:center;gap:.45em}
.copylink .cl-done{display:none}
.copylink.copied{color:var(--good);border-color:var(--good);background:var(--good-wash)}
.copylink.copied .cl-idle{display:none}
.copylink.copied .cl-done{display:inline-flex}
/* ---- language selector — flag buttons matching the segmented theme pill - */
.langctl{display:inline-flex;align-items:center;gap:.45rem}
.langseg{
@@ -950,6 +968,18 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
<div class="themebar" role="group" data-i18n-attr="aria-label:themebar_aria" aria-label="Theme and language">
<button type="button" class="copylink" data-copy-url="https://tutorials.pouyalab.de/SEC-01"
data-i18n-attr="aria-label:copy_aria" aria-label="Copy link to this guide">
<span class="cl-idle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 15l6-6M10.5 6.5l1-1a4.95 4.95 0 0 1 7 7l-1 1M13.5 17.5l-1 1a4.95 4.95 0 0 1-7-7l1-1"/></svg>
<span data-i18n="copy_label">Copy link</span>
</span>
<span class="cl-done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg>
<span data-i18n="copy_done">Copied</span>
</span>
</button>
<div class="langctl" role="group" data-i18n-attr="aria-label:lang_group_aria" aria-label="Language">
<span class="tglabel" data-i18n="lang_label">Language</span>
<div class="langseg" role="group" data-i18n-attr="aria-label:lang_select_aria" aria-label="Choose language">
@@ -1850,6 +1880,7 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
themebar_aria:"Theme and language",
lang_group_aria:"Language", lang_label:"Language", lang_select_aria:"Choose language",
theme_group_aria:"Colour theme", theme_label:"Theme",
copy_label:"Copy link", copy_done:"Copied", copy_aria:"Copy link to this guide",
theme_light:"LIGHT", theme_dark:"DARK",
theme_light_aria:"Light theme", theme_dark_aria:"Dark theme",
coach_kicker:"Languages",
@@ -2067,6 +2098,7 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
themebar_aria:"Design und Sprache",
lang_group_aria:"Sprache", lang_label:"Sprache", lang_select_aria:"Sprache wählen",
theme_group_aria:"Farbdesign", theme_label:"Design",
copy_label:"Link kopieren", copy_done:"Kopiert", copy_aria:"Link zu dieser Anleitung kopieren",
theme_light:"HELL", theme_dark:"DUNKEL",
theme_light_aria:"Helles Design", theme_dark_aria:"Dunkles Design",
coach_kicker:"Sprachen",
@@ -2284,6 +2316,7 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
themebar_aria:"تم و زبان",
lang_group_aria:"زبان", lang_label:"زبان", lang_select_aria:"انتخاب زبان",
theme_group_aria:"تم رنگی", theme_label:"تم",
copy_label:"کپی لینک", copy_done:"کپی شد", copy_aria:"کپی لینک این راهنما",
theme_light:"روشن", theme_dark:"تیره",
theme_light_aria:"تم روشن", theme_dark_aria:"تم تیره",
coach_kicker:"زبان‌ها",
@@ -2548,7 +2581,20 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
}
window.__setLang = setLang;
// initial language: localStorage -> navigator.language -> en
// A shareable ?lang= / #lang= in the URL forces the language, beating any
// saved choice — so a link can open straight in DE/FA without defaulting to
// EN. The hash form survives the compact-URL 302 redirect (the browser
// reattaches the fragment), so /SEC-01#lang=fa works with no server change.
function urlLang(){
try{
var m = (location.search + location.hash).match(/[?&#]lang=([a-z]{2})\b/i);
if(m){ var l = m[1].toLowerCase(); if(I18N[l]) return l; }
}catch(e){}
return null;
}
// initial language: URL ?lang=/#lang= -> localStorage -> navigator.language -> en
var forcedLang = urlLang();
var initLang = null;
try{ initLang = localStorage.getItem(LANG_STORE); }catch(e){}
if(!I18N[initLang]){
@@ -2557,12 +2603,14 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
else if(nav.indexOf('fa') === 0 || nav.indexOf('pe') === 0) initLang = 'fa';
else initLang = 'en';
}
if(forcedLang) initLang = forcedLang;
function wire(){
document.querySelectorAll('.langseg [data-lang-set]').forEach(function(b){
b.addEventListener('click', function(){ setLang(b.getAttribute('data-lang-set'), true); });
});
setLang(I18N[initLang] ? initLang : 'en', false);
// Persist only when the language came from the URL, so a shared link sticks.
setLang(I18N[initLang] ? initLang : 'en', !!forcedLang);
}
if(document.readyState === 'loading'){
document.addEventListener('DOMContentLoaded', wire);
@@ -2798,6 +2846,37 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
setTimeout(show, 1200);
})();
/* =========================================================================
COPY-LINK — copies the page's compact URL to the clipboard, with a brief
"Copied" confirmation (label is translated via the i18n dict). Falls back
to execCommand where the async clipboard API is unavailable.
========================================================================= */
(function(){
function fallback(text, cb){
try{
var ta = document.createElement('textarea');
ta.value = text; ta.setAttribute('readonly','');
ta.style.position = 'fixed'; ta.style.top = '-9999px';
document.body.appendChild(ta); ta.focus(); ta.select();
document.execCommand('copy'); document.body.removeChild(ta); cb();
}catch(e){}
}
document.querySelectorAll('.copylink').forEach(function(b){
var t = null;
b.addEventListener('click', function(){
var url = b.getAttribute('data-copy-url'); if(!url) return;
var done = function(){
b.classList.add('copied');
if(t) clearTimeout(t);
t = setTimeout(function(){ b.classList.remove('copied'); }, 1700);
};
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(url).then(done).catch(function(){ fallback(url, done); });
} else { fallback(url, done); }
});
});
})();
</script>
</body>
+60 -1
View File
@@ -244,6 +244,24 @@ code{font-family:var(--mono);font-size:.88em;background:var(--surface-2);
.themebar{top:.7rem;right:.9rem}
}
/* ---- copy-link control (shares the segmented-pill language) ------- */
.copylink{
appearance:none;cursor:pointer;display:inline-flex;align-items:center;
font-family:var(--mono);font-size:.72rem;font-weight:700;letter-spacing:.1em;
text-transform:uppercase;line-height:1;color:var(--muted);background:var(--surface);
border:1px solid var(--border-strong);border-radius:99px;padding:.42em .85em;
box-shadow:var(--shadow);
transition:color .16s ease, border-color .16s ease, background-color .16s ease;
}
.copylink:hover{color:var(--accent-ink);border-color:var(--accent)}
.copylink:focus-visible{outline:2.5px solid var(--accent);outline-offset:2px}
.copylink svg{width:13px;height:13px;flex:none}
.copylink .cl-idle,.copylink .cl-done{display:inline-flex;align-items:center;gap:.45em}
.copylink .cl-done{display:none}
.copylink.copied{color:var(--good);border-color:var(--good);background:var(--good-wash)}
.copylink.copied .cl-idle{display:none}
.copylink.copied .cl-done{display:inline-flex}
/* =========================================================================
SECTION SHELL
========================================================================= */
@@ -514,7 +532,17 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
<!-- ============================ MASTHEAD ============================ -->
<header class="masthead wrap">
<div class="themebar" role="group" aria-label="Colour theme">
<div class="themebar" role="group" aria-label="Colour theme and link">
<button type="button" class="copylink" data-copy-url="https://tutorials.pouyalab.de/" aria-label="Copy link to this page">
<span class="cl-idle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 15l6-6M10.5 6.5l1-1a4.95 4.95 0 0 1 7 7l-1 1M13.5 17.5l-1 1a4.95 4.95 0 0 1-7-7l1-1"/></svg>
<span>Copy link</span>
</span>
<span class="cl-done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg>
<span>Copied</span>
</span>
</button>
<div class="tgctl" role="group" aria-label="Colour theme">
<span class="tglabel">Theme</span>
<div class="themeseg">
@@ -851,6 +879,37 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 });
els.forEach(function(el){ io.observe(el); });
})();
/* =========================================================================
COPY-LINK — copies the page's compact URL to the clipboard, with a brief
"Copied" confirmation. Falls back to execCommand where the async clipboard
API is unavailable (older browsers / non-secure contexts).
========================================================================= */
(function(){
function fallback(text, cb){
try{
var ta = document.createElement('textarea');
ta.value = text; ta.setAttribute('readonly','');
ta.style.position = 'fixed'; ta.style.top = '-9999px';
document.body.appendChild(ta); ta.focus(); ta.select();
document.execCommand('copy'); document.body.removeChild(ta); cb();
}catch(e){}
}
document.querySelectorAll('.copylink').forEach(function(b){
var t = null;
b.addEventListener('click', function(){
var url = b.getAttribute('data-copy-url'); if(!url) return;
var done = function(){
b.classList.add('copied');
if(t) clearTimeout(t);
t = setTimeout(function(){ b.classList.remove('copied'); }, 1700);
};
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(url).then(done).catch(function(){ fallback(url, done); });
} else { fallback(url, done); }
});
});
})();
</script>
</body>
</html>
+60 -1
View File
@@ -244,6 +244,24 @@ code{font-family:var(--mono);font-size:.88em;background:var(--surface-2);
.themebar{top:.7rem;right:.9rem}
}
/* ---- copy-link control (shares the segmented-pill language) ------- */
.copylink{
appearance:none;cursor:pointer;display:inline-flex;align-items:center;
font-family:var(--mono);font-size:.72rem;font-weight:700;letter-spacing:.1em;
text-transform:uppercase;line-height:1;color:var(--muted);background:var(--surface);
border:1px solid var(--border-strong);border-radius:99px;padding:.42em .85em;
box-shadow:var(--shadow);
transition:color .16s ease, border-color .16s ease, background-color .16s ease;
}
.copylink:hover{color:var(--accent-ink);border-color:var(--accent)}
.copylink:focus-visible{outline:2.5px solid var(--accent);outline-offset:2px}
.copylink svg{width:13px;height:13px;flex:none}
.copylink .cl-idle,.copylink .cl-done{display:inline-flex;align-items:center;gap:.45em}
.copylink .cl-done{display:none}
.copylink.copied{color:var(--good);border-color:var(--good);background:var(--good-wash)}
.copylink.copied .cl-idle{display:none}
.copylink.copied .cl-done{display:inline-flex}
/* =========================================================================
SECTION SHELL (matches index.html)
========================================================================= */
@@ -407,7 +425,17 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
<!-- ============================ MASTHEAD ============================ -->
<header class="masthead wrap">
<div class="themebar" role="group" aria-label="Colour theme">
<div class="themebar" role="group" aria-label="Colour theme and link">
<button type="button" class="copylink" data-copy-url="https://tutorials.pouyalab.de/privacy" aria-label="Copy link to this page">
<span class="cl-idle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 15l6-6M10.5 6.5l1-1a4.95 4.95 0 0 1 7 7l-1 1M13.5 17.5l-1 1a4.95 4.95 0 0 1-7-7l1-1"/></svg>
<span>Copy link</span>
</span>
<span class="cl-done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg>
<span>Copied</span>
</span>
</button>
<div class="tgctl" role="group" aria-label="Colour theme">
<span class="tglabel">Theme</span>
<div class="themeseg">
@@ -621,6 +649,37 @@ footer .sig .dot{width:6px;height:6px;border-radius:50%;background:var(--accent)
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.08 });
els.forEach(function(el){ io.observe(el); });
})();
/* =========================================================================
COPY-LINK — copies the page's compact URL to the clipboard, with a brief
"Copied" confirmation. Falls back to execCommand where the async clipboard
API is unavailable (older browsers / non-secure contexts).
========================================================================= */
(function(){
function fallback(text, cb){
try{
var ta = document.createElement('textarea');
ta.value = text; ta.setAttribute('readonly','');
ta.style.position = 'fixed'; ta.style.top = '-9999px';
document.body.appendChild(ta); ta.focus(); ta.select();
document.execCommand('copy'); document.body.removeChild(ta); cb();
}catch(e){}
}
document.querySelectorAll('.copylink').forEach(function(b){
var t = null;
b.addEventListener('click', function(){
var url = b.getAttribute('data-copy-url'); if(!url) return;
var done = function(){
b.classList.add('copied');
if(t) clearTimeout(t);
t = setTimeout(function(){ b.classList.remove('copied'); }, 1700);
};
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(url).then(done).catch(function(){ fallback(url, done); });
} else { fallback(url, done); }
});
});
})();
</script>
</body>
</html>