Spaces:
Sleeping
Sleeping
Guide: click a section heading to copy a shareable deep-link
Browse filesSections already had stable ids (so /guide#limits worked), but there was no
way to discover or grab that link. Each id'd heading now shows a "#" on hover
and is click-to-copy: clicking sets the URL hash (scrolling to and briefly
flashing the section) and copies the full link to the clipboard, with a small
toast. Progressive enhancement - if the script fails, headings still render
and the ids still deep-link natively. Respects prefers-reduced-motion.
- backend/app/guide.html +67 -0
backend/app/guide.html
CHANGED
|
@@ -53,6 +53,30 @@
|
|
| 53 |
.lbl { fill: var(--fg); font: 600 13px sans-serif; }
|
| 54 |
.lbl-sm { fill: var(--muted); font: 11px sans-serif; }
|
| 55 |
.flow { stroke: var(--muted); stroke-width: 1.5; fill: none; marker-end: url(#arw); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
</style>
|
| 57 |
</head>
|
| 58 |
<body>
|
|
@@ -411,5 +435,48 @@ and what you saw; screenshots help. DMs to Deva work too, and email as a fallbac
|
|
| 411 |
|
| 412 |
</main>
|
| 413 |
<a class="top" href="#">↑ Top</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
</body>
|
| 415 |
</html>
|
|
|
|
| 53 |
.lbl { fill: var(--fg); font: 600 13px sans-serif; }
|
| 54 |
.lbl-sm { fill: var(--muted); font: 11px sans-serif; }
|
| 55 |
.flow { stroke: var(--muted); stroke-width: 1.5; fill: none; marker-end: url(#arw); }
|
| 56 |
+
/* Shareable section links: a "#" appears on hover; click a heading to copy
|
| 57 |
+
a direct link to that section. */
|
| 58 |
+
h2[id], h3[id] { scroll-margin-top: 1rem; }
|
| 59 |
+
.anchor {
|
| 60 |
+
opacity: 0; text-decoration: none; color: var(--muted); font-weight: 400;
|
| 61 |
+
margin-left: .4rem; cursor: pointer; transition: opacity .12s;
|
| 62 |
+
}
|
| 63 |
+
h2:hover .anchor, h3:hover .anchor, .anchor:focus { opacity: .7; }
|
| 64 |
+
.anchor:hover { opacity: 1; color: var(--accent); }
|
| 65 |
+
:target { animation: flash 1.4s ease-out; }
|
| 66 |
+
@keyframes flash {
|
| 67 |
+
from { background: var(--amber-bg); }
|
| 68 |
+
to { background: transparent; }
|
| 69 |
+
}
|
| 70 |
+
.toast {
|
| 71 |
+
position: fixed; left: 50%; bottom: 1.5rem; transform: translateX(-50%);
|
| 72 |
+
background: var(--accent); color: #fff; padding: .5rem .9rem; border-radius: 8px;
|
| 73 |
+
font-size: .85rem; opacity: 0; pointer-events: none; transition: opacity .2s;
|
| 74 |
+
}
|
| 75 |
+
.toast.show { opacity: 1; }
|
| 76 |
+
@media (prefers-reduced-motion: reduce) {
|
| 77 |
+
:target { animation: none; }
|
| 78 |
+
.anchor { transition: none; }
|
| 79 |
+
}
|
| 80 |
</style>
|
| 81 |
</head>
|
| 82 |
<body>
|
|
|
|
| 435 |
|
| 436 |
</main>
|
| 437 |
<a class="top" href="#">↑ Top</a>
|
| 438 |
+
<div class="toast" id="toast" role="status" aria-live="polite">Link copied</div>
|
| 439 |
+
<script>
|
| 440 |
+
(function () {
|
| 441 |
+
var toast = document.getElementById('toast'), timer;
|
| 442 |
+
function ping(msg) {
|
| 443 |
+
toast.textContent = msg;
|
| 444 |
+
toast.classList.add('show');
|
| 445 |
+
clearTimeout(timer);
|
| 446 |
+
timer = setTimeout(function () { toast.classList.remove('show'); }, 1600);
|
| 447 |
+
}
|
| 448 |
+
function share(id) {
|
| 449 |
+
// location.hash also makes the browser scroll to and :target-flash the section.
|
| 450 |
+
if (history.replaceState) history.replaceState(null, '', '#' + id);
|
| 451 |
+
location.hash = id;
|
| 452 |
+
var url = location.href;
|
| 453 |
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
| 454 |
+
navigator.clipboard.writeText(url).then(
|
| 455 |
+
function () { ping('Section link copied'); },
|
| 456 |
+
function () { ping('Link in address bar'); }
|
| 457 |
+
);
|
| 458 |
+
} else {
|
| 459 |
+
ping('Link in address bar');
|
| 460 |
+
}
|
| 461 |
+
}
|
| 462 |
+
// Give every id'd heading a click-to-copy "#" affordance.
|
| 463 |
+
var heads = document.querySelectorAll('h2[id], h3[id]');
|
| 464 |
+
Array.prototype.forEach.call(heads, function (h) {
|
| 465 |
+
var a = document.createElement('a');
|
| 466 |
+
a.className = 'anchor';
|
| 467 |
+
a.href = '#' + h.id;
|
| 468 |
+
a.textContent = '#';
|
| 469 |
+
a.setAttribute('aria-label', 'Copy link to this section');
|
| 470 |
+
a.addEventListener('click', function (e) { e.preventDefault(); share(h.id); });
|
| 471 |
+
h.appendChild(a);
|
| 472 |
+
// Clicking the heading text itself (not a link inside it) also shares.
|
| 473 |
+
h.style.cursor = 'pointer';
|
| 474 |
+
h.addEventListener('click', function (e) {
|
| 475 |
+
if (e.target.tagName === 'A') return; // let real links behave normally
|
| 476 |
+
share(h.id);
|
| 477 |
+
});
|
| 478 |
+
});
|
| 479 |
+
})();
|
| 480 |
+
</script>
|
| 481 |
</body>
|
| 482 |
</html>
|