diff --git a/docs/assets/og-charter.png b/docs/assets/og-charter.png new file mode 100644 index 0000000..51d2367 Binary files /dev/null and b/docs/assets/og-charter.png differ diff --git a/docs/assets/og-commons.png b/docs/assets/og-commons.png new file mode 100644 index 0000000..8a6e9f6 Binary files /dev/null and b/docs/assets/og-commons.png differ diff --git a/docs/assets/og-instruments.png b/docs/assets/og-instruments.png new file mode 100644 index 0000000..0bc9f35 Binary files /dev/null and b/docs/assets/og-instruments.png differ diff --git a/docs/assets/og-ledger.png b/docs/assets/og-ledger.png new file mode 100644 index 0000000..8dc1106 Binary files /dev/null and b/docs/assets/og-ledger.png differ diff --git a/docs/assets/og-research.png b/docs/assets/og-research.png new file mode 100644 index 0000000..5f4fda8 Binary files /dev/null and b/docs/assets/og-research.png differ diff --git a/docs/assets/og-writing.png b/docs/assets/og-writing.png new file mode 100644 index 0000000..bed949b Binary files /dev/null and b/docs/assets/og-writing.png differ diff --git a/docs/charter/index.html b/docs/charter/index.html index 6594590..74b0f9e 100644 --- a/docs/charter/index.html +++ b/docs/charter/index.html @@ -5,15 +5,6 @@ Charter · Foundation0 - - - - - - - - - @@ -28,6 +19,20 @@ + + + + + + + + + + + + + + Commons: free tools for the public good · Foundation0 - - - - - - - - - @@ -28,6 +19,20 @@ + + + + + + + + + + + + + + Instruments: a lab for one person · Foundation0 - - - - - - - - - @@ -29,6 +20,20 @@ + + + + + + + + + + + + + + The Ledger · Foundation0 - - - - - - - - - @@ -28,6 +19,20 @@ + + + + + + + + + + + + + + Research · Foundation0 - - - - - - - - - @@ -28,6 +19,20 @@ + + + + + + + + + + + + + + Writing · Foundation0 - - - - - - - - - @@ -28,6 +19,20 @@ + + + + + + + + + + + + + + + +Commons share card + + + + + + + +
+ + + + +
+
Foundation Zero Commons
+

Commons: free tools for the public good · Foundation0

+

Free public-benefit apps from Aska Digital. Nutrak is an offline nutrition tracker that shows its work, explained with working illustrations and labelled honestly as not clinically validated.

+
CommonsPublic good
+
fdn0.org/commons
+
+
+

Public benefit // Offline app

+
+ +
+

Daily intake

+
+ + +
+
+
+

Shows its work · Not clinically validated

+
+
+ + + diff --git a/scripts/og/cards/instruments.html b/scripts/og/cards/instruments.html new file mode 100644 index 0000000..29f6640 --- /dev/null +++ b/scripts/og/cards/instruments.html @@ -0,0 +1,64 @@ + + + + +Instruments share card + + + + + + + +
+ + + + +
+
Foundation Zero Instruments
+

Instruments: a lab for one person · Foundation0

+

The tools that let one person work with AI at the level of a lab: Aska Digital's Exo and Exo OS (private preview), Rohan Arun's Auto-research-agent and Ryan Shea's autofoundry.

+
InstrumentsLab harness
+
fdn0.org/instruments
+
+
+

Instrument // One person and agents

+
+ +
+

From first principles · Private preview

+
+
+ + + diff --git a/scripts/og/page-card.css b/scripts/og/page-card.css new file mode 100644 index 0000000..e9339c6 --- /dev/null +++ b/scripts/og/page-card.css @@ -0,0 +1,188 @@ +/* 1200x630 section share cards. Rendered by scripts/og/shoot-page.mjs with assets/f0.css (dark). */ +html, body { + margin: 0; + width: 1200px; + height: 630px; + overflow: hidden; + background: var(--bg); + color: var(--ink); +} +.pc { + position: relative; + width: 1200px; + height: 630px; + display: grid; + grid-template-columns: 580px 620px; + background: + linear-gradient(var(--grid) 1px, transparent 1px), + linear-gradient(90deg, var(--grid) 1px, transparent 1px); + background-size: 32px 32px; + background-color: var(--bg); +} +.pc::before { + content: ""; + position: absolute; + inset: 0; + background: radial-gradient(50% 80% at 78% 48%, rgba(92, 207, 236, .08), transparent 65%); + pointer-events: none; +} +.pc-bracket { + position: absolute; + width: 22px; + height: 22px; + border-color: var(--dig); + border-style: solid; + opacity: .85; +} +.pc-bracket.tl { top: 14px; left: 14px; border-width: 2px 0 0 2px; } +.pc-bracket.tr { top: 14px; right: 14px; border-width: 2px 2px 0 0; } +.pc-bracket.bl { bottom: 14px; left: 14px; border-width: 0 0 2px 2px; } +.pc-bracket.br { bottom: 14px; right: 14px; border-width: 0 2px 2px 0; } +.pc-text { + position: relative; + z-index: 1; + padding: 44px 36px 40px 52px; + display: flex; + flex-direction: column; + min-height: 0; +} +.pc-brand { + display: flex; + align-items: center; + gap: 12px; + font: 500 13px/1 var(--f-mono); + letter-spacing: .22em; + text-transform: uppercase; + color: var(--ink-2); +} +.pc-brand svg { width: 28px; height: 28px; flex-shrink: 0; } +.pc-title { + margin: 36px 0 0; + font: 600 38px/1.12 var(--f-sans); + letter-spacing: -.03em; + color: var(--ink); + max-width: 11.5em; +} +.pc-desc { + margin: 18px 0 0; + font: 400 17px/1.45 var(--f-serif); + color: var(--ink-2); + max-width: 28em; +} +.pc-pills { + margin-top: 22px; + display: flex; + flex-wrap: wrap; + gap: 10px; +} +.pc-pill { + font: 500 11px/1 var(--f-mono); + letter-spacing: .14em; + text-transform: uppercase; + color: var(--dig); + border: 1px solid color-mix(in srgb, var(--dig) 55%, transparent); + padding: 7px 12px; + background: color-mix(in srgb, var(--dig) 6%, transparent); +} +.pc-pill.dim { + color: var(--ink-3); + border-color: var(--line); + background: transparent; +} +.pc-foot { + margin-top: auto; + padding-top: 28px; + display: flex; + align-items: center; + gap: 12px; +} +.pc-foot::before { + content: ""; + flex: 1; + height: 1px; + background: var(--line); + max-width: 200px; +} +.pc-url { + font: 500 12px/1 var(--f-mono); + letter-spacing: .18em; + text-transform: uppercase; + color: var(--ink-2); +} +.pc-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--dig); + box-shadow: 0 0 12px var(--dig); + flex-shrink: 0; +} +.pc-vis { + position: relative; + z-index: 1; + padding: 36px 40px 36px 8px; + display: flex; + flex-direction: column; + align-items: stretch; + min-height: 0; +} +.pc-cap { + font: 500 11px/1 var(--f-mono); + letter-spacing: .12em; + text-transform: uppercase; + color: var(--ink-3); + text-align: right; + margin: 0 8px 12px 0; +} +.pc-cap.bot { + margin: auto 8px 0 0; + text-align: right; +} +.pc-art { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 20px; + min-height: 0; +} +.pc-art svg { display: block; max-height: 340px; } +.pc-chartbox { + border: 1px solid var(--line); + background: color-mix(in srgb, var(--bg-2) 90%, transparent); + padding: 10px 12px 8px; + min-width: 120px; +} +.pc-chartbox h3 { + margin: 0 0 8px; + font: 500 10px/1 var(--f-mono); + letter-spacing: .14em; + text-transform: uppercase; + color: var(--ink-3); + text-align: center; +} +.pc-bars { + display: flex; + align-items: flex-end; + justify-content: center; + gap: 6px; + height: 100px; + padding: 0 4px; +} +.pc-bars span { + width: 14px; + border-radius: 2px 2px 0 0; + background: var(--dig); +} +.pc-bars span:nth-child(even) { background: var(--bio); } +.pc-yaxis { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100px; + margin-right: 6px; + font: 500 9px/1 var(--f-mono); + color: var(--ink-3); + text-align: right; +} +.pc-barwrap { display: flex; align-items: flex-end; } diff --git a/scripts/og/shoot-page.mjs b/scripts/og/shoot-page.mjs new file mode 100755 index 0000000..c0599ba --- /dev/null +++ b/scripts/og/shoot-page.mjs @@ -0,0 +1,116 @@ +#!/usr/bin/env node +/* + * scripts/og/shoot-page.mjs: render a 1200x630 section share card from scripts/og/cards/*.html. + * + * node scripts/og/shoot-page.mjs commons + * node scripts/og/shoot-page.mjs instruments + * node scripts/og/shoot-page.mjs commons /tmp/out.png optional output path + * + * Serves docs/ for site CSS and scripts/og/ for the card HTML/CSS, then screenshots at 1200x630 + * in headless Chromium (same Playwright setup as shoot.mjs). Pass --full to skip PNG quantisation. + */ +import fs from 'node:fs'; +import http from 'node:http'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(HERE, '..', '..'); +const DOCS = path.join(ROOT, 'docs'); +const CARDS = path.join(HERE, 'cards'); +const PAGES = { + commons: { html: 'commons.html', out: path.join(DOCS, 'assets', 'og-commons.png') }, + instruments: { html: 'instruments.html', out: path.join(DOCS, 'assets', 'og-instruments.png') }, +}; +const ARGS = process.argv.slice(2); +const FULL = ARGS.includes('--full'); +const positional = ARGS.filter((a) => !a.startsWith('--')); +const PAGE = positional[0]; +const SPEC = PAGES[PAGE]; +if (!SPEC) { + console.error('Usage: node scripts/og/shoot-page.mjs [out.png] [--full]'); + process.exit(1); +} +const OUT = path.resolve(positional[1] || SPEC.out); +const QUANTIZE = "import sys,io\nfrom PIL import Image\nim=Image.open(io.BytesIO(sys.stdin.buffer.read())).convert('RGB')\n" + + "im.quantize(colors=256,method=Image.Quantize.MEDIANCUT,dither=Image.Dither.FLOYDSTEINBERG).save(sys.argv[1],optimize=True)"; +const LOCAL_CHROME = '/opt/pw-browsers/chromium-1194/chrome-linux/chrome'; +const TYPES = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2' }; + +function serve() { + return http.createServer((req, res) => { + const u = new URL(req.url, 'http://localhost'); + if (u.pathname === '/__og/page-card.css') { + res.writeHead(200, { 'Content-Type': TYPES['.css'] }).end(fs.readFileSync(path.join(HERE, 'page-card.css'))); + return; + } + if (u.pathname === '/__card.html') { + res.writeHead(200, { 'Content-Type': TYPES['.html'] }).end(fs.readFileSync(path.join(CARDS, SPEC.html))); + return; + } + const file = path.join(DOCS, decodeURIComponent(u.pathname)); + if (!file.startsWith(DOCS + path.sep)) { res.writeHead(403).end(); return; } + fs.readFile(file, (err, body) => { + if (err) { res.writeHead(404).end('not found'); return; } + res.writeHead(200, { 'Content-Type': TYPES[path.extname(file)] || 'application/octet-stream', 'Cache-Control': 'no-store' }).end(body); + }); + }); +} +async function loadPlaywright() { + for (const t of [process.env.PLAYWRIGHT_MODULE, 'playwright', '/opt/node22/lib/node_modules/playwright/index.mjs'].filter(Boolean)) { + try { return await import(path.isAbsolute(t) ? pathToFileURL(t).href : t); } catch { /* next */ } + } + throw new Error('Playwright not found: run `npm i --no-save playwright@1.56.1` (or set PLAYWRIGHT_MODULE)'); +} +function launchOptions() { + const v = process.env.PRERENDER_CHROMIUM; + if (v === 'playwright' || v === 'default') return {}; + if (v) return { executablePath: v }; + return fs.existsSync(LOCAL_CHROME) ? { executablePath: LOCAL_CHROME } : {}; +} + +async function main() { + const server = serve(); + await new Promise((r) => server.listen(0, '127.0.0.1', r)); + const origin = 'http://127.0.0.1:' + server.address().port; + const { chromium } = await loadPlaywright(); + const browser = await chromium.launch(launchOptions()); + try { + const ctx = await browser.newContext({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 1, colorScheme: 'dark', reducedMotion: 'reduce' }); + await ctx.route('**/*', (route) => { + const u = route.request().url(); + return u.startsWith(origin + '/') || /^https:\/\/fonts\.(googleapis|gstatic)\.com\//.test(u) ? route.continue() : route.abort('blockedbyclient'); + }); + const page = await ctx.newPage(); + const errs = []; + page.on('pageerror', (e) => errs.push('uncaught ' + e.message)); + page.on('console', (m) => { if (m.type() === 'error') errs.push(m.text()); }); + await page.goto(origin + '/__card.html', { waitUntil: 'networkidle', timeout: 60000 }); + await page.waitForFunction(() => window.__ready === true || window.__error, null, { timeout: 30000 }); + const st = await page.evaluate(() => ({ + error: window.__error || null, + fonts: ['600 38px Geist', '400 17px Newsreader', '500 13px "Geist Mono"', '500 11px "Geist Mono"'].map((f) => [f, document.fonts.check(f)]), + })); + if (st.error) throw new Error(st.error); + const missing = st.fonts.filter((f) => !f[1]).map((f) => f[0]); + if (missing.length) throw new Error('fonts did not load (' + missing.join(', ') + ')'); + if (errs.length) throw new Error(errs.join('; ')); + await page.waitForTimeout(400); + const png = await page.screenshot({ type: 'png' }); + let how = 'full colour'; + const q = FULL ? null : spawnSync('python3', ['-c', QUANTIZE, OUT], { input: png }); + if (q && q.status === 0) how = '256-colour palette'; + else { + if (q) console.warn('shoot-page: Pillow not available or failed; writing full-colour PNG'); + fs.writeFileSync(OUT, png); + } + console.log(`${OUT}: 1200x630, page ${PAGE}, ${how}, ${fs.statSync(OUT).size} bytes`); + } finally { + await browser.close(); + server.close(); + } +} + +main().catch((e) => { console.error('shoot-page:', e.message); process.exit(1); });