Skip to content

Commit 5409e39

Browse files
committed
chore(home): seven quick wins — framing, icons, a11y, copy hygiene
Stacked on perf/homepage-shiki-video (#840) because two items touch files introduced/changed there. - HomeProblems: add a section heading ("Four incidents Cycles prevents") — the incident cards previously appeared with no framing. - Features grid: new /icons/file-code.svg for Protocol Reference, which shared book-text.svg with Topic Guides. - Hero image alt text: replace arrow-chain shorthand ("agents -> gate -> allow/block") with a screen-reader-friendly sentence. - snippets.data.ts: GPT-5 per-token prices (125/1000 microcents) are now single constants interpolated into both the Java and Vercel snippets — a price change can't half-update the homepage. - HomeIntegrations: chips without a real brand icon (AWS Bedrock, Google Gemini, LlamaIndex, CrewAI, Pydantic AI, AutoGen, Groq, Ollama) now use a neutral layers glyph instead of a misleading TypeScript/Python logo. Language icons stay on language-framework chips (Django, Next.js, ...). - HomeCodeSnippet: proper WAI-ARIA tabs — role=tablist/tab/tabpanel, aria-selected, roving tabindex, ArrowLeft/Right/Home/End keyboard navigation. - Social proof: row now reads "N+ package installs · Apache 2.0 · self-hosted · no prompt storage". GitHub stars are fetched in installs.data.ts and tracked in the cache/installs.json for analytics but deliberately NOT displayed (count is too small to be persuasive — same rationale as clones). Stars are not a high-water mark: they can legitimately decrease, so a successful fetch wins and the cached value is only an API-failure fallback; the field is excluded from MONOTONIC_FIELDS. Verified: vitest 93/93, npm run build:fast clean, rendered homepage checked via vitepress preview + headless Edge (heading, chips, stat row, tablist SSR markup, interpolated Java snippet all correct). installs-cache.json / public/installs.json build side-effects left uncommitted; the stars field appears there on the next counts refresh.
1 parent 7ece549 commit 5409e39

9 files changed

Lines changed: 122 additions & 21 deletions

File tree

.vitepress/theme/FrameworkIcons.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ export const anthropicPath = 'M17.304 0H13.68L24 24h3.624zm-10.608 0L0 24h3.696l
3434
/** Mozilla — dinosaur head mark (Simple Icons, CC0) */
3535
export const mozillaPath = 'M3.346 1.168C1.632 2.326.586 4.142.21 6.07c-.376 1.929-.147 3.97.445 5.758a13.03 13.03 0 0 0 2.876 4.878c1.238 1.318 2.784 2.384 4.473 2.612 1.056.143 2.146-.036 3.058-.535.912-.499 1.624-1.344 1.924-2.341.274-.912.167-1.888-.069-2.822-.235-.935-.597-1.839-.834-2.771-.197-.774-.31-1.582-.175-2.371.134-.788.53-1.558 1.167-2.035.638-.477 1.547-.603 2.27-.276.724.326 1.185 1.097 1.341 1.869.156.773.076 1.573-.046 2.354-.183 1.17-.44 2.326-.517 3.501-.078 1.176.04 2.391.586 3.422.547 1.032 1.605 1.83 2.811 1.826 1.095-.004 2.124-.667 2.804-1.517.68-.85 1.11-1.876 1.395-2.93.57-2.109.536-4.396-.172-6.453-.708-2.058-2.103-3.861-3.888-5.107C18.875.457 16.765-.144 14.66.037c-1.583.136-3.133.72-4.354 1.746-1.222 1.027-2.086 2.513-2.28 4.141-.12.999.016 2.027.287 3.01.27.983.672 1.927.944 2.895.202.718.33 1.485.182 2.218-.147.733-.594 1.437-1.272 1.718-.737.306-1.609.054-2.168-.508-.56-.562-.862-1.323-1.055-2.099-.386-1.552-.381-3.19-.065-4.746.316-1.555.943-3.036 1.853-4.321L3.346 1.168Z'
3636

37+
/** Generic integration — layers mark, used when no brand icon is available (a wrong brand logo misleads more than a neutral glyph) */
38+
export const genericIntegrationPath = 'M12 2 22 8l-10 6L2 8l10-6zM3.8 10.9 2 12l10 6 10-6-1.8-1.1-8.2 4.92-8.2-4.92zM3.8 14.9 2 16l10 6 10-6-1.8-1.1-8.2 4.92-8.2-4.92z'
39+
3740
/** Spring AI — leaf mark reused from Spring Boot, with AI label context */
3841
export const springAiPath = 'm23.693 10.7058-4.73-8.1844c-.4094-.7106-1.4166-1.2942-2.2402-1.2942H7.2725c-.819 0-1.8308.5836-2.2402 1.2942L.307 10.7058c-.4095.7106-.4095 1.873 0 2.5837l4.7252 8.189c.4094.7107 1.4166 1.2943 2.2402 1.2943h9.455c.819 0 1.826-.5836 2.2402-1.2942l4.7252-8.189c.4095-.7107.4095-1.8732 0-2.5838zM10.9763 5.7547c0-.5365.4377-.9742.9742-.9742s.9742.4377.9742.9742v5.8217c0 .5366-.4377.9742-.9742.9742s-.9742-.4376-.9742-.9742zm.9742 12.4294c-3.6427 0-6.6077-2.965-6.6077-6.6077.0047-2.0896.993-4.0521 2.6685-5.304a.8657.8657 0 0 1 1.2142.1788.8657.8657 0 0 1-.1788 1.2143c-2.1602 1.6048-2.612 4.6592-1.0072 6.8194 1.6049 2.1603 4.6593 2.612 6.8195 1.0072 1.2378-.9177 1.9673-2.372 1.9673-3.9157a4.8972 4.8972 0 0 0-1.9861-3.925c-.386-.2824-.466-.8284-.1836-1.2143.2824-.386.8283-.466 1.2143-.1835 1.6895 1.2471 2.6826 3.2238 2.6873 5.3228 0 3.6474-2.965 6.6077-6.6077 6.6077z'

.vitepress/theme/HomeCodeSnippet.vue

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ const tabs = [
2727
{ key: 'vercel', label: 'Vercel AI', icon: vercelPath },
2828
{ key: 'openclaw', label: 'OpenClaw', icon: openclawPath },
2929
]
30+
31+
// WAI-ARIA tabs pattern: arrow keys move between tabs (roving tabindex),
32+
// Home/End jump to the ends.
33+
function onTablistKeydown(e) {
34+
if (!['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(e.key)) return
35+
e.preventDefault()
36+
const current = tabs.findIndex(t => t.key === activeTab.value)
37+
let next
38+
if (e.key === 'ArrowRight') next = (current + 1) % tabs.length
39+
else if (e.key === 'ArrowLeft') next = (current - 1 + tabs.length) % tabs.length
40+
else if (e.key === 'Home') next = 0
41+
else next = tabs.length - 1
42+
activeTab.value = tabs[next].key
43+
document.getElementById(`snippet-tab-${tabs[next].key}`)?.focus()
44+
}
3045
</script>
3146
3247
<template>
@@ -35,10 +50,15 @@ const tabs = [
3550
<h2 class="code-heading">Add runtime authority in a few lines</h2>
3651
<p class="code-caption"><code>@cycles</code> reserves budget before the action runs. No remaining cycles — no action.</p>
3752
<div class="code-container">
38-
<div class="tab-bar">
53+
<div class="tab-bar" role="tablist" aria-label="Integration code examples" @keydown="onTablistKeydown">
3954
<button
4055
v-for="tab in tabs"
4156
:key="tab.key"
57+
:id="`snippet-tab-${tab.key}`"
58+
role="tab"
59+
:aria-selected="activeTab === tab.key"
60+
aria-controls="snippet-panel"
61+
:tabindex="activeTab === tab.key ? 0 : -1"
4262
:class="['tab', { active: activeTab === tab.key }]"
4363
@click="activeTab = tab.key"
4464
>
@@ -48,7 +68,7 @@ const tabs = [
4868
{{ tab.label }}
4969
</button>
5070
</div>
51-
<div class="code-block">
71+
<div class="code-block" role="tabpanel" id="snippet-panel" :aria-labelledby="`snippet-tab-${activeTab}`">
5272
<button class="copy-btn" @click="copyCode" :aria-label="copied ? 'Copied' : 'Copy code'">
5373
{{ copied ? 'Copied!' : 'Copy' }}
5474
</button>

.vitepress/theme/HomeIntegrations.vue

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {
33
pythonPath, typescriptPath, springAiPath, mcpPath, langchainPath,
44
openaiAgentsPath, vercelPath, openclawPath, anthropicPath, mozillaPath,
5+
genericIntegrationPath,
56
} from './FrameworkIcons'
67
78
const integrations = [
@@ -14,15 +15,15 @@ const integrations = [
1415
{ name: 'MCP', icon: mcpPath, link: '/how-to/integrating-cycles-with-mcp' },
1516
{ name: 'OpenAI Agents', icon: openaiAgentsPath, link: '/how-to/integrating-cycles-with-openai-agents' },
1617
{ name: 'OpenClaw', icon: openclawPath, link: '/how-to/integrating-cycles-with-openclaw' },
17-
{ name: 'AWS Bedrock', icon: typescriptPath, link: '/how-to/integrating-cycles-with-aws-bedrock' },
18-
{ name: 'Google Gemini', icon: typescriptPath, link: '/how-to/integrating-cycles-with-google-gemini' },
19-
{ name: 'LlamaIndex', icon: pythonPath, link: '/how-to/integrating-cycles-with-llamaindex' },
20-
{ name: 'CrewAI', icon: pythonPath, link: '/how-to/integrating-cycles-with-crewai' },
21-
{ name: 'Pydantic AI', icon: pythonPath, link: '/how-to/integrating-cycles-with-pydantic-ai' },
22-
{ name: 'AutoGen', icon: pythonPath, link: '/how-to/integrating-cycles-with-autogen' },
18+
{ name: 'AWS Bedrock', icon: genericIntegrationPath, link: '/how-to/integrating-cycles-with-aws-bedrock' },
19+
{ name: 'Google Gemini', icon: genericIntegrationPath, link: '/how-to/integrating-cycles-with-google-gemini' },
20+
{ name: 'LlamaIndex', icon: genericIntegrationPath, link: '/how-to/integrating-cycles-with-llamaindex' },
21+
{ name: 'CrewAI', icon: genericIntegrationPath, link: '/how-to/integrating-cycles-with-crewai' },
22+
{ name: 'Pydantic AI', icon: genericIntegrationPath, link: '/how-to/integrating-cycles-with-pydantic-ai' },
23+
{ name: 'AutoGen', icon: genericIntegrationPath, link: '/how-to/integrating-cycles-with-autogen' },
2324
{ name: 'AnyAgent', icon: mozillaPath, link: '/how-to/integrating-cycles-with-anyagent' },
24-
{ name: 'Groq', icon: pythonPath, link: '/how-to/integrating-cycles-with-groq' },
25-
{ name: 'Ollama', icon: pythonPath, link: '/how-to/integrating-cycles-with-ollama' },
25+
{ name: 'Groq', icon: genericIntegrationPath, link: '/how-to/integrating-cycles-with-groq' },
26+
{ name: 'Ollama', icon: genericIntegrationPath, link: '/how-to/integrating-cycles-with-ollama' },
2627
{ name: 'Next.js', icon: typescriptPath, link: '/how-to/integrating-cycles-with-nextjs' },
2728
{ name: 'Express', icon: typescriptPath, link: '/how-to/integrating-cycles-with-express' },
2829
{ name: 'Django', icon: pythonPath, link: '/how-to/integrating-cycles-with-django' },

.vitepress/theme/HomeProblems.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const problems = [
3434
<template>
3535
<section class="home-problems">
3636
<div class="inner">
37+
<h2 class="heading">Four incidents Cycles prevents</h2>
3738
<div class="grid">
3839
<a
3940
v-for="item in problems"
@@ -71,6 +72,18 @@ const problems = [
7172
margin: 0 auto;
7273
}
7374
75+
.heading {
76+
font-size: 24px;
77+
font-weight: 700;
78+
color: var(--vp-c-text-1);
79+
margin: 0 0 32px;
80+
letter-spacing: -0.02em;
81+
line-height: 1.3;
82+
text-align: center;
83+
border: none;
84+
padding: 0;
85+
}
86+
7487
.grid {
7588
display: grid;
7689
grid-template-columns: 1fr;

.vitepress/theme/HomeSocialProof.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const showInstalls = computed(() => installs.value > 0)
2121
</script>
2222
2323
<template>
24-
<p v-if="showInstalls" class="social-proof">
25-
<span class="stat">
24+
<p class="social-proof">
25+
<span v-if="showInstalls" class="stat">
2626
<svg
2727
class="stat-icon"
2828
xmlns="http://www.w3.org/2000/svg"
@@ -38,6 +38,9 @@ const showInstalls = computed(() => installs.value > 0)
3838
</svg>
3939
<span class="stat-text">{{ formatted.format(installs) }}+ package installs</span>
4040
</span>
41+
<span class="stat">
42+
<span class="stat-text">Apache 2.0 &#183; self-hosted &#183; no prompt storage</span>
43+
</span>
4144
</p>
4245
</template>
4346
@@ -64,6 +67,12 @@ const showInstalls = computed(() => installs.value > 0)
6467
white-space: nowrap;
6568
}
6669
70+
.stat + .stat::before {
71+
content: '\00b7';
72+
margin-right: 14px;
73+
color: var(--vp-c-text-3);
74+
}
75+
6776
.stat-icon { flex-shrink: 0; }
6877
.stat-text { font-variant-numeric: tabular-nums; }
6978
</style>

.vitepress/theme/installs.data.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export interface InstallsData {
2121
total: number
2222
/** Accumulated clone count across tracked repos (cumulative via day-cursor). */
2323
clones: number
24+
/** GitHub stars across first-party org repos. Tracked for analytics but
25+
* NOT displayed on the homepage (count is currently too small to be
26+
* persuasive — same rationale as clones). Fresh value wins when the
27+
* fetch succeeds (stars can legitimately decrease, so no HWM); the
28+
* cached value is only a fallback for API failures. */
29+
stars: number
2430
fetchedAt: string
2531
}
2632

@@ -48,6 +54,7 @@ interface InstallsCache {
4854
releasesByRepo: Record<string, number>
4955
ghPackages: number
5056
maven: number
57+
stars: number
5158
total: number
5259
fetchedAt: string
5360
}
@@ -71,7 +78,7 @@ function readCache(): InstallsCache {
7178
clones: 0, clonesByRepo: {},
7279
releases: 0, releasesByRepo: {},
7380
ghPackages: 0,
74-
maven: 0, total: 0, fetchedAt: '',
81+
maven: 0, stars: 0, total: 0, fetchedAt: '',
7582
}
7683
}
7784

@@ -106,6 +113,7 @@ function readCache(): InstallsCache {
106113
releasesByRepo: raw.releasesByRepo ?? {},
107114
ghPackages: raw.ghPackages ?? 0,
108115
maven: raw.maven ?? 0,
116+
stars: raw.stars ?? 0,
109117
total: raw.total ?? 0,
110118
fetchedAt: raw.fetchedAt ?? '',
111119
}
@@ -328,6 +336,39 @@ async function listOrgRepos(): Promise<string[]> {
328336
return repos
329337
}
330338

339+
// ── GitHub stars ─────────────────────────────────────────────────────
340+
// Public endpoint (auth raises rate limits). Stars can legitimately
341+
// decrease, so this is NOT a high-water mark: a successful fetch wins
342+
// outright and the cache is only a fallback for API failures. `stars`
343+
// is deliberately absent from MONOTONIC_FIELDS for the same reason.
344+
async function fetchGithubStars(): Promise<number | null> {
345+
let total = 0
346+
let sawAny = false
347+
let page = 1
348+
while (page < 10 /* hard cap to avoid runaway loops */) {
349+
try {
350+
const res = await fetch(
351+
`https://api.github.com/orgs/${GITHUB_ORG}/repos?per_page=100&page=${page}`,
352+
{ headers: ghHeaders() }
353+
)
354+
if (!res.ok) break
355+
const list = await res.json() as Array<{
356+
fork: boolean; archived: boolean; stargazers_count?: number
357+
}>
358+
if (list.length === 0) break
359+
sawAny = true
360+
for (const r of list) {
361+
if (!r.fork && !r.archived) total += r.stargazers_count ?? 0
362+
}
363+
if (list.length < 100) break
364+
page++
365+
} catch {
366+
break
367+
}
368+
}
369+
return sawAny ? total : null
370+
}
371+
331372
// ── GitHub clones ────────────────────────────────────────────────────
332373
// API returns a 14-day rolling window with per-day counts. We accumulate
333374
// by tracking the lastSeenDay cursor per repo and only adding strictly
@@ -465,13 +506,15 @@ export default {
465506
clonesResult,
466507
releasesResult,
467508
mavenFetched,
509+
starsFetched,
468510
] = await Promise.all([
469511
fetchNpmDownloads(),
470512
fetchPypiDownloads(),
471513
fetchCratesDownloads(),
472514
fetchGithubClones(cached.clonesByRepo),
473515
fetchGithubReleaseDownloads(cached.releasesByRepo),
474516
Promise.resolve(fetchMavenDownloads()),
517+
fetchGithubStars(),
475518
])
476519

477520
// Per-package HWM: each package's count never decreases independently.
@@ -497,6 +540,10 @@ export default {
497540
const ghPackagesFetched = fetchManualPackageCounts()
498541
const ghPackages = Math.max(ghPackagesFetched, cached.ghPackages)
499542

543+
// Stars: fresh value wins on a successful fetch (no HWM — see
544+
// fetchGithubStars); cache is only the API-failure fallback.
545+
const stars = starsFetched ?? cached.stars
546+
500547
// Displayed total — excludes `clones`. Clones are still tracked in
501548
// the cache for analytics, but the home-page counter is limited to
502549
// "deliberately pulled the package" sources because total clones
@@ -528,7 +575,7 @@ export default {
528575
clones, clonesByRepo: clonesResult.updatedByRepo,
529576
releases, releasesByRepo: releasesResult.updatedByRepo,
530577
ghPackages,
531-
maven, total, fetchedAt: now,
578+
maven, stars, total, fetchedAt: now,
532579
}
533580

534581
if (
@@ -540,6 +587,7 @@ export default {
540587
|| releases > cached.releases
541588
|| ghPackages > cached.ghPackages
542589
|| maven > cached.maven
590+
|| stars !== cached.stars
543591
) {
544592
writeCache(newCache)
545593
}
@@ -548,10 +596,10 @@ export default {
548596
try {
549597
writeFileSync(
550598
PUBLIC_PATH,
551-
JSON.stringify({ total, clones, fetchedAt: now }) + '\n',
599+
JSON.stringify({ total, clones, stars, fetchedAt: now }) + '\n',
552600
)
553601
} catch { /* non-critical */ }
554602

555-
return { total, clones, fetchedAt: now }
603+
return { total, clones, stars, fetchedAt: now }
556604
},
557605
}

.vitepress/theme/snippets.data.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export interface HomeSnippet {
2020

2121
export declare const data: Record<string, HomeSnippet>
2222

23+
// GPT-5 list price in USD microcents per token ($1.25/1M in, $10/1M out).
24+
// Interpolated into the Java and Vercel snippets so a price change can't
25+
// half-update the homepage.
26+
const GPT5_IN_MICROCENTS = 125
27+
const GPT5_OUT_MICROCENTS = 1000
28+
2329
const snippets: Record<string, { lang: string; code: string }> = {
2430
python: {
2531
lang: 'python',
@@ -55,8 +61,8 @@ const ask = withCycles(
5561
import org.springframework.ai.chat.client.ChatClient;
5662
import org.springframework.ai.openai.OpenAiChatOptions;
5763
58-
// GPT-5: 125 microcents/token in, 1000 out ($1.25 / $10 per 1M)
59-
@Cycles(value = "#prompt.length() / 4 * 125 + #maxTokens * 1000",
64+
// GPT-5: ${GPT5_IN_MICROCENTS} microcents/token in, ${GPT5_OUT_MICROCENTS} out ($1.25 / $10 per 1M)
65+
@Cycles(value = "#prompt.length() / 4 * ${GPT5_IN_MICROCENTS} + #maxTokens * ${GPT5_OUT_MICROCENTS}",
6066
actionKind = "llm.completion",
6167
actionName = "gpt-5")
6268
public String chat(String prompt, int maxTokens) {
@@ -133,7 +139,7 @@ const handle = await reserveForStream({
133139
const result = streamText({
134140
model: openai("gpt-5"), messages,
135141
onFinish: async ({ usage }) =>
136-
handle.commit((usage.promptTokens ?? 0) * 125 + (usage.completionTokens ?? 0) * 1000),
142+
handle.commit((usage.promptTokens ?? 0) * ${GPT5_IN_MICROCENTS} + (usage.completionTokens ?? 0) * ${GPT5_OUT_MICROCENTS}),
137143
});`,
138144
},
139145

index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ hero:
1111
image:
1212
light: /runcycles-hero.svg?v=8
1313
dark: /runcycles-hero-dark.svg?v=8
14-
alt: Cycles hero — agents gate → allow/block → signed audit ledger
14+
alt: Three AI agents submit actions to the Cycles gate; allowed actions proceed, blocked ones are denied, and every decision is appended to a signed audit log.
1515
actions:
1616
- theme: brand
1717
text: "Start with Claude or Codex"
@@ -59,7 +59,7 @@ features:
5959
link: /how-to/add-cycles-with-claude-or-codex
6060
linkText: Browse guides
6161
- icon:
62-
src: /icons/book-text.svg
62+
src: /icons/file-code.svg
6363
alt: Protocol icon
6464
title: "Protocol Reference"
6565
details: "Reserve-commit lifecycle, authentication, scope derivation, units, caps, overage policies, TTL, decide, dry run, events, debt, balances, metrics, and error handling."

public/icons/file-code.svg

Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)