chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
Videos of a pull's way from push to deploy, and of the pages in use
marketing/video/change/sample.jsonnew+310−0
| @@ -0,0 +1,310 @@ | ||
{ | ||
"host": "knotless.dev", | ||
"repo": "chadtmiller.com/knotless", | ||
"pull": { | ||
"uri": "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.pull/3mzth2v6xpbk7", | ||
"url": "https://knotless.dev/chadtmiller.com/knotless/pull/did:plc:bcgltzqazw5tb6k2g3ttenbj/3mzth2v6xpbk7", | ||
"title": "A profile's private repositories show their pushes to those who may read them", | ||
"author": "chadtmiller.com", | ||
"branch": "serve/private-activity", | ||
"base": "main", | ||
"opened": "2026-11-08T22:20:45.478Z", | ||
"state": "merged" | ||
}, | ||
"diff": { | ||
"files": [ | ||
{ | ||
"path": "crates/knotless-serve/src/lib.rs", | ||
"added": 4, | ||
"removed": 4 | ||
}, | ||
{ | ||
"path": "crates/knotless-serve/src/web/actor.rs", | ||
"added": 30, | ||
"removed": 3 | ||
}, | ||
{ | ||
"path": "crates/knotless-serve/tests/gateway.rs", | ||
"added": 16, | ||
"removed": 9 | ||
}, | ||
{ | ||
"path": "docs/design/access.md", | ||
"added": 1, | ||
"removed": 1 | ||
}, | ||
{ | ||
"path": "docs/reference/api.md", | ||
"added": 5, | ||
"removed": 1 | ||
}, | ||
{ | ||
"path": "lexicons/dev/knotless/getActorActivity.json", | ||
"added": 1, | ||
"removed": 1 | ||
} | ||
], | ||
"excerpt": { | ||
"path": "crates/knotless-serve/src/web/actor.rs", | ||
"lines": [ | ||
{ | ||
"mark": " ", | ||
"n": 373, | ||
"text": "}" | ||
}, | ||
{ | ||
"mark": " ", | ||
"n": 374, | ||
"text": "" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 375, | ||
"text": "/// The pushes to `did`'s private repositories that `viewer` may read" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 376, | ||
"text": "/// ([`private_of`]), as this server's index of their spaces counts them." | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 377, | ||
"text": "fn private_pushes(" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 378, | ||
"text": " app: &Arc<App>," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 379, | ||
"text": " did: &str," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 380, | ||
"text": " viewer: Option<String>," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 381, | ||
"text": " since: Option<&str>," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 382, | ||
"text": " tz: &str," | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 383, | ||
"text": ") -> rusqlite::Result<Vec<crate::index::RepoPushes>> {" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 384, | ||
"text": " let shown = private_of(app, did, viewer).unwrap_or_default();" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 385, | ||
"text": " let names: HashSet<String> = shown.into_iter().map(|(name, _, _)| name).collect();" | ||
}, | ||
{ | ||
"mark": "+", | ||
"n": 386, | ||
"text": " let Some(index) = app.spaces.as_ref().filter(|_| !names.is_empty()) else {" | ||
} | ||
] | ||
} | ||
}, | ||
"pushes": [ | ||
{ | ||
"commit": "65f5493b372769d9b5c4b6d07d4038af49109cda", | ||
"at": "2026-11-08T22:20:52.622Z", | ||
"checks": [ | ||
{ | ||
"workflow": "docs", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:20:52.622Z", | ||
"finished": "2026-11-08T22:21:08.682Z" | ||
}, | ||
{ | ||
"workflow": "check", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:20:52.941Z", | ||
"finished": "2026-11-08T22:21:15.894Z" | ||
}, | ||
{ | ||
"workflow": "web", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:20:52.931Z", | ||
"finished": "2026-11-08T22:21:15.955Z" | ||
}, | ||
{ | ||
"workflow": "lexicons", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:21:14.293Z", | ||
"finished": "2026-11-08T22:21:23.684Z" | ||
}, | ||
{ | ||
"workflow": "ios", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:21:13.329Z", | ||
"finished": "2026-11-08T22:22:16.371Z" | ||
}, | ||
{ | ||
"workflow": "shots", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:21:22.499Z", | ||
"finished": "2026-11-08T22:22:16.685Z" | ||
}, | ||
{ | ||
"workflow": "e2e", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:20:52.623Z", | ||
"finished": "2026-11-08T22:25:49.817Z" | ||
}, | ||
{ | ||
"workflow": "test", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:22:13.073Z", | ||
"finished": "2026-11-08T22:29:08.956Z" | ||
} | ||
] | ||
}, | ||
{ | ||
"commit": "d5a939294d78ef674a3ed1f988b34129feea48b3", | ||
"at": "2026-11-08T22:29:14.102Z", | ||
"checks": [ | ||
{ | ||
"workflow": "docs", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:14.102Z", | ||
"finished": "2026-11-08T22:29:25.385Z" | ||
}, | ||
{ | ||
"workflow": "check", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:14.417Z", | ||
"finished": "2026-11-08T22:29:33.042Z" | ||
}, | ||
{ | ||
"workflow": "lexicons", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:31.445Z", | ||
"finished": "2026-11-08T22:29:35.737Z" | ||
}, | ||
{ | ||
"workflow": "ios", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:23.795Z", | ||
"finished": "2026-11-08T22:30:05.990Z" | ||
}, | ||
{ | ||
"workflow": "shots", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:35.268Z", | ||
"finished": "2026-11-08T22:30:12.063Z" | ||
}, | ||
{ | ||
"workflow": "web", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:30:10.093Z", | ||
"finished": "2026-11-08T22:30:38.109Z" | ||
}, | ||
{ | ||
"workflow": "e2e", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:29:14.102Z", | ||
"finished": "2026-11-08T22:33:25.681Z" | ||
}, | ||
{ | ||
"workflow": "test", | ||
"status": "success", | ||
"skipped": false, | ||
"started": "2026-11-08T22:30:03.015Z", | ||
"finished": "2026-11-08T22:37:10.101Z" | ||
} | ||
] | ||
} | ||
], | ||
"reviews": [ | ||
{ | ||
"by": "review.knotless.dev", | ||
"verdict": "changes", | ||
"at": "2026-11-08T22:22:05.245Z", | ||
"commit": "65f5493b372769d9b5c4b6d07d4038af49109cda", | ||
"said": "The activity change is sound. getActorActivity now sits behind the viewer middleware and adds only the private repositories private_of allows. The new test covers the member, no one, and an outsider.", | ||
"line": { | ||
"path": ".knotless/workflows/deploy.yml", | ||
"line": 104, | ||
"said": "Dropping chadtmiller.com/group-host here depends on an invite that was never left for the existing share." | ||
} | ||
}, | ||
{ | ||
"by": "review.knotless.dev", | ||
"verdict": "approve", | ||
"at": "2026-11-08T22:29:30.940Z", | ||
"commit": "d5a939294d78ef674a3ed1f988b34129feea48b3", | ||
"said": "The unrelated commit is gone. pull_diff now shows only the activity change, with the matching docs and lexicon text, so the title and body describe the pull.", | ||
"line": null | ||
} | ||
], | ||
"queued": "2026-11-08T22:37:24.182Z", | ||
"landed": { | ||
"commit": "9382c0bbb9f6ad694a8a39deaffc2ebc706c6ac5", | ||
"at": "2026-11-08T22:37:31Z", | ||
"batch": true | ||
}, | ||
"deploy": { | ||
"workflow": "deploy", | ||
"status": "success", | ||
"started": "2026-11-08T22:38:12.898Z", | ||
"finished": "2026-11-08T22:46:23.896Z", | ||
"steps": [ | ||
{ | ||
"name": "tools", | ||
"ms": 1139, | ||
"status": "success" | ||
}, | ||
{ | ||
"name": "build", | ||
"ms": 433042, | ||
"status": "success" | ||
}, | ||
{ | ||
"name": "deploy", | ||
"ms": 40955, | ||
"status": "success" | ||
}, | ||
{ | ||
"name": "image", | ||
"ms": 4894, | ||
"status": "success" | ||
}, | ||
{ | ||
"name": "runner host", | ||
"ms": 6291, | ||
"status": "success" | ||
} | ||
] | ||
} | ||
} | ||
marketing/video/change/story.mjsnew+276−0
| @@ -0,0 +1,276 @@ | ||
// A pull's way from its first push to the deploy that shipped it, read | ||
// from the appview through `knotless-mcp --read-only`, as the Change video | ||
// tells it: each push and its checks, each review, the queue, the commit it | ||
// landed as and the deploy of that commit. Written to out/stories/<rkey>.json. | ||
// | ||
// node change/story.mjs https://knotless.dev/<handle>/<name>/pull/<did>/<rkey> | ||
// node change/story.mjs <handle>/<name> <pull at-uri> [--deploy <workflow>] | ||
// | ||
// Nothing here is made up: a stage the pull never reached (unreviewed, merged | ||
// by hand, never deployed) is left out, and the video skips it. | ||
| ||
import { spawn } from 'node:child_process'; | ||
import { mkdirSync, writeFileSync } from 'node:fs'; | ||
import { dirname, join } from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
| ||
const root = join(dirname(fileURLToPath(import.meta.url)), '..'); | ||
const host = new URL(process.env.KNOTLESS_APPVIEW || 'https://knotless.dev').host; | ||
| ||
const args = process.argv.slice(2); | ||
const flag = (name, otherwise) => { | ||
const i = args.indexOf(name); | ||
if (i < 0) return otherwise; | ||
const [, value] = args.splice(i, 2); | ||
return value; | ||
}; | ||
const deployWorkflow = flag('--deploy', 'deploy'); | ||
const [repo, uri] = (() => { | ||
if (args[0]?.startsWith('http')) { | ||
const [handle, name, kind, did, rkey] = new URL(args[0]).pathname.split('/').filter(Boolean); | ||
if (kind !== 'pull') throw new Error(`not a pull's page: ${args[0]}`); | ||
return [`${handle}/${name}`, `at://${did}/dev.knotless.pull/${rkey}`]; | ||
} | ||
if (args.length !== 2) { | ||
console.error('usage: story.mjs <pull page url> | <handle>/<name> <pull at-uri>'); | ||
process.exit(2); | ||
} | ||
return args; | ||
})(); | ||
| ||
/// The tools of a read-only knotless-mcp, called in turn over its stdio. | ||
const mcp = () => { | ||
const child = spawn('knotless-mcp', ['--read-only'], { stdio: ['pipe', 'pipe', 'inherit'] }); | ||
const waiting = new Map(); | ||
let id = 0; | ||
let buf = ''; | ||
child.stdout.on('data', (d) => { | ||
buf += d; | ||
for (let i = buf.indexOf('\n'); i >= 0; i = buf.indexOf('\n')) { | ||
const line = buf.slice(0, i); | ||
buf = buf.slice(i + 1); | ||
if (!line.trim()) continue; | ||
const m = JSON.parse(line); | ||
waiting.get(m.id)?.(m); | ||
waiting.delete(m.id); | ||
} | ||
}); | ||
const ask = (method, params) => | ||
new Promise((resolve, reject) => { | ||
const n = ++id; | ||
waiting.set(n, (m) => (m.error ? reject(new Error(m.error.message)) : resolve(m.result))); | ||
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: n, method, params })}\n`); | ||
}); | ||
const ready = ask('initialize', { | ||
protocolVersion: '2025-06-18', | ||
capabilities: {}, | ||
clientInfo: { name: 'knotless-video', version: '0' } | ||
}).then(() => | ||
child.stdin.write( | ||
`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n` | ||
) | ||
); | ||
return { | ||
/// The tool's text, as JSON when `json`. | ||
call: async (name, args, json = true) => { | ||
await ready; | ||
const r = await ask('tools/call', { name, arguments: args }); | ||
const text = r.content.find((c) => c.type === 'text')?.text ?? ''; | ||
if (r.isError) throw new Error(`${name}: ${text}`); | ||
return json ? JSON.parse(text) : text; | ||
}, | ||
close: () => child.kill() | ||
}; | ||
}; | ||
| ||
/// The diff's files, each with its lines, from pull_diff's numbered form: | ||
/// `old new | ±text` under each `diff --git` and `@@` head. | ||
const parseDiff = (text) => { | ||
const files = []; | ||
let file; | ||
let hunk; | ||
for (const line of text.split('\n')) { | ||
if (line.startsWith('diff --git ')) { | ||
file = { path: line.split(' b/').pop(), added: 0, removed: 0, hunks: [] }; | ||
files.push(file); | ||
continue; | ||
} | ||
if (!file) continue; | ||
if (line.startsWith('@@')) { | ||
hunk = { head: line, lines: [] }; | ||
file.hunks.push(hunk); | ||
continue; | ||
} | ||
const m = /^\s*(\d*)\s+(\d*) \| ([ +-])(.*)$/.exec(line); | ||
if (!m || !hunk) continue; | ||
const [, old, now, mark, body] = m; | ||
if (mark === '+') file.added++; | ||
if (mark === '-') file.removed++; | ||
hunk.lines.push({ mark, n: Number(now || old), text: body }); | ||
} | ||
return files; | ||
}; | ||
| ||
// What a reader of the video learns least from: words about the change, | ||
// rather than the change, and then its tests. | ||
const prose = /(^docs\/|\.md$|^\.changeset\/|CHANGELOG|\.lock$|package-lock\.json$|fixtures)/; | ||
const tests = /(\/tests?\/|_test\.|\.test\.|\.spec\.)/; | ||
| ||
/// The stretch of the diff that says most: of the code (not its tests, if | ||
/// it has any other), the hunk adding the most lines, cut to `most` lines | ||
/// from just before its first change. | ||
const excerpt = (files, most = 14) => { | ||
const code = files.filter((f) => !prose.test(f.path)); | ||
const source = code.filter((f) => !tests.test(f.path)); | ||
const pool = source.length ? source : code.length ? code : files; | ||
const pick = pool | ||
.flatMap((f) => f.hunks.map((h) => ({ path: f.path, h }))) | ||
.map((x) => ({ ...x, added: x.h.lines.filter((l) => l.mark === '+').length })) | ||
.sort((a, b) => b.added - a.added)[0]; | ||
if (!pick) return null; | ||
const lines = pick.h.lines; | ||
const first = lines.findIndex((l) => l.mark !== ' '); | ||
const start = Math.max(0, Math.min(first - 2, lines.length - most)); | ||
return { | ||
path: pick.path, | ||
lines: lines.slice(start, start + most).map((l) => ({ | ||
...l, | ||
text: l.text.replace(/\t/g, ' ').trimEnd() | ||
})) | ||
}; | ||
}; | ||
| ||
/// A body's opening sentences, as many whole ones as fit in `most` | ||
/// characters (the first cut short if it alone does not), as plain text. | ||
const firstSaid = (body, most = 200) => { | ||
const plain = body | ||
.replace(/```[\s\S]*?```/g, '') | ||
.replace(/\*\*|`|^#+\s*|^>\s*/gm, '') | ||
.replace(/\s+/g, ' ') | ||
.trim(); | ||
const sentences = plain.split(/(?<=[.!?])\s+/); | ||
let said = ''; | ||
for (const s of sentences) { | ||
if (`${said} ${s}`.trim().length > most) break; | ||
said = `${said} ${s}`.trim(); | ||
} | ||
if (said) return said; | ||
return `${plain.slice(0, most - 1).trimEnd()}…`; | ||
}; | ||
| ||
const at = (t) => new Date(t).getTime(); | ||
| ||
const tools = mcp(); | ||
try { | ||
const shown = await tools.call('pull_show', { repo, uri }); | ||
const pull = shown.item; | ||
const files = parseDiff(await tools.call('pull_diff', { repo, uri }, false)); | ||
const runs = await tools.call('run_list', { repo, pull: uri }).catch(() => []); | ||
| ||
// A push is the commit the pull's checks ran at, first seen when the first | ||
// of them started; its checks are the latest of each workflow there. | ||
const byCommit = new Map(); | ||
for (const r of runs) { | ||
const list = byCommit.get(r.commit) ?? []; | ||
if (!list.some((s) => s.workflow === r.workflow)) list.push(r); | ||
byCommit.set(r.commit, list); | ||
} | ||
const pushes = [...byCommit.entries()] | ||
.map(([commit, checks]) => ({ | ||
commit, | ||
at: checks.map((c) => c.startedAt).sort()[0], | ||
checks: checks | ||
.map((c) => ({ | ||
workflow: c.workflow, | ||
status: c.status, | ||
skipped: Boolean(c.skipped), | ||
started: c.startedAt, | ||
finished: c.finishedAt ?? null | ||
})) | ||
.sort((a, b) => at(a.finished ?? a.started) - at(b.finished ?? b.started)) | ||
})) | ||
.sort((a, b) => at(a.at) - at(b.at)); | ||
| ||
// Each review, with the first of the line comments it left. | ||
const reviews = shown.comments | ||
.filter((c) => c.verdict) | ||
.map((c) => { | ||
const line = shown.comments.find((l) => l.review === c.uri && l.line); | ||
return { | ||
by: c.handle, | ||
verdict: c.verdict, | ||
at: c.createdAt, | ||
commit: c.commit, | ||
said: firstSaid(c.body), | ||
line: line | ||
? { path: line.line.path, line: line.line.line, said: firstSaid(line.body, 140) } | ||
: null | ||
}; | ||
}); | ||
| ||
// Landed: the commit on the base the merge made, dated when it was made. | ||
const merged = pull.merged ?? null; | ||
const landedCommit = merged && shown.compare?.commits?.find((c) => c.sha === merged); | ||
const landed = merged | ||
? { | ||
commit: merged, | ||
at: landedCommit?.committer?.date ?? null, | ||
batch: Boolean(pull.mergedBatch) | ||
} | ||
: null; | ||
| ||
// Deployed: the deploy workflow's run at the landed commit, on the base. | ||
let deploy = null; | ||
if (merged) { | ||
const onBase = await tools | ||
.call('run_list', { repo, ref: pull.base, commit: merged, workflow: deployWorkflow }) | ||
.catch(() => []); | ||
const run = onBase[0]; | ||
if (run) | ||
deploy = { | ||
workflow: run.workflow, | ||
status: run.status, | ||
started: run.startedAt, | ||
finished: run.finishedAt ?? null, | ||
steps: run.steps | ||
.filter((s) => s.durationMs >= 1000) | ||
.map((s) => ({ name: s.name, ms: s.durationMs, status: s.status })) | ||
}; | ||
} | ||
| ||
const story = { | ||
host, | ||
repo, | ||
pull: { | ||
uri, | ||
url: `https://${host}/${repo}/pull/${uri.split('/')[2]}/${uri.split('/').pop()}`, | ||
title: pull.title, | ||
author: pull.handle, | ||
branch: pull.head.branch, | ||
base: pull.base, | ||
opened: pull.createdAt, | ||
state: pull.state | ||
}, | ||
diff: { | ||
files: files.map(({ path, added, removed }) => ({ path, added, removed })), | ||
excerpt: excerpt(files) | ||
}, | ||
pushes, | ||
reviews, | ||
queued: pull.queue?.createdAt ?? null, | ||
landed, | ||
deploy | ||
}; | ||
const out = join(root, 'out/stories'); | ||
mkdirSync(out, { recursive: true }); | ||
const file = join(out, `${uri.split('/').pop()}.json`); | ||
writeFileSync(file, `${JSON.stringify(story, null, 1)}\n`); | ||
const span = deploy?.finished ?? landed?.at ?? pushes.at(-1)?.at; | ||
const minutes = span ? Math.round((at(span) - at(pull.createdAt)) / 60000) : 0; | ||
console.log( | ||
`${file}: ${files.length} files, ${pushes.length} pushes, ${reviews.length} reviews, ` + | ||
`${landed ? 'landed' : 'not landed'}, ${deploy ? 'deployed' : 'no deploy'}; ${minutes} min` | ||
); | ||
} finally { | ||
tools.close(); | ||
} | ||
marketing/video/shoot/scenes/review.mjsnew+60−0
| @@ -0,0 +1,60 @@ | ||
// alice reviews bob's pull into tally: a comment on a line it adds, the | ||
// review approved, then the pull merged from its page, squashed into one | ||
// commit on her protected main. | ||
| ||
import { film } from '../camera.mjs'; | ||
| ||
export default { | ||
as: 'alice', | ||
start: (world) => `${world.views['pull-open']}/files`, | ||
film: async ({ page, world, size, take }) => { | ||
const pull = world.base + world.views['pull-open']; | ||
const shot = await film(take, page, size); | ||
try { | ||
await shot.open(`${pull}/files`); | ||
await shot.hold(900); | ||
shot.mark('files'); | ||
// The line the pull adds that reads `--json`, with the button that comments on it. | ||
const row = page | ||
.locator('tr', { has: page.locator('button[aria-label^="Comment on line"]') }) | ||
.filter({ hasText: '"--json"' }) | ||
.first(); | ||
await shot.point(row); | ||
await shot.hold(300); | ||
await shot.click(row.locator('button[aria-label^="Comment on line"]')); | ||
const box = page.getByRole('textbox', { name: /^Comment on line/ }); | ||
await shot.type(box, 'Could this say `--format json`, to leave room for others?', { | ||
delay: 40 | ||
}); | ||
await shot.hold(300); | ||
await shot.click(page.getByRole('button', { name: 'Start a review' })); | ||
await page.locator('.pending-line').waitFor(); | ||
shot.mark('commented'); | ||
await shot.hold(700); | ||
await shot.click(page.locator('.review-menu > summary')); | ||
shot.mark('finish'); | ||
const menu = page.locator('.review-menu .menu'); | ||
await shot.type(menu.getByPlaceholder('Leave a comment'), 'Looks good with that.', { | ||
delay: 45 | ||
}); | ||
await shot.click(menu.getByText('Approve', { exact: true })); | ||
await shot.hold(250); | ||
await shot.click(menu.getByRole('button', { name: 'Approve', exact: true })); | ||
await page.locator('.review-menu .menu').waitFor({ state: 'hidden' }); | ||
shot.mark('approved'); | ||
await shot.hold(900); | ||
await shot.open(pull); | ||
await shot.hold(700); | ||
const box2 = page.locator('.merge-box'); | ||
await box2.scrollIntoViewIfNeeded(); | ||
shot.mark('merge'); | ||
await shot.click(box2.getByRole('button', { name: 'Merge pull' })); | ||
await page.locator('.event', { hasText: 'Merged as' }).waitFor({ timeout: 30_000 }); | ||
await page.locator('.event', { hasText: 'Merged as' }).scrollIntoViewIfNeeded(); | ||
shot.mark('merged'); | ||
await shot.hold(2200); | ||
} finally { | ||
await shot.cut(); | ||
} | ||
} | ||
}; | ||
marketing/video/shoot/scenes/star.mjsnew+22−0
| @@ -0,0 +1,22 @@ | ||
// bob finds alice's notes and stars it: the count goes up as he clicks. | ||
| ||
import { film } from '../camera.mjs'; | ||
| ||
export default { | ||
as: 'bob', | ||
start: () => '/alice.test/notes', | ||
film: async ({ page, world, size, take }) => { | ||
const shot = await film(take, page, size); | ||
try { | ||
await shot.open(`${world.base}/alice.test/notes`); | ||
await shot.hold(1000); | ||
shot.mark('star'); | ||
await shot.click(page.getByRole('button', { name: 'Star', exact: true })); | ||
await page.getByRole('button', { name: 'Starred' }).waitFor(); | ||
shot.mark('starred'); | ||
await shot.hold(1800); | ||
} finally { | ||
await shot.cut(); | ||
} | ||
} | ||
}; | ||
marketing/video/shoot/camera.mjsnew+259−0
| @@ -0,0 +1,259 @@ | ||
// Someone at the pages, filmed: a browser logged in as them, Chrome's own | ||
// screencast saving each frame it paints, and a cursor and keyboard moving | ||
// at a person's pace. A take is a folder of frames under public/footage | ||
// with a take.json of when each was painted, the marks the scene set, and | ||
// where each click landed, which the video plays back (video/Footage.tsx). | ||
| ||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; | ||
import { writeFile } from 'node:fs/promises'; | ||
import { dirname, join } from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
import { chromium } from 'playwright-core'; | ||
| ||
const root = join(dirname(fileURLToPath(import.meta.url)), '..'); | ||
| ||
export const SIZES = { | ||
wide: { viewport: { width: 1280, height: 800 }, deviceScaleFactor: 1.5 }, | ||
phone: { | ||
viewport: { width: 390, height: 844 }, | ||
deviceScaleFactor: 3, | ||
isMobile: true, | ||
hasTouch: true | ||
} | ||
}; | ||
| ||
/// A browser for the world: its gateway's name resolved here, its | ||
/// certificate taken, drawn dark as the videos are. | ||
export async function launch(size) { | ||
const browser = await chromium.launch({ | ||
headless: true, | ||
// Without it a headless screencast sends frames at 1x, whatever the | ||
// context's deviceScaleFactor. | ||
args: [ | ||
`--force-device-scale-factor=${SIZES[size].deviceScaleFactor}`, | ||
'--host-resolver-rules=MAP gateway.knotless.internal 127.0.0.1' | ||
] | ||
}); | ||
const context = await browser.newContext({ | ||
...SIZES[size], | ||
ignoreHTTPSErrors: true, | ||
colorScheme: 'dark', | ||
locale: 'en-US' | ||
}); | ||
return { browser, context }; | ||
} | ||
| ||
/// Until the page's script has taken it over, as the root says once mounted. | ||
export const ready = (page) => page.locator('html[data-ready]').waitFor({ state: 'attached' }); | ||
| ||
/// Logs `who` in from the header's dialog, through their PDS's page: done | ||
/// before filming, so the take starts logged in. | ||
export async function logIn(page, world, who) { | ||
await page.getByRole('button', { name: 'Log in' }).click(); | ||
const dialog = page.getByRole('dialog', { name: 'Log in with your Atmosphere account' }); | ||
await dialog.getByLabel('Handle').fill(who.handle); | ||
const back = page.url(); | ||
await dialog.getByRole('button', { name: 'Log in' }).click(); | ||
await page.waitForURL((u) => u.origin === new URL(world.pds).origin); | ||
await page.getByRole('textbox', { name: 'Password' }).fill(who.password); | ||
await page.getByRole('button', { name: 'Sign in' }).click(); | ||
await page.getByRole('button', { name: /^(accept|authorize|allow)$/i }).click(); | ||
await page.waitForURL(back); | ||
await ready(page); | ||
await page.getByLabel('Your account').waitFor(); | ||
} | ||
| ||
/// Film `page` as `take`, at `size`: frames into public/footage/<take>. | ||
export async function film(take, page, size) { | ||
const dir = join(root, 'public/footage', take); | ||
rmSync(dir, { recursive: true, force: true }); | ||
mkdirSync(dir, { recursive: true }); | ||
await page.addInitScript(CURSOR); | ||
await page.addInitScript(`(${LIVE})()`); | ||
// A phone is touched, not pointed at: no cursor, just the taps the video draws. | ||
if (size === 'phone') await page.addInitScript(HIDE_CURSOR); | ||
| ||
const { width, height } = SIZES[size].viewport; | ||
const scale = SIZES[size].deviceScaleFactor; | ||
const cdp = await page.context().newCDPSession(page); | ||
const frames = []; | ||
const marks = {}; | ||
const clicks = []; | ||
let t0 = 0; | ||
let n = 0; | ||
// Written as they come: a sync write here held up the scene's own moves. | ||
const writing = []; | ||
cdp.on('Page.screencastFrame', (e) => { | ||
void cdp.send('Page.screencastFrameAck', { sessionId: e.sessionId }).catch(() => {}); | ||
const t = e.metadata.timestamp ?? Date.now() / 1000; | ||
if (!t0) t0 = t; | ||
const f = `${String(n++).padStart(5, '0')}.jpg`; | ||
frames.push({ t: +(t - t0).toFixed(3), f }); | ||
writing.push(writeFile(join(dir, f), Buffer.from(e.data, 'base64'))); | ||
}); | ||
await cdp.send('Page.startScreencast', { | ||
format: 'jpeg', | ||
quality: 92, | ||
maxWidth: width * scale, | ||
maxHeight: height * scale, | ||
everyNthFrame: 1 | ||
}); | ||
const now = () => (t0 ? Date.now() / 1000 - t0 : 0); | ||
const at = { x: width / 2, y: height / 2 }; | ||
const cursor = (x, y, hand) => | ||
page.evaluate(([cx, cy, h]) => window.__cursor?.(cx, cy, h), [x, y, hand]).catch(() => {}); | ||
| ||
/// To `target`, the cursor a hand as it nears it when it is to be clicked. | ||
async function moveTo(target, hand = false) { | ||
await target.scrollIntoViewIfNeeded().catch(() => {}); | ||
const box = await target.boundingBox(); | ||
if (!box) throw new Error(`nothing to point at: ${target}`); | ||
const x = box.x + Math.min(box.width / 2, 40 + Math.random() * 10); | ||
const y = box.y + box.height / 2; | ||
const steps = 24; | ||
for (let i = 1; i <= steps; i++) { | ||
const k = i / steps; | ||
const e = k < 0.5 ? 2 * k * k : 1 - (-2 * k + 2) ** 2 / 2; | ||
const px = at.x + (x - at.x) * e; | ||
const py = at.y + (y - at.y) * e; | ||
await page.mouse.move(px, py); | ||
await cursor(px, py, hand && k > 0.6); | ||
await page.waitForTimeout(16); | ||
} | ||
at.x = x; | ||
at.y = y; | ||
} | ||
const press = async () => { | ||
clicks.push({ t: +now().toFixed(3), x: Math.round(at.x), y: Math.round(at.y) }); | ||
await page.evaluate(() => window.__press?.()).catch(() => {}); | ||
}; | ||
| ||
return { | ||
page, | ||
/// Go to `url`, and wait for the page to be ready. | ||
open: async (url) => { | ||
await page.goto(url); | ||
await ready(page); | ||
await cursor(at.x, at.y, false); | ||
}, | ||
/// Note where the scene is, for the edit to cut on. | ||
mark: (name) => { | ||
marks[name] = +now().toFixed(3); | ||
}, | ||
/// Move the cursor to something, without clicking. | ||
point: (target) => moveTo(target), | ||
click: async (target, { pause = 180 } = {}) => { | ||
await moveTo(target, true); | ||
await page.waitForTimeout(pause); | ||
await press(); | ||
await target.click(); | ||
await cursor(at.x, at.y, false); | ||
await page.waitForTimeout(250); | ||
}, | ||
/// Click into a field and type, a key at a time. | ||
type: async (target, text, { delay = 55 } = {}) => { | ||
await moveTo(target, true); | ||
await press(); | ||
await target.click(); | ||
// Off what is about to be typed, as anyone moves the mouse aside. | ||
const box = await target.boundingBox(); | ||
if (box) { | ||
at.x = Math.min(box.x + box.width - 24, width - 30); | ||
at.y = box.y + box.height - 10; | ||
await cursor(at.x, at.y, false); | ||
} | ||
await page.waitForTimeout(200); | ||
await target.pressSequentially(text, { delay }); | ||
}, | ||
hold: (ms) => page.waitForTimeout(ms), | ||
/// Scroll the window, smoothly, by `dy`. | ||
scroll: async (dy) => { | ||
const steps = Math.max(10, Math.round(Math.abs(dy) / 12)); | ||
for (let i = 0; i < steps; i++) { | ||
await page.mouse.wheel(0, dy / steps); | ||
await page.waitForTimeout(16); | ||
} | ||
}, | ||
cut: async () => { | ||
await cdp.send('Page.stopScreencast').catch(() => {}); | ||
await Promise.all(writing); | ||
writeFileSync( | ||
join(dir, 'take.json'), | ||
JSON.stringify({ size, width, height, scale, frames, marks, clicks }, null, 1) | ||
); | ||
console.log( | ||
` ${take}: ${frames.length} frames, ${now().toFixed(1)} s, marks ${JSON.stringify(marks)}` | ||
); | ||
} | ||
}; | ||
} | ||
| ||
// The world's own address, as knotless.dev's would read, in the text a page | ||
// shows (never its scripts). | ||
const LIVE = `() => { | ||
if (window.__live) return; | ||
window.__live = true; | ||
const at = /gateway\\.knotless\\.internal(:\\d+)?/g; | ||
const live = (n) => { | ||
if (n.nodeType === 3) { | ||
const p = n.parentElement?.tagName; | ||
if (p !== 'SCRIPT' && p !== 'STYLE' && at.test(n.data)) n.data = n.data.replace(at, 'knotless.dev'); | ||
} else n.childNodes.forEach(live); | ||
}; | ||
const start = () => { | ||
live(document); | ||
new MutationObserver((ms) => ms.forEach((m) => (m.type === 'characterData' ? live(m.target) : m.addedNodes.forEach(live)))).observe(document, { childList: true, subtree: true, characterData: true }); | ||
}; | ||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); | ||
else start(); | ||
}`; | ||
| ||
const HIDE_CURSOR = `document.addEventListener('DOMContentLoaded', () => { | ||
const s = document.createElement('style'); | ||
s.textContent = '#film-cursor svg{display:none!important}'; | ||
document.head.append(s); | ||
});`; | ||
| ||
// A cursor drawn into the page, in its top layer over any dialog, since a | ||
// screencast does not show the real one: moved by the camera (moveTo). An | ||
// arrow, or a hand on its way to a click, which presses in as it clicks; the | ||
// video draws the click itself. A pixel blinking in a corner keeps the | ||
// screencast painting while a page is still. | ||
const CURSOR = `(() => { | ||
if (window !== window.top) return; | ||
const draw = () => { | ||
if (document.getElementById('film-cursor')) return; | ||
const c = document.createElement('div'); | ||
c.id = 'film-cursor'; | ||
c.setAttribute('popover', 'manual'); | ||
c.innerHTML = '<svg class="arrow" width="26" height="26" viewBox="0 0 24 24"><path d="M4 2.5 19 13l-6.6 1.2L16 21.3l-2.9 1.3-3.5-7.2L4 19.6z" fill="#fff" stroke="#111" stroke-width="1.6" stroke-linejoin="round"/></svg><svg class="hand" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="#111" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11a2 2 0 1 1 4 0v3a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15V4a2 2 0 0 1 4 0v5.5V9a2 2 0 0 1 4 0v1a2 2 0 0 1 3 1z" fill="#fff"/><path d="M11 9.5V4M15 10V9M18 11v-1"/></svg>'; | ||
c.style.cssText = 'position:fixed;left:0;top:0;z-index:2147483647;pointer-events:none;transform:translate(640px,400px);will-change:transform'; | ||
const s = document.createElement('style'); | ||
s.textContent = '#film-cursor{inset:auto;margin:0;border:0;padding:0;background:none;overflow:visible;width:0;height:0}#film-cursor svg{position:absolute;max-width:none;max-height:none;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5));transform-origin:30% 10%}#film-cursor .arrow{left:-5px;top:-3px}#film-cursor .hand{left:-10px;top:-3px;display:none}#film-cursor.hand .arrow{display:none}#film-cursor.hand .hand{display:block}#film-cursor.press svg{animation:film-press .3s ease-out}@keyframes film-press{40%{transform:scale(.82)}100%{transform:scale(1)}}#film-tick{position:fixed;right:0;bottom:0;width:1px;height:1px;z-index:2147483647;pointer-events:none;animation:film-tick 1s steps(2) infinite}@keyframes film-tick{0%{background:rgba(128,128,128,.01)}100%{background:rgba(128,128,128,.02)}}'; | ||
const tick = document.createElement('div'); | ||
tick.id = 'film-tick'; | ||
document.documentElement.append(s, c, tick); | ||
const last = sessionStorage.getItem('film-cursor'); | ||
if (last) c.style.transform = last; | ||
}; | ||
window.__cursor = (x, y, hand) => { | ||
draw(); | ||
const c = document.getElementById('film-cursor'); | ||
c.classList.toggle('hand', !!hand); | ||
try { | ||
if (c.matches(':popover-open')) c.hidePopover(); | ||
c.showPopover(); | ||
} catch {} | ||
c.style.transform = 'translate(' + x + 'px,' + y + 'px)'; | ||
sessionStorage.setItem('film-cursor', c.style.transform); | ||
}; | ||
window.__press = () => { | ||
const c = document.getElementById('film-cursor'); | ||
if (!c) return; | ||
c.classList.remove('press'); | ||
void c.offsetWidth; | ||
c.classList.add('press'); | ||
}; | ||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', draw); | ||
else draw(); | ||
})();`; | ||
marketing/video/shoot/shoot.mjsnew+45−0
| @@ -0,0 +1,45 @@ | ||
// `node shoot/shoot.mjs <scene>... [--phone]`: the e2e world seeded and | ||
// served as a gateway (harness/e2e/setup.mjs), then each scene filmed over | ||
// it in turn, logged in as its person first. Each scene, in ./scenes, films | ||
// its own takes into public/footage. Needs `cargo build --bins` and web/build, | ||
// as `just e2e` does. | ||
| ||
import { launch, logIn, ready } from './camera.mjs'; | ||
import review from './scenes/review.mjs'; | ||
import star from './scenes/star.mjs'; | ||
| ||
const SCENES = { review, star }; | ||
| ||
const args = process.argv.slice(2); | ||
const size = args.includes('--phone') ? 'phone' : 'wide'; | ||
const names = args.filter((a) => !a.startsWith('--')); | ||
const unknown = names.filter((n) => !SCENES[n]); | ||
if (!names.length || unknown.length) { | ||
console.error(`which scenes? ${Object.keys(SCENES).join(', ')}`); | ||
process.exit(2); | ||
} | ||
| ||
// The e2e world's own ports, apart from `shots-up`'s, as `just e2e` takes. | ||
process.env.E2E ??= '1'; | ||
const { default: setup } = await import('../../../harness/e2e/setup.mjs'); | ||
const teardown = await setup(); | ||
const world = JSON.parse(process.env.E2E_WORLD); | ||
const who = Object.fromEntries(world.accounts.map((a) => [a.handle.split('.')[0], a])); | ||
| ||
try { | ||
for (const name of names) { | ||
const scene = SCENES[name]; | ||
const { browser, context } = await launch(size); | ||
try { | ||
const page = await context.newPage(); | ||
await page.goto(world.base + scene.start(world)); | ||
await ready(page); | ||
await logIn(page, world, who[scene.as]); | ||
await scene.film({ page, world, size, take: `${name}-${size}` }); | ||
} finally { | ||
await browser.close(); | ||
} | ||
} | ||
} finally { | ||
teardown(); | ||
} | ||
marketing/video/video/change/Checks.tsxnew+153−0
| @@ -0,0 +1,153 @@ | ||
// A push's checks, run as a time-lapse: each workflow turning from running | ||
// to passed (or failed) when, in the run's own time, it did, with a clock of | ||
// the minutes gone since the push. | ||
import { interpolate, useCurrentFrame, useVideoConfig } from 'remotion'; | ||
import { look } from '../look.ts'; | ||
import { Code, Icon, risen, useRise, useStage } from '../kit.tsx'; | ||
import { ms, short, took, type Push } from '../story.ts'; | ||
| ||
const LEAD = 14; | ||
const TAIL = 34; | ||
| ||
const clock = (span: number) => { | ||
const s = Math.max(0, Math.round(span / 1000)); | ||
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; | ||
}; | ||
| ||
export function Checks({ push, again }: { push: Push; again: boolean }) { | ||
const frame = useCurrentFrame(); | ||
const { durationInFrames } = useVideoConfig(); | ||
const { inner, wide } = useStage(); | ||
const start = ms(push.at); | ||
const end = Math.max(start + 1000, ...push.checks.map((c) => ms(c.finished ?? c.started))); | ||
const run = durationInFrames - LEAD - TAIL; | ||
// The run's time at this frame, and the frame a time in the run shows at. | ||
const now = interpolate(frame, [LEAD, LEAD + run], [start, end], { | ||
extrapolateLeft: 'clamp', | ||
extrapolateRight: 'clamp' | ||
}); | ||
const head = useRise(0); | ||
const passed = push.checks.filter((c) => c.finished && ms(c.finished) <= now); | ||
const width = Math.min(inner, 1100); | ||
const row = push.checks.length > 8 ? 52 : 64; | ||
return ( | ||
<div style={{ width, display: 'flex', flexDirection: 'column', gap: 22 }}> | ||
<div | ||
style={{ | ||
display: 'flex', | ||
alignItems: 'baseline', | ||
justifyContent: 'space-between', | ||
gap: 24, | ||
...risen(head) | ||
}} | ||
> | ||
<div style={{ fontSize: wide ? 46 : 40, fontWeight: 600, letterSpacing: '-0.02em' }}> | ||
{again ? 'Pushed again, checked again' : 'Every push is checked'} | ||
</div> | ||
<div | ||
style={{ | ||
fontFamily: look.mono, | ||
fontSize: 34, | ||
color: look.yellow, | ||
fontVariantNumeric: 'tabular-nums' | ||
}} | ||
> | ||
+{clock(now - start)} | ||
</div> | ||
</div> | ||
<div | ||
style={{ | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: 14, | ||
fontSize: 22, | ||
color: look.mute, | ||
...risen(head) | ||
}} | ||
> | ||
at <Code>{short(push.commit)}</Code> | ||
<span> | ||
· {passed.length} of {push.checks.length} done · time-lapse | ||
</span> | ||
</div> | ||
<div | ||
style={{ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
border: `1px solid ${look.line}`, | ||
borderRadius: 14, | ||
background: look.panel, | ||
overflow: 'hidden' | ||
}} | ||
> | ||
{push.checks.map((c, i) => ( | ||
<CheckRow | ||
key={c.workflow} | ||
check={c} | ||
now={now} | ||
delay={6 + i * 2} | ||
height={row} | ||
last={i === push.checks.length - 1} | ||
/> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
} | ||
| ||
function CheckRow({ | ||
check, | ||
now, | ||
delay, | ||
height, | ||
last | ||
}: { | ||
check: Push['checks'][number]; | ||
now: number; | ||
delay: number; | ||
height: number; | ||
last: boolean; | ||
}) { | ||
const frame = useCurrentFrame(); | ||
const p = useRise(delay, 14); | ||
const done = check.finished !== null && ms(check.finished) <= now; | ||
const failed = done && !['success'].includes(check.status); | ||
const icon = !done | ||
? { name: 'spin' as const, color: look.yellow } | ||
: check.skipped | ||
? { name: 'skip' as const, color: look.faint } | ||
: failed | ||
? { name: 'x' as const, color: look.red } | ||
: { name: 'check' as const, color: look.green }; | ||
return ( | ||
<div | ||
style={{ | ||
height, | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: 18, | ||
padding: '0 26px', | ||
borderBottom: last ? 'none' : `1px solid ${look.line}`, | ||
fontSize: 26, | ||
...risen(p, 8) | ||
}} | ||
> | ||
<Icon name={icon.name} color={icon.color} size={30} turn={frame * 12} /> | ||
<span style={{ flex: 1, fontWeight: 600 }}>{check.workflow}</span> | ||
<span | ||
style={{ | ||
fontFamily: look.mono, | ||
fontSize: 22, | ||
color: done ? look.mute : look.yellow, | ||
fontVariantNumeric: 'tabular-nums' | ||
}} | ||
> | ||
{done | ||
? check.skipped | ||
? 'skipped' | ||
: took(ms(check.finished!) - ms(check.started)) | ||
: 'running'} | ||
</span> | ||
</div> | ||
); | ||
} | ||
marketing/video/video/change/Deploy.tsxnew+100−0
| @@ -0,0 +1,100 @@ | ||
// The deploy of the landed commit: its steps filling in turn, each as long | ||
// as it took (in a time-lapse), then the change live. | ||
import { interpolate, useCurrentFrame, useVideoConfig } from 'remotion'; | ||
import { look } from '../look.ts'; | ||
import { Icon, risen, useRise, useStage } from '../kit.tsx'; | ||
import { ms, took, type Story } from '../story.ts'; | ||
| ||
const LEAD = 16; | ||
const TAIL = 40; | ||
| ||
export function Deploy({ deploy }: { deploy: NonNullable<Story['deploy']> }) { | ||
const frame = useCurrentFrame(); | ||
const { durationInFrames } = useVideoConfig(); | ||
const { inner, wide } = useStage(); | ||
const head = useRise(0); | ||
const width = Math.min(inner, 1100); | ||
const total = deploy.steps.reduce((s, x) => s + x.ms, 0) || 1; | ||
// The share of the deploy done at this frame. | ||
const done = interpolate(frame, [LEAD, durationInFrames - TAIL], [0, total], { | ||
extrapolateLeft: 'clamp', | ||
extrapolateRight: 'clamp' | ||
}); | ||
const live = useRise(durationInFrames - TAIL + 4, 16); | ||
const passed = deploy.status === 'success'; | ||
let before = 0; | ||
return ( | ||
<div style={{ width, display: 'flex', flexDirection: 'column', gap: 26 }}> | ||
<div | ||
style={{ | ||
fontSize: wide ? 46 : 40, | ||
fontWeight: 600, | ||
letterSpacing: '-0.02em', | ||
...risen(head) | ||
}} | ||
> | ||
Then deployed | ||
</div> | ||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}> | ||
{deploy.steps.map((s, i) => { | ||
const share = Math.min(1, Math.max(0, (done - before) / s.ms)); | ||
before += s.ms; | ||
return ( | ||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 18, fontSize: 25 }}> | ||
<span style={{ width: wide ? 220 : 170, color: share > 0 ? look.fg : look.faint }}> | ||
{s.name} | ||
</span> | ||
<div | ||
style={{ | ||
flex: 1, | ||
height: 16, | ||
borderRadius: 8, | ||
background: look.soft, | ||
overflow: 'hidden' | ||
}} | ||
> | ||
<div | ||
style={{ | ||
width: `${share * 100}%`, | ||
height: '100%', | ||
background: share >= 1 ? look.green : look.yellow | ||
}} | ||
/> | ||
</div> | ||
<span | ||
style={{ | ||
width: 110, | ||
textAlign: 'right', | ||
fontFamily: look.mono, | ||
fontSize: 21, | ||
color: look.mute, | ||
opacity: share >= 1 ? 1 : 0.35 | ||
}} | ||
> | ||
{took(s.ms)} | ||
</span> | ||
</div> | ||
); | ||
})} | ||
</div> | ||
<div | ||
style={{ | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: 16, | ||
fontSize: 36, | ||
fontWeight: 600, | ||
marginTop: 14, | ||
...risen(live, 14) | ||
}} | ||
> | ||
<Icon name={passed ? 'check' : 'x'} color={passed ? look.green : look.red} size={40} /> | ||
{passed | ||
? deploy.finished | ||
? `Deployed in ${took(ms(deploy.finished) - ms(deploy.started))}` | ||
: 'Deployed' | ||
: `The deploy ended: ${deploy.status}`} | ||
</div> | ||
</div> | ||
); | ||
} | ||
marketing/video/video/change/Diff.tsxnew+158−0
| @@ -0,0 +1,158 @@ | ||
// What the pull changes: its files with the lines each adds and takes away, | ||
// then the stretch of code that says most, line by line. | ||
import { useCurrentFrame } from 'remotion'; | ||
import { look } from '../look.ts'; | ||
import { risen, useRise, useStage, Window } from '../kit.tsx'; | ||
import type { Story } from '../story.ts'; | ||
| ||
function Blocks({ added, removed }: { added: number; removed: number }) { | ||
const total = added + removed; | ||
const green = total ? Math.round((added / total) * 5) : 0; | ||
return ( | ||
<span style={{ display: 'inline-flex', gap: 3 }}> | ||
{[0, 1, 2, 3, 4].map((i) => ( | ||
<span | ||
key={i} | ||
style={{ | ||
width: 12, | ||
height: 12, | ||
borderRadius: 2, | ||
background: i < green ? look.green : total ? look.red : look.line | ||
}} | ||
/> | ||
))} | ||
</span> | ||
); | ||
} | ||
| ||
function Files({ files, most }: { files: Story['diff']['files']; most: number }) { | ||
const shown = files.slice(0, most); | ||
const rest = files.length - shown.length; | ||
const added = files.reduce((s, f) => s + f.added, 0); | ||
const removed = files.reduce((s, f) => s + f.removed, 0); | ||
const head = useRise(0); | ||
return ( | ||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, minWidth: 0 }}> | ||
<div style={{ fontSize: 30, fontWeight: 600, ...risen(head) }}> | ||
{files.length} {files.length === 1 ? 'file' : 'files'}{' '} | ||
<span style={{ color: look.green }}>+{added}</span>{' '} | ||
<span style={{ color: look.red }}>−{removed}</span> | ||
</div> | ||
{shown.map((f, i) => ( | ||
<FileRow key={f.path} file={f} delay={6 + i * 4} /> | ||
))} | ||
{rest > 0 && <div style={{ fontSize: 22, color: look.mute }}>and {rest} more</div>} | ||
</div> | ||
); | ||
} | ||
| ||
function FileRow({ file, delay }: { file: Story['diff']['files'][number]; delay: number }) { | ||
const p = useRise(delay, 16); | ||
const slash = file.path.lastIndexOf('/'); | ||
return ( | ||
<div | ||
style={{ | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: 14, | ||
fontFamily: look.mono, | ||
fontSize: 21, | ||
...risen(p, 10) | ||
}} | ||
> | ||
<Blocks added={file.added} removed={file.removed} /> | ||
<span | ||
style={{ | ||
flex: 1, | ||
minWidth: 0, | ||
overflow: 'hidden', | ||
textOverflow: 'ellipsis', | ||
whiteSpace: 'nowrap', | ||
direction: 'rtl', | ||
textAlign: 'left' | ||
}} | ||
> | ||
<bdi> | ||
<span style={{ color: look.faint }}>{file.path.slice(0, slash + 1)}</span> | ||
{file.path.slice(slash + 1)} | ||
</bdi> | ||
</span> | ||
<span style={{ color: look.green }}>+{file.added}</span> | ||
<span style={{ color: look.red }}>−{file.removed}</span> | ||
</div> | ||
); | ||
} | ||
| ||
function Excerpt({ | ||
excerpt, | ||
width, | ||
from | ||
}: { | ||
excerpt: NonNullable<Story['diff']['excerpt']>; | ||
width: number; | ||
from: number; | ||
}) { | ||
const frame = useCurrentFrame(); | ||
const size = 21; | ||
return ( | ||
<Window title={excerpt.path} width={width}> | ||
<div style={{ padding: '14px 0', fontFamily: look.mono, fontSize: size, lineHeight: 1.6 }}> | ||
{excerpt.lines.map((l, i) => { | ||
const at = from + i * 2.5; | ||
const on = Math.min(1, Math.max(0, (frame - at) / 6)); | ||
const bg = l.mark === '+' ? look.ins : l.mark === '-' ? look.del : 'transparent'; | ||
const color = l.mark === '+' ? look.green : l.mark === '-' ? look.red : look.faint; | ||
return ( | ||
<div | ||
key={i} | ||
style={{ | ||
display: 'flex', | ||
background: bg, | ||
opacity: on, | ||
transform: `translateX(${(1 - on) * -12}px)` | ||
}} | ||
> | ||
<span style={{ width: 70, textAlign: 'right', color: look.faint, flexShrink: 0 }}> | ||
{l.n} | ||
</span> | ||
<span style={{ width: 34, textAlign: 'center', color, flexShrink: 0 }}> | ||
{l.mark === ' ' ? '' : l.mark === '-' ? '−' : '+'} | ||
</span> | ||
<span | ||
style={{ | ||
whiteSpace: 'pre', | ||
overflow: 'hidden', | ||
textOverflow: 'ellipsis', | ||
paddingRight: 24, | ||
color: l.mark === ' ' ? look.mute : look.fg | ||
}} | ||
> | ||
{l.text} | ||
</span> | ||
</div> | ||
); | ||
})} | ||
</div> | ||
</Window> | ||
); | ||
} | ||
| ||
export function Diff({ story }: { story: Story }) { | ||
const { wide, inner } = useStage(); | ||
const { files, excerpt } = story.diff; | ||
if (wide) | ||
return ( | ||
<div style={{ display: 'flex', gap: 56, alignItems: 'center', width: inner }}> | ||
<div style={{ width: 560, flexShrink: 0 }}> | ||
<Files files={files} most={7} /> | ||
</div> | ||
{excerpt && <Excerpt excerpt={excerpt} width={inner - 560 - 56} from={18} />} | ||
</div> | ||
); | ||
return ( | ||
<div style={{ display: 'flex', flexDirection: 'column', gap: 44, width: inner }}> | ||
<Files files={files} most={5} /> | ||
{excerpt && <Excerpt excerpt={excerpt} width={inner} from={22} />} | ||
</div> | ||
); | ||
} | ||
marketing/video/video/change/Land.tsxnew+90−0
| @@ -0,0 +1,90 @@ | ||
// The pull landing: queued, then squashed onto its base as one commit, drawn | ||
// as its branch joining the base's line. | ||
import { interpolate, useCurrentFrame } from 'remotion'; | ||
import { look } from '../look.ts'; | ||
import { Code, risen, useRise, useStage, Words } from '../kit.tsx'; | ||
import { ms, short, took, type Story } from '../story.ts'; | ||
| ||
export function Land({ story }: { story: Story }) { | ||
const frame = useCurrentFrame(); | ||
const { inner, wide } = useStage(); | ||
const { landed, queued, pull } = story; | ||
const width = Math.min(inner, 1300); | ||
const h = 220; | ||
const join = width * 0.62; | ||
// The branch drawn in, then the commit it lands as. | ||
const draw = interpolate(frame, [18, 48], [0, 1], { | ||
extrapolateLeft: 'clamp', | ||
extrapolateRight: 'clamp' | ||
}); | ||
const dot = useRise(46, 14); | ||
const note = useRise(56, 16); | ||
const wait = queued && landed?.at ? took(ms(landed.at) - ms(queued)) : null; | ||
const branch = `M 40 ${h - 40} C ${join * 0.45} ${h - 40}, ${join * 0.7} ${h / 2}, ${join} ${h / 2}`; | ||
return ( | ||
<div style={{ width, display: 'flex', flexDirection: 'column', gap: 34, alignItems: 'center' }}> | ||
<Words | ||
text={landed ? (queued ? 'Queued, then landed' : 'Merged') : 'Queued to land'} | ||
size={wide ? 76 : 66} | ||
/> | ||
<svg width={width} height={h} style={{ overflow: 'visible' }}> | ||
<line | ||
x1={0} | ||
y1={h / 2} | ||
x2={width} | ||
y2={h / 2} | ||
stroke={look.line} | ||
strokeWidth={6} | ||
strokeLinecap="round" | ||
/> | ||
{[0.12, 0.3, 0.46].map((x) => ( | ||
<circle key={x} cx={width * x} cy={h / 2} r={11} fill={look.faint} /> | ||
))} | ||
<path | ||
d={branch} | ||
fill="none" | ||
stroke={look.purple} | ||
strokeWidth={6} | ||
strokeLinecap="round" | ||
pathLength={1} | ||
strokeDasharray={1} | ||
strokeDashoffset={1 - draw} | ||
/> | ||
<circle cx={40} cy={h - 40} r={11} fill={look.purple} /> | ||
{landed && ( | ||
<circle | ||
cx={join} | ||
cy={h / 2} | ||
r={17 * dot} | ||
fill={look.purple} | ||
stroke={look.bg} | ||
strokeWidth={5} | ||
/> | ||
)} | ||
<text x={0} y={h / 2 - 30} fill={look.mute} fontSize={24} fontFamily={look.mono}> | ||
{pull.base} | ||
</text> | ||
<text x={20} y={h + 4} fill={look.purple} fontSize={22} fontFamily={look.mono}> | ||
{pull.branch} | ||
</text> | ||
</svg> | ||
{landed && ( | ||
<div | ||
style={{ | ||
display: 'flex', | ||
alignItems: 'center', | ||
gap: 14, | ||
fontSize: 28, | ||
color: look.mute, | ||
flexWrap: 'wrap', | ||
justifyContent: 'center', | ||
...risen(note, 12) | ||
}} | ||
> | ||
on {pull.base} as <Code size={26}>{short(landed.commit)}</Code> | ||
{wait && <span>· {wait} after queueing</span>} | ||
</div> | ||
)} | ||
</div> | ||
); | ||
} | ||
marketing/video/video/change/Review.tsxnew+93−0
marketing/video/video/change/Terminal.tsxnew+72−0
marketing/video/video/assets.d.tsnew+8−0
marketing/video/video/Change.tsxnew+329−0
marketing/video/video/Feature.tsxnew+166−0
marketing/video/video/Footage.tsxnew+193−0
marketing/video/video/index.tsnew+4−0
marketing/video/video/kit.tsxnew+338−0
marketing/video/video/look.tsnew+40−0
marketing/video/video/Root.tsxnew+39−0
marketing/video/video/story.tsnew+65−0
marketing/video/package-lock.jsonnew+4175−0
A large diff, not shown.