diff --git a/site/astro.config.mjs b/site/astro.config.mjs index b2687c4e3..e8dc4c163 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -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 ?? ''; diff --git a/site/public/_headers b/site/public/_headers index a450879ca..4108a2867 100644 --- a/site/public/_headers +++ b/site/public/_headers @@ -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 diff --git a/site/src/components/CompareDemo.astro b/site/src/components/CompareDemo.astro new file mode 100644 index 000000000..0554137bd --- /dev/null +++ b/site/src/components/CompareDemo.astro @@ -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'; +--- + +
+ Do not take our word for it +

+ 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 + demo. +

+ +
diff --git a/site/src/components/CompareSources.astro b/site/src/components/CompareSources.astro new file mode 100644 index 000000000..f631534e4 --- /dev/null +++ b/site/src/components/CompareSources.astro @@ -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; +--- + +
+ Sources, checked {CHECKED_ON} +

+ Every cell above comes from one of these pages. If a vendor changes something, tell us on + Discord and we will fix the row. +

+
+ { + competitorSources.map((g) => ( +
+
{g.name}
+
    + {g.sources.map((s) => ( +
  • + {s.label} +
  • + ))} +
+
+ )) + } +
+
PCBJam
+
    + { + pcbjamSources.map((s) => ( +
  • + {s.label} +
  • + )) + } +
+
+
+
+ + diff --git a/site/src/components/CompareSwitcher.astro b/site/src/components/CompareSwitcher.astro new file mode 100644 index 000000000..4f6109038 --- /dev/null +++ b/site/src/components/CompareSwitcher.astro @@ -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' }, +]; +--- + + + + diff --git a/site/src/components/CompareTable.astro b/site/src/components/CompareTable.astro new file mode 100644 index 000000000..a77c3ae38 --- /dev/null +++ b/site/src/components/CompareTable.astro @@ -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; +--- + +
+ +
+ + + + + {columns.map((c) => )} + + + + { + shown.map((r) => ( + + + {columns.map((c) => { + const cell = r.cells[c]; + return ( + + ); + })} + + )) + } + +
Criterion{columnNames[c]}
{r.label} + + {toneWord[cell.tone]} + {cell.note || toneWord[cell.tone]} +
+
+

[x] yes · [~] partly · [ ] no

+
+ + + + diff --git a/site/src/components/Footer.astro b/site/src/components/Footer.astro index 4dd3cc96c..47b01119f 100644 --- a/site/src/components/Footer.astro +++ b/site/src/components/Footer.astro @@ -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' }, diff --git a/site/src/data/compare.ts b/site/src/data/compare.ts new file mode 100644 index 000000000..50141a9be --- /dev/null +++ b/site/src/data/compare.ts @@ -0,0 +1,271 @@ +/** + * Comparison data for /compare and /compare/. + * + * Every competitor cell was checked against the vendor's own pages on + * CHECKED_ON; the URLs are in `sources` and render on the page. Rule for + * edits: only state what an official page or one of our own devblogs says. + * If a fact cannot be sourced, leave it out rather than guess. Prices and + * Apple's Safari schedule go stale first — re-check them before a redeploy. + */ + +export const CHECKED_ON = '3 September 2026'; + +export type Tone = 'yes' | 'part' | 'no' | 'info'; +export type ColKey = 'pcbjam' | 'kicad' | 'easyeda' | 'flux'; + +export interface Cell { + tone: Tone; + note: string; +} + +export interface Row { + label: string; + cells: Record; +} + +export interface Source { + label: string; + url: string; +} + +export interface Competitor { + slug: 'kicad' | 'easyeda' | 'flux'; + name: string; + /** Short label for the switcher. */ + tab: string; + /** for the pair page (must contain "PCBJam" — see BaseLayout). */ + title: string; + description: string; + lead: string; + pickThem: string; + pickUs: string; + sources: Source[]; +} + +export const columnNames: Record<ColKey, string> = { + pcbjam: 'PCBJam', + kicad: 'KiCad', + easyeda: 'EasyEDA', + flux: 'Flux', +}; + +export const competitors: Competitor[] = [ + { + slug: 'kicad', + name: 'KiCad', + tab: 'KiCad (desktop)', + title: 'PCBJam vs KiCad: the same KiCad, in the browser', + description: + 'PCBJam is KiCad compiled to WebAssembly: the same editors and the same files, running in a browser, with several people in one board. Where the two differ, checked against the docs.', + lead: 'Same editors, same files, same shortcuts. The difference is where it runs and how many people can be in the board at once. This is not either-or: PCBJam reads and writes the files desktop KiCad does.', + pickThem: + 'You work alone, you need to work offline, or you depend on KiCad’s Python plugins. KiCad is free, open source and runs on your own machine.', + pickUs: + 'You want the KiCad you already know with your team in the same board, nothing to install, and a link instead of a zip when someone needs to look at a design.', + sources: [ + { label: 'KiCad licenses', url: 'https://www.kicad.org/about/licenses/' }, + { label: 'KiCad 10.0.0 release', url: 'https://www.kicad.org/blog/2026/03/Version-10.0.0-Released/' }, + { label: 'KiCad downloads', url: 'https://www.kicad.org/download/' }, + ], + }, + { + slug: 'easyeda', + name: 'EasyEDA', + tab: 'EasyEDA', + title: 'PCBJam vs EasyEDA', + description: + 'Both run in a browser and both are free to use. EasyEDA is built around the LCSC catalogue in its own format; PCBJam is KiCad, with files you can take anywhere. Thirteen criteria, checked against the vendors’ docs.', + lead: 'Both run in a browser and both are free to use. EasyEDA is built around the LCSC catalogue and ordering from JLCPCB, in its own format; PCBJam is KiCad, with files you can take anywhere.', + pickThem: + 'You order from JLCPCB and the LCSC catalogue is your parts bin, and EasyEDA’s own file format is fine for you.', + pickUs: + 'You want KiCad’s editors and KiCad’s file format, so you can move between desktop KiCad and the browser at any time, with your team in the board.', + sources: [ + { label: 'EasyEDA pricing', url: 'https://easyeda.com/page/pricing' }, + { label: 'EasyEDA Pro: import KiCad', url: 'https://prodocs.easyeda.com/en/import-export/import-kicad/' }, + { label: 'EasyEDA Std: import KiCad', url: 'https://docs.easyeda.com/en/Import/Import-KiCAD/' }, + { label: 'EasyEDA Pro 3.0 release notes', url: 'https://prodocs.easyeda.com/en/release-note/v3.0.x/' }, + { label: 'EasyEDA Pro: project collaboration', url: 'https://prodocs.easyeda.com/en/project/project-collaboration/' }, + { label: 'EasyEDA Pro: desktop client', url: 'https://prodocs.easyeda.com/en/faq/client/' }, + { label: 'EasyEDA Pro: extension API', url: 'https://prodocs.easyeda.com/en/api/guide/' }, + { label: 'EasyEDA Pro: simulation', url: 'https://prodocs.easyeda.com/en/simulation/introduction/' }, + { label: 'EasyEDA mobile viewer', url: 'https://easyeda.com/editor-mobile/' }, + { label: 'KiCad: importing EasyEDA projects', url: 'https://dev-docs.kicad.org/en/import-formats/easyeda/index.html' }, + ], + }, + { + slug: 'flux', + name: 'Flux', + tab: 'Flux', + title: 'PCBJam vs Flux', + description: + 'Both are browser-based and collaborative. Flux is its own editor with an AI copilot and a proprietary format on a subscription; PCBJam is the real KiCad engine with KiCad’s files and a free tier. Checked against the vendors’ docs.', + lead: 'Both are browser-based and collaborative. Flux is its own editor with an AI copilot and a proprietary format; PCBJam is the real KiCad engine with KiCad’s own files.', + pickThem: + 'AI assistance is the point for you, and a proprietary format plus a subscription from $50 a month is an acceptable trade.', + pickUs: + 'You want the KiCad you know, open files and a free tier, with real-time collaboration.', + sources: [ + { label: 'Flux pricing', url: 'https://www.flux.ai/p/pricing' }, + { label: 'Flux: importing KiCad components', url: 'https://docs.flux.ai/reference/reference-import-kicad' }, + { label: 'Flux: collaboration', url: 'https://docs.flux.ai/tutorials/tutorial-collaboration-deep-dive' }, + { label: 'Flux: how the API works', url: 'https://docs.flux.ai/reference/how-the-api-works' }, + { label: 'Flux: the simulator', url: 'https://docs.flux.ai/flux-beta/Introduction/the-simulator' }, + { label: 'Flux: not available on mobile', url: 'https://feedback.flux.ai/bugreports/p/sign-in-on-mobile-leads-to-beta-application' }, + ], + }, +]; + +export const pcbjamSources: Source[] = [ + { label: 'Devblog week 28: collaboration, presence, comments, mobile view', url: '/blog/devblog-2026-w28/' }, + { label: 'Devblog week 29: ngspice in WebAssembly', url: '/blog/devblog-2026-w29/' }, + { label: 'Devblog week 30: comment threads, reactions, mentions', url: '/blog/devblog-2026-w30/' }, + { label: 'Devblog weeks 32 to 35: the JSPI switch and Safari', url: '/blog/devblog-2026-w32-35/' }, + { label: 'PCBJam pricing', url: '/pricing/' }, + { label: 'PCBJam on GitHub (GPL-3.0)', url: 'https://github.com/emergence-engineering/pcbjam' }, +]; + +const SAFARI = + 'Chromium browsers and Firefox. Safari, iPhone and iPad wait on a WebAssembly feature (JSPI) that Apple has scheduled for its September 2026 Safari release.'; + +export const rows: Row[] = [ + { + label: 'Runs in the browser, nothing to install', + cells: { + pcbjam: { tone: 'part', note: SAFARI }, + kicad: { tone: 'no', note: 'an install on Windows, macOS or Linux' }, + easyeda: { tone: 'yes', note: 'browser, or a desktop client' }, + flux: { tone: 'yes', note: 'desktop browsers' }, + }, + }, + { + label: 'It is KiCad: the same editors, shortcuts and libraries', + cells: { + pcbjam: { tone: 'yes', note: 'the KiCad engine compiled to WebAssembly' }, + kicad: { tone: 'yes', note: '' }, + easyeda: { tone: 'no', note: 'its own editor' }, + flux: { tone: 'no', note: 'its own editor' }, + }, + }, + { + label: 'An open file format you can take anywhere', + cells: { + pcbjam: { tone: 'yes', note: '.kicad_pro, .kicad_sch and .kicad_pcb; import and export at any time' }, + kicad: { tone: 'yes', note: 'the same files' }, + easyeda: { + tone: 'part', + note: 'its own format. Imports KiCad 5-era files only; no export to KiCad, though KiCad’s own importer reads EasyEDA projects', + }, + flux: { + tone: 'part', + note: 'its own format. Imports KiCad parts and libraries, not schematics or boards; no export to KiCad', + }, + }, + }, + { + label: 'Several people in one board at the same time', + cells: { + pcbjam: { tone: 'yes', note: 'live cursors, selection highlights, follow a teammate' }, + kicad: { tone: 'no', note: 'no built-in collaboration' }, + easyeda: { + tone: 'yes', + note: 'since EasyEDA Pro 3.0 (June 2026): the same document at once, with history and rollback', + }, + flux: { tone: 'yes', note: 'real-time multiplayer' }, + }, + }, + { + label: 'Review by link and comments, nothing to install for the reviewer', + cells: { + pcbjam: { tone: 'yes', note: 'invite links; reader and commenter roles; comment threads with mentions' }, + kicad: { tone: 'no', note: 'send the files, or use a third-party viewer' }, + easyeda: { tone: 'part', note: 'member roles including an observer role; public projects' }, + flux: { tone: 'yes', note: 'share the URL, permissions, comments on the design' }, + }, + }, + { + label: 'Open a board on a phone or tablet', + cells: { + pcbjam: { + tone: 'part', + note: 'a touch-friendly read-only view on mobile, except iPhone and iPad until the Safari release above; mobile editing is on the roadmap', + }, + kicad: { tone: 'no', note: '' }, + easyeda: { tone: 'part', note: 'a limited mobile viewer' }, + flux: { tone: 'no', note: 'desktop browsers only' }, + }, + }, + { + label: 'Schematic, layout, DRC, 3D and Gerber export', + cells: { + pcbjam: { tone: 'yes', note: 'the full KiCad flow' }, + kicad: { tone: 'yes', note: '' }, + easyeda: { tone: 'yes', note: '' }, + flux: { tone: 'yes', note: '' }, + }, + }, + { + label: 'Circuit simulation', + cells: { + pcbjam: { tone: 'yes', note: 'ngspice runs in the browser; XSPICE and CIDER work' }, + kicad: { tone: 'yes', note: 'ngspice, built in' }, + easyeda: { tone: 'yes', note: 'NGSpice, plus SimulIDE in Pro' }, + flux: { tone: 'yes', note: 'built in' }, + }, + }, + { + label: 'Plugins and extensions', + cells: { + pcbjam: { tone: 'part', note: 'custom extensions are on the roadmap' }, + kicad: { tone: 'yes', note: 'Python API and a plugin manager' }, + easyeda: { tone: 'yes', note: 'Pro extension API and a marketplace' }, + flux: { tone: 'part', note: 'an API for integrations' }, + }, + }, + { + label: 'Component libraries', + cells: { + pcbjam: { tone: 'yes', note: 'KiCad’s libraries, plus libraries shared inside a team' }, + kicad: { tone: 'yes', note: 'the official libraries' }, + easyeda: { tone: 'yes', note: 'the LCSC catalogue' }, + flux: { tone: 'yes', note: 'its own parts library; KiCad parts can be imported' }, + }, + }, + { + label: 'Open source', + cells: { + pcbjam: { tone: 'part', note: 'the editor is GPL-3.0 on GitHub; the collaboration platform is not open source' }, + kicad: { tone: 'yes', note: 'GPLv3 or later' }, + easyeda: { tone: 'no', note: '' }, + flux: { tone: 'no', note: '' }, + }, + }, + { + label: 'Where your files live', + cells: { + pcbjam: { tone: 'yes', note: 'in your PCBJam projects, exportable at any time; self-hosting is on the roadmap' }, + kicad: { tone: 'yes', note: 'on your disk' }, + easyeda: { + tone: 'part', + note: 'in EasyEDA’s cloud by default; the Pro desktop client has offline modes that keep projects local', + }, + flux: { tone: 'no', note: 'in Flux’s cloud' }, + }, + }, + { + label: `Price, checked ${CHECKED_ON}`, + cells: { + pcbjam: { tone: 'info', note: 'free tier; Pro $10 a month' }, + kicad: { tone: 'info', note: 'free' }, + easyeda: { tone: 'info', note: 'free (Std and Pro); paid tiers from $19.90 a month add storage and support' }, + flux: { tone: 'info', note: 'no free plan; 14-day trial, then $50 to $250 a month; Teams $158 per editor' }, + }, + }, +]; + +export const toneMark: Record<Tone, string> = { yes: '[x]', part: '[~]', no: '[ ]', info: '' }; +export const toneWord: Record<Tone, string> = { yes: 'yes', part: 'partly', no: 'no', info: '' }; + +export function competitorBySlug(slug: string): Competitor | undefined { + return competitors.find((c) => c.slug === slug); +} diff --git a/site/src/middleware.ts b/site/src/middleware.ts index 3ecf7d7ff..ea549d344 100644 --- a/site/src/middleware.ts +++ b/site/src/middleware.ts @@ -16,7 +16,7 @@ import { defineMiddleware } from 'astro:middleware'; * Production is static (output: 'static'); the headers come from public/_headers, * so we no-op outside dev to keep the prerender/build output untouched. */ -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']; export const onRequest = defineMiddleware(async (context, next) => { const response = await next(); diff --git a/site/src/pages/collaboration.astro b/site/src/pages/collaboration.astro new file mode 100644 index 000000000..bcda9c00a --- /dev/null +++ b/site/src/pages/collaboration.astro @@ -0,0 +1,271 @@ +--- +import BaseLayout from '../layouts/BaseLayout.astro'; +import SectionTitle from '../components/SectionTitle.astro'; +import WaitlistForm from '../components/WaitlistForm.astro'; + +// Sourced from devblog week 28 (presence, selection highlights, soft locks, +// follow, comment threads, undo, library sync, mobile read-only view), week 30 +// (comment bubbles, seen marks, reactions, mentions), week 31 (library sync per +// team), the pricing page (roles, public projects, free seats) and weeks 32-35 +// (Safari / JSPI). The three clips are the ones published with week 28. +const DISCORD_URL = 'https://discord.gg/ybhqJxjR3E'; + +const title = 'Real-time KiCad collaboration: one board, your whole team'; +const description = + 'Several people in one KiCad board at the same time, in a browser: live cursors, selection highlights, follow mode, comment threads with mentions, invite links and roles. Nothing to install, files stay in KiCad’s format.'; + +const clips = [ + { src: '/blog/w28-assets/follow.mp4', caption: 'follow a teammate: see what they see, move with their viewport' }, + { src: '/blog/w28-assets/comments.mp4', caption: 'a comment thread on the board: reply, hide, resolve' }, +]; +--- + +<BaseLayout title={title} description={description} wide> + <section class="readme hero"> + <p class="eyebrow-line mono small muted">KiCad collaboration</p> + <h1>One board. Your whole team. At the same time.</h1> + <p class="lead"> + Real-time collaboration in KiCad. Everyone opens the same project in a browser, sees each + other’s cursors and selections, and edits together. Comments and invite links for review. + Nothing to install, and the files stay KiCad’s own. + </p> + <WaitlistForm source="collaboration" class="hero-form"> + <Fragment slot="micro"> + or <a href={DISCORD_URL} rel="noopener" target="_blank">join the Discord</a> · + <a href="/privacy">privacy</a> + </Fragment> + </WaitlistForm> + <figure class="loop"> + <div class="frame"> + <video data-loop autoplay muted loop playsinline preload="metadata" aria-label="Two people on one board: live cursors and selection highlights"> + <source src="/blog/w28-assets/presence-and-select.mp4" type="video/mp4" /> + </video> + </div> + <figcaption class="mono small muted">two people on one board: cursors and selection highlights · <a href="/blog/devblog-2026-w28/">devblog, week 28</a></figcaption> + </figure> + </section> + + <section class="readme readme-section" id="today"> + <SectionTitle>What works today</SectionTitle> + <ul class="todo mono"> + <li><span class="box" aria-hidden="true">[x]</span> live cursors and selection highlights</li> + <li><span class="box" aria-hidden="true">[x]</span> highlights cross files: select a footprint on the board, its symbol lights up in the schematic</li> + <li><span class="box" aria-hidden="true">[x]</span> selected elements are soft-locked, so nobody moves them from under you</li> + <li><span class="box" aria-hidden="true">[x]</span> follow a teammate: see what they see, move with their cursor and viewport</li> + <li><span class="box" aria-hidden="true">[x]</span> comments on the board, in threads: reply, hide, resolve</li> + <li><span class="box" aria-hidden="true">[x]</span> avatar pins on the canvas, a panel listing the threads, seen marks, reactions, @-mentions</li> + <li><span class="box" aria-hidden="true">[x]</span> invite links with an expiry and a use limit</li> + <li><span class="box" aria-hidden="true">[x]</span> readers, commenters and editors</li> + <li><span class="box" aria-hidden="true">[x]</span> component libraries synced across the team</li> + <li><span class="box" aria-hidden="true">[x]</span> public projects: free for everyone, unlimited members</li> + </ul> + <ul class="clips"> + { + clips.map((c) => ( + <li> + <div class="frame"> + <video data-loop autoplay muted loop playsinline preload="metadata" aria-label={c.caption}> + <source src={c.src} type="video/mp4" /> + </video> + </div> + <span class="mono small muted">{c.caption}</span> + </li> + )) + } + </ul> + </section> + + <section class="readme readme-section" id="how"> + <SectionTitle>How it works</SectionTitle> + <p> + Edits travel as small changes over our own sync protocol; a move is about 30 bytes on the + wire. Undo is yours alone: it takes back your own edits, not your teammate’s. Libraries sync + per team, so a part added by one person is there for the next. The details, including what + was hard, are in the <a href="/blog/devblog-2026-w28/">week-28 devblog</a>. + </p> + </section> + + <section class="readme readme-section" id="review"> + <SectionTitle>A review, start to finish</SectionTitle> + <ol class="steps"> + <li>Create an invite link. Seven days and one use, or whatever the reviewer needs.</li> + <li>They open it in a browser and sign in. Nothing to install. <a href="/viewer/">Reading only</a> works on mobile too, except iPhone and iPad until Apple’s September 2026 Safari release.</li> + <li>They leave a comment on the board, on the exact footprint, and mention you.</li> + <li>You fix it in the same project, resolve the thread, and export to desktop KiCad if you like. Same file.</li> + </ol> + </section> + + <section class="readme readme-section" id="next"> + <SectionTitle>Next for collaboration</SectionTitle> + <ul class="todo mono two"> + <li><span class="box" aria-hidden="true">[ ]</span> comment-only mode</li> + <li><span class="box" aria-hidden="true">[ ]</span> per-project invites</li> + <li><span class="box" aria-hidden="true">[ ]</span> history: who changed what</li> + <li><span class="box" aria-hidden="true">[ ]</span> git integration</li> + <li><span class="box" aria-hidden="true">[ ]</span> organization support</li> + <li><span class="box" aria-hidden="true">[ ]</span> mobile editing</li> + </ul> + </section> + + <section class="readme readme-section" id="faq"> + <SectionTitle>FAQ</SectionTitle> + <dl class="faq"> + <div class="qa"> + <dt>Does everyone need an account?</dt> + <dd>Joining a project from an invite link needs a sign-in, so comments have a name on them. The <a href="https://demo.pcbjam.com" rel="noopener" target="_blank">demo</a> needs no account.</dd> + </div> + <div class="qa"> + <dt>What if two people grab the same thing?</dt> + <dd>Whatever someone has selected is soft-locked: others cannot move it until they let go. Undo only ever takes back your own edits.</dd> + </div> + <div class="qa"> + <dt>What does it cost for a team?</dt> + <dd>Public projects are free for everyone, editing included. Private work is free for three people and seven projects in your personal team, then one seat per person. Details on the <a href="/pricing/">pricing page</a>.</dd> + </div> + </dl> + </section> + + <section class="readme readme-section contact"> + <SectionTitle>Get your team in the board</SectionTitle> + <WaitlistForm source="collaboration-final" class="contact-form"> + <Fragment slot="micro"> + <a href={DISCORD_URL} rel="noopener" target="_blank">discord.gg/ybhqJxjR3E</a> · + <a href="mailto:hello@pcbjam.com">hello@pcbjam.com</a> · + <a href="/privacy">privacy</a> + </Fragment> + </WaitlistForm> + </section> +</BaseLayout> + +<style> + .hero { + padding-block: 2.5rem 1rem; + display: flex; + flex-direction: column; + gap: 1.1rem; + } + .eyebrow-line { + margin: 0; + letter-spacing: 0.12em; + text-transform: uppercase; + } + h1 { + font-size: clamp(1.9rem, 5vw, 2.6rem); + font-weight: 600; + letter-spacing: -0.015em; + line-height: 1.12; + margin: 0; + text-wrap: balance; + } + .lead { + margin: 0; + font-size: 1.1rem; + color: var(--fg-muted); + max-width: 40rem; + text-wrap: pretty; + } + .hero-form, + .contact-form { + max-width: 32rem; + } + .loop { + margin: 0.5rem 0 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + } + video { + display: block; + width: 100%; + height: auto; + background: #000; + } + .todo { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.9rem; + color: var(--fg-2); + } + .todo.two { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.35rem 1.5rem; + } + .todo li { + display: flex; + gap: 0.5rem; + } + .box { + color: var(--blue); + flex-shrink: 0; + } + .clips { + list-style: none; + margin: 0.25rem 0 0; + padding: 0; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + } + .clips li { + display: flex; + flex-direction: column; + gap: 0.35rem; + } + .steps { + margin: 0; + padding-left: 1.4rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + color: var(--fg-2); + } + .faq { + margin: 0; + display: flex; + flex-direction: column; + gap: 1rem; + } + .qa { + display: flex; + flex-direction: column; + gap: 0.2rem; + } + dt { + font-weight: 600; + } + dd { + margin: 0; + color: var(--fg-2); + font-size: 0.95rem; + } + .contact { + border-top: 2px solid var(--red); + margin-top: 1rem; + padding-block: 2rem 3rem; + } + @media (max-width: 560px) { + .clips, + .todo.two { + grid-template-columns: 1fr; + } + } +</style> + +<script> + // The clips autoplay muted, like GIFs; with reduced motion they pause and + // show controls instead. Bound on astro:page-load for ClientRouter. + function initLoops() { + if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + document.querySelectorAll<HTMLVideoElement>('video[data-loop]').forEach((v) => { + v.removeAttribute('autoplay'); + v.pause(); + v.controls = true; + }); + } + document.addEventListener('astro:page-load', initLoops); +</script> diff --git a/site/src/pages/compare/[competitor].astro b/site/src/pages/compare/[competitor].astro new file mode 100644 index 000000000..e821e9368 --- /dev/null +++ b/site/src/pages/compare/[competitor].astro @@ -0,0 +1,114 @@ +--- +import type { GetStaticPaths } from 'astro'; +import BaseLayout from '../../layouts/BaseLayout.astro'; +import SectionTitle from '../../components/SectionTitle.astro'; +import CompareSwitcher from '../../components/CompareSwitcher.astro'; +import CompareTable from '../../components/CompareTable.astro'; +import CompareDemo from '../../components/CompareDemo.astro'; +import CompareSources from '../../components/CompareSources.astro'; +import WaitlistForm from '../../components/WaitlistForm.astro'; +import { competitors, type Competitor } from '../../data/compare'; + +// One URL per pair: search wants a page whose title, heading and body are all +// about "PCBJam vs <tool>". The index page shows all three side by side. +export const getStaticPaths = (() => + competitors.map((c) => ({ params: { competitor: c.slug }, props: { c } }))) satisfies GetStaticPaths; + +const { c } = Astro.props as { c: Competitor }; +--- + +<BaseLayout title={c.title} description={c.description} wide> + <section class="readme hero"> + <p class="eyebrow-line mono small muted">Compare</p> + <h1>PCBJam vs {c.name}</h1> + <p class="lead">{c.lead}</p> + <CompareSwitcher current={c.slug} /> + </section> + + <section class="readme readme-section"> + <CompareTable columns={['pcbjam', c.slug]} /> + </section> + + <section class="readme readme-section"> + <SectionTitle>Which one, honestly</SectionTitle> + <div class="picks"> + <div class="pick"> + <div class="mono small muted">Choose {c.name} if</div> + <p>{c.pickThem}</p> + </div> + <div class="pick"> + <div class="mono small muted">Choose PCBJam if</div> + <p>{c.pickUs}</p> + </div> + </div> + </section> + + <CompareDemo /> + + <CompareSources competitorSources={[{ name: c.name, sources: c.sources }]} /> + + <section class="readme readme-section contact"> + <SectionTitle>Bring your KiCad project, keep your KiCad project</SectionTitle> + <WaitlistForm source={`compare-${c.slug}`} class="contact-form" /> + </section> +</BaseLayout> + +<style> + .hero { + padding-block: 2.5rem 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + } + .eyebrow-line { + margin: 0; + letter-spacing: 0.12em; + text-transform: uppercase; + } + h1 { + font-size: clamp(1.8rem, 4.5vw, 2.4rem); + font-weight: 600; + letter-spacing: -0.015em; + line-height: 1.12; + margin: 0; + text-wrap: balance; + } + .lead { + margin: 0; + font-size: 1.05rem; + color: var(--fg-muted); + max-width: 40rem; + text-wrap: pretty; + } + .picks { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + } + .pick { + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.9rem 1rem; + display: flex; + flex-direction: column; + gap: 0.35rem; + } + .pick p { + margin: 0; + font-size: 0.95rem; + color: var(--fg-2); + } + .contact { + border-top: 2px solid var(--red); + margin-top: 1rem; + padding-block: 2rem 3rem; + } + .contact-form { + max-width: 32rem; + } + @media (max-width: 560px) { + .picks { + grid-template-columns: 1fr; + } + } +</style> diff --git a/site/src/pages/compare/index.astro b/site/src/pages/compare/index.astro new file mode 100644 index 000000000..60c038857 --- /dev/null +++ b/site/src/pages/compare/index.astro @@ -0,0 +1,120 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import SectionTitle from '../../components/SectionTitle.astro'; +import CompareSwitcher from '../../components/CompareSwitcher.astro'; +import CompareTable from '../../components/CompareTable.astro'; +import CompareDemo from '../../components/CompareDemo.astro'; +import CompareSources from '../../components/CompareSources.astro'; +import WaitlistForm from '../../components/WaitlistForm.astro'; +import { competitors } from '../../data/compare'; + +const title = 'PCBJam vs KiCad, EasyEDA and Flux'; +const description = + 'How PCBJam compares with desktop KiCad and with two other browser-based PCB editors, EasyEDA and Flux: file format, collaboration, simulation, plugins, price. Checked against the vendors’ own docs.'; +--- + +<BaseLayout title={title} description={description} wide> + <section class="readme hero"> + <p class="eyebrow-line mono small muted">Compare</p> + <h1>PCBJam vs KiCad, EasyEDA and Flux</h1> + <p class="lead"> + Three tools people weigh PCBJam against: the desktop KiCad it is built from, and two other + browser-based editors. Pick one for the two-column view, or read all three side by side. + </p> + <CompareSwitcher current="all" /> + </section> + + <section class="readme readme-section"> + <CompareTable columns={['pcbjam', 'kicad', 'easyeda', 'flux']} /> + </section> + + <section class="readme readme-section"> + <SectionTitle>Which one, honestly</SectionTitle> + <div class="picks"> + { + competitors.map((c) => ( + <div class="pick"> + <div class="mono small muted">Choose {c.name} if</div> + <p>{c.pickThem}</p> + <a class="mono small" href={`/compare/${c.slug}/`}>PCBJam vs {c.name}</a> + </div> + )) + } + <div class="pick"> + <div class="mono small muted">Choose PCBJam if</div> + <p> + You want real KiCad, open files and your team in the board, without installing anything. + </p> + </div> + </div> + </section> + + <CompareDemo /> + + <CompareSources competitorSources={competitors.map((c) => ({ name: c.name, sources: c.sources }))} /> + + <section class="readme readme-section contact"> + <SectionTitle>Bring your KiCad project, keep your KiCad project</SectionTitle> + <WaitlistForm source="compare" class="contact-form" /> + </section> +</BaseLayout> + +<style> + .hero { + padding-block: 2.5rem 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + } + .eyebrow-line { + margin: 0; + letter-spacing: 0.12em; + text-transform: uppercase; + } + h1 { + font-size: clamp(1.8rem, 4.5vw, 2.4rem); + font-weight: 600; + letter-spacing: -0.015em; + line-height: 1.12; + margin: 0; + text-wrap: balance; + } + .lead { + margin: 0; + font-size: 1.05rem; + color: var(--fg-muted); + max-width: 40rem; + text-wrap: pretty; + } + .picks { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; + } + .pick { + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 0.9rem 1rem; + display: flex; + flex-direction: column; + gap: 0.35rem; + } + .pick p { + margin: 0; + font-size: 0.95rem; + color: var(--fg-2); + } + .contact { + border-top: 2px solid var(--red); + margin-top: 1rem; + padding-block: 2rem 3rem; + } + .contact-form { + max-width: 32rem; + } + @media (max-width: 560px) { + .picks { + grid-template-columns: 1fr; + } + } +</style> diff --git a/site/src/pages/viewer.astro b/site/src/pages/viewer.astro new file mode 100644 index 000000000..f4080953e --- /dev/null +++ b/site/src/pages/viewer.astro @@ -0,0 +1,284 @@ +--- +import { Image } from 'astro:assets'; +import BaseLayout from '../layouts/BaseLayout.astro'; +import SectionTitle from '../components/SectionTitle.astro'; +import WaitlistForm from '../components/WaitlistForm.astro'; +import eeschema from '../assets/screens/eeschema.png'; +import pcbnew from '../assets/screens/pcbnew.png'; +import gerbview from '../assets/screens/gerbview.png'; + +// Everything on this page is sourced: the demo's own description ("nothing +// uploaded", "open your own KiCad project"), the pricing page (roles, public +// projects), devblog week 28 (mobile read-only view, comments) and weeks 32-35 +// (Safari / JSPI). Do not add claims that are not in one of those. +const DEMO_URL = 'https://demo.pcbjam.com'; +const DISCORD_URL = 'https://discord.gg/ybhqJxjR3E'; + +const title = 'KiCad viewer online: open and share KiCad files in a browser'; +const description = + 'View a KiCad schematic or board in a browser without installing KiCad: layers, 3D, Gerbers, rendered by KiCad itself. Share a project with an invite link; the other person needs a browser and nothing else.'; + +const shots = [ + { img: eeschema, label: 'schematic', alt: 'KiCad schematic open in the PCBJam viewer in a browser tab' }, + { img: pcbnew, label: 'board', alt: 'KiCad board open in the PCBJam viewer in a browser tab' }, + { img: gerbview, label: 'gerbers', alt: 'Gerber files open in the PCBJam viewer in a browser tab' }, +]; +--- + +<BaseLayout title={title} description={description} wide> + <section class="readme hero"> + <p class="eyebrow-line mono small muted">KiCad viewer online</p> + <h1>View any KiCad project in a browser.</h1> + <p class="lead"> + Open a schematic, a board or a whole KiCad project without installing KiCad. It is KiCad’s own + renderer, running in your browser, so what you see is what the designer saw. Share it with a + link; the other person needs a browser and nothing else. + </p> + <div class="cta-row"> + <a class="btn btn-primary" href={DEMO_URL} rel="noopener" target="_blank">Open the viewer</a> + <span class="mono small muted">no account · nothing is uploaded, your file is read in your browser</span> + </div> + <p class="mono small muted">Open the example boards, or your own KiCad project.</p> + </section> + + <section class="readme readme-section"> + <SectionTitle>What the viewer shows</SectionTitle> + <p>Not an approximation of the file. The file, drawn by KiCad.</p> + <ul class="todo mono"> + <li><span class="box" aria-hidden="true">[x]</span> schematics and boards, as KiCad draws them</li> + <li><span class="box" aria-hidden="true">[x]</span> layers on and off</li> + <li><span class="box" aria-hidden="true">[x]</span> the 3D view</li> + <li><span class="box" aria-hidden="true">[x]</span> Gerber and drill files, for the fab check</li> + <li><span class="box" aria-hidden="true">[~]</span> a touch-friendly read-only view on mobile, except iPhone and iPad until Apple’s September 2026 Safari release</li> + </ul> + <ul class="shots"> + { + shots.map((s) => ( + <li> + <a class="frame" href={DEMO_URL} rel="noopener" target="_blank"> + <Image src={s.img} alt={s.alt} widths={[360, 720]} sizes="(max-width: 560px) 100vw, 230px" loading="lazy" /> + </a> + <span class="mono small muted">{s.label}</span> + </li> + )) + } + </ul> + </section> + + <section class="readme readme-section" id="share"> + <SectionTitle>Share a KiCad project with someone who does not have KiCad</SectionTitle> + <p> + Send an invite link with an expiry and a use limit; anyone with an active link can join after + signing in. Members are readers, commenters or editors. Public projects are free for everyone, + with unlimited members. Comments live on the board itself, in threads, with mentions. + <a href="/collaboration/">More on collaboration.</a> + </p> + <figure class="invite frame"> + <div class="invite-head"> + <strong>Invite links</strong> + <span class="muted">Anyone with an active link can join after signing in.</span> + </div> + <div class="invite-row"> + <span class="field"><span class="muted small">Expires (days)</span><span class="input">7</span></span> + <span class="field"><span class="muted small">Max uses</span><span class="input">1</span></span> + <span class="create">Create invite link</span> + </div> + <figcaption class="sr-only">The invite links panel of a PCBJam project: expiry in days and maximum uses.</figcaption> + </figure> + </section> + + <section class="readme readme-section"> + <SectionTitle>When looking is not enough</SectionTitle> + <p> + The viewer is the same KiCad as the editor. Open the project in the editor and route, run DRC + and export Gerbers, in the same tab. The file stays in KiCad’s own format the whole way. + <a href="/">More about the editor.</a> + </p> + </section> + + <section class="readme readme-section" id="faq"> + <SectionTitle>FAQ</SectionTitle> + <dl class="faq"> + <div class="qa"> + <dt>Does my file get uploaded?</dt> + <dd>Not in the demo viewer: the file is read in your browser. Sharing by link needs the project in PCBJam, where your team can open it.</dd> + </div> + <div class="qa"> + <dt>Which KiCad is this?</dt> + <dd>PCBJam is built on KiCad 10.</dd> + </div> + <div class="qa"> + <dt>How is this different from KiCanvas?</dt> + <dd>KiCanvas is a browser viewer that re-implements KiCad’s rendering in TypeScript. PCBJam runs KiCad itself, so everything renders as KiCad renders it, and the same project can be edited.</dd> + </div> + </dl> + </section> + + <section class="readme readme-section contact"> + <SectionTitle>Open a board now, or get the full editor</SectionTitle> + <WaitlistForm source="viewer" class="contact-form"> + <Fragment slot="micro"> + <a href={DEMO_URL} rel="noopener" target="_blank">open the viewer</a> (no account) · + <a href={DISCORD_URL} rel="noopener" target="_blank">discord</a> · + <a href="/privacy">privacy</a> + </Fragment> + </WaitlistForm> + </section> +</BaseLayout> + +<style> + .hero { + padding-block: 2.5rem 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + } + .eyebrow-line { + margin: 0; + letter-spacing: 0.12em; + text-transform: uppercase; + } + h1 { + font-size: clamp(1.9rem, 5vw, 2.6rem); + font-weight: 600; + letter-spacing: -0.015em; + line-height: 1.12; + margin: 0; + text-wrap: balance; + } + .lead { + margin: 0; + font-size: 1.1rem; + color: var(--fg-muted); + max-width: 40rem; + text-wrap: pretty; + } + .cta-row { + display: flex; + align-items: center; + gap: 0.9rem; + flex-wrap: wrap; + } + .hero > p.mono { + margin: 0; + } + .todo { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.35rem; + font-size: 0.9rem; + color: var(--fg-2); + } + .todo li { + display: flex; + gap: 0.5rem; + } + .box { + color: var(--blue); + flex-shrink: 0; + } + .shots { + list-style: none; + margin: 0.25rem 0 0; + padding: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.75rem; + } + .shots li { + display: flex; + flex-direction: column; + gap: 0.35rem; + } + .shots a { + display: block; + } + .shots img { + display: block; + width: 100%; + height: auto; + aspect-ratio: 16 / 10; + object-fit: cover; + object-position: left top; + } + .invite { + margin: 0; + padding: 1rem 1.1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + font-size: 0.9rem; + } + .invite-head { + display: flex; + flex-direction: column; + gap: 0.1rem; + } + .invite-row { + display: flex; + align-items: flex-end; + gap: 0.6rem; + flex-wrap: wrap; + } + .field { + display: flex; + flex-direction: column; + gap: 0.25rem; + width: 7.5rem; + } + .input { + height: 2.35rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + padding: 0 0.6rem; + background: var(--bg); + } + .create { + height: 2.35rem; + padding: 0 0.85rem; + border-radius: var(--radius-sm); + background: var(--fg); + color: var(--bg); + display: inline-flex; + align-items: center; + font-weight: 600; + white-space: nowrap; + } + .faq { + margin: 0; + display: flex; + flex-direction: column; + gap: 1rem; + } + .qa { + display: flex; + flex-direction: column; + gap: 0.2rem; + } + dt { + font-weight: 600; + } + dd { + margin: 0; + color: var(--fg-2); + font-size: 0.95rem; + } + .contact { + border-top: 2px solid var(--red); + margin-top: 1rem; + padding-block: 2rem 3rem; + } + .contact-form { + max-width: 32rem; + } + @media (max-width: 560px) { + .shots { + grid-template-columns: 1fr; + } + } +</style> diff --git a/site/src/sections/FullKicad.astro b/site/src/sections/FullKicad.astro index 6fb212336..988575fed 100644 --- a/site/src/sections/FullKicad.astro +++ b/site/src/sections/FullKicad.astro @@ -20,7 +20,8 @@ const shots = [ The same KiCad you already use, online. Schematic capture, PCB layout, DRC, 3D view, Gerber and drill export. Nothing stripped down, no rewrite. New to KiCad? It is the <a href="https://www.kicad.org" rel="noopener" target="_blank">open-source suite</a> for - designing circuit boards. + designing circuit boards. <a href="/compare/">How PCBJam compares</a> with desktop KiCad, + EasyEDA and Flux. </p> <ul class="shots"> { diff --git a/site/src/sections/SharePublish.astro b/site/src/sections/SharePublish.astro index 98e70fad4..06703a709 100644 --- a/site/src/sections/SharePublish.astro +++ b/site/src/sections/SharePublish.astro @@ -6,7 +6,7 @@ import SectionTitle from '../components/SectionTitle.astro'; <SectionTitle>Share and publish components and designs</SectionTitle> <p> Component libraries are shared inside your team. Projects can be published to the public, so - a review is one link away. + a review is one link away. <a href="/collaboration/">How collaboration works.</a> </p> {/* The project's invite-links panel, redrawn as a static figure. */} @@ -32,7 +32,7 @@ import SectionTitle from '../components/SectionTitle.astro'; <p class="mobile"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="7" y="2" width="10" height="20" rx="2"></rect><path d="M11 18h2"></path></svg> - <span>Viewer mode also works on Android phones and tablets. Open a board on your phone to check a footprint or review a change. iPhone and iPad follow with Apple’s September Safari release.</span> + <span><a href="/viewer/">Viewer mode</a> also works on Android phones and tablets. Open a board on your phone to check a footprint or review a change. iPhone and iPad follow with Apple’s September Safari release.</span> </p> </section>