Skip to content

Commit 726cb3d

Browse files
authored
Site: honest offline copy, privacy engines, copy a11y (#247)
* Site: honest offline copy, privacy engines, copy a11y * Site: v0.9.0 social preview * Site: pin Netlify Hugo so hugo.toml is found * Fix overlapping site copy-button feedback timers Use one generation token and a shared restore timer so a second click cannot let an older timeout wipe newer Copied feedback. * Align site copy-feedback tests with shared timer helpers * Tie site copy feedback to click-time generation Advance the shared token on click so a slower clipboard settle from an earlier request cannot overwrite newer button or live-region state. Keep site tests aligned with the requestToken helpers.
1 parent c71f20d commit 726cb3d

8 files changed

Lines changed: 114 additions & 17 deletions

File tree

netlify.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Pin the same Hugo GitHub Actions uses (peaceiris hugo-version 0.165.0).
2+
# Netlify UI still owns base=web, command=hugo, publish=web/public.
3+
# Older default Hugo does not read hugo.toml, so deploy-preview fails with
4+
# "Unable to locate config file".
5+
[build.environment]
6+
HUGO_VERSION = "0.165.0"

web/content/features/clipboard-preservation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Many voice-to-text apps on macOS handle clipboard carelessly. VocaMac is one of
117117
| Feature | VocaMac | Typical App |
118118
|---------|---------|-------------|
119119
| **Preserves clipboard** | Yes | No |
120-
| **Works offline** | Yes | No |
120+
| **On-device transcription after model download** | Yes | No |
121121
| **Adjustable silence detection** | Yes | Rare |
122122
| **Menu bar integration** | Yes | No |
123123
| **Open source** | Yes | No |

web/layouts/features/single.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ <h1 id="page-title">{{ .Title }}</h1>
3030
</div>
3131
</div>
3232
</section>
33-
<section class="section" aria-labelledby="feature-content-title">
33+
<section class="section">
3434
<div class="shell doc-layout">
35-
<article class="prose" id="feature-content-title">
35+
<article class="prose">
3636
{{ .Content }}
3737
<div class="feature-cta">
3838
<p>Ready to try {{ .Title }}?</p>

web/layouts/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,8 @@ <h2 class="section-title" id="privacy-title">Your voice has a path.<br>We show y
201201
<li class="route-step">
202202
<div class="route-node">
203203
<span class="route-icon">{{ partial "icon.html" "chip" }}</span>
204-
<span class="route-name">{{ $p.engine.name }} / {{ $p.engine.runtime }}</span>
205-
<span class="route-sub">Runs on your Mac</span>
204+
<span class="route-name">Selected on-device engine</span>
205+
<span class="route-sub">WhisperKit/CoreML, Parakeet, Apple Speech, ONNX</span>
206206
</div>
207207
<svg class="route-link" width="56" height="24" viewBox="0 0 56 24" aria-hidden="true"><path class="route-track" d="M2 12h48"/><path class="route-pulse" d="M2 12h48"/><path class="route-head" d="M46 7.5 50.5 12 46 16.5"/></svg>
208208
</li>

web/static/og-image.png

-2.28 KB
Loading

web/static/og-image.svg

Lines changed: 2 additions & 2 deletions
Loading

web/static/script.js

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,67 @@
6161
/* ---------- Copy buttons ---------- */
6262

6363
var canCopy = !!(navigator.clipboard && navigator.clipboard.writeText);
64+
var copyBlocks = document.querySelectorAll("[data-copy]");
65+
var copyStatus = null;
66+
67+
if (canCopy && copyBlocks.length) {
68+
copyStatus = document.createElement("div");
69+
copyStatus.className = "sr-only";
70+
copyStatus.setAttribute("aria-live", "polite");
71+
copyStatus.setAttribute("aria-atomic", "true");
72+
document.body.appendChild(copyStatus);
73+
}
74+
75+
var announceCopy = function (message) {
76+
if (!copyStatus) { return; }
77+
copyStatus.textContent = "";
78+
copyStatus.textContent = message;
79+
};
80+
81+
// Generation advances on each click. Clipboard settlements and restore
82+
// timers ignore stale generations so out-of-order promises cannot wipe the
83+
// newest button or live-region feedback.
84+
var copyFeedbackToken = 0;
85+
var copyFeedbackTimer = null;
86+
var activeCopyButton = null;
6487

65-
document.querySelectorAll("[data-copy]").forEach(function (block) {
88+
var restoreCopyButton = function (button) {
89+
button.textContent = "Copy";
90+
button.classList.remove("is-copied");
91+
};
92+
93+
var clearCopyFeedbackTimer = function () {
94+
if (copyFeedbackTimer) {
95+
clearTimeout(copyFeedbackTimer);
96+
copyFeedbackTimer = null;
97+
}
98+
};
99+
100+
var showCopyFeedback = function (button, label, token) {
101+
if (token !== copyFeedbackToken) { return; }
102+
if (activeCopyButton && activeCopyButton !== button) {
103+
restoreCopyButton(activeCopyButton);
104+
}
105+
activeCopyButton = button;
106+
button.textContent = label;
107+
if (label === "Copied") {
108+
button.classList.add("is-copied");
109+
} else {
110+
button.classList.remove("is-copied");
111+
}
112+
announceCopy(label);
113+
114+
clearCopyFeedbackTimer();
115+
copyFeedbackTimer = setTimeout(function () {
116+
if (token !== copyFeedbackToken) { return; }
117+
restoreCopyButton(button);
118+
if (activeCopyButton === button) { activeCopyButton = null; }
119+
announceCopy("");
120+
copyFeedbackTimer = null;
121+
}, 2000);
122+
};
123+
124+
copyBlocks.forEach(function (block) {
66125
var source = block.querySelector("code");
67126
if (!source || !canCopy) { return; }
68127

@@ -81,20 +140,15 @@
81140
.join("")
82141
.trim();
83142

84-
var restore = function () {
85-
button.textContent = "Copy";
86-
button.classList.remove("is-copied");
87-
};
143+
var requestToken = ++copyFeedbackToken;
144+
clearCopyFeedbackTimer();
88145

89146
navigator.clipboard.writeText(text).then(
90147
function () {
91-
button.textContent = "Copied";
92-
button.classList.add("is-copied");
93-
setTimeout(restore, 2000);
148+
showCopyFeedback(button, "Copied", requestToken);
94149
},
95150
function () {
96-
button.textContent = "Press ⌘C";
97-
setTimeout(restore, 2000);
151+
showCopyFeedback(button, "Press ⌘C", requestToken);
98152
}
99153
);
100154
});

web/tests/site.test.mjs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,43 @@ test("every rendered page has one heading and image alternatives", async () => {
164164
}
165165
});
166166

167+
test("keeps the site-audit copy and a11y fixes", async () => {
168+
assert.match(index, /Selected on-device engine/);
169+
assert.match(index, /WhisperKit\/CoreML, Parakeet, Apple Speech, ONNX/);
170+
assert.doesNotMatch(
171+
index,
172+
/<span class="route-name">WhisperKit \/ CoreML<\/span>/,
173+
);
174+
175+
const clipboard = await readFile(
176+
join(outputRoot, "features/clipboard-preservation/index.html"),
177+
"utf8",
178+
);
179+
assert.match(clipboard, /On-device transcription after model download/);
180+
assert.doesNotMatch(clipboard, /Works offline/);
181+
182+
for (const page of pages) {
183+
const rel = relative(outputRoot, page);
184+
if (!rel.startsWith("features/") || rel === "features/index.html") continue;
185+
const html = await readFile(page, "utf8");
186+
assert.doesNotMatch(html, /aria-labelledby="feature-content-title"/);
187+
assert.doesNotMatch(html, /id="feature-content-title"/);
188+
}
189+
190+
assert.match(script, /aria-live", "polite"/);
191+
assert.match(script, /announceCopy\(label\)/);
192+
assert.match(script, /copyFeedbackToken/);
193+
assert.match(script, /clearTimeout\(copyFeedbackTimer\)/);
194+
assert.match(script, /requestToken = \+\+copyFeedbackToken/);
195+
assert.match(script, /showCopyFeedback\(button, "Copied", requestToken\)/);
196+
assert.match(script, /showCopyFeedback\(button, "Press C", requestToken\)/);
197+
assert.match(script, /if \(token !== copyFeedbackToken\)/);
198+
199+
const ogSvg = await readFile(join(siteRoot, "static/og-image.svg"), "utf8");
200+
assert.match(ogSvg, /v0\.9\.0/);
201+
assert.doesNotMatch(ogSvg, /v0\.8\.0/);
202+
});
203+
167204
test("all rendered local references resolve", async () => {
168205
const references = new Set();
169206
for (const page of pages) {

0 commit comments

Comments
 (0)