Skip to content
Open
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
2 changes: 1 addition & 1 deletion site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import sitemap from '@astrojs/sitemap';
*/
function scopedIsolation() {
// Keep in sync with public/_headers and src/middleware.ts.
const ISOLATED_PREFIXES = ['/blog/porting-kicad-graphics-to-webgl-in-2026', '/gerber-demo'];
const ISOLATED_PREFIXES = ['/blog/porting-kicad-graphics-to-webgl-in-2026', '/gerber-demo', '/compare'];
/** @type {import('vite').Connect.NextHandleFunction} */
const middleware = (req, res, next) => {
const url = req.url ?? '';
Expand Down
11 changes: 11 additions & 0 deletions site/public/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,14 @@
/gerber-demo/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

# The comparison pages embed the same Gerber viewer as the blog post, so they
# need the same isolation. Both URL forms for the same reason as above. The
# landing page stays un-isolated (YouTube embed).
/compare
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

/compare/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
23 changes: 23 additions & 0 deletions site/src/components/CompareDemo.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
/**
* "Do not take our word for it": the real KiCad Gerber viewer, embedded. Loads
* ~22 MB on demand (click), nothing on page view.
*
* Needs COOP/COEP on the route: public/_headers covers /compare and /compare/*,
* and src/middleware.ts + the vite plugin in astro.config.mjs mirror it in dev.
* The glue is loaded through a same-origin blob in public/gerber-demo/boot.js,
* which is what makes the pthread worker spawn legal on a cross-origin CDN.
*/
import GerberDemo from './GerberDemo.astro';
import SectionTitle from './SectionTitle.astro';
---

<section class="readme readme-section" id="try">
<SectionTitle>Do not take our word for it</SectionTitle>
<p>
This is the real KiCad Gerber viewer, running on this page. Pan, zoom, toggle layers. The full
editor, with schematic capture and layout, is one click away in the
<a href="https://demo.pcbjam.com" rel="noopener" target="_blank">demo</a>.
</p>
<GerberDemo />
</section>
72 changes: 72 additions & 0 deletions site/src/components/CompareSources.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
import { pcbjamSources, CHECKED_ON, type Source } from '../data/compare';
import SectionTitle from './SectionTitle.astro';

interface Props {
competitorSources: { name: string; sources: Source[] }[];
}
const { competitorSources } = Astro.props;
---

<section class="readme readme-section" id="sources">
<SectionTitle>Sources, checked {CHECKED_ON}</SectionTitle>
<p>
Every cell above comes from one of these pages. If a vendor changes something, tell us on
<a href="https://discord.gg/ybhqJxjR3E" rel="noopener" target="_blank">Discord</a> and we will fix the row.
</p>
<div class="src mono small">
{
competitorSources.map((g) => (
<div class="group">
<div class="who">{g.name}</div>
<ul>
{g.sources.map((s) => (
<li>
<a href={s.url} rel="noopener" target="_blank">{s.label}</a>
</li>
))}
</ul>
</div>
))
}
<div class="group">
<div class="who">PCBJam</div>
<ul>
{
pcbjamSources.map((s) => (
<li>
<a href={s.url} rel={s.url.startsWith('http') ? 'noopener' : undefined} target={s.url.startsWith('http') ? '_blank' : undefined}>{s.label}</a>
</li>
))
}
</ul>
</div>
</div>
</section>

<style>
.src {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem 2rem;
color: var(--fg-muted);
}
.who {
color: var(--fg);
font-weight: 600;
margin-bottom: 0.25rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
@media (max-width: 560px) {
.src {
grid-template-columns: 1fr;
}
}
</style>
47 changes: 47 additions & 0 deletions site/src/components/CompareSwitcher.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
/** Links between the comparison pages (each pair is its own URL for search). */
import { competitors } from '../data/compare';

interface Props {
current: 'all' | 'kicad' | 'easyeda' | 'flux';
}
const { current } = Astro.props;
const items = [
...competitors.map((c) => ({ href: `/compare/${c.slug}/`, label: c.tab, key: c.slug })),
{ href: '/compare/', label: 'All three', key: 'all' },
];
---

<nav class="switch mono small" aria-label="Compare PCBJam with">
{
items.map((it) => (
<a href={it.href} class:list={[{ on: it.key === current }]} aria-current={it.key === current ? 'page' : undefined}>
{it.label}
</a>
))
}
</nav>

<style>
.switch {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.switch a {
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 0.4rem 0.75rem;
color: var(--fg);
background: var(--bg);
}
.switch a:hover {
text-decoration: none;
border-color: var(--fg);
}
.switch a.on {
background: var(--fg);
color: var(--bg);
border-color: var(--fg);
}
</style>
146 changes: 146 additions & 0 deletions site/src/components/CompareTable.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
/**
* CompareTable — PCBJam beside one or more competitors, one criterion per row.
* Rows where every shown column has the same tone carry data-same="1"; the
* optional "show only differences" checkbox hides them (progressive: without
* JS every row shows).
*/
import { rows, columnNames, toneMark, toneWord, type ColKey } from '../data/compare';

interface Props {
columns: ColKey[]; // always starts with 'pcbjam'
id?: string;
}
const { columns, id = 'compare' } = Astro.props;
const shown = rows.map((r) => {
const tones = columns.map((c) => r.cells[c].tone);
return { ...r, same: tones.every((t) => t === tones[0]) };
});
const diffCount = shown.filter((r) => !r.same).length;
---

<div class="cmp" data-compare id={id}>
<label class="diff-toggle mono small">
<input type="checkbox" data-diff-toggle />
show only the differences ({diffCount} of {rows.length} rows)
</label>
<div class="scroll">
<table class="table">
<thead>
<tr>
<th scope="col">Criterion</th>
{columns.map((c) => <th scope="col">{columnNames[c]}</th>)}
</tr>
</thead>
<tbody>
{
shown.map((r) => (
<tr data-same={r.same ? '1' : undefined}>
<th scope="row">{r.label}</th>
{columns.map((c) => {
const cell = r.cells[c];
return (
<td>
<span class={`mark mark-${cell.tone}`} aria-hidden="true">{toneMark[cell.tone]}</span>
<span class="sr-only">{toneWord[cell.tone]}</span>
<span class="note">{cell.note || toneWord[cell.tone]}</span>
</td>
);
})}
</tr>
))
}
</tbody>
</table>
</div>
<p class="legend mono small muted">[x] yes · [~] partly · [ ] no</p>
</div>

<style>
.cmp {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.diff-toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--accent);
cursor: pointer;
align-self: flex-end;
}
.scroll {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
border-top: 1px solid var(--border);
}
thead th {
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--fg-muted);
font-weight: 500;
text-align: left;
padding: 0.55rem 0.5rem;
background: var(--bg-soft);
border-bottom: 1px solid var(--border);
}
tbody th {
text-align: left;
font-weight: 400;
color: var(--fg);
padding: 0.6rem 0.5rem;
vertical-align: top;
border-bottom: 1px solid var(--border);
min-width: 12rem;
}
td {
padding: 0.6rem 0.5rem;
vertical-align: top;
border-bottom: 1px solid var(--border);
color: var(--fg-2);
min-width: 11rem;
}
.mark {
font-family: var(--font-mono);
font-weight: 600;
margin-right: 0.45rem;
white-space: nowrap;
}
.mark-yes {
color: #1f7a4d;
}
.mark-part {
color: #9a6200;
}
.mark-no {
color: var(--red);
}
.legend {
margin: 0;
}
.cmp[data-diff-only] tr[data-same] {
display: none;
}
</style>

<script>
function initCompare() {
document.querySelectorAll<HTMLElement>('[data-compare]').forEach((wrap) => {
const box = wrap.querySelector<HTMLInputElement>('[data-diff-toggle]');
if (!box || box.dataset.bound === '1') return;
box.dataset.bound = '1';
box.addEventListener('change', () => {
if (box.checked) wrap.setAttribute('data-diff-only', '');
else wrap.removeAttribute('data-diff-only');
});
});
}
document.addEventListener('astro:page-load', initCompare);
</script>
3 changes: 3 additions & 0 deletions site/src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const BUILD_COMMIT_URL = `https://github.com/PCBJam/pcbjam/commit/${BUILD_SHA}`;
const links = [
{ href: '/blog', label: 'blog' },
{ href: '/pricing', label: 'pricing' },
{ href: '/compare/', label: 'compare' },
{ href: '/viewer/', label: 'viewer' },
{ href: '/collaboration/', label: 'collaboration' },
{ href: GITHUB_URL, label: 'github', external: true },
{ href: DISCORD_URL, label: 'discord', external: true },
{ href: 'mailto:hello@pcbjam.com', label: 'hello@pcbjam.com' },
Expand Down
Loading