@scribe-platform/cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ import type { ProofStagedUpload, ProofVersionDetail, ScribeClient } from './client.js';
2
+ import type { ProofFile, ProofRef } from './proof-folder.js';
3
+ export type ProofApi = Pick<ScribeClient, 'stageProofVersion' | 'finalizeProofVersion' | 'getProofVersion' | 'downloadProofFile'>;
4
+ /**
5
+ * PUTs one blob to its presigned URL. Not through `ScribeClient`: the URL is
6
+ * storage's, not Scribe's, and must not carry the API token.
7
+ *
8
+ * `content-length` is left for fetch to set from the body: it's signed into
9
+ * the URL, and the body is exactly the declared bytes, so the two agree.
10
+ */
11
+ export declare function uploadBlob(upload: ProofStagedUpload, bytes: Uint8Array, fetchImpl?: typeof fetch): Promise<void>;
12
+ export interface PushOutcome {
13
+ version: ProofVersionDetail;
14
+ previewUrl: string;
15
+ uploaded: number;
16
+ reused: number;
17
+ }
18
+ export declare function pushProof(args: {
19
+ api: ProofApi;
20
+ ref: ProofRef;
21
+ files: ProofFile[];
22
+ note?: string;
23
+ onUpload?: (path: string) => void;
24
+ upload?: (upload: ProofStagedUpload, bytes: Uint8Array) => Promise<void>;
25
+ }): Promise<PushOutcome>;
26
+ /**
27
+ * Downloads one version into `dir`, checking each file against its hash.
28
+ * Refuses a folder that already has files in it unless `force`, so a pull can
29
+ * never quietly overwrite the work in Ashley's current Proof folder.
30
+ */
31
+ export declare function pullProof(args: {
32
+ api: ProofApi;
33
+ ref: ProofRef;
34
+ version: number | 'draft';
35
+ dir: string;
36
+ force?: boolean;
37
+ }): Promise<ProofVersionDetail>;
38
+ /** A pull that was refused before anything was downloaded. */
39
+ export declare class PullRefused extends Error {
40
+ }
41
+ export declare function defaultPullDir(cwd: string, ref: ProofRef, version: number | 'draft'): string;
42
+ //# sourceMappingURL=proof-push.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof-push.d.ts","sourceRoot":"","sources":["../../src/lib/proof-push.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAe7D,MAAM,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,EAAE,mBAAmB,GAAG,sBAAsB,GAAG,iBAAiB,GAAG,mBAAmB,CAAC,CAAC;AAElI;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,UAAU,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAS7H;AAUD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,GAAG,EAAE,QAAQ,CAAC;IACd,GAAG,EAAE,QAAQ,CAAC;IACd,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E,GAAG,OAAO,CAAC,WAAW,CAAC,CAmBvB;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,GAAG,EAAE,QAAQ,CAAC;IACd,GAAG,EAAE,QAAQ,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsB9B;AAED,8DAA8D;AAC9D,qBAAa,WAAY,SAAQ,KAAK;CAAG;AAEzC,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAE5F"}
@@ -0,0 +1,91 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, resolve, sep } from 'node:path';
4
+ /**
5
+ * RFC 0029 — moving a Proof's files between Ashley's folder and Scribe.
6
+ *
7
+ * A push is three calls: declare the files (the server answers with upload
8
+ * URLs for the ones it doesn't have), upload those straight to storage, then
9
+ * finalize (the server checks each upload's size and hash and makes it the
10
+ * draft). Unchanged files are never re-sent, because the server already holds
11
+ * them under their hash.
12
+ */
13
+ /** How many uploads run at once. Enough to hide latency, few enough not to swamp a home connection. */
14
+ const UPLOAD_CONCURRENCY = 4;
15
+ /**
16
+ * PUTs one blob to its presigned URL. Not through `ScribeClient`: the URL is
17
+ * storage's, not Scribe's, and must not carry the API token.
18
+ *
19
+ * `content-length` is left for fetch to set from the body: it's signed into
20
+ * the URL, and the body is exactly the declared bytes, so the two agree.
21
+ */
22
+ export async function uploadBlob(upload, bytes, fetchImpl = fetch) {
23
+ const headers = Object.fromEntries(Object.entries(upload.headers).filter(([name]) => name.toLowerCase() !== 'content-length'));
24
+ // Copied into a fresh ArrayBuffer-backed array: fetch's body type rejects a
25
+ // Uint8Array over a possibly-shared buffer, which is what readFileSync's is typed as.
26
+ const res = await fetchImpl(upload.url, { method: 'PUT', headers, body: new Uint8Array(bytes) });
27
+ if (!res.ok) {
28
+ const detail = (await res.text().catch(() => '')).slice(0, 300);
29
+ throw new Error(`Uploading ${upload.path} failed: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`);
30
+ }
31
+ }
32
+ async function inBatches(items, size, fn) {
33
+ let next = 0;
34
+ const workers = Array.from({ length: Math.min(size, items.length) }, async () => {
35
+ while (next < items.length)
36
+ await fn(items[next++]);
37
+ });
38
+ await Promise.all(workers);
39
+ }
40
+ export async function pushProof(args) {
41
+ const { api, ref, files } = args;
42
+ const upload = args.upload ?? ((u, bytes) => uploadBlob(u, bytes));
43
+ const staged = await api.stageProofVersion(ref.client, ref.proof, {
44
+ manifest: files.map(({ path, sha256, bytes }) => ({ path, sha256, bytes })),
45
+ note: args.note ?? null,
46
+ });
47
+ const byHash = new Map(files.map((f) => [f.sha256, f]));
48
+ await inBatches(staged.uploads, UPLOAD_CONCURRENCY, async (u) => {
49
+ const file = byHash.get(u.sha256);
50
+ if (!file)
51
+ throw new Error(`The server asked for a file this folder doesn't have (${u.path})`);
52
+ await upload(u, readFileSync(file.absolute));
53
+ args.onUpload?.(u.path);
54
+ });
55
+ const finalized = await api.finalizeProofVersion(ref.client, ref.proof);
56
+ return { version: finalized.version, previewUrl: finalized.previewUrl, uploaded: staged.uploads.length, reused: staged.reused };
57
+ }
58
+ /**
59
+ * Downloads one version into `dir`, checking each file against its hash.
60
+ * Refuses a folder that already has files in it unless `force`, so a pull can
61
+ * never quietly overwrite the work in Ashley's current Proof folder.
62
+ */
63
+ export async function pullProof(args) {
64
+ const { api, ref } = args;
65
+ const dir = resolve(args.dir);
66
+ if (existsSync(dir) && readdirSync(dir).length > 0 && !args.force) {
67
+ throw new PullRefused(`${dir} isn't empty. Pull into a new folder, or pass --force to overwrite.`);
68
+ }
69
+ const version = await api.getProofVersion(ref.client, ref.proof, args.version);
70
+ for (const entry of version.manifest) {
71
+ const target = resolve(dir, ...entry.path.split('/'));
72
+ // The server validates paths at push time; this is the client's own check
73
+ // that a manifest can't write outside the folder it was asked to fill.
74
+ if (!target.startsWith(dir + sep))
75
+ throw new Error(`Refusing to write ${entry.path} outside ${dir}`);
76
+ const bytes = await api.downloadProofFile(ref.client, ref.proof, version.number, entry.path);
77
+ const sha = createHash('sha256').update(bytes).digest('hex');
78
+ if (sha !== entry.sha256)
79
+ throw new Error(`${entry.path} arrived corrupted (hash mismatch) — try the pull again`);
80
+ mkdirSync(dirname(target), { recursive: true });
81
+ writeFileSync(target, bytes);
82
+ }
83
+ return version;
84
+ }
85
+ /** A pull that was refused before anything was downloaded. */
86
+ export class PullRefused extends Error {
87
+ }
88
+ export function defaultPullDir(cwd, ref, version) {
89
+ return join(cwd, `${ref.proof}-${version === 'draft' ? 'draft' : `v${version}`}`);
90
+ }
91
+ //# sourceMappingURL=proof-push.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof-push.js","sourceRoot":"","sources":["../../src/lib/proof-push.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scribe-platform/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Command-line interface for Scribe CMS — scaffold sites, edit content, and triage form submissions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "Dave Weaver",
@@ -32,6 +32,7 @@
32
32
  "files": [
33
33
  "bin",
34
34
  "dist",
35
+ "runtime",
35
36
  "skills",
36
37
  "README.md",
37
38
  "LICENSE"
@@ -43,9 +44,9 @@
43
44
  "access": "public"
44
45
  },
45
46
  "scripts": {
46
- "build": "tsc -p tsconfig.json",
47
+ "build": "node scripts/bundle-runtime.mjs && tsc -p tsconfig.json",
47
48
  "prepack": "node scripts/bundle-skill.mjs",
48
- "test": "NODE_OPTIONS=--experimental-vm-modules jest",
49
+ "test": "node scripts/bundle-runtime.mjs && NODE_OPTIONS=--experimental-vm-modules jest",
49
50
  "lint": "tsc -p tsconfig.json --noEmit",
50
51
  "prepublishOnly": "npm run build"
51
52
  },
@@ -53,6 +54,7 @@
53
54
  "commander": "^12.1.0"
54
55
  },
55
56
  "devDependencies": {
57
+ "@scribe-cms/proof-runtime": "*",
56
58
  "@types/jest": "^29.5.13",
57
59
  "@types/node": "^22.7.5",
58
60
  "jest": "^29.7.0",
@@ -0,0 +1,139 @@
1
+ "use strict";(()=>{function Se(r){for(let i=r;i&&i!==i.ownerDocument.documentElement;i=i.parentElement)if(i.id||i.hasAttribute("data-proof-option"))return i;return r}function qe(r){let i=globalThis.CSS;return i?.escape?i.escape(r):r.replace(/([^A-Za-z0-9_-])/g,"\\$1")}function Ke(r){if(r.id)return`#${qe(r.id)}`;let i=r.getAttribute("data-proof-option");if(i)return`[data-proof-option="${i.replace(/"/g,'\\"')}"]`;let h=[],d=r;for(;d&&h.length<4;){if(d.id){h.unshift(`#${qe(d.id)}`);break}let p=d.tagName.toLowerCase();if(p==="body"||p==="html"){h.unshift(p);break}let f=d.parentElement,T=f?Array.from(f.children).filter(L=>L.tagName===d.tagName):[];h.unshift(T.length>1?`${p}:nth-of-type(${T.indexOf(d)+1})`:p),d=f}return h.join(" > ")}function me(r){return Number.isFinite(r)?Math.min(1,Math.max(0,r)):0}function Ze(r,i,h,d){let p=Se(r),f=p.getBoundingClientRect();return{selector:Ke(p),x:me(f.width>0?(i-f.left)/f.width:0),y:me(f.height>0?(h-f.top)/f.height:0),viewportWidth:Math.round(d)}}function Qe(r,i,h){let d=yt(Se(r),i),p=d.getBoundingClientRect(),f=D=>me(p.width>0?(D-p.left)/p.width:0),T=D=>me(p.height>0?(D-p.top)/p.height:0),L=f(i.x),E=T(i.y);return{selector:Ke(d),x:L,y:E,w:Math.max(f(i.x+i.width)-L,.001),h:Math.max(T(i.y+i.height)-E,.001),viewportWidth:Math.round(h)}}function yt(r,i){let h=r.ownerDocument.documentElement;for(let d=r;d&&d!==h;d=d.parentElement){let p=d.getBoundingClientRect();if(p.left<=i.x+1&&p.top<=i.y+1&&p.right+1>=i.x+i.width&&p.bottom+1>=i.y+i.height)return Se(d)}return r.ownerDocument.body??r}function Je(r,i){let h=ge(r,i);return h&&{x:h.x,y:h.y}}function ge(r,i){let h=null;try{h=r.querySelector(i.selector)}catch{return null}if(!h)return null;let d=h.getBoundingClientRect();return d.width===0&&d.height===0?null:{x:d.left+i.x*d.width,y:d.top+i.y*d.height,width:(i.w??0)*d.width,height:(i.h??0)*d.height}}var Te="proof";function M(){return{scribe:Te,protocolVersion:8}}var q=/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/,oe=5e3;function be(r){return typeof r=="object"&&r!==null}function et(r){if(!be(r)||r.scribe!==Te)return null;let i=r.protocolVersion;return typeof i=="number"&&i!==8?i:null}function wt(r){return be(r)&&r.scribe===Te&&r.protocolVersion===8}function tt(r){if(!wt(r))return null;switch(r.type){case"init":return(r.mode==="review"||r.mode==="readonly")&&be(r.feedback)&&(r.canReply===void 0||typeof r.canReply=="boolean")?r:null;case"feedback":return be(r.feedback)?r:null;case"open-pin":return typeof r.pinId=="string"?r:null;case"open-option":return typeof r.optionId=="string"&&q.test(r.optionId)?r:null;case"comment-result":return typeof r.ok=="boolean"&&(r.error===null||typeof r.error=="string")?r:null;default:return null}}var kt={options:{},pins:[]},Et={up:0,down:0,mine:null,comments:0,reactions:[]},Rt="M1 21h4V9H1v12zM23 10a2 2 0 0 0-2-2h-6.31l.95-4.57.03-.32a1.5 1.5 0 0 0-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10a2 2 0 0 0 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z",nt="http://www.w3.org/2000/svg",Mt=`
2
+ :host { all: initial; }
3
+ .layer { position: fixed; inset: 0; pointer-events: none; z-index: 2147483646;
4
+ font: 500 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
5
+ --accent: #2563eb; --ink: #2c2c2c; --go: #14ae5c; --dim: rgb(255 255 255 / .55);
6
+ --up: #5bc48f; --down: #e2a05c; }
7
+ /* The badge on an Option, built like a pin: dark, so the two markers on a
8
+ page read as the same kind of thing, and so the faces on it are cut out of
9
+ the same colour. */
10
+ .toolbar { position: fixed; display: flex; gap: 2px; padding: 3px; border-radius: 999px; background: #1e1e1e; white-space: nowrap;
11
+ box-shadow: 0 2px 8px rgb(0 0 0 / .35); transform: translate(calc(-100% - 8px), 8px); pointer-events: auto; }
12
+ .toolbar button { all: unset; box-sizing: border-box; cursor: pointer; color: #fff;
13
+ display: inline-flex; align-items: center; gap: 5px; min-height: 28px; padding: 0 10px; border-radius: 999px; }
14
+ .toolbar button:hover { background: rgb(255 255 255 / .12); }
15
+ .toolbar button:focus-visible, .pin:focus-visible, .spot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
16
+ .toolbar button[aria-pressed="true"] { background: var(--accent); }
17
+ .toolbar button:disabled { cursor: default; opacity: .7; }
18
+ .toolbar.open, .toolbar:hover { box-shadow: 0 0 0 2px var(--go), 0 2px 8px rgb(0 0 0 / .35); }
19
+ .count:empty { display: none; }
20
+ .facerow { display: inline-flex; padding: 0 6px 0 2px; }
21
+ .facerow .av + .av { margin-left: -8px; box-shadow: 0 0 0 2px #1e1e1e; }
22
+ .ring { position: fixed; border: 3px solid var(--accent); border-radius: 6px; box-sizing: border-box; pointer-events: none; }
23
+ /* The marker, the way Figma draws one: a dark teardrop whose point is the spot
24
+ being talked about, holding the faces of everyone in the thread. Rounded on
25
+ three corners and square on the fourth \u2014 that corner *is* the anchor, which
26
+ is why the whole thing is translated up and right of the point. */
27
+ .pin, .spot { all: unset; box-sizing: border-box; position: fixed; z-index: 1; display: flex; padding: 3px;
28
+ border-radius: 999px 999px 999px 3px; background: #1e1e1e; cursor: pointer; pointer-events: auto;
29
+ transform: translate(0, -100%); box-shadow: 0 2px 8px rgb(0 0 0 / .35); }
30
+ .pin:hover, .pin.open { box-shadow: 0 0 0 2px var(--go), 0 2px 8px rgb(0 0 0 / .35); }
31
+ .av { flex: none; width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center;
32
+ font-size: 10px; font-weight: 700; color: #fff; }
33
+ /* Overlapped, each cut out of the one behind it. */
34
+ .pin .av + .av, .faces .av + .av { margin-left: -8px; box-shadow: 0 0 0 2px #1e1e1e; }
35
+ /* Where a comment being written will land: the same marker, empty. */
36
+ .spot { color: #fff; background: #1e1e1e; }
37
+ .spot .av { background: rgb(255 255 255 / .18); }
38
+ /* The region a comment is about: dashed, so it reads as a selection over the
39
+ work rather than a border someone designed into it. Drawn while it's being
40
+ dragged out, and again whenever its thread is open or under the pointer. */
41
+ .area { position: fixed; border: 2px dashed var(--go); border-radius: 3px; box-sizing: border-box;
42
+ pointer-events: none; }
43
+ /* The corners the drag ran between, the way a selection shows its grip. */
44
+ .grip { position: fixed; width: 14px; height: 14px; margin: -7px 0 0 -7px; border-radius: 50%;
45
+ background: var(--go); pointer-events: none; }
46
+ /* Over the whole page, all the time a reviewer can comment \u2014 so a click
47
+ anywhere is a comment and the cursor says so before they make one. No tint:
48
+ this is the resting state of every Proof, and a permanent wash over somebody's
49
+ design would be the first thing they asked us to take off. The bubble is a
50
+ data URI rather than a file because the runtime is one script with nothing
51
+ beside it, and crosshair is the fallback where SVG cursors aren't drawn.
52
+
53
+ Drawn as an outline, twice: a fat white stroke under a thin dark one, so the
54
+ bubble is legible over a black hero and over a white page without knowing
55
+ which it is over. Unfilled on purpose \u2014 a solid cursor hides the pixel being
56
+ pointed at, which on a Proof is the whole subject of the comment. The hotspot
57
+ is the tail's tip, so the pin lands where the point is. */
58
+ .catcher { position: fixed; inset: 0; pointer-events: auto;
59
+ cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26"><g fill="none" stroke-linejoin="round" stroke-linecap="round"><path d="M3 7.5A4 4 0 017 3.5h12a4 4 0 014 4v6a4 4 0 01-4 4h-6.5L8 22v-4.5H7a4 4 0 01-4-4z" stroke="%23fff" stroke-width="4"/><path d="M3 7.5A4 4 0 017 3.5h12a4 4 0 014 4v6a4 4 0 01-4 4h-6.5L8 22v-4.5H7a4 4 0 01-4-4z" stroke="%231f1f1f" stroke-width="1.75"/></g></svg>') 8 22, crosshair; }
60
+ /* The composer and the thread a pin opens are one surface, dark like the
61
+ marker they grow out of. */
62
+ .card, .bubble { position: fixed; width: 340px; max-width: calc(100vw - 24px); box-sizing: border-box;
63
+ border-radius: 12px; background: var(--ink); color: #fff; pointer-events: auto;
64
+ box-shadow: 0 10px 30px rgb(0 0 0 / .4); }
65
+ .card { z-index: 3; }
66
+ /* Tall threads scroll inside the card rather than off the top of the page. */
67
+ .bubble { z-index: 2; max-height: min(70vh, 460px); overflow-y: auto; overscroll-behavior: contain; }
68
+ /* Opening out of the marker it belongs to, rather than being there suddenly.
69
+ Hung off [hidden] alone, so no code has to know the animation exists:
70
+ allow-discrete holds display back until the fade has run, and the pointer
71
+ is let through to the work underneath the moment it starts closing. */
72
+ .bubble { transform-origin: bottom left;
73
+ transition: opacity 190ms ease, transform 190ms ease, display 190ms allow-discrete; }
74
+ .bubble[hidden] { opacity: 0; transform: translateY(5px) scale(.97); pointer-events: none; }
75
+ @starting-style { .bubble:not([hidden]) { opacity: 0; transform: translateY(5px) scale(.97); } }
76
+ @media (prefers-reduced-motion: reduce) { .bubble { transition: none; } }
77
+ .who { display: block; font-size: 13px; font-weight: 600; }
78
+ .ago { margin-left: 7px; font-weight: 400; color: var(--dim); }
79
+ .what { margin-top: 5px; font-size: 13px; font-weight: 400; line-height: 1.55; white-space: pre-wrap; overflow-wrap: break-word; }
80
+ /* The hover preview: who, what, and how many answered \u2014 no controls, because
81
+ nothing here is meant to be clicked except the pin it came from. */
82
+ .peek { padding: 12px 14px; cursor: pointer; }
83
+ .faces { display: flex; margin-bottom: 8px; }
84
+ .more { margin-top: 8px; font-size: 12px; font-weight: 400; color: var(--dim); }
85
+ /* Someone who reacted and left it at that. Dimmed rather than left blank: an
86
+ empty space under a name reads as a comment that failed to load. */
87
+ .none { margin-top: 5px; font-size: 13px; font-weight: 400; font-style: italic; color: var(--dim); }
88
+ /* The thumb: against a name in the list and the preview, and on its own in
89
+ the badge. Sized in ems so it tracks whatever it sits beside. */
90
+ .thumb { width: 1.15em; height: 1.15em; fill: currentColor; vertical-align: -.2em; }
91
+ .who .thumb { margin-right: 6px; }
92
+ /* Chosen, rather than offered: a thumb standing for a decision someone has
93
+ already made takes a colour, while the untouched pair on the badge stays
94
+ white so neither of them looks half-pressed. Muted on purpose \u2014 these sit
95
+ next to the work, and a saturated green would read as part of the design. */
96
+ .thumb[data-reaction="up"] { --react: var(--up); }
97
+ .thumb[data-reaction="down"] { --react: var(--down); }
98
+ .who .thumb, button[aria-pressed="true"] .thumb { fill: var(--react); }
99
+ /* An answer under someone's reaction, stepped in so the list still reads as
100
+ who-said-what rather than one flat run of names. */
101
+ .sub { margin-left: 22px; }
102
+ .reply { all: unset; cursor: pointer; margin-top: 6px; margin-left: 14px; font-size: 12px; color: var(--dim); }
103
+ .reply:hover { color: #fff; }
104
+ .reply:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
105
+ .head { display: flex; align-items: center; justify-content: space-between;
106
+ padding: 12px 14px; border-bottom: 1px solid rgb(255 255 255 / .09); font-size: 13px; font-weight: 600; }
107
+ .head button { all: unset; cursor: pointer; padding: 0 4px; font-size: 15px; line-height: 1; color: var(--dim); }
108
+ .head button:hover { color: #fff; }
109
+ .msgs { padding: 14px; display: flex; flex-direction: column; gap: 16px; }
110
+ .msg { display: flex; gap: 10px; }
111
+ .msg > div { min-width: 0; flex: 1; }
112
+ .msg .av { width: 26px; height: 26px; font-size: 11px; }
113
+ /* Taking a comment back: quiet until it's hovered, and louder once armed \u2014
114
+ the second click is the one that does it. */
115
+ .del { all: unset; cursor: pointer; margin-top: 6px; font-size: 12px; color: var(--dim); }
116
+ .del:hover { color: #fff; }
117
+ .del.armed { color: #fca5a5; }
118
+ .del:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
119
+ .del:disabled { cursor: default; opacity: .5; }
120
+ /* The box that takes a reply, and the one that takes a new comment: the same
121
+ row, so the second thing a reviewer types into behaves like the first. */
122
+ .replybox { display: flex; gap: 10px; align-items: flex-start; padding: 0 14px 14px; }
123
+ .card .replybox { padding: 12px 14px; }
124
+ .replybox .av { width: 26px; height: 26px; font-size: 11px; margin-top: 4px; }
125
+ .field { position: relative; flex: 1; min-width: 0; }
126
+ /* The reset above takes the caret with it, and over a page whose cursor is a
127
+ comment bubble, an arrow in a text box reads as "not here". */
128
+ textarea { all: unset; cursor: text; box-sizing: border-box; display: block; width: 100%; height: 36px; max-height: 160px; resize: none;
129
+ padding: 9px 40px 9px 14px; border-radius: 18px; background: transparent;
130
+ box-shadow: inset 0 0 0 1px rgb(255 255 255 / .22);
131
+ font-size: 13px; font-weight: 400; line-height: 1.4; color: #fff; }
132
+ textarea::placeholder { color: var(--dim); }
133
+ textarea:focus { box-shadow: inset 0 0 0 1px var(--go); }
134
+ .go { all: unset; position: absolute; right: 6px; top: 6px; width: 24px; height: 24px; border-radius: 50%;
135
+ cursor: pointer; display: grid; place-items: center; font-size: 13px; color: #fff; background: var(--go); }
136
+ .go:disabled { cursor: default; background: rgb(255 255 255 / .16); color: var(--dim); }
137
+ .oops { margin: 7px 0 0 2px; font-size: 12px; font-weight: 400; color: #fca5a5; }
138
+ [hidden] { display: none !important; }
139
+ `;function ot(r){let i=r.document,h=r.parent,d=h!==r,p=null,f=!1,T=null,L=null,E=kt,D="",R=[],V=[],xe=!1,g=null,re=!1,K=null,u=null,C=null,y=!1,Z=0,Le=null,ie={feedback:new Set,reviewer:new Set},se=i.createElement("div");se.setAttribute("data-scribe-proof-layer","");let it=se.attachShadow({mode:"open"}),Ae=i.createElement("style");Ae.textContent=Mt;let B=i.createElement("div");B.className="layer",B.hidden=!0;let _=i.createElement("div");_.className="catcher",_.hidden=!0;function l(e,t,n){let o=i.createElement(e);return t&&(o.className=t),n&&(o.textContent=n),o}function ye(e){let t=i.createElementNS(nt,"svg");t.setAttribute("class","thumb"),t.setAttribute("data-reaction",e),t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("aria-hidden","true");let n=i.createElementNS(nt,"path");return n.setAttribute("d",Rt),e==="down"&&n.setAttribute("transform","rotate(180 12 12)"),t.append(n),t}function $(e){return ae(l("span","av"),e)}function ae(e,t){return e&&(e.textContent=t?.initials??"",e.style.background=t?.color??"rgb(255 255 255 / .18)",e)}function Pe(e){let t=l("div","replybox"),n=e.face?$(null):null,o=l("div","field"),s=l("textarea");s.rows=1,s.placeholder=e.placeholder,s.setAttribute("aria-label",e.aria);let a=l("button","go","\u2191");a.type="button",a.setAttribute("aria-label","Send");let c=l("p","oops");c.hidden=!0,o.append(s,a,c),t.append(...n?[n,o]:[o]);function m(){s.style.height="auto",s.style.height=`${s.scrollHeight}px`}let w={box:t,face:n,text:s,send:a,oops:c,setBusy(v){s.readOnly=v,a.disabled=v||s.value.trim().length===0},fail(v,F){w.setBusy(!1),c.textContent=v??F,c.hidden=!1},clear(){s.value="",s.readOnly=!1,s.style.height="",c.hidden=!0,w.setBusy(!1)}};return s.addEventListener("input",()=>{m(),w.setBusy(s.readOnly)}),s.addEventListener("keydown",v=>{v.key==="Enter"&&!v.shiftKey&&(v.preventDefault(),e.submit())}),a.addEventListener("click",e.submit),w.setBusy(!1),w}let A=l("div","card");A.hidden=!0,A.setAttribute("role","dialog");let P=Pe({placeholder:"Add a comment",aria:"Your comment",face:!1,submit:()=>ct()});A.append(P.box);let z=l("div","spot");z.hidden=!0;let Oe=$(null);z.append(Oe);let W=l("div","area");W.hidden=!0;let le=l("div","grip"),de=l("div","grip");le.hidden=!0,de.hidden=!0;let Y=l("div","area");Y.hidden=!0;let x=l("div","bubble");x.hidden=!0,x.setAttribute("role","dialog"),x.setAttribute("aria-label","Comment thread");let O=l("div","peek");O.setAttribute("role","button"),O.tabIndex=0,O.addEventListener("click",()=>He()),O.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),He())});let we=l("div","faces"),Q=l("p","who"),J=l("div","what"),ee=l("p","more");O.append(we,Q,J,ee);let te=l("div");te.hidden=!0;let Ie=l("div","head"),ve=l("span","","Comment"),ce=l("button","","\u2715");ce.type="button",ce.setAttribute("aria-label","Close"),ce.addEventListener("click",()=>k({now:!0,force:!0})),Ie.append(ve,ce);let ke=l("div","msgs"),b=Pe({placeholder:"Reply",aria:"Your reply",face:!0,submit:()=>ft()});b.text.addEventListener("keydown",e=>{e.key==="Escape"&&e.stopPropagation()}),te.append(Ie,ke,b.box),x.append(O,te),B.append(_,Y,W,le,de,z,A,x),it.append(Ae,B);function S(e){d&&h.postMessage(e,"*")}function st(e){return e===se}function Ce(){for(let t of R)t.toolbar.remove(),t.ring.remove();R=[];let e=new Set;for(let t of Array.from(i.querySelectorAll("[data-proof-option]"))){let n=t.getAttribute("data-proof-option")?.trim()??"";if(!q.test(n)||e.has(n))continue;e.add(n);let o={id:n,label:t.getAttribute("data-proof-label")?.trim()||null};R.push(at(o,t))}return Me(),R.map(t=>t.info)}function at(e,t){let n=e.label??e.id,o=i.createElement("div");o.className="toolbar",o.setAttribute("role","group"),o.setAttribute("aria-label",`Feedback on ${n}`);function s(v){let F=i.createElement("button");return F.type="button",F.setAttribute("aria-label",`${v==="up"?"Like":"Dislike"} ${n}`),F.append(ye(v),l("span","count")),F.addEventListener("click",()=>{if(lt(e.id)){G(e.id,!0);return}Ue.react(e.id,v),Ee({kind:"reaction",optionId:e.id,element:t,reaction:v})}),F}let a=s("up"),c=s("down"),m=i.createElement("button");m.type="button",m.className="facerow",m.addEventListener("click",()=>G(e.id,!0)),dt(m,e.id),o.append(a,c,m);let w=i.createElement("div");return w.className="ring",w.hidden=!0,B.append(w,o),{info:e,element:t,toolbar:o,up:a,down:c,facesButton:m,ring:w}}function Be(e,t){let n=e.querySelector(".count");n&&(n.textContent=t>0?String(t):"")}function lt(e){return!!E.options[e]?.mine}function dt(e,t){let n=()=>{pe(t).reactions.length>0&&G(t,!1)};e.addEventListener("mouseenter",n),e.addEventListener("focus",n),e.addEventListener("mouseleave",()=>k()),e.addEventListener("blur",()=>k())}function pe(e){return E.options[e]??Et}function $e(e){return R.find(t=>t.info.id===e)?.info.label??e}function Ee(e){p==="review"&&(k({now:!0,force:!0}),g=e,A.setAttribute("aria-label",e.kind==="reaction"?`Say why you ${e.reaction==="up"?"liked":"disliked"} ${$e(e.optionId)}`:e.pin.w?"Comment on this area of the page":"Comment on this spot on the page"),P.text.placeholder=e.kind==="reaction"?"Say why (optional)":"Add a comment",P.clear(),ae(Oe,L),A.hidden=!1,z.hidden=e.kind!=="pin",N(),P.text.focus())}function ue(){g=null,re=!1,A.hidden=!0,z.hidden=!0,I(W,null),Re(null),P.clear(),N()}function I(e,t){e.hidden=t===null,t&&Object.assign(e.style,{left:`${t.x}px`,top:`${t.y}px`,width:`${t.width}px`,height:`${t.height}px`})}function Re(e){le.hidden=e===null,de.hidden=e===null,e&&(Object.assign(le.style,{left:`${e.x}px`,top:`${e.y}px`}),Object.assign(de.style,{left:`${e.x+e.width}px`,top:`${e.y+e.height}px`}))}function ct(){let e=P.text.value.trim();!e||!g||re||p!=="review"||(K="composer",re=!0,P.setBusy(!0),S(g.kind==="reaction"?{...M(),type:"react",optionId:g.optionId,reaction:g.reaction,body:e.slice(0,oe)}:{...M(),type:"comment",body:e.slice(0,oe),optionId:g.optionId,pin:g.kind==="pin"?g.pin:null,parentId:null}))}function ze(e){let t=new Set,n=[];for(let o of[e.person,...e.replies.map(s=>s.person)]){let s=o.initials+o.color;t.has(s)||n.length===3||(t.add(s),n.push(o))}return n}function he(e){let t=l("div","msg"),n=l("div"),o=l("p","who");e.reaction&&o.append(ye(e.reaction)),o.append(i.createTextNode(e.author),l("span","ago",e.when));let s=e.body?l("div","what"):l("div","none","No comment");if(e.body&&(s.textContent=e.body),n.append(o,s),e.mine&&f&&n.append(pt(e.id,e.withReplies??!1,e.undo??"Delete")),e.onReply&&f){let a=l("button","reply","Reply");a.type="button",a.addEventListener("click",e.onReply),n.append(a)}return t.append($(e.person),n),t}let j=null;function pt(e,t,n){let o=l("button","del",n);o.type="button";let s=t?`${n} thread?`:`${n}?`,a=0;function c(){j===e&&(j=null),o.textContent=n,o.classList.remove("armed")}return j===e&&(o.textContent=s,o.classList.add("armed"),a=r.setTimeout(c,4e3)),o.addEventListener("click",()=>{if(j!==e){j=e,o.textContent=s,o.classList.add("armed"),a=r.setTimeout(c,4e3);return}r.clearTimeout(a),j=null,o.disabled=!0,S({...M(),type:"delete-comment",commentId:e})}),o}function ut(e){let t=null;try{t=i.querySelector(e.selector)}catch{return}t?.scrollIntoView({behavior:"smooth",block:"center"})}function He(){if(!u||y)return;if(u.kind==="option"){G(u.id,!0);return}let e=E.pins.find(t=>t.id===u.id);e&&(X(e,!0),S({...M(),type:"focus-pin",pinId:e.id}))}function G(e,t){if(p===null)return;r.clearTimeout(Z);let n=pe(e);if(n.reactions.length===0){if(!t)return;k({now:!0,force:!0});return}let o=u?.kind==="option"&&u.id===e;u={kind:"option",id:e},t&&(y=!0);let s=y;if(s)ve.textContent=$e(e),ke.replaceChildren(...n.reactions.flatMap(a=>[he({...a,author:a.mine?"You":a.author,withReplies:a.replies.length>0,undo:"Clear",onReply:()=>ht(a.id,a.mine?"You":a.author)}),...a.replies.map(c=>{let m=he({...c,author:c.mine?"You":c.author});return m.classList.add("sub"),m})])),o||(C=null,b.clear()),b.box.hidden=C===null||!f,ae(b.face,L);else{let c=n.reactions.find(w=>w.mine)??n.reactions[n.reactions.length-1];we.replaceChildren(...Ne(n).map($)),Q.replaceChildren(ye(c.reaction),i.createTextNode(c.mine?"You":c.author)),Q.append(l("span","ago",c.when)),J.textContent=c.body||"No comment",J.classList.toggle("none",!c.body);let m=n.reactions.length-1;ee.textContent=m===1?"1 other":`${m} others`,ee.hidden=m===0}O.hidden=s,te.hidden=!s;for(let a of V)a.button.classList.remove("open");for(let a of R)a.toolbar.classList.toggle("open",s&&a.info.id===e);x.hidden=!1,x.scrollTop=0,N()}function ht(e,t){C=e,b.text.placeholder=`Reply to ${t}`,b.box.hidden=!f,b.text.focus()}function Ne(e){return e.reactions.slice(0,3).map(t=>t.person)}function X(e,t){r.clearTimeout(Z);let n=u?.kind==="pin"&&u.id===e.id,o=y;u={kind:"pin",id:e.id,anchor:e.anchor},t&&(y=!0);let s=y;if(s)ke.replaceChildren(he({...e,author:e.mine?"You":e.author,withReplies:e.replies.length>0}),...e.replies.map(a=>he({...a,author:a.mine?"You":a.author}))),C=e.id,b.text.placeholder="Reply",b.box.hidden=!f,ae(b.face,L),n||b.clear();else{we.replaceChildren(...ze(e).map($)),Q.textContent=e.mine?"You":e.author,Q.append(l("span","ago",e.when)),J.textContent=e.body;let a=e.replies.length;ee.textContent=a===1?"1 reply":`${a} replies`,ee.hidden=a===0}ve.textContent="Comment",O.hidden=s,J.classList.remove("none"),te.hidden=!s;for(let a of V)a.button.classList.toggle("open",s&&a.id===e.id);for(let a of R)a.toolbar.classList.remove("open");x.hidden=!1,x.scrollTop=0,N(),s&&f&&(!n||!o)&&b.text.focus()}function ft(){let e=b.text.value.trim();!e||!C||!f||K||(K="reply",b.oops.hidden=!0,b.setBusy(!0),S({...M(),type:"comment",body:e.slice(0,oe),optionId:null,pin:null,parentId:C}))}function k(e={}){if(y&&!e.force||!e.force&&b.text.value.trim())return;y=!1,r.clearTimeout(Z);let t=()=>{x.hidden=!0,u=null,C=null,I(Y,null),b.box.hidden=!0,b.clear();for(let n of V)n.button.classList.remove("open");for(let n of R)n.toolbar.classList.remove("open");H(null)};e.now?t():Z=r.setTimeout(t,180)}x.addEventListener("mouseenter",()=>{r.clearTimeout(Z),H(u?.kind==="pin"?u.id:null)}),x.addEventListener("mouseleave",()=>{k(),H(null)});function H(e){Le!==e&&(Le=e,S({...M(),type:"hover-pin",pinId:e}))}function Me(){let e=p!=="review";for(let t of R){let n=pe(t.info.id),o=n.mine!==null;Be(t.up,n.up),Be(t.down,n.down),t.up.hidden=o&&n.mine!=="up",t.down.hidden=o&&n.mine!=="down",t.up.setAttribute("aria-pressed",String(n.mine==="up")),t.down.setAttribute("aria-pressed",String(n.mine==="down")),t.up.disabled=e&&!o,t.down.disabled=e&&!o,t.facesButton.replaceChildren(...Ne(n).map($)),t.facesButton.hidden=n.reactions.length===0,t.facesButton.setAttribute("aria-label",`${n.reactions.length} ${n.reactions.length===1?"reaction":"reactions"} on ${t.info.label??t.info.id}`)}for(let t of V)t.button.remove();if(V=E.pins.map(t=>{let n=l("button","pin");return n.type="button",n.append(...ze(t).map($)),n.setAttribute("aria-label",`Comment ${t.number} from ${t.mine?"you":t.author}, ${t.replies.length} replies`),n.addEventListener("mouseenter",()=>{X(t,!1),H(t.id)}),n.addEventListener("mouseleave",()=>{k(),H(null)}),n.addEventListener("focus",()=>{X(t,!1),H(t.id)}),n.addEventListener("blur",()=>{k(),H(null)}),n.addEventListener("click",()=>{y&&u?.id===t.id?k({now:!0,force:!0}):X(t,!0),S({...M(),type:"focus-pin",pinId:t.id})}),B.append(n),{id:t.id,button:n,anchor:t.anchor}}),u?.kind==="option")pe(u.id).reactions.length>0?G(u.id,y):k({now:!0,force:!0});else if(u){let t=E.pins.find(n=>n.id===u.id);t?X(t,y):k({now:!0,force:!0})}N()}function N(){if(xe)return;xe=!0,(r.requestAnimationFrame?.bind(r)??(t=>r.setTimeout(t,16)))(()=>{xe=!1,mt()})}function mt(){for(let n of R){let o=n.element.getBoundingClientRect(),s=o.width>0||o.height>0;n.toolbar.hidden=!s,n.toolbar.style.left=`${o.right}px`,n.toolbar.style.top=`${o.top}px`,n.ring.hidden=!s||g?.kind!=="reaction"||g.optionId!==n.info.id,Object.assign(n.ring.style,{left:`${o.left}px`,top:`${o.top}px`,width:`${o.width}px`,height:`${o.height}px`})}let e=[];for(let n of V){let o=Je(i,n.anchor);if(n.button.hidden=!o,!o){e.push(n.id);continue}n.button.style.left=`${o.x}px`,n.button.style.top=`${o.y}px`}if(g)if(g.kind==="pin"){let n=ge(i,g.pin);n&&(z.style.left=`${n.x}px`,z.style.top=`${n.y}px`,I(W,g.pin.w?n:null),De(A,{x:n.x+n.width,y:n.y},n.x))}else{let n=g.element.getBoundingClientRect();(n.width>0||n.height>0)&&De(A,{x:n.right,y:n.top},n.left)}if(u?.kind==="option"){let n=R.find(c=>c.info.id===u.id),o=n?.element.getBoundingClientRect();x.hidden=!o||o.width===0&&o.height===0,I(Y,null);let s=n?.toolbar.getBoundingClientRect(),a=s&&(s.width>0||s.height>0);if(o&&!x.hidden){let c=a?{x:s.left,y:s.bottom}:{x:o.right,y:o.top};Fe(c,(a?s.bottom:o.top+34)+8)}}else if(u){let n=ge(i,u.anchor);x.hidden=!n,I(Y,n&&u.anchor.w?n:null),n&&Fe(n)}else I(Y,null);let t=e.join(",");t!==D&&(D=t,S({...M(),type:"unplaced-pins",pinIds:e}))}function Fe(e,t=e.y+36){let o=x.offsetWidth||300,s=x.offsetHeight||150,a=r.innerWidth||o+16,c=r.innerHeight||s+16,m=e.x+(y?40:0);m+o+8>a&&(m=e.x-o-(y?8:0)),x.style.left=`${fe(m,8,a-o-8)}px`;let w=e.y-(y?Math.min(s,70):s);x.style.top=`${fe(w<8?t:w,8,c-s-8)}px`}function De(e,t,n=t.x){let a=e.offsetWidth||272,c=e.offsetHeight||120,m=r.innerWidth||a+16,w=r.innerHeight||c+16,v=t.x+18;v+a+8>m&&(v=n-18-a),e.style.left=`${fe(v,8,m-a-8)}px`,e.style.top=`${fe(t.y-12,8,w-c-8)}px`}function fe(e,t,n){return Math.max(t,Math.min(e,Math.max(t,n)))}function gt(){_.hidden=p!=="review"}function Ve(e,t){return(i.elementsFromPoint?i.elementsFromPoint(e,t):[i.elementFromPoint(e,t)]).find(o=>!!o&&!st(o)&&o!==i.documentElement)??i.body}let bt=6,ne=null,U=!1;function _e(e,t,n){return{x:Math.min(e.x,t),y:Math.min(e.y,n),width:Math.abs(t-e.x),height:Math.abs(n-e.y)}}_.addEventListener("mousedown",e=>{g||y||(ne={x:e.clientX,y:e.clientY},U=!1,e.preventDefault())}),r.addEventListener("mousemove",e=>{if(!ne)return;let t=_e(ne,e.clientX,e.clientY);!U&&Math.max(t.width,t.height)<bt||(U=!0,I(W,t),Re(t))}),r.addEventListener("mouseup",e=>{let t=ne;if(ne=null,!t||!U)return;let n=_e(t,e.clientX,e.clientY);Re(null);let o=Ve(t.x,t.y);if(!o){I(W,null);return}let s=Qe(o,n,r.innerWidth);Ee({kind:"pin",pin:s,optionId:We(o)})}),_.addEventListener("click",e=>{if(U){U=!1;return}if(g){ue();return}if(y){k({now:!0,force:!0});return}let t=Ve(e.clientX,e.clientY);if(!t)return;let n=Ze(t,e.clientX,e.clientY,r.innerWidth);Ee({kind:"pin",pin:n,optionId:We(t)})});function We(e){let t=e.closest("[data-proof-option]")?.getAttribute("data-proof-option")??null;return t&&q.test(t)?t:null}r.addEventListener("keydown",e=>{e.key==="Escape"&&(g?ue():y&&k({now:!0,force:!0}))}),r.addEventListener("message",e=>{if(e.source!==h)return;let t=tt(e.data);if(!t){xt(et(e.data));return}if(t.type==="init"){p=t.mode,f=t.canReply??t.mode==="review",T=t.reviewer,L=t.me??null,E=t.feedback,B.hidden=!1,gt(),p!=="review"&&ue(),Me();for(let n of ie.reviewer)n(T);for(let n of ie.feedback)n(E)}else if(t.type==="feedback"){E=t.feedback,Me();for(let n of ie.feedback)n(E)}else if(t.type==="open-pin"){let n=E.pins.find(o=>o.id===t.pinId);if(!n)return;ut(n.anchor),k({now:!0,force:!0}),X(n,!0)}else if(t.type==="open-option"){let n=R.find(o=>o.info.id===t.optionId);if(!n)return;n.element.scrollIntoView({behavior:"smooth",block:"center"}),k({now:!0,force:!0}),G(t.optionId,!0)}else if(t.type==="comment-result"){let n=K;if(K=null,n==="reply"){t.ok?k({now:!0,force:!0}):b.fail(t.error,"That didn\u2019t send.");return}if(!g)return;t.ok?ue():(re=!1,P.fail(t.error,"That didn\u2019t save."))}});for(let e of["scroll","resize","load"])r.addEventListener(e,N,{capture:!0,passive:!0});function Ye(){i.documentElement.append(se);let e=Ce(),t=r.ResizeObserver;if(t){let s=new t(N);s.observe(i.documentElement);for(let a of R)s.observe(a.element)}let o=r.location.pathname.split("/").slice(3).join("/")||"index.html";S({...M(),type:"ready",options:e,path:o})}let je=!1;function xt(e){e===null||je||(je=!0,console.warn(`ScribeProof: this page's script speaks protocol ${8} and the page around it speaks ${e}. Reload to pick up the current script.`))}let Ge=!1;function Xe(){return d&&p==="review"?!0:(Ge||(Ge=!0,console.warn("ScribeProof: feedback is only recorded when this page is opened from its review link.")),!1)}let Ue={get reviewer(){return T},get mode(){return p},react(e,t){!q.test(e)||t!=="up"&&t!=="down"||!Xe()||S({...M(),type:"react",optionId:e,reaction:t,body:null})},comment(e,t){let n=t?.option??null;typeof e!="string"||!e.trim()||n!==null&&!q.test(n)||!Xe()||S({...M(),type:"comment",body:e.slice(0,oe),optionId:n,pin:null,parentId:null})},on(e,t){let n=ie[e];return n.add(t),()=>n.delete(t)},refresh(){Ce()}};return i.readyState==="loading"?i.addEventListener("DOMContentLoaded",Ye,{once:!0}):Ye(),Ue}var rt=window;rt.ScribeProof||(rt.ScribeProof=ot(window));})();
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: scribe-site-builder
3
- description: Guides building a website on Scribe CMS end to end — designing the Astro site freely against Scribe as its backend, editing the `.scribe.yml` schema, adding one or many forms, the local dev loop, pushing content, and deploying — using the `scribe` CLI and the recipes in `recipes/`. Also the reference for working an existing Scribe site (adding fields/collections, publishing content, triaging form submissions and inquiries, renaming, archiving, restoring or deleting a site), for taking a site live on a custom domain — checking whether a client has a domain, buying one, DNS/CNAME setup, GitHub Pages domain config, HTTPS — for the queue of requests clients have sent the agency (triaging, claiming, replying, closing), and for the agency's prospect pipeline (logging calls, follow-ups, won/lost). Use whenever the user wants to build/create/start a new website or site with Scribe, add a form to a site, work on an existing Scribe site, publish/launch/go live with a site, point a site at a domain, set up or fix a custom domain or DNS for a Scribe site, work the inquiries a site has received, triage or reply to a client's request for work, log a call or follow-up with a prospect or client, or asks about the `scribe` CLI.
3
+ description: Guides building a website on Scribe CMS end to end — designing the Astro site freely against Scribe as its backend, editing the `.scribe.yml` schema, adding one or many forms, the local dev loop, pushing content, and deploying — using the `scribe` CLI and the recipes in `recipes/`. Also the reference for working an existing Scribe site (adding fields/collections, publishing content, triaging form submissions and inquiries, renaming, archiving, restoring or deleting a site), for taking a site live on a custom domain — checking whether a client has a domain, buying one, DNS/CNAME setup, GitHub Pages domain config, HTTPS — for the queue of requests clients have sent the agency (triaging, claiming, replying, closing), for the agency's prospect pipeline (logging calls, follow-ups, won/lost), and for proofs — sending a client work to review (logo boards, mockups, colour schemes), reading what they said, and sending the next version. Use whenever the user wants to build/create/start a new website or site with Scribe, add a form to a site, work on an existing Scribe site, publish/launch/go live with a site, point a site at a domain, set up or fix a custom domain or DNS for a Scribe site, work the inquiries a site has received, triage or reply to a client's request for work, log a call or follow-up with a prospect or client, wants to send a client something to look at or approve (logo ideas, mockups, options, a proof), asks what a client said about work they were sent, wants to do another round or version of something a client reviewed, or asks about the `scribe` CLI.
4
4
  ---
5
5
 
6
6
  # Building a site with Scribe
@@ -30,10 +30,30 @@ instead of reading all of them:
30
30
  | [`recipes/domains.md`](./recipes/domains.md) | anything about a domain — whether the client has one, getting them one, the DNS, taking a site live on it, fixing one |
31
31
  | [`recipes/requests.md`](./recipes/requests.md) | working the queue of jobs clients have asked the agency for |
32
32
  | [`recipes/prospects.md`](./recipes/prospects.md) | logging calls and follow-ups on prospects (not site-building) |
33
+ | [`recipes/proofs.md`](./recipes/proofs.md) | sending a client work to review — logo boards, mockups, options — and working the rounds |
33
34
 
34
35
  [`recipes/README.md`](./recipes/README.md) lists the five things that are a hard
35
36
  contract (because Scribe writes into the repo) as opposed to advice.
36
37
 
38
+ ## Sending a client work to look at: it's a Proof, never an artifact
39
+
40
+ If the ask is "send them some logo ideas", "show the client these options",
41
+ "get her to approve the mockup" or anything else where a **client looks at
42
+ work and reacts to it**, that is a Proof: read
43
+ [`recipes/proofs.md`](./recipes/proofs.md).
44
+
45
+ Three rules, in full, because getting them wrong is expensive:
46
+
47
+ 1. **Never run `scribe proof send` without an explicit yes to that send.** It
48
+ emails a real person and freezes a version permanently. Everything else in
49
+ the proof workflow is reversible.
50
+ 2. **Never use the Artifact tool for client review work**, even when the word
51
+ "artifact" is used. A claude.ai artifact needs an account to open, is
52
+ branded Claude rather than the agency, and nothing a client says on it
53
+ reaches their record — the three problems Proofs exist to fix.
54
+ 3. **A Proof needs a client to belong to.** No prospect yet? Offer to create
55
+ one first.
56
+
37
57
  ## Mental model
38
58
 
39
59
  - **A site = a GitHub repo.** Schema, content, and the Astro source all live in one
@@ -14,11 +14,12 @@ generated config to keep in sync.
14
14
  | [`domains.md`](./domains.md) | finding out whether a client has a domain, getting them one, the DNS, taking a Site live on it, or repairing one |
15
15
  | [`requests.md`](./requests.md) | working the queue of jobs clients have asked the agency for |
16
16
  | [`prospects.md`](./prospects.md) | recording and nurturing prospects in the calling pipeline |
17
+ | [`proofs.md`](./proofs.md) | sending a client work to review (logo boards, mockups) and working the rounds |
17
18
 
18
- `requests.md` and `prospects.md` are the odd ones out — they're about the
19
- agency's own work (its queue and its sales pipeline) rather than anything in a
20
- Site's repo, and they live here because the `scribe` CLI is the interface for
21
- all of it.
19
+ `requests.md`, `prospects.md` and `proofs.md` are the odd ones out — they're
20
+ about the agency's own work (its queue, its sales pipeline, and the work it
21
+ sends clients to review) rather than anything in a Site's repo, and they live
22
+ here because the `scribe` CLI is the interface for all of it.
22
23
 
23
24
  ## The part that isn't advice
24
25