@scribe-platform/cli 0.8.0 → 0.9.1
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.
- package/README.md +28 -0
- package/dist/commands/proof.d.ts +5 -0
- package/dist/commands/proof.d.ts.map +1 -0
- package/dist/commands/proof.js +440 -0
- package/dist/commands/proof.js.map +1 -0
- package/dist/commands/prospect.d.ts.map +1 -1
- package/dist/commands/prospect.js +9 -0
- package/dist/commands/prospect.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/client.d.ts +169 -0
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +77 -0
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/proof-folder.d.ts +86 -0
- package/dist/lib/proof-folder.d.ts.map +1 -0
- package/dist/lib/proof-folder.js +226 -0
- package/dist/lib/proof-folder.js.map +1 -0
- package/dist/lib/proof-preview.d.ts +39 -0
- package/dist/lib/proof-preview.d.ts.map +1 -0
- package/dist/lib/proof-preview.js +318 -0
- package/dist/lib/proof-preview.js.map +1 -0
- package/dist/lib/proof-push.d.ts +42 -0
- package/dist/lib/proof-push.d.ts.map +1 -0
- package/dist/lib/proof-push.js +91 -0
- package/dist/lib/proof-push.js.map +1 -0
- package/package.json +5 -3
- package/runtime/proof.js +139 -0
- package/skills/scribe-site-builder/SKILL.md +69 -1
- package/skills/scribe-site-builder/recipes/README.md +10 -4
- package/skills/scribe-site-builder/recipes/collections.md +5 -0
- package/skills/scribe-site-builder/recipes/deploy.md +5 -0
- package/skills/scribe-site-builder/recipes/domains.md +5 -0
- package/skills/scribe-site-builder/recipes/forms.md +5 -0
- package/skills/scribe-site-builder/recipes/proofs.md +343 -0
- package/skills/scribe-site-builder/recipes/prospects.md +5 -0
- package/skills/scribe-site-builder/recipes/requests.md +5 -0
- package/skills/scribe-site-builder/recipes/schema.md +5 -0
|
@@ -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.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|
package/runtime/proof.js
ADDED
|
@@ -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),
|
|
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
|
|
@@ -15,6 +15,54 @@ missing, it is a gap worth telling Dave about, not a file to go hunting for.
|
|
|
15
15
|
Scribe repo. That matters only to whoever edits it there; those paths do not
|
|
16
16
|
exist on a machine that installed the CLI from npm.)
|
|
17
17
|
|
|
18
|
+
## Who you're talking to — keep the CLI to yourself
|
|
19
|
+
|
|
20
|
+
The person in this chat is an **account manager at a web agency, not a
|
|
21
|
+
developer**. She runs all of Scribe by asking for things in plain English;
|
|
22
|
+
that is the promise her own guide makes her — *"You don't need to memorize
|
|
23
|
+
commands."* Every `scribe` command in this file and in `recipes/` is **yours
|
|
24
|
+
to run on her behalf, not hers to learn.**
|
|
25
|
+
|
|
26
|
+
So when she asks how something works — *"how do proofs work?"*, *"how do I add
|
|
27
|
+
a form?"*, *"how do I take a site live?"* — **do not answer with a list of
|
|
28
|
+
commands.** That is the wrong answer even though a command list is literally
|
|
29
|
+
what this reference contains. She can't use it, and handing it over tells her
|
|
30
|
+
there's a layer underneath that she's expected to learn. There isn't one.
|
|
31
|
+
|
|
32
|
+
Answer with four things instead:
|
|
33
|
+
|
|
34
|
+
1. **What the thing is**, in two or three sentences, in her terms. *A proof is
|
|
35
|
+
a page you send a client so they can look at your work and tell you what
|
|
36
|
+
they think.*
|
|
37
|
+
2. **What she'd say to get it** — the actual sentences she would type, not the
|
|
38
|
+
commands they turn into. *"Make Acme six logo concepts from the brand notes
|
|
39
|
+
in `research/`"* → *"show me"* → *"send it to Dana"*.
|
|
40
|
+
3. **The decisions that stay hers**, and only the ones that really are:
|
|
41
|
+
sending emails a real person so you'll always check first; a proof needs a
|
|
42
|
+
client to belong to.
|
|
43
|
+
4. **An offer to do it now**, named against something she's actually working
|
|
44
|
+
on.
|
|
45
|
+
|
|
46
|
+
Keep commands, flags, ids and file formats out of the prose. She will see
|
|
47
|
+
commands scroll past as you run them and that's fine — this rule is about what
|
|
48
|
+
you *teach* her and what you *ask of her*, not about hiding your tool calls.
|
|
49
|
+
The same goes for everything else that is plumbing: tokens, manifests, hashes,
|
|
50
|
+
version numbers she didn't ask about, option ids where a label will do ("the
|
|
51
|
+
wheat star one", not `logo-5`).
|
|
52
|
+
|
|
53
|
+
Two exceptions:
|
|
54
|
+
|
|
55
|
+
- **She asks for the command**, or says she's working in a terminal. Then give
|
|
56
|
+
it to her straight.
|
|
57
|
+
- **The one-time machine setup** — installing the CLI and `scribe login` — is
|
|
58
|
+
genuinely typed by hand, and her setup guide says so. Walk her through that
|
|
59
|
+
one at the keyboard.
|
|
60
|
+
|
|
61
|
+
And one thing that is never hers to deal with: when a command fails for a
|
|
62
|
+
reason that has nothing to do with her work — no login, an npm permission, a
|
|
63
|
+
CLI behind the API — fix it, or say plainly that it's one for Dave. Don't paste
|
|
64
|
+
her the error.
|
|
65
|
+
|
|
18
66
|
## Recipes — read the one you need
|
|
19
67
|
|
|
20
68
|
Per RFC 0010 the Astro site is **yours to design**. Scribe doesn't supply a
|
|
@@ -30,10 +78,30 @@ instead of reading all of them:
|
|
|
30
78
|
| [`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
79
|
| [`recipes/requests.md`](./recipes/requests.md) | working the queue of jobs clients have asked the agency for |
|
|
32
80
|
| [`recipes/prospects.md`](./recipes/prospects.md) | logging calls and follow-ups on prospects (not site-building) |
|
|
81
|
+
| [`recipes/proofs.md`](./recipes/proofs.md) | sending a client work to review — logo boards, mockups, options — and working the rounds |
|
|
33
82
|
|
|
34
83
|
[`recipes/README.md`](./recipes/README.md) lists the five things that are a hard
|
|
35
84
|
contract (because Scribe writes into the repo) as opposed to advice.
|
|
36
85
|
|
|
86
|
+
## Sending a client work to look at: it's a Proof, never an artifact
|
|
87
|
+
|
|
88
|
+
If the ask is "send them some logo ideas", "show the client these options",
|
|
89
|
+
"get her to approve the mockup" or anything else where a **client looks at
|
|
90
|
+
work and reacts to it**, that is a Proof: read
|
|
91
|
+
[`recipes/proofs.md`](./recipes/proofs.md).
|
|
92
|
+
|
|
93
|
+
Three rules, in full, because getting them wrong is expensive:
|
|
94
|
+
|
|
95
|
+
1. **Never run `scribe proof send` without an explicit yes to that send.** It
|
|
96
|
+
emails a real person and freezes a version permanently. Everything else in
|
|
97
|
+
the proof workflow is reversible.
|
|
98
|
+
2. **Never use the Artifact tool for client review work**, even when the word
|
|
99
|
+
"artifact" is used. A claude.ai artifact needs an account to open, is
|
|
100
|
+
branded Claude rather than the agency, and nothing a client says on it
|
|
101
|
+
reaches their record — the three problems Proofs exist to fix.
|
|
102
|
+
3. **A Proof needs a client to belong to.** No prospect yet? Offer to create
|
|
103
|
+
one first.
|
|
104
|
+
|
|
37
105
|
## Mental model
|
|
38
106
|
|
|
39
107
|
- **A site = a GitHub repo.** Schema, content, and the Astro source all live in one
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Recipes
|
|
2
2
|
|
|
3
|
+
> **The commands below are yours to run, not hers to read.** If she's asking
|
|
4
|
+
> what this is or how to do it, answer it the way *Who you're talking to* in
|
|
5
|
+
> [`../SKILL.md`](../SKILL.md) says to: what it is, what she'd say to get it,
|
|
6
|
+
> what's her call, and an offer to do it now.
|
|
7
|
+
|
|
3
8
|
Patterns for building an Astro site against Scribe as the backend. Per RFC
|
|
4
9
|
0010 these are **advice, not enforcement** — read the one you need, adapt it to the site you're
|
|
5
10
|
building, and ignore the rest. There is no template to conform to and no
|
|
@@ -14,11 +19,12 @@ generated config to keep in sync.
|
|
|
14
19
|
| [`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
20
|
| [`requests.md`](./requests.md) | working the queue of jobs clients have asked the agency for |
|
|
16
21
|
| [`prospects.md`](./prospects.md) | recording and nurturing prospects in the calling pipeline |
|
|
22
|
+
| [`proofs.md`](./proofs.md) | sending a client work to review (logo boards, mockups) and working the rounds |
|
|
17
23
|
|
|
18
|
-
`requests.md` and `
|
|
19
|
-
agency's own work (its queue
|
|
20
|
-
|
|
21
|
-
all of it.
|
|
24
|
+
`requests.md`, `prospects.md` and `proofs.md` are the odd ones out — they're
|
|
25
|
+
about the agency's own work (its queue, its sales pipeline, and the work it
|
|
26
|
+
sends clients to review) rather than anything in a Site's repo, and they live
|
|
27
|
+
here because the `scribe` CLI is the interface for all of it.
|
|
22
28
|
|
|
23
29
|
## The part that isn't advice
|
|
24
30
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Recipe: rendering content collections
|
|
2
2
|
|
|
3
|
+
> **The commands below are yours to run, not hers to read.** If she's asking
|
|
4
|
+
> what this is or how to do it, answer it the way *Who you're talking to* in
|
|
5
|
+
> [`../SKILL.md`](../SKILL.md) says to: what it is, what she'd say to get it,
|
|
6
|
+
> what's her call, and an offer to do it now.
|
|
7
|
+
|
|
3
8
|
A content collection (anything without `form: true`) is a folder of files the
|
|
4
9
|
admin writes directly into the repo. Your job is to read them from the `path` the
|
|
5
10
|
schema declares, in the `format` it declares. That part isn't negotiable — the
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Recipe: build and deploy
|
|
2
2
|
|
|
3
|
+
> **The commands below are yours to run, not hers to read.** If she's asking
|
|
4
|
+
> what this is or how to do it, answer it the way *Who you're talking to* in
|
|
5
|
+
> [`../SKILL.md`](../SKILL.md) says to: what it is, what she'd say to get it,
|
|
6
|
+
> what's her call, and an offer to do it now.
|
|
7
|
+
|
|
3
8
|
Sites deploy to GitHub Pages from their own repo. Two parts of this are a
|
|
4
9
|
contract with Scribe rather than a preference: the workflow must accept
|
|
5
10
|
`workflow_dispatch`, and the Astro `base` must follow `public/CNAME`.
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Taking a site live on a custom domain
|
|
2
2
|
|
|
3
|
+
> **The commands below are yours to run, not hers to read.** If she's asking
|
|
4
|
+
> what this is or how to do it, answer it the way *Who you're talking to* in
|
|
5
|
+
> [`../SKILL.md`](../SKILL.md) says to: what it is, what she'd say to get it,
|
|
6
|
+
> what's her call, and an offer to do it now.
|
|
7
|
+
|
|
3
8
|
One command does all of it. Reach for `scribe site promote` whenever the ask is
|
|
4
9
|
"publish the site at example.com", "point the domain at the site", "take it
|
|
5
10
|
live", or "set up the custom domain".
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Recipe: forms
|
|
2
2
|
|
|
3
|
+
> **The commands below are yours to run, not hers to read.** If she's asking
|
|
4
|
+
> what this is or how to do it, answer it the way *Who you're talking to* in
|
|
5
|
+
> [`../SKILL.md`](../SKILL.md) says to: what it is, what she'd say to get it,
|
|
6
|
+
> what's her call, and an offer to do it now.
|
|
7
|
+
|
|
3
8
|
A Site can have **as many forms as it likes**. Each one is a collection in
|
|
4
9
|
`.scribe.yml` with `form: true`, each gets its own inbox and its own workflow in
|
|
5
10
|
the admin, and each posts to the same endpoint with a different `collection`.
|