egregore-artifacts 0.9.9 → 0.10.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.
package/bin/cli.js CHANGED
@@ -1,10 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import { generateArtifact, openArtifact } from '../lib/index.js';
3
2
  import { execSync } from 'node:child_process';
4
3
  import fs from 'node:fs';
5
4
  import path from 'node:path';
6
5
 
7
- const KNOWN_TYPES = ['quest', 'handoff', 'handoff-v1', 'activity', 'document', 'board', 'network'];
6
+ // Import the renderer dynamically so a missing dependency tree (fresh clone
7
+ // or git worktree without `npm install`) produces a clear instruction
8
+ // instead of a raw ERR_MODULE_NOT_FOUND stack trace.
9
+ let generateArtifact, openArtifact;
10
+ try {
11
+ ({ generateArtifact, openArtifact } = await import('../lib/index.js'));
12
+ } catch (err) {
13
+ if (err.code === 'ERR_MODULE_NOT_FOUND') {
14
+ const pkgRoot = path.dirname(new URL('.', import.meta.url).pathname.replace(/\/$/, ''));
15
+ console.error('✗ Dependencies not installed for the local egregore-artifacts package.');
16
+ console.error(` Run: npm install --prefix "${pkgRoot}"`);
17
+ console.error(' Or use the published renderer instead: npx egregore-artifacts <args>');
18
+ process.exit(1);
19
+ }
20
+ throw err;
21
+ }
22
+
23
+ const KNOWN_TYPES = ['quest', 'handoff', 'handoff-v1', 'emissary', 'activity', 'document', 'board', 'network'];
8
24
  const args = process.argv.slice(2);
9
25
 
10
26
  let type, filePath;
@@ -51,6 +67,18 @@ if (!viewBase) viewBase = 'https://egregore.xyz/view';
51
67
  if (KNOWN_TYPES.includes(positional[0])) {
52
68
  type = positional[0];
53
69
  filePath = positional[1];
70
+ } else if (
71
+ positional.length >= 2 &&
72
+ !fs.existsSync(positional[0]) &&
73
+ !positional[0].includes('/') &&
74
+ !positional[0].endsWith('.md')
75
+ ) {
76
+ // Looks like "<unknown-type> <file>" (e.g. a caller passing a new artifact
77
+ // type this CLI version doesn't know). Don't fail silently — render with
78
+ // the document template and say so.
79
+ console.error(`⚠ Unknown artifact type "${positional[0]}" — rendering as document`);
80
+ type = 'document';
81
+ filePath = positional[1];
54
82
  } else {
55
83
  // Auto-detect: first arg is a file path
56
84
  filePath = positional[0];
@@ -0,0 +1,183 @@
1
+ // Editorial design language — extracted from the findings-5 field-report style
2
+ // (egregore.xyz/view/curvelabs/emissary-install-link-findings-5).
3
+ //
4
+ // Scoped under `.edi` so it can coexist with the legacy shell CSS while
5
+ // templates migrate one by one. Dark mode hooks into the shell's
6
+ // [data-theme="dark"] attribute. All theme-sensitive colors are CSS vars —
7
+ // never inline hex in templates (SSR inline styles can't be themed).
8
+ //
9
+ // Serif: LT Superior Serif served from egregore.xyz (same files the site
10
+ // uses). Local file:// renders fall back to Georgia — acceptable degradation.
11
+
12
+ export const EDITORIAL_CSS = `
13
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
14
+ @font-face{font-family:'LT Superior Serif';font-weight:400;font-style:normal;font-display:swap;
15
+ src:url('https://egregore.xyz/fonts/LTSuperiorSerif-Regular.otf') format('opentype')}
16
+ @font-face{font-family:'LT Superior Serif';font-weight:500;font-style:normal;font-display:swap;
17
+ src:url('https://egregore.xyz/fonts/LTSuperiorSerif-Medium.otf') format('opentype')}
18
+
19
+ .edi{
20
+ --e-bg:#F5F2ED; --e-bg-soft:#FAF8F5; --e-surface:#FAF8F5; --e-surface-2:#F1ECE4;
21
+ --e-text:#16100B; --e-text-dim:#6B5E52; --e-text-faint:#9A8D7E;
22
+ --e-border:#E0D8CC; --e-border-soft:#ECE6DD;
23
+ --e-accent:#D4875A; --e-accent-2:#C26A3A; --e-accent-deep:#C4673A;
24
+ --e-green:#6BBF6B; --e-red:#C2603A;
25
+ --e-win-bg:#FBF2EA; --e-win-border:#E8C9AC;
26
+ --e-card-bg:#FBF7F2;
27
+ --e-chip-fg:#9A5A33; --e-chip-bg:#FBF2EA; --e-chip-border:#E8C9AC;
28
+ --e-ok-bg:#E7F1E7; --e-ok-fg:#2E6B3E; --e-ok-border:#C9E0C9;
29
+ --e-warn-bg:#F8EFD6; --e-warn-fg:#876618; --e-warn-border:#E7D6A6;
30
+ --e-fail-bg:#F7E7E2; --e-fail-fg:#A23B27; --e-fail-border:#E8C9BE;
31
+ --e-na-bg:#EEE8DF; --e-na-fg:#6B5E52; --e-na-border:#DCD3C5;
32
+ --e-pre-bg:#1c1712; --e-pre-fg:#f0e8df; --e-pre-border:#2a2018;
33
+ --e-divider:rgba(59,45,33,.28); --e-divider-strong:rgba(59,45,33,.4);
34
+ --e-radius:14px; --e-maxw:860px;
35
+ --e-serif:'LT Superior Serif',Georgia,'Times New Roman',serif;
36
+ --e-mono:'IBM Plex Mono',ui-monospace,Menlo,Consolas,monospace;
37
+ --e-font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
38
+
39
+ background:var(--e-bg);color:var(--e-text);font-family:var(--e-font);
40
+ line-height:1.62;font-size:16.5px;-webkit-font-smoothing:antialiased;
41
+ }
42
+ [data-theme="dark"] .edi{
43
+ --e-bg:#1D1611; --e-bg-soft:#241E19; --e-surface:#241E19; --e-surface-2:#161210;
44
+ --e-text:rgba(255,255,255,.92); --e-text-dim:rgba(255,255,255,.75); --e-text-faint:rgba(255,255,255,.50);
45
+ --e-border:rgba(255,255,255,.10); --e-border-soft:rgba(255,255,255,.06);
46
+ --e-accent:#E7794B; --e-accent-2:#D4875A; --e-accent-deep:#D4875A;
47
+ --e-win-bg:#2A231D; --e-win-border:rgba(231,121,75,.35);
48
+ --e-card-bg:#241E19;
49
+ --e-chip-fg:#E7A47B; --e-chip-bg:#2A231D; --e-chip-border:rgba(231,121,75,.35);
50
+ --e-ok-bg:rgba(107,191,107,.12); --e-ok-fg:#8FD08F; --e-ok-border:rgba(107,191,107,.3);
51
+ --e-warn-bg:rgba(194,138,46,.12); --e-warn-fg:#D8B05E; --e-warn-border:rgba(194,138,46,.3);
52
+ --e-fail-bg:rgba(194,96,58,.12); --e-fail-fg:#DB8A6A; --e-fail-border:rgba(194,96,58,.3);
53
+ --e-na-bg:rgba(255,255,255,.06); --e-na-fg:rgba(255,255,255,.55); --e-na-border:rgba(255,255,255,.12);
54
+ --e-divider:rgba(255,255,255,.18); --e-divider-strong:rgba(255,255,255,.28);
55
+ }
56
+
57
+ .edi *{box-sizing:border-box}
58
+ /* The shell's .eg-artifact already centers and pads; the wrap is a no-op
59
+ inside it but keeps the geometry when a template is rendered standalone. */
60
+ .eg-artifact .edi .edi-wrap{max-width:none;padding:0}
61
+ .edi .edi-wrap{max-width:var(--e-maxw);margin:0 auto;padding:0 28px}
62
+ .edi a{color:var(--e-accent);text-decoration:none}
63
+ .edi h1,.edi h2,.edi h3{font-family:var(--e-serif);font-weight:400;line-height:1.18;letter-spacing:-.02em;color:var(--e-text)}
64
+ .edi h2{font-size:1.92rem;margin:0 0 6px}
65
+ .edi h3{font-size:1.28rem;margin:32px 0 10px}
66
+ .edi h4{font-family:var(--e-serif);font-weight:400;color:var(--e-text)}
67
+ .edi p{color:var(--e-text-dim)}
68
+ .edi p strong,.edi li strong{color:var(--e-text);font-weight:600}
69
+ .edi .mono{font-family:var(--e-mono)}
70
+ .edi code{font-family:var(--e-mono);font-size:.84em;background:var(--e-surface-2);
71
+ border:1px solid var(--e-border);padding:1px 6px;border-radius:5px;color:var(--e-accent-2)}
72
+
73
+ /* header */
74
+ .edi header{padding:54px 0 38px;border-bottom:1px dotted var(--e-divider);background:none}
75
+ .edi .edi-kicker{font-family:var(--e-mono);font-size:.72rem;letter-spacing:2.5px;text-transform:uppercase;
76
+ color:var(--e-accent);margin-bottom:18px}
77
+ .edi header h1{font-size:2.6rem;margin:0 0 18px;max-width:24ch;line-height:1.08}
78
+ .edi .edi-lede{font-size:1.2rem;color:var(--e-text-dim);max-width:60ch;margin:0;font-family:var(--e-serif);
79
+ font-style:italic;letter-spacing:-.01em;line-height:1.4}
80
+ .edi .edi-meta{display:flex;gap:26px;flex-wrap:wrap;margin-top:28px;font-family:var(--e-mono);
81
+ font-size:.76rem;color:var(--e-text-faint)}
82
+ .edi .edi-meta b{color:var(--e-text-dim);font-weight:500}
83
+
84
+ /* sections + dotted divider w/ diamond */
85
+ .edi section{padding:42px 0;border-bottom:1px dotted var(--e-divider);position:relative}
86
+ .edi section::after{content:"";position:absolute;left:50%;bottom:-4px;width:7px;height:7px;
87
+ transform:translateX(-50%) rotate(45deg);background:var(--e-bg);
88
+ border-right:1px dotted var(--e-divider-strong);border-bottom:1px dotted var(--e-divider-strong)}
89
+ .edi section:last-of-type{border-bottom:none}
90
+ .edi section:last-of-type::after{display:none}
91
+ .edi .secnum{font-family:var(--e-mono);font-size:.74rem;color:var(--e-accent);letter-spacing:2px;margin-bottom:4px}
92
+ .edi .lead-in{font-size:1.04rem;color:var(--e-text-dim);max-width:64ch}
93
+
94
+ /* callout + note */
95
+ .edi .callout{border-left:2px solid var(--e-accent);background:var(--e-surface);
96
+ border-radius:0 10px 10px 0;padding:16px 22px;margin:24px 0;color:var(--e-text-dim)}
97
+ .edi .callout b{color:var(--e-text)}
98
+ .edi .note{border:1px solid var(--e-border);border-left:4px solid var(--e-text-faint);border-radius:8px;
99
+ background:var(--e-surface-2);padding:12px 15px;margin:12px 0;color:var(--e-text-dim);font-size:14px}
100
+ .edi .note.warn{border-left-color:var(--e-warn-fg)}
101
+
102
+ /* accent-left card (decisions, concepts) */
103
+ .edi .edicard{border:1px solid var(--e-border);border-left:4px solid var(--e-accent-deep);border-radius:8px;
104
+ background:var(--e-card-bg);padding:14px 16px;margin:10px 0}
105
+ .edi .edicard .cid{display:inline-block;font:600 12px/1 var(--e-mono);color:#fff;background:var(--e-accent-deep);
106
+ border-radius:5px;padding:4px 7px;margin-right:8px;vertical-align:middle}
107
+ .edi .edicard h4{display:inline;font-size:16px;margin:0}
108
+ .edi .edicard p{margin:8px 0 0;font-size:15px}
109
+ .edi .edicard .lbl{font:600 11px/1 var(--e-font);letter-spacing:.06em;text-transform:uppercase;
110
+ color:var(--e-text-faint);margin:10px 0 2px}
111
+
112
+ /* stat / claim cards */
113
+ .edi .claims{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:24px}
114
+ .edi .claims.three{grid-template-columns:1fr 1fr 1fr}
115
+ @media(max-width:680px){.edi .claims,.edi .claims.three{grid-template-columns:1fr}}
116
+ .edi .claim{background:var(--e-bg);border:1px solid var(--e-border);border-radius:10px;padding:18px 20px}
117
+ .edi .claim .n{font-family:var(--e-serif);font-size:1.62rem;color:var(--e-accent)}
118
+ .edi .claim .l{font-size:.85rem;color:var(--e-text-dim);margin-top:6px;line-height:1.45}
119
+
120
+ /* tables */
121
+ .edi table{width:100%;border-collapse:collapse;margin:24px 0;font-size:.9rem}
122
+ .edi th,.edi td{text-align:left;padding:13px 14px;border-bottom:1px solid var(--e-border);vertical-align:top}
123
+ .edi th{font-family:var(--e-mono);font-size:.72rem;letter-spacing:.5px;text-transform:uppercase;
124
+ color:var(--e-text-faint);font-weight:500}
125
+ .edi td{color:var(--e-text-dim)}
126
+ .edi tr td:first-child{color:var(--e-text)}
127
+ .edi .hl{color:var(--e-accent);font-weight:600}
128
+
129
+ /* chips / pills / badges */
130
+ .edi .chip{display:inline-block;font:600 11px/1 var(--e-font);text-decoration:none;color:var(--e-chip-fg);
131
+ background:var(--e-chip-bg);border:1px solid var(--e-chip-border);border-radius:20px;padding:4px 9px;white-space:nowrap}
132
+ .edi .pill{display:inline-block;font-family:var(--e-mono);font-size:.66rem;padding:2px 8px;border-radius:20px;
133
+ border:1px solid var(--e-border);background:var(--e-bg);color:var(--e-text-faint)}
134
+ .edi .badge{font:600 11px/1 var(--e-font);border-radius:20px;padding:5px 10px;white-space:nowrap}
135
+ .edi .b-win{background:var(--e-ok-bg);color:var(--e-ok-fg);border:1px solid var(--e-ok-border)}
136
+ .edi .b-fail{background:var(--e-fail-bg);color:var(--e-fail-fg);border:1px solid var(--e-fail-border)}
137
+ .edi .b-warn{background:var(--e-warn-bg);color:var(--e-warn-fg);border:1px solid var(--e-warn-border)}
138
+ .edi .b-na{background:var(--e-na-bg);color:var(--e-na-fg);border:1px solid var(--e-na-border)}
139
+
140
+ /* paste blocks */
141
+ .edi .tryblock{border:1px solid var(--e-border);border-radius:10px;background:var(--e-card-bg);padding:14px 16px;margin:12px 0}
142
+ .edi .tryhead{font-size:14px;color:var(--e-text-dim);margin-bottom:10px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
143
+ .edi .tryhead b{color:var(--e-text)}
144
+ .edi pre.copyline,.edi pre{background:var(--e-pre-bg);color:var(--e-pre-fg);border:1px solid var(--e-pre-border);
145
+ border-radius:8px;padding:12px 14px;font-family:var(--e-mono);font-size:12.5px;line-height:1.5;
146
+ white-space:pre-wrap;word-break:break-word;overflow-x:auto;margin:0}
147
+ .edi pre code{background:none;border:none;padding:0;color:inherit;font-size:inherit}
148
+
149
+ /* numbered steps */
150
+ .edi .steps{counter-reset:estep;margin:22px 0}
151
+ .edi .step{display:grid;grid-template-columns:34px 1fr;gap:16px;padding:14px 0;border-bottom:1px solid var(--e-border-soft)}
152
+ .edi .step:last-child{border-bottom:none}
153
+ .edi .step .b{counter-increment:estep;font-family:var(--e-serif);color:var(--e-accent);font-size:1.2rem}
154
+ .edi .step .b::before{content:counter(estep)}
155
+ .edi .step h4{margin:0 0 4px;font-size:1.06rem}
156
+ .edi .step p{margin:0;font-size:.88rem}
157
+
158
+ /* checkbox thread list */
159
+ .edi .threads{list-style:none;padding:0;margin:18px 0}
160
+ .edi .threads li{display:flex;gap:12px;padding:9px 0;border-bottom:1px solid var(--e-border-soft);
161
+ font-size:.95rem;color:var(--e-text-dim);align-items:baseline}
162
+ .edi .threads li::before{content:"";flex:0 0 auto;width:11px;height:11px;border:1.5px solid var(--e-accent);
163
+ border-radius:3px;transform:translateY(1px)}
164
+
165
+ .edi ul,.edi ol{color:var(--e-text-dim)}
166
+ .edi li{margin:5px 0}
167
+
168
+ /* timeline (extension chain) */
169
+ .edi .chainline{position:relative;padding-left:20px;border-left:2px solid var(--e-win-border);margin-top:18px}
170
+ .edi .chainline .dot{position:absolute;left:-7px;width:12px;height:12px;border-radius:50%;
171
+ background:var(--e-accent);border:2px solid var(--e-bg)}
172
+
173
+ /* footer */
174
+ .edi footer{padding:38px 0 64px;color:var(--e-text-faint);font-size:.8rem;font-family:var(--e-mono);background:none}
175
+ .edi .tag-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px}
176
+ .edi .tg{font-family:var(--e-mono);font-size:.66rem;color:var(--e-text-faint);border:1px solid var(--e-border);
177
+ border-radius:6px;padding:3px 9px;background:var(--e-surface)}
178
+ `;
179
+
180
+ // One <style> tag per render — safe to call from multiple components.
181
+ export function editorialStyleTag(h) {
182
+ return h('style', { dangerouslySetInnerHTML: { __html: EDITORIAL_CSS } });
183
+ }
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ import { renderToHtml, renderSpecToHtml } from './render.js';
6
6
  import { parseQuest } from './parsers/quest.js';
7
7
  import { parseHandoff } from './parsers/handoff.js';
8
8
  import { parseHandoffV1 } from './parsers/handoff-v1.js';
9
+ import { parseEmissary } from './parsers/emissary.js';
9
10
  import { parseActivity } from './parsers/activity.js';
10
11
  import { parseDocument } from './parsers/document.js';
11
12
  import { parseBoard } from './parsers/board.js';
@@ -13,6 +14,7 @@ import { parseNetwork } from './parsers/network.js';
13
14
  import { questTemplate } from './templates/quest.js';
14
15
  import { handoffTemplate } from './templates/handoff.js';
15
16
  import { handoffV1Template } from './templates/handoff-v1.js';
17
+ import { emissaryTemplate } from './templates/emissary.js';
16
18
  import { activityTemplate } from './templates/activity.js';
17
19
  import { documentTemplate } from './templates/document.js';
18
20
  import { boardTemplate } from './templates/board.js';
@@ -21,8 +23,8 @@ import { openInBrowser } from './open.js';
21
23
  import { setLinkContext, clearLinkContext } from './markdown.js';
22
24
  import { derivedParent, loadComments } from './comments.js';
23
25
 
24
- const PARSERS = { quest: parseQuest, handoff: parseHandoff, 'handoff-v1': parseHandoffV1, activity: parseActivity, document: parseDocument, board: parseBoard, network: parseNetwork };
25
- const TEMPLATES = { quest: questTemplate, handoff: handoffTemplate, 'handoff-v1': handoffV1Template, activity: activityTemplate, document: documentTemplate, board: boardTemplate, network: networkTemplate };
26
+ const PARSERS = { quest: parseQuest, handoff: parseHandoff, 'handoff-v1': parseHandoffV1, emissary: parseEmissary, activity: parseActivity, document: parseDocument, board: parseBoard, network: parseNetwork };
27
+ const TEMPLATES = { quest: questTemplate, handoff: handoffTemplate, 'handoff-v1': handoffV1Template, emissary: emissaryTemplate, activity: activityTemplate, document: documentTemplate, board: boardTemplate, network: networkTemplate };
26
28
 
27
29
  const COMMENTABLE_TYPES = new Set(['quest', 'handoff', 'document']);
28
30
  export async function generateArtifact(type, input, options = {}) {
@@ -0,0 +1,56 @@
1
+ // Parse the emissary human-render input.
2
+ //
3
+ // An emissary's human render is METADATA-ONLY. The input is NOT the full
4
+ // handoff v1 artifact — it is a small projection the API composes
5
+ // (topic / summary / kind / distribution_mode / author / CTA URLs /
6
+ // render mode), deliberately excluding receiver_instructions,
7
+ // body.prose, and body.executable_spec. Those live only in the agent
8
+ // payload at /emissary/e/{id}/raw and must never reach the human render.
9
+ //
10
+ // Input: file path to a JSON file, OR a JSON string, OR a parsed object.
11
+ // Output: the normalized object the emissary template consumes.
12
+ import fs from 'node:fs';
13
+
14
+ export function parseEmissary(input) {
15
+ let obj;
16
+
17
+ if (typeof input === 'string') {
18
+ if (input.trim().startsWith('{')) {
19
+ obj = JSON.parse(input);
20
+ } else {
21
+ if (!fs.existsSync(input)) throw new Error(`File not found: ${input}`);
22
+ obj = JSON.parse(fs.readFileSync(input, 'utf-8'));
23
+ }
24
+ } else if (typeof input === 'object' && input !== null) {
25
+ obj = input;
26
+ } else {
27
+ throw new Error('parseEmissary expects a file path, JSON string, or object');
28
+ }
29
+
30
+ if (!obj.topic) {
31
+ throw new Error('emissary render input is missing required field: topic');
32
+ }
33
+
34
+ const renderMode = obj.render_mode === 'custom' ? 'custom' : 'default';
35
+
36
+ return {
37
+ id: obj.id || '',
38
+ topic: obj.topic,
39
+ summary: obj.summary || '',
40
+ kind: obj.kind || 'handoff',
41
+ distribution_mode: obj.distribution_mode || 'public',
42
+ author_name: obj.author_name || '',
43
+ author_verified: Boolean(obj.author_verified),
44
+ created_at: obj.created_at || null,
45
+ expires_at: obj.expires_at || null,
46
+ render_url: obj.render_url || '',
47
+ raw_url: obj.raw_url || '',
48
+ render_mode: renderMode,
49
+ // Custom user HTML — only honored for render_mode === 'custom'. The
50
+ // template embeds it inside a sandboxed iframe; it is never trusted
51
+ // as page chrome.
52
+ render_html: renderMode === 'custom' ? (obj.render_html || '') : null,
53
+ // Convenience for the shell title.
54
+ title: obj.topic,
55
+ };
56
+ }
package/lib/shell.js CHANGED
@@ -208,6 +208,33 @@ export function htmlShell(bodyHtml, { title = 'Egregore Artifact', type = 'artif
208
208
  color: var(--muted);
209
209
  margin-bottom: 0.75rem;
210
210
  }
211
+ .eg-copy-row {
212
+ display: flex;
213
+ flex-wrap: wrap;
214
+ gap: 8px;
215
+ margin-top: 12px;
216
+ }
217
+ .eg-copy-button {
218
+ appearance: none;
219
+ border: 1px solid var(--border);
220
+ background: var(--surface);
221
+ color: var(--black);
222
+ border-radius: 5px;
223
+ padding: 6px 10px;
224
+ font-family: var(--font-mono);
225
+ font-size: 12px;
226
+ line-height: 1.2;
227
+ cursor: pointer;
228
+ }
229
+ .eg-copy-button:hover {
230
+ border-color: var(--terracotta);
231
+ color: var(--terracotta);
232
+ }
233
+ .eg-copy-button.eg-copy-ok {
234
+ background: var(--success-bg);
235
+ border-color: var(--success-fg);
236
+ color: var(--success-fg);
237
+ }
211
238
 
212
239
  /* Text */
213
240
  .eg-lead {
@@ -734,6 +761,52 @@ export function htmlShell(bodyHtml, { title = 'Egregore Artifact', type = 'artif
734
761
  <div class="eg-artifact" data-type="${escapeHtml(type)}">
735
762
  ${bodyHtml}
736
763
  </div>
764
+ <script>
765
+ // Generic clipboard affordance for rendered artifacts. Buttons declare
766
+ // their payload with data-copy-text so templates do not need custom JS.
767
+ (function() {
768
+ function copyText(text) {
769
+ if (!text) return Promise.reject(new Error('nothing to copy'));
770
+ if (navigator.clipboard && navigator.clipboard.writeText) {
771
+ return navigator.clipboard.writeText(text);
772
+ }
773
+ return new Promise(function(resolve, reject) {
774
+ var ta = document.createElement('textarea');
775
+ ta.value = text;
776
+ ta.setAttribute('readonly', '');
777
+ ta.style.position = 'fixed';
778
+ ta.style.left = '-9999px';
779
+ document.body.appendChild(ta);
780
+ ta.select();
781
+ try {
782
+ if (document.execCommand && document.execCommand('copy')) resolve();
783
+ else reject(new Error('copy unavailable'));
784
+ } catch (e) {
785
+ reject(e);
786
+ } finally {
787
+ document.body.removeChild(ta);
788
+ }
789
+ });
790
+ }
791
+
792
+ document.addEventListener('click', function(e) {
793
+ var btn = e.target.closest && e.target.closest('[data-copy-text]');
794
+ if (!btn) return;
795
+ var original = btn.getAttribute('data-copy-label') || btn.textContent;
796
+ copyText(btn.getAttribute('data-copy-text')).then(function() {
797
+ btn.textContent = 'Copied';
798
+ btn.classList.add('eg-copy-ok');
799
+ setTimeout(function() {
800
+ btn.textContent = original;
801
+ btn.classList.remove('eg-copy-ok');
802
+ }, 1600);
803
+ }, function() {
804
+ btn.textContent = 'Select text';
805
+ setTimeout(function() { btn.textContent = original; }, 1600);
806
+ });
807
+ });
808
+ })();
809
+ </script>
737
810
  <script>
738
811
  // Comment composer — assembles a structured payload and copies it to the
739
812
  // clipboard. The user pastes into Claude; the /comment skill recognizes
@@ -1,52 +1,66 @@
1
- // Generic document → React element tree
1
+ // Generic document → React element tree — editorial field-report style.
2
+ // Markdown ## headings become numbered sections with dotted dividers.
2
3
  import React from 'react';
3
- import { ArtifactHeader, SectionCard, TextBlock, ArtifactFooter } from '../components.js';
4
+ import { renderMarkdown } from '../markdown.js';
5
+ import { editorialStyleTag } from '../editorial.js';
4
6
 
5
7
  const h = React.createElement;
6
8
 
7
9
  export function documentTemplate(doc) {
8
10
  const sections = [];
11
+ let num = 0;
9
12
 
10
13
  // Header
11
14
  sections.push(
12
- h(ArtifactHeader, {
13
- key: 'header',
14
- title: doc.title,
15
- type: 'document',
16
- date: doc.date,
17
- author: doc.author,
18
- status: doc.status,
19
- priority: 0,
20
- projects: doc.frontmatter?.projects || [],
21
- })
15
+ h('header', { key: 'header' },
16
+ h('div', { className: 'edi-kicker' }, '✦ egregore · document'),
17
+ h('h1', null, doc.title),
18
+ h('div', { className: 'edi-meta' },
19
+ doc.author && h('span', null, 'Author ', h('b', null, doc.author)),
20
+ doc.date && h('span', null, 'Date ', h('b', null, doc.date)),
21
+ doc.status && h('span', null, 'Status ', h('b', null, doc.status)),
22
+ ...(doc.frontmatter?.projects || []).map((p, i) =>
23
+ h('span', { key: `p-${i}` }, h('b', null, p))),
24
+ ),
25
+ )
22
26
  );
23
27
 
24
28
  // Preamble (text before first ## heading)
25
29
  if (doc.preamble) {
26
30
  sections.push(
27
- h(SectionCard, { key: 'preamble', label: null },
28
- h(TextBlock, { text: doc.preamble, variant: 'lead' })
31
+ h('section', { key: 'preamble' },
32
+ h('div', { className: 'lead-in', style: { fontSize: '1.1rem' } },
33
+ renderMarkdown(doc.preamble)),
29
34
  )
30
35
  );
31
36
  }
32
37
 
33
- // Body sections
38
+ // Body sections — each ## heading gets a secnum
34
39
  for (const section of doc.sections) {
40
+ num += 1;
35
41
  sections.push(
36
- h(SectionCard, { key: `s-${section.heading}`, label: section.heading },
37
- h(TextBlock, { text: section.body })
42
+ h('section', { key: `s-${section.heading}` },
43
+ h('div', { className: 'secnum' }, `${String(num).padStart(2, '0')} — ${section.heading}`),
44
+ h('h2', null, section.heading),
45
+ renderMarkdown(section.body),
38
46
  )
39
47
  );
40
48
  }
41
49
 
42
50
  // Footer
43
51
  sections.push(
44
- h(ArtifactFooter, {
45
- key: 'footer',
46
- generatedAt: new Date().toISOString(),
47
- source: doc.source,
48
- })
52
+ h('footer', { key: 'footer' },
53
+ h('div', null, `Egregore — ${doc.title}`),
54
+ h('div', { className: 'tag-row' },
55
+ h('span', { className: 'tg' }, 'document'),
56
+ doc.author && h('span', { className: 'tg' }, doc.author),
57
+ doc.source && h('span', { className: 'tg' }, doc.source),
58
+ ),
59
+ )
49
60
  );
50
61
 
51
- return h('div', null, ...sections);
62
+ return h('div', { className: 'edi' },
63
+ editorialStyleTag(h),
64
+ h('div', { className: 'edi-wrap' }, ...sections),
65
+ );
52
66
  }