@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,226 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
+ import { join, relative, sep } from 'node:path';
4
+ /**
5
+ * RFC 0029 — a Proof folder on Ashley's machine: `proofs/<name>/` inside the
6
+ * client folder, holding the current version's files plus `.scribe-proof.json`,
7
+ * which says which Proof the folder belongs to so commands run inside it need
8
+ * no argument.
9
+ *
10
+ * The rules below mirror the server's (`@scribe-cms/core/proofs`'s
11
+ * `validateManifest`), so `scribe proof check` can say what a push would
12
+ * refuse before anything is uploaded. **The server is the authority**: if the
13
+ * two ever disagree, the push fails with the server's reason, which is the
14
+ * safe direction to drift.
15
+ */
16
+ export const PROOF_CONFIG_FILE = '.scribe-proof.json';
17
+ export const ENTRY_PATH = 'index.html';
18
+ export const MAX_FILE_BYTES = 25 * 1024 * 1024;
19
+ export const MAX_VERSION_BYTES = 100 * 1024 * 1024;
20
+ const RESERVED_PREFIX = '_scribe/';
21
+ /** File types a Proof may contain, by lowercase extension — the server's table. */
22
+ export const PROOF_CONTENT_TYPES = {
23
+ html: 'text/html; charset=utf-8',
24
+ htm: 'text/html; charset=utf-8',
25
+ css: 'text/css; charset=utf-8',
26
+ js: 'text/javascript; charset=utf-8',
27
+ mjs: 'text/javascript; charset=utf-8',
28
+ json: 'application/json; charset=utf-8',
29
+ txt: 'text/plain; charset=utf-8',
30
+ svg: 'image/svg+xml',
31
+ png: 'image/png',
32
+ jpg: 'image/jpeg',
33
+ jpeg: 'image/jpeg',
34
+ gif: 'image/gif',
35
+ webp: 'image/webp',
36
+ avif: 'image/avif',
37
+ ico: 'image/x-icon',
38
+ woff: 'font/woff',
39
+ woff2: 'font/woff2',
40
+ ttf: 'font/ttf',
41
+ otf: 'font/otf',
42
+ mp4: 'video/mp4',
43
+ webm: 'video/webm',
44
+ pdf: 'application/pdf',
45
+ };
46
+ export const PROOF_EXTENSIONS = Object.keys(PROOF_CONTENT_TYPES);
47
+ /** Same rule as the server and the runtime: ids appear in URLs, event rows and CLI output. */
48
+ export const OPTION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
49
+ /** `acme-bakery/logo-concepts` → `{ client, proof }`, or null if it isn't one. */
50
+ export function parseProofRef(value) {
51
+ const match = /^([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/.exec(value.trim());
52
+ return match ? { client: match[1], proof: match[2] } : null;
53
+ }
54
+ export function readProofConfig(dir) {
55
+ const file = join(dir, PROOF_CONFIG_FILE);
56
+ if (!existsSync(file))
57
+ return null;
58
+ try {
59
+ const data = JSON.parse(readFileSync(file, 'utf8'));
60
+ return typeof data.client === 'string' && typeof data.proof === 'string' ? { client: data.client, proof: data.proof } : null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ export function writeProofConfig(dir, ref) {
67
+ writeFileSync(join(dir, PROOF_CONFIG_FILE), `${JSON.stringify(ref, null, 2)}\n`);
68
+ }
69
+ /**
70
+ * Which Proof a command means: the `<client>/<proof>` argument if given,
71
+ * otherwise the folder's `.scribe-proof.json`.
72
+ */
73
+ export function resolveProofRef(arg, dir) {
74
+ if (arg) {
75
+ const ref = parseProofRef(arg);
76
+ return ref ? { ok: true, ref } : { ok: false, error: `"${arg}" isn't a Proof — expected <client>/<proof>, e.g. acme-bakery/logo-concepts` };
77
+ }
78
+ const ref = readProofConfig(dir);
79
+ if (ref)
80
+ return { ok: true, ref };
81
+ return {
82
+ ok: false,
83
+ error: `No ${PROOF_CONFIG_FILE} in ${dir}. Run this inside a Proof folder, or name the Proof as <client>/<proof>.`,
84
+ };
85
+ }
86
+ /** Hidden files and folders never ship: `.scribe-proof.json`, `.DS_Store`, `.git`. */
87
+ function isHidden(name) {
88
+ return name.startsWith('.');
89
+ }
90
+ /** Every file a push would send, sorted by path so manifests are stable. */
91
+ export function scanFolder(dir) {
92
+ const files = [];
93
+ const walk = (current) => {
94
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
95
+ if (isHidden(entry.name) || entry.name === 'node_modules')
96
+ continue;
97
+ const absolute = join(current, entry.name);
98
+ if (entry.isDirectory())
99
+ walk(absolute);
100
+ else if (entry.isFile()) {
101
+ const content = readFileSync(absolute);
102
+ files.push({
103
+ path: relative(dir, absolute).split(sep).join('/'),
104
+ absolute,
105
+ bytes: statSync(absolute).size,
106
+ sha256: createHash('sha256').update(content).digest('hex'),
107
+ });
108
+ }
109
+ }
110
+ };
111
+ walk(dir);
112
+ return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
113
+ }
114
+ export function extensionOf(path) {
115
+ const name = path.slice(path.lastIndexOf('/') + 1);
116
+ const dot = name.lastIndexOf('.');
117
+ return dot > 0 ? name.slice(dot + 1).toLowerCase() : null;
118
+ }
119
+ function mb(bytes) {
120
+ return `${Math.round((bytes / (1024 * 1024)) * 10) / 10} MB`;
121
+ }
122
+ /** `data-proof-option` elements in one HTML file — a lighter read than the server's, for warnings only. */
123
+ export function findOptions(html) {
124
+ const visible = html.replace(/<!--[\s\S]*?-->/g, '').replace(/<script\b[\s\S]*?<\/script\s*>/gi, '');
125
+ const options = [];
126
+ const duplicates = [];
127
+ const invalid = [];
128
+ const seen = new Set();
129
+ for (const tag of visible.matchAll(/<[A-Za-z][^>]*\sdata-proof-option\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>/g)) {
130
+ const id = (tag[2] ?? tag[3] ?? tag[4] ?? '').trim();
131
+ if (!OPTION_ID.test(id)) {
132
+ invalid.push(id);
133
+ continue;
134
+ }
135
+ if (seen.has(id)) {
136
+ duplicates.push(id);
137
+ continue;
138
+ }
139
+ seen.add(id);
140
+ const label = /\sdata-proof-label\s*=\s*("([^"]*)"|'([^']*)')/.exec(tag[0]);
141
+ options.push({ id, label: label ? (label[2] ?? label[3] ?? '').trim() || null : null });
142
+ }
143
+ return { options, duplicates, invalid };
144
+ }
145
+ /**
146
+ * Links that start with `/`. A Proof is served under `/c/<token>/`, so a
147
+ * root-absolute `/images/logo.png` points outside it and 404s for the Client —
148
+ * while working fine in a local dev server, which is what makes it worth
149
+ * catching before a push. Protocol-relative `//cdn…` links are fine.
150
+ */
151
+ export function findRootAbsoluteLinks(text) {
152
+ const found = [];
153
+ const patterns = [/\s(?:src|href|poster|action)\s*=\s*["'](\/(?!\/)[^"']*)["']/gi, /url\(\s*["']?(\/(?!\/)[^"')]*)["']?\s*\)/gi];
154
+ text.split('\n').forEach((lineText, index) => {
155
+ for (const pattern of patterns) {
156
+ for (const match of lineText.matchAll(pattern))
157
+ found.push({ line: index + 1, link: match[1] });
158
+ }
159
+ });
160
+ return found;
161
+ }
162
+ /**
163
+ * Everything `scribe proof check` reports. `previousOptions` — the last pushed
164
+ * version's Option ids — lets it say which Options a revision dropped.
165
+ */
166
+ /** Mirrors core's `isGalleryManifest`: images and PDFs are what a generated page can be built from. */
167
+ function isGalleryType(path) {
168
+ const type = PROOF_CONTENT_TYPES[extensionOf(path) ?? ''];
169
+ return Boolean(type) && (type.startsWith('image/') || type === 'application/pdf');
170
+ }
171
+ export function checkFolder(dir, previousOptions = []) {
172
+ const files = scanFolder(dir);
173
+ const errors = [];
174
+ const brokenLinks = [];
175
+ const warnings = [];
176
+ let totalBytes = 0;
177
+ let options = [];
178
+ if (files.length === 0)
179
+ errors.push('The folder is empty.');
180
+ // A folder of images and nothing else is allowed to arrive with no page:
181
+ // the server generates one and makes each file an Option, which is what
182
+ // makes "send them these four PNGs" work with no HTML at all (RFC 0029).
183
+ const gallery = files.length > 0 && files.every((f) => isGalleryType(f.path));
184
+ if (files.length > 0 && !gallery && !files.some((f) => f.path === ENTRY_PATH)) {
185
+ errors.push(`${ENTRY_PATH} is missing — it's the page the Proof opens on.`);
186
+ }
187
+ for (const file of files) {
188
+ totalBytes += file.bytes;
189
+ const ext = extensionOf(file.path);
190
+ if (!ext || !PROOF_EXTENSIONS.includes(ext))
191
+ errors.push(`${file.path}: not a file type a Proof may contain.`);
192
+ if (file.bytes > MAX_FILE_BYTES)
193
+ errors.push(`${file.path}: ${mb(file.bytes)} is over the ${mb(MAX_FILE_BYTES)} per-file limit.`);
194
+ if (file.path.startsWith(RESERVED_PREFIX))
195
+ errors.push(`${file.path}: ${RESERVED_PREFIX} is reserved for Scribe's own files.`);
196
+ if (/[?#]/.test(file.path))
197
+ errors.push(`${file.path}: file names can't contain ? or #.`);
198
+ if (ext === 'html' || ext === 'htm' || ext === 'css') {
199
+ const text = readFileSync(file.absolute, 'utf8');
200
+ for (const { line, link } of findRootAbsoluteLinks(text)) {
201
+ brokenLinks.push(`${file.path}:${line}: "${link}" starts with / — make it relative ("${link.slice(1)}")`);
202
+ }
203
+ if (ext !== 'css') {
204
+ const found = findOptions(text);
205
+ if (file.path === ENTRY_PATH)
206
+ options = found.options;
207
+ for (const id of found.duplicates)
208
+ warnings.push(`${file.path}: Option "${id}" appears more than once; only the first counts.`);
209
+ for (const id of found.invalid) {
210
+ warnings.push(`${file.path}: "${id}" isn't a usable Option id (letters, numbers, - and _ only, up to 64).`);
211
+ }
212
+ }
213
+ }
214
+ }
215
+ if (totalBytes > MAX_VERSION_BYTES)
216
+ errors.push(`${mb(totalBytes)} in total is over the ${mb(MAX_VERSION_BYTES)} per-version limit.`);
217
+ // Often deliberate — the revision workflow drops the Options nobody liked —
218
+ // so it's one line of information, not a warning per Option.
219
+ const current = new Set(options.map((o) => o.id));
220
+ const dropped = previousOptions.filter((id) => !current.has(id));
221
+ if (dropped.length > 0) {
222
+ warnings.push(`Dropped since the last version: ${dropped.join(', ')}. Feedback on those stays on the older version.`);
223
+ }
224
+ return { files, totalBytes, errors, brokenLinks, warnings, options, gallery };
225
+ }
226
+ //# sourceMappingURL=proof-folder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof-folder.js","sourceRoot":"","sources":["../../src/lib/proof-folder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * RFC 0029 — `scribe proof preview`: a Proof folder served the way a Client
3
+ * will see it, before anything is pushed.
4
+ *
5
+ * Same sandbox CSP, same runtime injected the same way, same iframe sandbox
6
+ * attribute — so a Proof that works here works for the Client, and one that
7
+ * breaks under the sandbox (a script reading `localStorage`, say) breaks here
8
+ * first. The panel around it is a stand-in for the real review page: it keeps
9
+ * hearts, comments and pins in memory so they show up on the page as they
10
+ * would, and prints each one to the terminal. Nothing is saved anywhere.
11
+ */
12
+ /** Mirrors the admin's `PROOF_CONTENT_CSP`. No `allow-same-origin`, ever. */
13
+ export declare const PREVIEW_CSP = "sandbox allow-scripts allow-forms allow-popups allow-modals";
14
+ /**
15
+ * The runtime bundle, copied in from `@scribe-cms/proof-runtime` at build time
16
+ * by scripts/bundle-runtime.mjs — the CLI is published to npm and can't depend
17
+ * on the private workspace package at install time. `../../runtime/` resolves
18
+ * to the package root from both `src/lib/` (tests) and `dist/lib/` (published).
19
+ */
20
+ export declare function loadRuntimeSource(): string;
21
+ /** Same placement rule as the admin's `injectRuntime`: first thing in `<head>`, src relative to the file's depth. */
22
+ export declare function injectRuntime(html: string, path: string): string;
23
+ /**
24
+ * The file a request path names inside the folder, or null. Hidden files and
25
+ * anything outside the folder are never served — the same files a push would
26
+ * leave out.
27
+ */
28
+ export declare function resolvePreviewFile(dir: string, requestPath: string): string | null;
29
+ export interface PreviewServer {
30
+ url: string;
31
+ close(): Promise<void>;
32
+ }
33
+ export declare function startPreview(args: {
34
+ dir: string;
35
+ port?: number;
36
+ log?: (line: string) => void;
37
+ runtimeSource?: string;
38
+ }): Promise<PreviewServer>;
39
+ //# sourceMappingURL=proof-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof-preview.d.ts","sourceRoot":"","sources":["../../src/lib/proof-preview.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;GAUG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,WAAW,gEAAgE,CAAC;AAWzF;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAM1C;AAED,qHAAqH;AACrH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAWhE;AAaD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAalF;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,aAAa,CAAC,CA6DzB"}
@@ -0,0 +1,318 @@
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { createServer } from 'node:http';
3
+ import { basename, resolve, sep } from 'node:path';
4
+ import { ENTRY_PATH, extensionOf, PROOF_CONTENT_TYPES } from './proof-folder.js';
5
+ /**
6
+ * RFC 0029 — `scribe proof preview`: a Proof folder served the way a Client
7
+ * will see it, before anything is pushed.
8
+ *
9
+ * Same sandbox CSP, same runtime injected the same way, same iframe sandbox
10
+ * attribute — so a Proof that works here works for the Client, and one that
11
+ * breaks under the sandbox (a script reading `localStorage`, say) breaks here
12
+ * first. The panel around it is a stand-in for the real review page: it keeps
13
+ * hearts, comments and pins in memory so they show up on the page as they
14
+ * would, and prints each one to the terminal. Nothing is saved anywhere.
15
+ */
16
+ /** Mirrors the admin's `PROOF_CONTENT_CSP`. No `allow-same-origin`, ever. */
17
+ export const PREVIEW_CSP = 'sandbox allow-scripts allow-forms allow-popups allow-modals';
18
+ const PREFIX = '/c/local/';
19
+ const RUNTIME_PATH = '_scribe/proof.js';
20
+ /**
21
+ * Must match `PROTOCOL_VERSION` in `@scribe-cms/proof-runtime`'s protocol
22
+ * module. Not imported from it: the CLI ships to npm and only gets the bundled
23
+ * script copied in at build time, never the package. A mismatch is loud — the
24
+ * runtime drops every message and the preview draws no affordances at all.
25
+ */
26
+ const PROTOCOL_VERSION = 7;
27
+ /**
28
+ * The runtime bundle, copied in from `@scribe-cms/proof-runtime` at build time
29
+ * by scripts/bundle-runtime.mjs — the CLI is published to npm and can't depend
30
+ * on the private workspace package at install time. `../../runtime/` resolves
31
+ * to the package root from both `src/lib/` (tests) and `dist/lib/` (published).
32
+ */
33
+ export function loadRuntimeSource() {
34
+ const file = new URL('../../runtime/proof.js', import.meta.url);
35
+ if (!existsSync(file)) {
36
+ throw new Error('The Proof runtime is missing from this install. Reinstall the CLI (or, in the repo, run `npm run build --workspace=@scribe-platform/cli`).');
37
+ }
38
+ return readFileSync(file, 'utf8');
39
+ }
40
+ /** Same placement rule as the admin's `injectRuntime`: first thing in `<head>`, src relative to the file's depth. */
41
+ export function injectRuntime(html, path) {
42
+ const depth = path.split('/').length - 1;
43
+ const tag = `<script src="${'../'.repeat(depth)}${RUNTIME_PATH}"></script>`;
44
+ const at = (index) => html.slice(0, index) + tag + html.slice(index);
45
+ const head = /<head(\s[^>]*)?>/i.exec(html);
46
+ if (head)
47
+ return at(head.index + head[0].length);
48
+ const body = /<body(\s[^>]*)?>/i.exec(html);
49
+ if (body)
50
+ return at(body.index);
51
+ const doctype = /^\s*<!doctype[^>]*>/i.exec(html);
52
+ if (doctype)
53
+ return at(doctype[0].length);
54
+ return tag + html;
55
+ }
56
+ function contentHeaders(contentType) {
57
+ return {
58
+ 'Content-Type': contentType,
59
+ 'Content-Security-Policy': PREVIEW_CSP,
60
+ 'Referrer-Policy': 'no-referrer',
61
+ 'X-Content-Type-Options': 'nosniff',
62
+ // Ashley edits and reloads; never serve her a stale copy.
63
+ 'Cache-Control': 'no-store',
64
+ };
65
+ }
66
+ /**
67
+ * The file a request path names inside the folder, or null. Hidden files and
68
+ * anything outside the folder are never served — the same files a push would
69
+ * leave out.
70
+ */
71
+ export function resolvePreviewFile(dir, requestPath) {
72
+ let segments;
73
+ try {
74
+ segments = requestPath.split('/').map(decodeURIComponent);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ if (segments.some((s) => s === '' || s === '.' || s === '..' || s.startsWith('.') || s.includes('/') || s.includes('\\')))
80
+ return null;
81
+ const root = resolve(dir);
82
+ const target = resolve(root, ...segments);
83
+ if (!target.startsWith(root + sep))
84
+ return null;
85
+ if (!existsSync(target) || !statSync(target).isFile())
86
+ return null;
87
+ return target;
88
+ }
89
+ export async function startPreview(args) {
90
+ const dir = resolve(args.dir);
91
+ const log = args.log ?? ((line) => console.log(line));
92
+ const runtime = args.runtimeSource ?? loadRuntimeSource();
93
+ const shell = shellPage(basename(dir));
94
+ const server = createServer((req, res) => {
95
+ const url = new URL(req.url ?? '/', 'http://localhost');
96
+ const send = (status, headers, body) => {
97
+ res.writeHead(status, headers);
98
+ res.end(body);
99
+ };
100
+ if (req.method === 'POST' && url.pathname === '/__log') {
101
+ let body = '';
102
+ req.on('data', (chunk) => (body += chunk));
103
+ req.on('end', () => {
104
+ try {
105
+ const { line } = JSON.parse(body);
106
+ if (typeof line === 'string')
107
+ log(line.slice(0, 2000));
108
+ }
109
+ catch {
110
+ // a malformed log line from our own page; nothing to print
111
+ }
112
+ send(204, {});
113
+ });
114
+ return;
115
+ }
116
+ if (req.method !== 'GET' && req.method !== 'HEAD')
117
+ return send(405, { 'Content-Type': 'text/plain' }, 'Method not allowed\n');
118
+ if (url.pathname === '/')
119
+ return send(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }, shell);
120
+ if (url.pathname === '/c/local' || url.pathname === PREFIX)
121
+ return send(307, { Location: `${PREFIX}${ENTRY_PATH}` });
122
+ if (!url.pathname.startsWith(PREFIX))
123
+ return send(404, contentHeaders('text/plain; charset=utf-8'), 'Not found\n');
124
+ const path = url.pathname.slice(PREFIX.length);
125
+ if (path === RUNTIME_PATH)
126
+ return send(200, contentHeaders('text/javascript; charset=utf-8'), runtime);
127
+ const file = resolvePreviewFile(dir, path);
128
+ const ext = file ? extensionOf(file) : null;
129
+ const contentType = ext ? PROOF_CONTENT_TYPES[ext] : undefined;
130
+ if (!file || !contentType) {
131
+ // Same 404 for a missing file and a type a push would refuse: either
132
+ // way the Client would never see it, so neither should the preview.
133
+ return send(404, contentHeaders('text/plain; charset=utf-8'), `Not in this Proof: ${path}\n`);
134
+ }
135
+ const bytes = readFileSync(file);
136
+ if (contentType.startsWith('text/html')) {
137
+ return send(200, contentHeaders(contentType), injectRuntime(bytes.toString('utf8'), decodeURIComponent(path)));
138
+ }
139
+ return send(200, contentHeaders(contentType), bytes);
140
+ });
141
+ await new Promise((resolveListen, reject) => {
142
+ server.once('error', reject);
143
+ // Loopback only: a preview is for the machine it runs on.
144
+ server.listen(args.port ?? 0, '127.0.0.1', () => resolveListen());
145
+ });
146
+ const { port } = server.address();
147
+ return {
148
+ url: `http://127.0.0.1:${port}/`,
149
+ close: () => new Promise((done) => server.close(() => done())),
150
+ };
151
+ }
152
+ function escapeHtml(value) {
153
+ return value.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
154
+ }
155
+ /**
156
+ * The stand-in review page. Deliberately plain — the real one (plan PR 6) is
157
+ * branded and built from the admin's design system; this one only has to make
158
+ * the runtime's behaviour visible and print what a Client's feedback would be.
159
+ */
160
+ function shellPage(folder) {
161
+ return `<!doctype html>
162
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
163
+ <title>Preview · ${escapeHtml(folder)}</title>
164
+ <style>
165
+ :root { color-scheme: light dark; --bg: #f8fafc; --panel: #fff; --text: #0f172a; --muted: #64748b; --line: #e2e8f0; --accent: #2563eb; }
166
+ @media (prefers-color-scheme: dark) { :root { --bg: #0b1220; --panel: #111827; --text: #e5e7eb; --muted: #94a3b8; --line: #1f2937; } }
167
+ * { box-sizing: border-box; }
168
+ body { margin: 0; height: 100vh; display: grid; grid-template-columns: 1fr 320px; background: var(--bg); color: var(--text);
169
+ font: 14px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
170
+ main { display: grid; place-items: start center; overflow: auto; padding: 16px; }
171
+ iframe { width: 100%; height: calc(100vh - 32px); border: 1px solid var(--line); border-radius: 8px; background: #fff; transition: width .2s; }
172
+ iframe.phone { width: 390px; }
173
+ aside { border-left: 1px solid var(--line); background: var(--panel); display: flex; flex-direction: column; min-height: 0; }
174
+ header { padding: 16px; border-bottom: 1px solid var(--line); }
175
+ h1 { margin: 0 0 4px; font-size: 15px; } p { margin: 0; color: var(--muted); font-size: 13px; }
176
+ .tools { display: flex; flex-wrap: wrap; gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--line); }
177
+ button { font: inherit; padding: 6px 12px; border-radius: 6px; border: 1px solid var(--line); background: var(--panel); color: var(--text); cursor: pointer; }
178
+ button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }
179
+ ol { list-style: none; margin: 0; padding: 8px 16px; overflow: auto; flex: 1; }
180
+ li { padding: 8px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
181
+ li.focus { background: color-mix(in srgb, var(--accent) 12%, transparent); }
182
+ li .when { color: var(--muted); font-size: 12px; }
183
+ @media (max-width: 800px) { body { grid-template-columns: 1fr; grid-template-rows: 1fr 40vh; } aside { border-left: 0; border-top: 1px solid var(--line); } }
184
+ </style></head>
185
+ <body>
186
+ <main><iframe id="proof" title="The Proof" src="${PREFIX}${ENTRY_PATH}" sandbox="allow-scripts allow-forms allow-popups allow-modals"></iframe></main>
187
+ <aside>
188
+ <header><h1>Preview · ${escapeHtml(folder)}</h1><p>What the Client will see: click anywhere on the work to leave a comment. This is a rehearsal — nothing is saved or sent.</p></header>
189
+ <div class="tools">
190
+ <button id="phone" type="button" aria-pressed="false">Phone width</button>
191
+ <button id="reset" type="button">Clear</button>
192
+ </div>
193
+ <ol id="log" aria-live="polite"></ol>
194
+ </aside>
195
+ <script>
196
+ const env = { scribe: 'proof', protocolVersion: ${PROTOCOL_VERSION} };
197
+ const frame = document.getElementById('proof');
198
+ const list = document.getElementById('log');
199
+ const ME = { initials: 'Y', color: '#2563eb' };
200
+ let state = { options: {}, pins: [] };
201
+ let seq = 0;
202
+ let labels = {};
203
+ let lastUnplaced = '';
204
+
205
+ function post(message) { frame.contentWindow.postMessage({ ...env, ...message }, '*'); }
206
+ function push() { post({ type: 'feedback', feedback: state }); }
207
+ function option(id) { return state.options[id] || (state.options[id] = { up: 0, down: 0, mine: null, comments: 0, reactions: [] }); }
208
+ function name(id) { return labels[id] ? labels[id] + ' (' + id + ')' : id; }
209
+
210
+ function log(line, id) {
211
+ const li = document.createElement('li');
212
+ if (id) li.dataset.pin = id;
213
+ li.innerHTML = '<div></div><div class="when"></div>';
214
+ li.firstChild.textContent = line;
215
+ li.lastChild.textContent = new Date().toLocaleTimeString();
216
+ list.prepend(li);
217
+ fetch('/__log', { method: 'POST', body: JSON.stringify({ line }) }).catch(() => {});
218
+ }
219
+
220
+ window.addEventListener('message', (event) => {
221
+ if (event.source !== frame.contentWindow) return;
222
+ const m = event.data;
223
+ if (!m || m.scribe !== 'proof' || m.protocolVersion !== ${PROTOCOL_VERSION}) return;
224
+ switch (m.type) {
225
+ case 'ready':
226
+ labels = Object.fromEntries((m.options || []).map((o) => [o.id, o.label]));
227
+ post({ type: 'init', mode: 'review', reviewer: { name: 'You (preview)' }, me: ME, canReply: true, feedback: state });
228
+ log('Loaded ' + m.path + ' — ' + (m.options.length ? m.options.length + ' option(s): ' + m.options.map((o) => o.id).join(', ') : 'no options marked'));
229
+ break;
230
+ case 'react': {
231
+ // One Reaction per person per Option, so a second one rewrites the
232
+ // first — the same rule the real API enforces.
233
+ const o = option(m.optionId);
234
+ const body = (m.body || '').trim();
235
+ let entry = o.reactions.find((r) => r.mine);
236
+ if (!entry) {
237
+ entry = { id: 'r' + (++seq), reaction: m.reaction, author: 'You', body: '', mine: true, when: 'just now', person: ME, replies: [] };
238
+ o.reactions.push(entry);
239
+ }
240
+ if (entry.reaction !== m.reaction) o[entry.reaction]--;
241
+ entry.reaction = m.reaction;
242
+ if (body) entry.body = body;
243
+ o.up = o.reactions.filter((r) => r.reaction === 'up').length;
244
+ o.down = o.reactions.filter((r) => r.reaction === 'down').length;
245
+ o.mine = m.reaction;
246
+ log((m.reaction === 'up' ? '👍 ' : '👎 ') + name(m.optionId) + (body ? ' — ' + body : ''));
247
+ // The composer in the page is waiting on this whenever words came
248
+ // with the thumb, exactly as it is for a comment.
249
+ if (m.body !== null && m.body !== undefined) post({ type: 'comment-result', ok: true, error: null });
250
+ push();
251
+ break;
252
+ }
253
+ case 'comment': {
254
+ // The composer lives in the page now, so this one case covers what the
255
+ // reviewer typed and what the Proof's own script said.
256
+ const body = (m.body || '').trim();
257
+ if (m.parentId) {
258
+ // A reply, so it belongs under the pin it answers rather than on the
259
+ // page — the same filing the real shell does.
260
+ // Under a pin, or under one of an Option's Reactions — the real
261
+ // shell files a reply by its parent, wherever that parent lives.
262
+ const parent = state.pins.find((p) => p.id === m.parentId) ||
263
+ Object.values(state.options).flatMap((o) => o.reactions).find((r) => r.id === m.parentId);
264
+ if (parent) parent.replies.push({ id: m.parentId + '-r' + (parent.replies.length + 1), author: 'You (preview)', body, mine: true, when: 'just now', person: ME });
265
+ log('↩ Replied: "' + body + '"', m.parentId);
266
+ } else if (m.pin) {
267
+ const n = state.pins.length + 1, id = 'pin-' + n;
268
+ state.pins.push({ id, number: n, anchor: m.pin, mine: true, author: 'You (preview)', body, when: 'just now', person: ME, replies: [] });
269
+ if (m.optionId) option(m.optionId).comments++;
270
+ log('📍 Pin ' + n + ' on ' + m.pin.selector + ' (' + Math.round(m.pin.x * 100) + '%, ' + Math.round(m.pin.y * 100) + '%) at ' + m.pin.viewportWidth + 'px: "' + body + '"', id);
271
+ } else if (m.optionId) {
272
+ option(m.optionId).comments++;
273
+ log('💬 On ' + name(m.optionId) + ': "' + body + '"');
274
+ } else {
275
+ log('💬 "' + body + '"');
276
+ }
277
+ // Nothing is saved in a preview, so it always "saved" — the composer
278
+ // needs an answer either way or it sits there saying Saving….
279
+ post({ type: 'comment-result', ok: true, error: null });
280
+ push();
281
+ break;
282
+ }
283
+ case 'focus-pin':
284
+ for (const li of list.querySelectorAll('li.focus')) li.classList.remove('focus');
285
+ list.querySelector('[data-pin="' + CSS.escape(m.pinId) + '"]')?.classList.add('focus');
286
+ break;
287
+ case 'unplaced-pins': { const key = m.pinIds.join(','); if (key && key !== lastUnplaced) log('⚠ Can’t place ' + m.pinIds.join(', ') + ' on this page — the element moved'); lastUnplaced = key; break; }
288
+ case 'delete-comment': {
289
+ // The whole thread goes, the way it does for real.
290
+ const before = state.pins.length;
291
+ state.pins = state.pins.filter((p) => p.id !== m.commentId);
292
+ for (const p of state.pins) p.replies = p.replies.filter((r) => r.id !== m.commentId);
293
+ let cleared = false;
294
+ for (const o of Object.values(state.options)) {
295
+ const kept = o.reactions.filter((r) => r.id !== m.commentId);
296
+ cleared = cleared || kept.length !== o.reactions.length;
297
+ o.reactions = kept;
298
+ for (const r of o.reactions) r.replies = r.replies.filter((x) => x.id !== m.commentId);
299
+ o.up = o.reactions.filter((r) => r.reaction === 'up').length;
300
+ o.down = o.reactions.filter((r) => r.reaction === 'down').length;
301
+ o.mine = (o.reactions.find((r) => r.mine) || {}).reaction || null;
302
+ }
303
+ log(cleared ? '🗑 Cleared a reaction' : before === state.pins.length ? '🗑 Deleted a reply' : '🗑 Deleted a comment and everything under it');
304
+ push();
305
+ break;
306
+ }
307
+ }
308
+ });
309
+
310
+ document.getElementById('phone').addEventListener('click', (e) => {
311
+ const on = frame.classList.toggle('phone');
312
+ e.currentTarget.setAttribute('aria-pressed', String(on));
313
+ });
314
+ document.getElementById('reset').addEventListener('click', () => { state = { options: {}, pins: [] }; list.innerHTML = ''; push(); });
315
+ </script>
316
+ </body></html>`;
317
+ }
318
+ //# sourceMappingURL=proof-preview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proof-preview.js","sourceRoot":"","sources":["../../src/lib/proof-preview.ts"],"names":[],"mappings":"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"}