Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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.
72 changes: 61 additions & 11 deletions web/static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,66 @@
/* ---------- 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;
};

// One shared restore timer + generation token so rapid clicks (same button
// or another copy control) cannot let an older timeout wipe newer feedback.
var copyFeedbackToken = 0;
var copyFeedbackTimer = null;
var activeCopyButton = null;

var restoreCopyButton = function (button) {
button.textContent = "Copy";
button.classList.remove("is-copied");
};

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

document.querySelectorAll("[data-copy]").forEach(function (block) {
var showCopyFeedback = function (button, label) {
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();
var token = ++copyFeedbackToken;
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
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 +139,12 @@
.join("")
.trim();

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

navigator.clipboard.writeText(text).then(
function () {
button.textContent = "Copied";
button.classList.add("is-copied");
setTimeout(restore, 2000);
showCopyFeedback(button, "Copied");
},
function () {
button.textContent = "Press ⌘C";
setTimeout(restore, 2000);
showCopyFeedback(button, "Press ⌘C");
}
);
});
Expand Down
35 changes: 35 additions & 0 deletions web/tests/site.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,41 @@ 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\("Copied"\)/);
assert.match(script, /copyFeedbackToken/);
assert.match(script, /clearTimeout\(copyFeedbackTimer\)/);
assert.match(script, /showCopyFeedback/);
assert.match(script, /announceCopy\("Press ⌘C"\)/);

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