Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Pin the same Hugo GitHub Actions uses (peaceiris hugo-version 0.165.0).
# Netlify UI still owns base=web, command=hugo, publish=web/public.
# Older default Hugo does not read hugo.toml, so deploy-preview fails with
# "Unable to locate config file".
[build.environment]
HUGO_VERSION = "0.165.0"
2 changes: 1 addition & 1 deletion web/content/features/clipboard-preservation.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ Many voice-to-text apps on macOS handle clipboard carelessly. VocaMac is one of
| Feature | VocaMac | Typical App |
|---------|---------|-------------|
| **Preserves clipboard** | Yes | No |
| **Works offline** | Yes | No |
| **On-device transcription after model download** | Yes | No |
| **Adjustable silence detection** | Yes | Rare |
| **Menu bar integration** | Yes | No |
| **Open source** | Yes | No |
Expand Down
4 changes: 2 additions & 2 deletions web/layouts/features/single.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ <h1 id="page-title">{{ .Title }}</h1>
</div>
</div>
</section>
<section class="section" aria-labelledby="feature-content-title">
<section class="section">
<div class="shell doc-layout">
<article class="prose" id="feature-content-title">
<article class="prose">
{{ .Content }}
<div class="feature-cta">
<p>Ready to try {{ .Title }}?</p>
Expand Down
4 changes: 2 additions & 2 deletions web/layouts/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ <h2 class="section-title" id="privacy-title">Your voice has a path.<br>We show y
<li class="route-step">
<div class="route-node">
<span class="route-icon">{{ partial "icon.html" "chip" }}</span>
<span class="route-name">{{ $p.engine.name }} / {{ $p.engine.runtime }}</span>
<span class="route-sub">Runs on your Mac</span>
<span class="route-name">Selected on-device engine</span>
<span class="route-sub">WhisperKit/CoreML, Parakeet, Apple Speech, ONNX</span>
</div>
<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>
</li>
Expand Down
Binary file modified web/static/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions web/static/og-image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 64 additions & 10 deletions web/static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,67 @@
/* ---------- Copy buttons ---------- */

var canCopy = !!(navigator.clipboard && navigator.clipboard.writeText);
var copyBlocks = document.querySelectorAll("[data-copy]");
var copyStatus = null;

if (canCopy && copyBlocks.length) {
copyStatus = document.createElement("div");
copyStatus.className = "sr-only";
copyStatus.setAttribute("aria-live", "polite");
copyStatus.setAttribute("aria-atomic", "true");
document.body.appendChild(copyStatus);
}

var announceCopy = function (message) {
if (!copyStatus) { return; }
copyStatus.textContent = "";
copyStatus.textContent = message;
};

// Generation advances on each click. Clipboard settlements and restore
// timers ignore stale generations so out-of-order promises cannot wipe the
// newest button or live-region feedback.
var copyFeedbackToken = 0;
var copyFeedbackTimer = null;
var activeCopyButton = null;

document.querySelectorAll("[data-copy]").forEach(function (block) {
var restoreCopyButton = function (button) {
button.textContent = "Copy";
button.classList.remove("is-copied");
};

var clearCopyFeedbackTimer = function () {
if (copyFeedbackTimer) {
clearTimeout(copyFeedbackTimer);
copyFeedbackTimer = null;
}
};

var showCopyFeedback = function (button, label, token) {
if (token !== copyFeedbackToken) { return; }
if (activeCopyButton && activeCopyButton !== button) {
restoreCopyButton(activeCopyButton);
}
activeCopyButton = button;
button.textContent = label;
if (label === "Copied") {
button.classList.add("is-copied");
} else {
button.classList.remove("is-copied");
}
announceCopy(label);
Comment thread
jatinkrmalik marked this conversation as resolved.

clearCopyFeedbackTimer();
copyFeedbackTimer = setTimeout(function () {
if (token !== copyFeedbackToken) { return; }
restoreCopyButton(button);
if (activeCopyButton === button) { activeCopyButton = null; }
announceCopy("");
copyFeedbackTimer = null;
}, 2000);
};

copyBlocks.forEach(function (block) {
var source = block.querySelector("code");
if (!source || !canCopy) { return; }

Expand All @@ -81,20 +140,15 @@
.join("")
.trim();

var restore = function () {
button.textContent = "Copy";
button.classList.remove("is-copied");
};
var requestToken = ++copyFeedbackToken;
clearCopyFeedbackTimer();

navigator.clipboard.writeText(text).then(
function () {
button.textContent = "Copied";
button.classList.add("is-copied");
setTimeout(restore, 2000);
showCopyFeedback(button, "Copied", requestToken);
},
function () {
button.textContent = "Press ⌘C";
setTimeout(restore, 2000);
showCopyFeedback(button, "Press ⌘C", requestToken);
}
);
});
Expand Down
37 changes: 37 additions & 0 deletions web/tests/site.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,43 @@ test("every rendered page has one heading and image alternatives", async () => {
}
});

test("keeps the site-audit copy and a11y fixes", async () => {
assert.match(index, /Selected on-device engine/);
assert.match(index, /WhisperKit\/CoreML, Parakeet, Apple Speech, ONNX/);
assert.doesNotMatch(
index,
/<span class="route-name">WhisperKit \/ CoreML<\/span>/,
);

const clipboard = await readFile(
join(outputRoot, "features/clipboard-preservation/index.html"),
"utf8",
);
assert.match(clipboard, /On-device transcription after model download/);
assert.doesNotMatch(clipboard, /Works offline/);

for (const page of pages) {
const rel = relative(outputRoot, page);
if (!rel.startsWith("features/") || rel === "features/index.html") continue;
const html = await readFile(page, "utf8");
assert.doesNotMatch(html, /aria-labelledby="feature-content-title"/);
assert.doesNotMatch(html, /id="feature-content-title"/);
}

assert.match(script, /aria-live", "polite"/);
assert.match(script, /announceCopy\(label\)/);
assert.match(script, /copyFeedbackToken/);
assert.match(script, /clearTimeout\(copyFeedbackTimer\)/);
assert.match(script, /requestToken = \+\+copyFeedbackToken/);
assert.match(script, /showCopyFeedback\(button, "Copied", requestToken\)/);
assert.match(script, /showCopyFeedback\(button, "Press ⌘C", requestToken\)/);
assert.match(script, /if \(token !== copyFeedbackToken\)/);

const ogSvg = await readFile(join(siteRoot, "static/og-image.svg"), "utf8");
assert.match(ogSvg, /v0\.9\.0/);
assert.doesNotMatch(ogSvg, /v0\.8\.0/);
});

test("all rendered local references resolve", async () => {
const references = new Set();
for (const page of pages) {
Expand Down
Loading