Add site copy feedback and command block fixes (#200)

* Add copy confirmation animation

* Add live mode copy confirmation

* Add Neo Mirai copy confirmation

* Fix Neo Mirai command overflow on narrow screens

* Link footer logo to homepage

* Fix copy feedback helper

---------

Co-authored-by: Paul Bakaus <43004+pbakaus@users.noreply.github.com>
This commit is contained in:
Vyctor H. Brzezowski
2026-06-05 15:47:17 -07:00
committed by GitHub
co-authored by Paul Bakaus
parent 08d50f215b
commit 397d3cb4b7
9 changed files with 348 additions and 57 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<footer class="site-footer">
<div class="footer-row">
<span class="footer-logo">Impeccable</span>
<a class="footer-logo" href="/">Impeccable</a>
<nav class="footer-links" aria-label="Footer">
<a href="/changelog">Changelog</a>
<a href="/faq">FAQ</a>
+13 -12
View File
@@ -87,21 +87,22 @@ import '../../styles/sub-pages.css';
<h2>Use craft when the output has to feel designed.</h2>
<p><code>/impeccable craft</code> is the right command when a feature needs shaping, visual direction, implementation, and browser iteration in one run.</p>
</div>
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable craft retro-futurist AI design conference website</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable craft retro-futurist AI design conference website" aria-label="Copy to clipboard"></button></div>
<div class="code-block-wrap"><pre class="code-block"><code>/impeccable craft retro-futurist AI design conference website</code></pre><button class="code-block-copy" type="button" data-copy="/impeccable craft retro-futurist AI design conference website" aria-label="Copy to clipboard">
<svg class="code-block-copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
</svg>
<svg class="code-block-check-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</button></div>
</section>
</div>
<script is:inline>
document.addEventListener('click', (event) => {
const button = event.target.closest('[data-copy]');
if (!button) return;
const text = button.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
button.classList.add('is-copied');
setTimeout(() => button.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
<script>
import { initCopyFeedback } from "../../scripts/utils/copy-feedback.js";
initCopyFeedback();
</script>
</Base>
+16 -4
View File
@@ -597,10 +597,13 @@ import '../styles/testimonials.css';
<span class="downloads-rebuild-prompt" aria-hidden="true">$</span>
<code>npx impeccable skills install</code>
<button class="downloads-rebuild-copy" type="button" aria-label="Copy install command" data-copy="npx impeccable skills install">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<svg class="downloads-rebuild-copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
<svg class="downloads-rebuild-check-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5"/>
</svg>
<span>Copy</span>
</button>
</div>
@@ -612,10 +615,13 @@ import '../styles/testimonials.css';
<span class="downloads-rebuild-prompt" aria-hidden="true">$</span>
<code>npx impeccable skills update</code>
<button class="downloads-rebuild-copy" type="button" aria-label="Copy update command" data-copy="npx impeccable skills update">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<svg class="downloads-rebuild-copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
<svg class="downloads-rebuild-check-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5"/>
</svg>
<span>Copy</span>
</button>
</div>
@@ -634,10 +640,13 @@ import '../styles/testimonials.css';
<div class="downloads-rebuild-cmd downloads-rebuild-cmd--alt">
<code>/plugin marketplace add pbakaus/impeccable</code>
<button class="downloads-rebuild-copy" type="button" aria-label="Copy Claude Code plugin command" data-copy="/plugin marketplace add pbakaus/impeccable">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<svg class="downloads-rebuild-copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
<svg class="downloads-rebuild-check-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5"/>
</svg>
<span>Copy</span>
</button>
</div>
@@ -649,10 +658,13 @@ import '../styles/testimonials.css';
<div class="downloads-rebuild-cmd downloads-rebuild-cmd--alt">
<code>npx skills add pbakaus/impeccable</code>
<button class="downloads-rebuild-copy" type="button" aria-label="Copy npx skills command" data-copy="npx skills add pbakaus/impeccable">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<svg class="downloads-rebuild-copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/>
</svg>
<svg class="downloads-rebuild-check-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5"/>
</svg>
<span>Copy</span>
</button>
</div>
+7 -12
View File
@@ -24,10 +24,13 @@ import '../../styles/live-mode-kinpaku.css';
<span class="live-mode-start-prompt">$</span>
<code class="live-mode-start-cmd">/impeccable live</code>
<button class="live-mode-start-copy" type="button" aria-label="Copy command" data-copy="/impeccable live">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg class="live-mode-start-copy-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
</svg>
<svg class="live-mode-start-check-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</button>
</div>
<p class="live-mode-page-alpha-note"><strong>Why beta:</strong> works end-to-end across the common frameworks and is ready for daily use. Uncommon setups may still hit rough edges. Please report what breaks.</p>
@@ -223,24 +226,16 @@ import '../../styles/live-mode-kinpaku.css';
<script>
import { initLiveDemo, initGbarPageChat } from "../../scripts/components/live-demo.js";
import { initCopyFeedback } from "../../scripts/utils/copy-feedback.js";
document.addEventListener("DOMContentLoaded", () => {
initLiveDemo();
initGbarPageChat();
initCopyFeedback();
});
</script>
<script is:inline>
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-copy]');
if (!btn) return;
const text = btn.getAttribute('data-copy');
if (!text) return;
navigator.clipboard.writeText(text).then(() => {
btn.classList.add('is-copied');
setTimeout(() => btn.classList.remove('is-copied'), 1500);
}).catch(() => {});
});
document.addEventListener('click', (e) => {
const toggle = e.target.closest('.skills-sidebar-toggle');
if (!toggle) return;
+2 -20
View File
@@ -5,6 +5,7 @@ import {
import { initFrameworkViz } from "./components/framework-viz.js";
import { initScrollReveal } from "./utils/reveal.js";
import { initAnchorScroll, initHashTracking } from "./utils/scroll.js";
import { initCopyFeedback } from "./utils/copy-feedback.js";
import { initSectionNav } from "./components/section-nav.js";
import { initFoundationGrid } from "./components/foundation-grid.js";
import { initLiveDemo, initGbarPageChat } from "./components/live-demo.js";
@@ -208,26 +209,6 @@ document.addEventListener("click", (e) => {
const bundleName = bundleBtn.dataset.bundle;
window.location.href = `/api/download/bundle/${bundleName}`;
}
// Handle copy button clicks
const copyBtn = e.target.closest("[data-copy]");
if (copyBtn) {
const textToCopy = copyBtn.dataset.copy;
const onCopied = () => {
copyBtn.classList.add('copied');
setTimeout(() => copyBtn.classList.remove('copied'), 1500);
};
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(textToCopy).then(onCopied).catch(() => {});
} else {
// Fallback for non-HTTPS or older browsers
const ta = Object.assign(document.createElement('textarea'), { value: textToCopy, style: 'position:fixed;left:-9999px' });
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); onCopied(); } catch {}
ta.remove();
}
}
});
@@ -261,6 +242,7 @@ function initHeaderScroll() {
function init() {
initAnchorScroll();
initHashTracking();
initCopyFeedback();
initHeaderScroll();
initScrollReveal();
initGlassTerminal();
+62
View File
@@ -0,0 +1,62 @@
const copyResetTimers = new WeakMap();
function copyWithTextareaFallback(text) {
const ta = Object.assign(document.createElement('textarea'), {
value: text,
style: 'position:fixed;left:-9999px',
});
document.body.appendChild(ta);
ta.focus();
ta.select();
try {
return document.execCommand('copy');
} catch {
return false;
} finally {
ta.remove();
}
}
function showCopiedState(button, { copiedClass = 'copied', resetMs = 1200 } = {}) {
window.clearTimeout(copyResetTimers.get(button));
button.classList.remove(copiedClass);
void button.offsetWidth;
button.classList.add(copiedClass);
copyResetTimers.set(
button,
window.setTimeout(() => button.classList.remove(copiedClass), resetMs),
);
}
async function copyText(text) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {}
}
return copyWithTextareaFallback(text);
}
export function initCopyFeedback({
selector = '[data-copy]',
copiedClass = 'copied',
resetMs = 1200,
} = {}) {
document.addEventListener('click', async (event) => {
if (!(event.target instanceof Element)) return;
const button = event.target.closest(selector);
if (!(button instanceof HTMLElement)) return;
const text = button.getAttribute('data-copy');
if (!text) return;
if (await copyText(text)) {
showCopiedState(button, { copiedClass, resetMs });
}
});
}
+116 -5
View File
@@ -3327,6 +3327,10 @@
border-radius: 2px;
}
.neon-case-command > * {
min-width: 0;
}
.neon-case-command h2 {
color: var(--ks-champagne);
}
@@ -3341,12 +3345,28 @@
}
.neon-case-command .code-block-wrap {
position: relative;
margin: 0;
max-width: 100%;
min-width: 0;
}
.neon-case-command .code-block-wrap::after {
content: "";
position: absolute;
top: 1px;
right: 1px;
bottom: 31px;
width: 142px;
pointer-events: none;
background: linear-gradient(90deg, transparent, var(--ks-code-block-bg) 74%);
z-index: 1;
}
.neon-case-command .code-block {
max-width: 100%;
margin: 0;
padding: var(--spacing-md);
padding: var(--spacing-md) 96px var(--spacing-md) var(--spacing-md);
background: var(--ks-code-block-bg);
color: var(--ks-code-block-fg);
border: 1px solid var(--ks-code-block-border);
@@ -3365,10 +3385,101 @@
font-size: inherit;
}
.neon-case-command .code-block-copy.is-copied {
background: var(--ks-kinpaku);
color: var(--ks-lacquer-deep);
border-color: var(--ks-kinpaku);
.neon-case-command .code-block-copy {
top: 0;
right: 14px;
bottom: 17px;
margin: auto 0;
z-index: 2;
display: grid;
place-items: center;
width: 34px;
height: 34px;
padding: 0;
opacity: 1;
background: oklch(5% 0.004 95 / 0.94);
border: 1px solid var(--ks-code-block-border);
border-radius: 3px;
color: var(--ks-kinpaku);
transform: none;
transition:
background 180ms var(--ks-ease),
border-color 180ms var(--ks-ease),
color 180ms var(--ks-ease),
transform 120ms var(--ks-ease);
}
.neon-case-command .code-block-copy::before {
content: none;
}
.neon-case-command .code-block-copy:hover {
background: oklch(8% 0.006 95);
border-color: currentColor;
color: var(--ks-kinpaku);
}
.neon-case-command .code-block-copy:active {
transform: scale(0.96);
}
.neon-case-command .code-block-copy-icon,
.neon-case-command .code-block-check-icon {
grid-area: 1 / 1;
transition:
opacity 140ms var(--ks-ease),
transform 180ms var(--ks-ease);
}
.neon-case-command .code-block-copy-icon {
opacity: 1;
transform: scale(1);
}
.neon-case-command .code-block-check-icon {
opacity: 0;
transform: scale(0.86);
}
.neon-case-command .code-block-check-icon path {
stroke-dasharray: 24;
stroke-dashoffset: 24;
}
.neon-case-command .code-block-copy.copied {
background: oklch(8% 0.006 95);
border-color: currentColor;
}
.neon-case-command .code-block-copy.copied .code-block-copy-icon {
opacity: 0;
transform: scale(0.82);
}
.neon-case-command .code-block-copy.copied .code-block-check-icon {
opacity: 1;
transform: scale(1);
}
.neon-case-command .code-block-copy.copied .code-block-check-icon path {
animation: neon-case-check-draw 260ms var(--ks-ease) forwards;
}
@keyframes neon-case-check-draw {
to { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
.neon-case-command .code-block-copy,
.neon-case-command .code-block-copy-icon,
.neon-case-command .code-block-check-icon {
transition: none;
}
.neon-case-command .code-block-copy.copied .code-block-check-icon path {
animation: none;
stroke-dashoffset: 0;
}
}
@media (max-width: 980px) {
+69 -1
View File
@@ -738,6 +738,7 @@
/* Compact icon-only copy button nested inside the cmd box, on the right. */
.home-kinpaku .downloads-rebuild-copy {
flex: none;
position: relative;
display: inline-grid;
place-items: center;
width: 40px;
@@ -747,7 +748,74 @@
color: var(--card-copy-fg);
border-radius: 2px;
cursor: pointer;
transition: background 180ms var(--ks-ease), border-color 180ms var(--ks-ease);
transition:
background 180ms var(--ks-ease),
border-color 180ms var(--ks-ease),
color 180ms var(--ks-ease),
transform 120ms var(--ks-ease);
}
.home-kinpaku .downloads-rebuild-copy:active {
transform: scale(0.96);
}
.home-kinpaku .downloads-rebuild-copy-icon,
.home-kinpaku .downloads-rebuild-check-icon {
grid-area: 1 / 1;
transition:
opacity 140ms var(--ks-ease),
transform 180ms var(--ks-ease);
}
.home-kinpaku .downloads-rebuild-copy-icon {
opacity: 1;
transform: scale(1);
}
.home-kinpaku .downloads-rebuild-check-icon {
opacity: 0;
transform: scale(0.86);
}
.home-kinpaku .downloads-rebuild-check-icon path {
stroke-dasharray: 24;
stroke-dashoffset: 24;
}
.home-kinpaku .downloads-rebuild-copy.copied {
background: var(--card-copy-hover-bg);
border-color: currentColor;
}
.home-kinpaku .downloads-rebuild-copy.copied .downloads-rebuild-copy-icon {
opacity: 0;
transform: scale(0.82);
}
.home-kinpaku .downloads-rebuild-copy.copied .downloads-rebuild-check-icon {
opacity: 1;
transform: scale(1);
}
.home-kinpaku .downloads-rebuild-copy.copied .downloads-rebuild-check-icon path {
animation: downloads-check-draw 260ms var(--ks-ease) forwards;
}
@keyframes downloads-check-draw {
to { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
.home-kinpaku .downloads-rebuild-copy,
.home-kinpaku .downloads-rebuild-copy-icon,
.home-kinpaku .downloads-rebuild-check-icon {
transition: none;
}
.home-kinpaku .downloads-rebuild-copy.copied .downloads-rebuild-check-icon path {
animation: none;
stroke-dashoffset: 0;
}
}
.home-kinpaku .downloads-rebuild-copy span {
+62 -2
View File
@@ -163,17 +163,77 @@
place-items: center;
padding: 4px;
border-radius: 2px;
transition: color 160ms var(--ks-ease);
transition:
color 160ms var(--ks-ease),
transform 120ms var(--ks-ease);
}
.live-mode-kinpaku .live-mode-start-copy:hover {
color: var(--ks-kinpaku);
}
.live-mode-kinpaku .live-mode-start-copy.is-copied {
.live-mode-kinpaku .live-mode-start-copy:active {
transform: scale(0.96);
}
.live-mode-kinpaku .live-mode-start-copy-icon,
.live-mode-kinpaku .live-mode-start-check-icon {
grid-area: 1 / 1;
transition:
opacity 140ms var(--ks-ease),
transform 180ms var(--ks-ease);
}
.live-mode-kinpaku .live-mode-start-copy-icon {
opacity: 1;
transform: scale(1);
}
.live-mode-kinpaku .live-mode-start-check-icon {
opacity: 0;
transform: scale(0.86);
}
.live-mode-kinpaku .live-mode-start-check-icon path {
stroke-dasharray: 24;
stroke-dashoffset: 24;
}
.live-mode-kinpaku .live-mode-start-copy.copied {
color: var(--ks-kinpaku);
}
.live-mode-kinpaku .live-mode-start-copy.copied .live-mode-start-copy-icon {
opacity: 0;
transform: scale(0.82);
}
.live-mode-kinpaku .live-mode-start-copy.copied .live-mode-start-check-icon {
opacity: 1;
transform: scale(1);
}
.live-mode-kinpaku .live-mode-start-copy.copied .live-mode-start-check-icon path {
animation: live-mode-check-draw 260ms var(--ks-ease) forwards;
}
@keyframes live-mode-check-draw {
to { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
.live-mode-kinpaku .live-mode-start-copy,
.live-mode-kinpaku .live-mode-start-copy-icon,
.live-mode-kinpaku .live-mode-start-check-icon {
transition: none;
}
.live-mode-kinpaku .live-mode-start-copy.copied .live-mode-start-check-icon path {
animation: none;
stroke-dashoffset: 0;
}
}
/* ============================================================================
3. INTERACTIVE DEMO — frame chrome + Impeccable picker bar re-skin