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
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ jobs:
# numbers cannot drift from the code that produced them.
- run: node scripts/check-tonestack-spice.mjs
working-directory: site
# Phase-0 roadmap for this workflow:
# - kiutils round-trip of amps/*/schematic.kicad_sch
# - schematic SVG rendering via kicad-cli
# The lineage caption gate reads the built page and proves the prose still
# names every derivation arrow the SVG draws. INDEXNOW=0 so a slow
# search-engine ping cannot fail CI (the ping is not a corpus claim).
- run: INDEXNOW=0 npm run build
working-directory: site
- run: node scripts/check-lineage-caption.mjs
working-directory: site
20 changes: 20 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Security

Circuit Codex is a public static archive. There are no user accounts and no
authentication. The published site is generated files; being able to read this
repository is the product, not a vulnerability.

## Reporting a vulnerability

Report security issues privately via
[GitHub Security Advisories](https://github.com/TheAnalogMaker/circuit-codex/security/advisories/new)
on TheAnalogMaker/circuit-codex. If advisories are unavailable, [open an
issue](https://github.com/TheAnalogMaker/circuit-codex/issues/new).

## What is not a vulnerability

- **Public source.** Circuit data, the site, the Worker, and the CI config are
meant to be readable. Do not file “I can see the source” as a vulnerability.
- **Lethal voltages.** Tube amplifiers carry hundreds of volts, and filter
capacitors hold charge after power-off. That is a safety topic, not a software
bug. See the [high-voltage safety primer](https://circuitcodex.com/reference/safety/).
31 changes: 21 additions & 10 deletions site/src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,25 @@ const ldBlocks = [
a:hover,a:focus-visible{text-decoration:underline}
:focus-visible{outline:2px solid var(--amber);outline-offset:2px;border-radius:2px}
nav{border-bottom:1px solid var(--line);position:sticky;top:0;background:rgba(23,19,16,.94);backdrop-filter:blur(6px);z-index:9}
.nav-in{display:flex;align-items:baseline;gap:28px;padding:14px 0;flex-wrap:wrap}
.nav-in{display:flex;align-items:center;gap:28px;padding:14px 0;flex-wrap:wrap}
.brand{font-family:var(--disp);text-transform:uppercase;letter-spacing:.22em;font-size:15px;color:var(--ink)}
.brand b{color:var(--amber);font-weight:600}
/* Eight links do not fit one phone-width row: unwrapped, they pushed the whole
document 150 px wider than the viewport, so every page — not just the nav —
scrolled sideways, and a figure with its own sideways scroll had to compete
with the page for the same gesture. */
.nav-links{display:flex;flex-wrap:wrap;gap:8px 20px;margin-left:auto;font-family:var(--disp);text-transform:uppercase;letter-spacing:.14em;font-size:12.5px}
/* Links plus the search field do not fit one phone-width row: unwrapped, they
pushed the whole document wider than the viewport, so every page — not just
the nav — scrolled sideways, and a figure with its own sideways scroll had
to compete with the page for the same gesture. Wrap is the fix; the field
shrinks rather than forcing a horizontal page scroll. */
.nav-links{display:flex;flex-wrap:wrap;align-items:center;gap:8px 20px;margin-left:auto;font-family:var(--disp);text-transform:uppercase;letter-spacing:.14em;font-size:12.5px}
.nav-links a{color:var(--muted)}
.nav-search{flex:0 1 11rem;min-width:0;margin:0}
.nav-search input{
appearance:none;-webkit-appearance:none;box-sizing:border-box;width:100%;min-width:0;
font-family:var(--body);font-size:13px;letter-spacing:0;text-transform:none;
color:var(--ink);background:var(--well);border:1px solid var(--line);border-radius:99px;
padding:4px 12px;
}
.nav-search input::placeholder{color:var(--faint);font-family:var(--disp);font-size:12px;letter-spacing:.1em;text-transform:uppercase}
.nav-search input:focus{outline:2px solid var(--amber);outline-offset:2px;border-color:var(--amber-dim)}
h1,h2,h3{font-family:var(--disp);text-transform:uppercase;font-weight:600;letter-spacing:.06em;text-wrap:balance}
.chip{font-family:var(--disp);text-transform:uppercase;letter-spacing:.1em;font-size:10.5px;border:1px solid var(--line);border-radius:99px;padding:2px 9px;color:var(--muted);white-space:nowrap}
.chip.v{color:var(--amber);border-color:var(--amber-dim)}
Expand All @@ -138,16 +148,17 @@ const ldBlocks = [
<body data-pagefind-body data-figure-page={isFigurePage ? '' : undefined}>
<nav data-pagefind-ignore><div class="wrap nav-in">
<a href="/" style="text-decoration:none"><span class="brand">The Analog Maker <b>/ Circuit Codex</b></span></a>
<span class="nav-links">
<div class="nav-links">
<a href="/#circuits">Circuits</a>
<a href="/history/">History</a>
<a href="/lineage/">Lineage</a>
<a href="/reference/">Reference</a>
<a href="/about/">About</a>
<a href="/search/">Search</a>
<a href={GITHUB}>GitHub</a>
<a href="https://www.youtube.com/@theanalogmaker">YouTube</a>
</span>
<form class="nav-search" action="/search/" method="get" role="search">
<input type="search" name="q" placeholder="Search" aria-label="Search the archive" enterkeyhint="search" autocomplete="off" />
</form>
</div>
</div></nav>
<slot />
<footer data-pagefind-ignore><div class="wrap">
Expand Down
46 changes: 45 additions & 1 deletion site/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ const itemList = {
<p class="sub">Every documented circuit, grouped by the line it belongs to, oldest era first.
<a href="/history/">The history tier</a> places each line's other models;
<a href={`${GITHUB}/blob/main/CONTRIBUTING.md`}>contributions welcome</a>.</p>
<div class="corpus-filter" role="group" aria-label="Filter circuits by status">
<button type="button" class="chip v" data-filter="all" aria-pressed="true">All</button>
<button type="button" class="chip" data-filter="verified" aria-pressed="false">Verified</button>
<button type="button" class="chip" data-filter="draft" aria-pressed="false">Draft</button>
</div>
{families.map((f) => (
<div class="fam">
<div class="fam-head">
Expand All @@ -99,7 +104,8 @@ const itemList = {
</div>
<div class="grid">
{f.amps.map((a) => (
<a class="card" href={`/amps/${a.id}/`}>
<a class="card" href={`/amps/${a.id}/`}
data-status={a.meta.verification?.status === 'verified' ? 'verified' : 'draft'}>
<div class="id">{displayId(a.id)}</div>
<div class="style">{a.meta.name_style} · {a.meta.wattage} W</div>
<div class="tubes">{a.meta.tubes.join(' · ')}</div>
Expand All @@ -116,6 +122,41 @@ const itemList = {
</div>
))}
</section>
<script is:inline>
(function () {
const root = document.getElementById('circuits');
if (!root) return;
const buttons = root.querySelectorAll('[data-filter]');
const cards = root.querySelectorAll('.card[data-status]');
const fams = root.querySelectorAll('.fam');

function apply(status) {
const allowed = status === 'verified' || status === 'draft' ? status : 'all';
buttons.forEach(function (b) {
const on = b.getAttribute('data-filter') === allowed;
b.setAttribute('aria-pressed', on ? 'true' : 'false');
b.classList.toggle('v', on);
});
cards.forEach(function (card) {
card.hidden = allowed !== 'all' && card.getAttribute('data-status') !== allowed;
});
fams.forEach(function (fam) {
fam.hidden = !fam.querySelector('.card[data-status]:not([hidden])');
});
const url = new URL(location.href);
if (allowed === 'all') url.searchParams.delete('status');
else url.searchParams.set('status', allowed);
if (url.href !== location.href) history.replaceState(null, '', url);
}

const initial = new URLSearchParams(location.search).get('status');
apply(initial === 'verified' || initial === 'draft' ? initial : 'all');
root.addEventListener('click', function (e) {
const btn = e.target.closest('[data-filter]');
if (btn && root.contains(btn)) apply(btn.getAttribute('data-filter'));
});
})();
</script>
</div>
</Base>

Expand All @@ -142,6 +183,7 @@ const itemList = {
.fam-meta{font-family:var(--mono);font-size:11.5px;color:var(--faint)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:18px 18px 14px;display:flex;flex-direction:column;gap:8px;color:inherit}
.card[hidden],.fam[hidden]{display:none}
.card:hover{border-color:var(--amber-dim);text-decoration:none}
.card .id{font-family:var(--disp);font-size:24px;letter-spacing:.04em}
.card .style{color:var(--muted);font-size:13.5px;margin-top:-6px}
Expand All @@ -150,4 +192,6 @@ const itemList = {
.explore{margin:8px 0 28px}
.explore-grid{margin-top:4px}
.inline-link{color:var(--amber)}
.corpus-filter{display:flex;flex-wrap:wrap;gap:8px;margin:-6px 0 20px}
.corpus-filter .chip{background:transparent;cursor:pointer}
</style>
6 changes: 5 additions & 1 deletion site/src/pages/search.astro
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const description = 'Search the Circuit Codex archive — circuits, tube referen
document.addEventListener('DOMContentLoaded', mount);
return;
}
new PagefindUI({
const ui = new PagefindUI({
element: '#search',
showSubResults: true,
showImages: false,
Expand All @@ -33,6 +33,10 @@ const description = 'Search the Circuit Codex archive — circuits, tube referen
one_result: '[COUNT] result for [SEARCH_TERM]',
},
});
// Header search submits here as ?q=. triggerSearch is PagefindUI's
// documented way to preload that term (and fill the input).
const q = new URLSearchParams(location.search).get('q');
if (q) ui.triggerSearch(q);
})();
</script>
</Base>
Expand Down
Loading