atris 3.30.1 → 3.30.3

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.
Files changed (67) hide show
  1. package/AGENTS.md +6 -0
  2. package/atris/AGENTS.md +11 -0
  3. package/atris/CLAUDE.md +5 -0
  4. package/atris/atris.md +19 -4
  5. package/atris/policies/atris-design.md +71 -0
  6. package/atris/policies/design-seed.md +187 -0
  7. package/atris/skills/atris/SKILL.md +26 -3
  8. package/atris/skills/design/SKILL.md +3 -2
  9. package/atris/skills/loop/SKILL.md +5 -3
  10. package/atris/team/_template/MEMBER.md +19 -0
  11. package/atris.md +63 -23
  12. package/ax +1434 -1698
  13. package/bin/atris.js +5 -1
  14. package/commands/agent-spawn.js +2 -2
  15. package/commands/brain.js +92 -7
  16. package/commands/brainstorm.js +62 -22
  17. package/commands/business-sync.js +92 -8
  18. package/commands/business.js +13 -7
  19. package/commands/chat-scan.js +102 -0
  20. package/commands/computer.js +758 -15
  21. package/commands/deck.js +505 -105
  22. package/commands/init.js +6 -1
  23. package/commands/launchpad.js +638 -0
  24. package/commands/log-sync.js +44 -13
  25. package/commands/loop-front.js +290 -0
  26. package/commands/member.js +124 -3
  27. package/commands/mission.js +717 -32
  28. package/commands/pull.js +37 -28
  29. package/commands/pulse.js +11 -8
  30. package/commands/run.js +79 -39
  31. package/commands/sync.js +36 -17
  32. package/commands/task.js +342 -66
  33. package/commands/xp.js +3 -0
  34. package/commands/youtube.js +221 -7
  35. package/decks/README.md +89 -0
  36. package/decks/archetype-catalog.json +180 -0
  37. package/decks/atris-antislop-pitch.json +48 -0
  38. package/decks/atris-archetypes-v2.json +83 -0
  39. package/decks/atris-one-loop-pitch.json +80 -0
  40. package/decks/atris-seed-pitch-v3.json +118 -0
  41. package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
  42. package/decks/atris-seed-pitch-v5.json +109 -0
  43. package/decks/atris-seed-pitch-v6.json +137 -0
  44. package/decks/atris-seed-pitch-v7.json +133 -0
  45. package/decks/atris-single-shot-proof.json +74 -0
  46. package/decks/mark-pincus-narrative.json +102 -0
  47. package/decks/mark-pincus-sourcery.json +94 -0
  48. package/decks/yash-applied-compute-detailed.json +150 -0
  49. package/decks/yash-applied-compute-generalist.json +82 -0
  50. package/decks/yash-applied-compute-narrative.json +54 -0
  51. package/lib/ax-prefs.js +5 -12
  52. package/lib/chat-log-scan.js +377 -0
  53. package/lib/context-gatherer.js +35 -1
  54. package/lib/deck-compose.js +145 -0
  55. package/lib/deck-history.js +64 -0
  56. package/lib/deck-layout.js +169 -0
  57. package/lib/deck-review.js +431 -0
  58. package/lib/deck-schema.js +154 -0
  59. package/lib/file-ops.js +2 -2
  60. package/lib/functional-owner.js +189 -0
  61. package/lib/slides-deck.js +512 -58
  62. package/lib/task-db.js +109 -2
  63. package/package.json +2 -1
  64. package/templates/business-starter/team/START_HERE.md +12 -8
  65. package/utils/auth.js +4 -0
  66. package/utils/config.js +4 -0
  67. package/atris/atrisDev.md +0 -717
@@ -8,7 +8,7 @@
8
8
  // stay sentence case, no gradient text, no glassmorphism, one accent hue.
9
9
  //
10
10
  // Spec shape (see commands/deck.js for the CLI):
11
- // { theme: 'terminal'|'paper',
11
+ // { theme: 'terminal'|'paper'|'ink'|'noir',
12
12
  // brand: { name: 'Sentinel', accent: '.' },
13
13
  // slides: [ { type, ...fields } ] }
14
14
  // Emphasis: wrap a phrase in **double asterisks** to render it in the accent.
@@ -29,6 +29,20 @@ const THEMES = {
29
29
  accent: '#B5572E', accent2: '#9A4723', onAccent: '#FFFFFF',
30
30
  sev: ['#B5572E', '#C0883A', '#5F7787'] },
31
31
  },
32
+ ink: { // stark light "magazine proof"
33
+ fonts: { display: 'Fraunces', body: 'Outfit', mono: 'Roboto Mono' },
34
+ color: { bg: '#FAFAF8', panel: '#FFFFFF', panelAlt: '#F2F2EE', line: '#D8D8D2',
35
+ ink: '#111110', soft: '#4A4A46', faint: '#7A7A74',
36
+ accent: '#C41E3A', accent2: '#9B1830', onAccent: '#FFFFFF',
37
+ sev: ['#C41E3A', '#B8860B', '#2F4F6F'] },
38
+ },
39
+ noir: { // cool dark "broadcast / podcast"
40
+ fonts: { display: 'Fraunces', body: 'Outfit', mono: 'Roboto Mono' },
41
+ color: { bg: '#0E1117', panel: '#161B22', panelAlt: '#1C2129', line: '#2D333B',
42
+ ink: '#E6EDF3', soft: '#9BA7B4', faint: '#6E7681',
43
+ accent: '#58A6FF', accent2: '#79C0FF', onAccent: '#0E1117',
44
+ sev: ['#58A6FF', '#D2A854', '#3FB950'] },
45
+ },
32
46
  };
33
47
  const COLOR_ROLES = ['bg', 'panel', 'line', 'ink', 'soft', 'faint', 'accent', 'accent2', 'onAccent'];
34
48
 
@@ -62,6 +76,33 @@ function parseEmph(text) {
62
76
  return { plain, ranges };
63
77
  }
64
78
 
79
+ // ---------- fit estimation (keep dense text on the slide) ----------
80
+ // Slides text boxes do not auto-shrink: text that wraps past the slide edge
81
+ // (H = 405 PT) is clipped. These estimate wrapped height so an archetype can
82
+ // pick the largest font that still fits the vertical space it has.
83
+ function estLines(text, w, size) {
84
+ const cpl = Math.max(1, Math.floor(w / (size * 0.52))); // ~chars per line, proportional
85
+ return String(text == null ? '' : text)
86
+ .split('\n')
87
+ .reduce((acc, line) => acc + Math.max(1, Math.ceil(line.length / cpl)), 0);
88
+ }
89
+ function lineHeight(size, linePct) { return size * 1.15 * (linePct / 100); }
90
+ // Largest size in [minSize, baseSize] whose wrapped text fits maxH.
91
+ function fitSize(text, w, maxH, baseSize, minSize, linePct = 120) {
92
+ for (let s = baseSize; s > minSize; s -= 0.5) {
93
+ if (estLines(text, w, s) * lineHeight(s, linePct) <= maxH) return s;
94
+ }
95
+ return minSize;
96
+ }
97
+ // Smallest fit across several texts sharing one column width (uniform sizing).
98
+ function fitSizeAll(texts, w, maxH, baseSize, minSize, linePct = 120) {
99
+ return Math.min(baseSize, ...texts.map((t) => fitSize(t, w, maxH, baseSize, minSize, linePct)));
100
+ }
101
+
102
+ // Coerce a value to an array so a malformed spec degrades instead of crashing
103
+ // the engine (which is called directly, bypassing the schema gate).
104
+ const arr = (v) => (Array.isArray(v) ? v : []);
105
+
65
106
  function makeCtx(theme) {
66
107
  const C = theme.color, F = theme.fonts, requests = [];
67
108
  let uid = 0; const nid = (p) => `${p}_${String(++uid).padStart(4, '0')}`;
@@ -113,9 +154,9 @@ function makeCtx(theme) {
113
154
  return id; }
114
155
 
115
156
  // generalized data panel: header + rows + footer
116
- function panel(slide, x, y, w, data) {
117
- const rows = (data.rows || []).slice(0, 4);
118
- const rowH = 38, headH = data.header ? 30 : 0, footH = data.footer ? 26 : 0;
157
+ function panel(slide, x, y, w, data = {}) {
158
+ const rows = (Array.isArray(data.rows) ? data.rows : []).slice(0, 4).map((r) => r || {});
159
+ const rowH = 38, headH = data.header ? 30 : 0, footH = data.footer ? 28 : 0;
119
160
  const h = headH + rowH * rows.length + footH;
120
161
  fill(shape('ROUND_RECTANGLE', slide, x, y, w, h), C.panel, C.line, 1);
121
162
  if (data.header) {
@@ -125,9 +166,8 @@ function makeCtx(theme) {
125
166
  }
126
167
  rows.forEach((r, i) => {
127
168
  const ry = y + headH + i * rowH;
128
- if (r.active) fill(shape('RECTANGLE', slide, x, ry, 2, rowH), C.accent);
129
169
  const sev = C.sev[(r.sev != null ? r.sev : 0) % C.sev.length];
130
- fill(shape('ELLIPSE', slide, x + 16, ry + rowH / 2 - 3.5, 7, 7), sev);
170
+ fill(shape('ELLIPSE', slide, x + 16, ry + rowH / 2 - 3.5, 7, 7), r.active ? C.accent : sev);
131
171
  const nameTxt = sanitize(r.title || '') + (r.sub ? '\n' + sanitize(r.sub) : '');
132
172
  const nb = box(slide, x + 30, ry + 6, w - 110, 28, nameTxt, { family: F.body, size: 11, color: C.ink, line: 108 });
133
173
  if (r.sub && nb) styleRange(nb, sanitize(r.title || '').length + 1, nameTxt.length, { family: F.body, size: 9, color: C.faint });
@@ -139,19 +179,37 @@ function makeCtx(theme) {
139
179
  }
140
180
  if (i < rows.length - 1) fill(shape('RECTANGLE', slide, x, ry + rowH, w, 0.75), C.panelAlt);
141
181
  });
142
- if (data.footer) { const fy = y + headH + rowH * rows.length;
143
- box(slide, x + 14, fy + 6, w * 0.62, 14, data.footer.left || '', { family: F.body, size: 9, color: C.faint });
144
- if (data.footer.right) box(slide, x + w - 84, fy + 6, 70, 14, data.footer.right, { family: F.body, size: 9, color: C.accent2, align: 'END' }); }
182
+ if (data.footer) {
183
+ const fy = y + headH + rowH * rows.length;
184
+ box(slide, x + 14, fy + 5, w * 0.55, 16, data.footer.left || '',
185
+ { family: F.body, size: 9, color: C.faint });
186
+ if (data.footer.right) {
187
+ box(slide, x + w - 108, fy + 5, 94, 16, data.footer.right,
188
+ { family: F.body, size: 9, color: C.accent2, align: 'END' });
189
+ }
190
+ }
145
191
  return h; }
146
192
 
147
- function chips(slide, x, y, list) { let cx = x;
148
- (list || []).forEach((label) => { const t = sanitize(label); const w = 16 + t.length * 6.3;
149
- fill(shape('ROUND_RECTANGLE', slide, cx, y, w, 24), C.panel, C.line, 1);
150
- box(slide, cx, y, w, 24, t, { family: F.mono, size: 9.5, color: C.faint, align: 'CENTER', vmid: true });
151
- cx += w + 10; }); }
193
+ function chips(slide, x, y, list, maxW = W - M * 2) {
194
+ let cx = x;
195
+ let cy = y;
196
+ const rowH = 32;
197
+ arr(list).forEach((label) => {
198
+ const t = sanitize(label);
199
+ const w = Math.min(maxW, 16 + t.length * 6.3);
200
+ if (cx > x && cx + w > x + maxW) {
201
+ cx = x;
202
+ cy += rowH;
203
+ }
204
+ fill(shape('ROUND_RECTANGLE', slide, cx, cy, w, 24), C.panel, C.line, 1);
205
+ box(slide, cx, cy, w, 24, t, { family: F.mono, size: 9.5, color: C.faint, align: 'CENTER', vmid: true });
206
+ cx += w + 10;
207
+ });
208
+ return cy + 24;
209
+ }
152
210
 
153
211
  function buttons(slide, y, list) {
154
- const items = (list || []).slice(0, 3); const bw = 150, bh = 34, gap = 12;
212
+ const items = arr(list).slice(0, 3); const bw = 150, bh = 34, gap = 12;
155
213
  const total = items.length * bw + (items.length - 1) * gap; let bx = (W - total) / 2;
156
214
  items.forEach((b) => { const primary = b.primary;
157
215
  fill(shape('ROUND_RECTANGLE', slide, bx, y, bw, bh), primary ? C.ink : C.bg, primary ? null : C.line, 1);
@@ -164,74 +222,470 @@ function makeCtx(theme) {
164
222
  // ---------- slide archetypes ----------
165
223
  const ARCHE = {
166
224
  title(ctx, slide, s, spec) {
167
- ctx.wordmark(slide, M, 34, 15, spec.brand);
168
- ctx.rule(slide, M, 96, 40);
169
225
  const hasPanel = !!s.panel;
170
- ctx.box(slide, M, 110, hasPanel ? 360 : 600, 180, s.headline || s.title || '',
171
- { family: ctx.F.display, size: 40, color: ctx.C.ink, line: 100, emphItalic: true });
172
- if (s.sub) ctx.box(slide, M, hasPanel ? 300 : 300, hasPanel ? 350 : 540, 70, s.sub, { family: ctx.F.body, size: 12.5, color: ctx.C.soft, line: 120 });
173
- if (hasPanel) ctx.panel(slide, 432, 96, 240, s.panel);
226
+ ctx.wordmark(slide, M, 28, 14, spec.brand);
227
+ ctx.rule(slide, M, 86, 40);
228
+ const headlineSize = hasPanel ? 34 : 38;
229
+ const headlineW = hasPanel ? 340 : 580;
230
+ const headlineH = hasPanel ? 132 : 150;
231
+ ctx.box(slide, M, 98, headlineW, headlineH, s.headline || s.title || '',
232
+ { family: ctx.F.display, size: headlineSize, color: ctx.C.ink, line: 96 });
233
+ if (s.sub) {
234
+ ctx.box(slide, M, hasPanel ? 236 : 258, hasPanel ? 330 : 520, 88, s.sub,
235
+ { family: ctx.F.body, size: 12, color: ctx.C.soft, line: 118 });
236
+ }
237
+ if (hasPanel) ctx.panel(slide, 404, 88, 268, s.panel);
174
238
  },
175
239
  statement(ctx, slide, s, spec) {
176
- ctx.wordmark(slide, M, 34, 13, spec.brand);
177
- ctx.rule(slide, M, 150, 40);
178
- ctx.box(slide, M, 164, 600, 120, s.text || s.headline || '', { family: ctx.F.display, size: 46, color: ctx.C.ink, line: 100, emphItalic: true });
179
- if (s.sub) ctx.box(slide, M, 286, 480, 70, s.sub, { family: ctx.F.body, size: 14, color: ctx.C.soft, line: 130 });
240
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
241
+ ctx.rule(slide, M, 138, 40);
242
+ const text = s.text || s.headline || '';
243
+ const textSize = fitSize(text, 580, 120, 38, 26, 96);
244
+ ctx.box(slide, M, 150, 580, 118, text,
245
+ { family: ctx.F.display, size: textSize, color: ctx.C.ink, line: 96 });
246
+ if (s.sub) {
247
+ const subSize = fitSize(s.sub, 560, 110, 12.5, 10, 120);
248
+ ctx.box(slide, M, 276, 560, 108, s.sub,
249
+ { family: ctx.F.body, size: subSize, color: ctx.C.soft, line: 120 });
250
+ }
180
251
  },
181
252
  columns(ctx, slide, s, spec) {
182
- ctx.wordmark(slide, M, 34, 13, spec.brand);
183
- if (s.heading) ctx.box(slide, M, 110, 560, 34, s.heading, { family: ctx.F.display, size: 26, color: ctx.C.ink });
184
- const cols = (s.columns || []).slice(0, 4); const n = cols.length || 1;
185
- const span = W - M * 2, gap = 16, colW = (span - (n - 1) * gap) / n, cy = s.heading ? 188 : 150;
186
- cols.forEach((c, i) => { const cx = M + i * (colW + gap);
187
- if (i > 0) ctx.fill(ctx.shape('RECTANGLE', slide, cx - gap / 2, cy, 0.75, 120), ctx.C.line);
188
- ctx.box(slide, cx, cy, colW - 8, 30, c.h || c.title || '', { family: ctx.F.display, size: 17, color: ctx.C.ink });
189
- ctx.box(slide, cx, cy + 34, colW - 8, 120, c.b || c.body || '', { family: ctx.F.body, size: 11.5, color: ctx.C.soft, line: 134 }); });
253
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
254
+ const headingY = 96;
255
+ if (s.heading) {
256
+ ctx.box(slide, M, headingY, 580, 40, s.heading,
257
+ { family: ctx.F.display, size: 24, color: ctx.C.ink, line: 110 });
258
+ }
259
+ const cols = arr(s.columns).slice(0, 4);
260
+ const n = cols.length || 1;
261
+ const span = W - M * 2;
262
+ const gap = 14;
263
+ const colW = (span - (n - 1) * gap) / n;
264
+ const cy = s.heading ? 168 : 132;
265
+ // One uniform body size: the largest that fits every column's text in the
266
+ // space down to the slide floor. Narrower columns (more of them) shrink more.
267
+ const bodyMaxH = 388 - (cy + 32);
268
+ const bodySize = fitSizeAll(cols.map((c) => c.b || c.body || ''), colW - 6, bodyMaxH, 11, 8.5, 128);
269
+ cols.forEach((c, i) => {
270
+ const cx = M + i * (colW + gap);
271
+ if (i > 0) ctx.fill(ctx.shape('RECTANGLE', slide, cx - gap / 2, cy, 0.75, 132), ctx.C.line);
272
+ ctx.box(slide, cx, cy, colW - 6, 28, c.h || c.title || '',
273
+ { family: ctx.F.display, size: 16, color: ctx.C.ink, line: 110 });
274
+ ctx.box(slide, cx, cy + 32, colW - 6, 148, c.b || c.body || '',
275
+ { family: ctx.F.body, size: bodySize, color: ctx.C.soft, line: 128 });
276
+ });
190
277
  },
191
278
  panel(ctx, slide, s, spec) {
192
- ctx.wordmark(slide, M, 34, 13, spec.brand);
193
- if (s.heading) ctx.box(slide, M, 120, 260, 50, s.heading, { family: ctx.F.display, size: 30, color: ctx.C.ink });
194
- if (s.sub) ctx.box(slide, M, 178, 260, 160, s.sub, { family: ctx.F.body, size: 12.5, color: ctx.C.soft, line: 132 });
195
- ctx.panel(slide, 360, 110, 312, s.panel || { rows: [] });
279
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
280
+ const headingY = 90;
281
+ const subY = 152;
282
+ if (s.heading) {
283
+ ctx.box(slide, M, headingY, 312, 54, s.heading,
284
+ { family: ctx.F.display, size: 24, color: ctx.C.ink, line: 108 });
285
+ }
286
+ if (s.sub) {
287
+ ctx.box(slide, M, subY, 312, 84, s.sub,
288
+ { family: ctx.F.body, size: 11.5, color: ctx.C.soft, line: 122 });
289
+ }
290
+ const rows = ((s.panel && s.panel.rows) || []).length;
291
+ const panelH = (s.panel && s.panel.header ? 30 : 0) + 38 * rows + (s.panel && s.panel.footer ? 28 : 0);
292
+ const panelY = Math.max(82, 118 - Math.max(0, panelH - 176) / 2);
293
+ ctx.panel(slide, 370, panelY, 302, s.panel || { rows: [] });
196
294
  },
197
295
  chips(ctx, slide, s, spec) {
198
- ctx.wordmark(slide, M, 34, 13, spec.brand);
199
- if (s.heading) ctx.box(slide, M, 110, 580, 34, s.heading, { family: ctx.F.display, size: 28, color: ctx.C.ink });
200
- if (s.sub) ctx.box(slide, M, 162, 480, 64, s.sub, { family: ctx.F.body, size: 12.5, color: ctx.C.soft, line: 132 });
201
- ctx.chips(slide, M, 250, s.chips || []);
202
- if (s.mono) ctx.box(slide, M, 300, 520, 24, s.mono, { family: ctx.F.mono, size: 12, color: ctx.C.accent2 });
296
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
297
+ if (s.heading) {
298
+ ctx.box(slide, M, 96, 580, 32, s.heading,
299
+ { family: ctx.F.display, size: 26, color: ctx.C.ink });
300
+ }
301
+ if (s.sub) {
302
+ ctx.box(slide, M, 136, 520, 48, s.sub,
303
+ { family: ctx.F.body, size: 12, color: ctx.C.soft, line: 120 });
304
+ }
305
+ const chipBottom = ctx.chips(slide, M, 196, s.chips || []);
306
+ if (s.mono) {
307
+ ctx.box(slide, M, chipBottom + 14, 520, 28, s.mono,
308
+ { family: ctx.F.mono, size: 11, color: ctx.C.accent2, line: 120 });
309
+ }
203
310
  },
204
311
  bignumber(ctx, slide, s, spec) {
205
- ctx.wordmark(slide, M, 34, 13, spec.brand);
206
- ctx.box(slide, M, 138, W - M * 2, 120, s.number || s.value || '', { family: ctx.F.display, size: 92, color: ctx.C.accent2, line: 100 });
207
- if (s.label) ctx.box(slide, M, 268, 520, 30, s.label, { family: ctx.F.body, size: 16, color: ctx.C.ink });
208
- if (s.sub) ctx.box(slide, M, 300, 480, 50, s.sub, { family: ctx.F.body, size: 12.5, color: ctx.C.soft, line: 130 });
312
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
313
+ // pick number, else value; coalesce on null (not falsiness) so a literal 0 renders
314
+ const big = s.number != null ? s.number : (s.value != null ? s.value : '');
315
+ ctx.box(slide, M, 118, W - M * 2, 108, String(big),
316
+ { family: ctx.F.display, size: 84, color: ctx.C.accent2, line: 96 });
317
+ if (s.label) {
318
+ ctx.box(slide, M, 236, 560, 36, s.label,
319
+ { family: ctx.F.body, size: 15, color: ctx.C.ink, line: 118 });
320
+ }
321
+ if (s.sub) {
322
+ ctx.box(slide, M, 278, 560, 88, s.sub,
323
+ { family: ctx.F.body, size: 11.5, color: ctx.C.soft, line: 122 });
324
+ }
209
325
  },
210
326
  close(ctx, slide, s, spec) {
211
- ctx.rule(slide, (W - 48) / 2, 150, 48);
327
+ ctx.rule(slide, (W - 48) / 2, 132, 48);
212
328
  const name = (spec.brand && spec.brand.name) || 'Atris';
213
329
  const ac = (spec.brand && spec.brand.accent) || '';
214
- const id = ctx.box(slide, 0, 168, W, 64, name + ac, { family: ctx.F.display, size: 52, color: ctx.C.ink, align: 'CENTER' });
215
- if (ac && id) ctx.styleRange(id, name.length, name.length + ac.length, { family: ctx.F.display, size: 52, color: ctx.C.accent });
216
- if (s.tagline) ctx.box(slide, 0, 244, W, 24, s.tagline, { family: ctx.F.body, size: 14, color: ctx.C.soft, align: 'CENTER' });
217
- if (s.buttons) ctx.buttons(slide, 296, s.buttons);
218
- if (s.footer) ctx.box(slide, 0, 360, W, 20, s.footer, { family: ctx.F.body, size: 10, color: ctx.C.faint, align: 'CENTER' });
330
+ const id = ctx.box(slide, 0, 148, W, 58, name + ac,
331
+ { family: ctx.F.display, size: 48, color: ctx.C.ink, align: 'CENTER' });
332
+ if (ac && id) {
333
+ ctx.styleRange(id, name.length, name.length + ac.length,
334
+ { family: ctx.F.display, size: 48, color: ctx.C.accent });
335
+ }
336
+ if (s.tagline) {
337
+ ctx.box(slide, 48, 218, W - 96, 40, s.tagline,
338
+ { family: ctx.F.body, size: 13.5, color: ctx.C.soft, align: 'CENTER', line: 120 });
339
+ }
340
+ if (s.buttons) ctx.buttons(slide, 278, s.buttons);
341
+ if (s.footer) {
342
+ ctx.box(slide, 0, 352, W, 20, s.footer,
343
+ { family: ctx.F.body, size: 10, color: ctx.C.faint, align: 'CENTER' });
344
+ }
345
+ },
346
+ // --- new archetypes (v2) ---
347
+ timeline(ctx, slide, s, spec) {
348
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
349
+ const headingY = 88;
350
+ if (s.heading) {
351
+ ctx.box(slide, M, headingY, 580, 36, s.heading,
352
+ { family: ctx.F.display, size: 26, color: ctx.C.ink, line: 110 });
353
+ }
354
+ const steps = arr(s.steps).slice(0, 5);
355
+ const n = steps.length || 1;
356
+ const pad = 58;
357
+ const startX = M + pad;
358
+ const endX = W - M - pad;
359
+ const trackW = endX - startX;
360
+ const cy = 210;
361
+ if (n > 1) {
362
+ ctx.fill(ctx.shape('RECTANGLE', slide, startX, cy - 0.5, trackW, 1.5), ctx.C.line);
363
+ }
364
+ steps.forEach((step, i) => {
365
+ const x = n === 1 ? W / 2 : startX + (trackW * i) / (n - 1);
366
+ const active = !!step.active;
367
+ const dotR = active ? 8 : 6;
368
+ ctx.fill(ctx.shape('ELLIPSE', slide, x - dotR, cy - dotR, dotR * 2, dotR * 2),
369
+ active ? ctx.C.accent : ctx.C.panelAlt, active ? ctx.C.accent : ctx.C.line, active ? 0 : 1);
370
+ if (active) {
371
+ ctx.fill(ctx.shape('ELLIPSE', slide, x - 3, cy - 3, 6, 6), ctx.C.onAccent);
372
+ }
373
+ const labelW = 104;
374
+ const labelX = Math.max(M, Math.min(W - M - labelW, x - labelW / 2));
375
+ ctx.box(slide, labelX, cy + 18, labelW, 22, step.label || '',
376
+ { family: ctx.F.body, size: 11.5, color: active ? ctx.C.ink : ctx.C.soft, align: 'CENTER', line: 110 });
377
+ if (step.sub) {
378
+ ctx.box(slide, labelX, cy + 40, labelW, 36, step.sub,
379
+ { family: ctx.F.body, size: 9, color: ctx.C.faint, align: 'CENTER', line: 108 });
380
+ }
381
+ });
382
+ if (s.sub) {
383
+ ctx.box(slide, M, 318, 580, 48, s.sub,
384
+ { family: ctx.F.body, size: 11.5, color: ctx.C.soft, line: 122 });
385
+ }
386
+ },
387
+ versus(ctx, slide, s, spec) {
388
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
389
+ if (s.heading) {
390
+ ctx.box(slide, M, 88, 580, 32, s.heading,
391
+ { family: ctx.F.display, size: 24, color: ctx.C.ink });
392
+ }
393
+ const colW = 296;
394
+ const colY = 138;
395
+ const colH = 210;
396
+ const sides = [
397
+ { data: s.left || {}, x: M, accent: false },
398
+ { data: s.right || {}, x: M + colW + 32, accent: true },
399
+ ];
400
+ sides.forEach(({ data, x, accent }) => {
401
+ ctx.fill(ctx.shape('ROUND_RECTANGLE', slide, x, colY, colW, colH),
402
+ ctx.C.panel, accent ? ctx.C.accent : ctx.C.line, accent ? 1.5 : 1);
403
+ ctx.box(slide, x + 16, colY + 16, colW - 32, 24, data.label || '',
404
+ { family: ctx.F.display, size: 16, color: accent ? ctx.C.accent2 : ctx.C.soft, line: 110 });
405
+ (Array.isArray(data.items) ? data.items : []).slice(0, 5).forEach((item, i) => {
406
+ const iy = colY + 52 + i * 30;
407
+ ctx.fill(ctx.shape('ELLIPSE', slide, x + 20, iy + 8, 5, 5),
408
+ accent ? ctx.C.accent : ctx.C.faint);
409
+ ctx.box(slide, x + 32, iy, colW - 48, 22, item,
410
+ { family: ctx.F.body, size: 11.5, color: ctx.C.ink, line: 118 });
411
+ });
412
+ });
413
+ },
414
+ metricgrid(ctx, slide, s, spec) {
415
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
416
+ if (s.heading) {
417
+ ctx.box(slide, M, 88, 580, 32, s.heading,
418
+ { family: ctx.F.display, size: 24, color: ctx.C.ink });
419
+ }
420
+ const metrics = (Array.isArray(s.metrics) ? s.metrics : []).slice(0, 4).map((m) => m || {});
421
+ const cols = 2;
422
+ const gap = 16;
423
+ const cardW = (W - M * 2 - gap) / cols;
424
+ const cardH = 108;
425
+ const gridY = s.heading ? 138 : 108;
426
+ metrics.forEach((m, i) => {
427
+ const col = i % cols;
428
+ const row = Math.floor(i / cols);
429
+ const x = M + col * (cardW + gap);
430
+ const y = gridY + row * (cardH + gap);
431
+ ctx.fill(ctx.shape('ROUND_RECTANGLE', slide, x, y, cardW, cardH), ctx.C.panel, ctx.C.line, 1);
432
+ ctx.box(slide, x + 16, y + 16, cardW - 32, 42, m.value || '',
433
+ { family: ctx.F.display, size: 32, color: ctx.C.accent2, line: 96 });
434
+ ctx.box(slide, x + 16, y + 60, cardW - 32, 44, m.label || '',
435
+ { family: ctx.F.body, size: 10, color: ctx.C.soft, line: 116 });
436
+ });
437
+ },
438
+ receipt(ctx, slide, s, spec) {
439
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
440
+ const headingY = 76;
441
+ if (s.heading) {
442
+ ctx.box(slide, M, headingY, 320, 52, s.heading,
443
+ { family: ctx.F.display, size: 24, color: ctx.C.ink, line: 108 });
444
+ }
445
+ if (s.sub) {
446
+ ctx.box(slide, M, headingY + 60, 300, 56, s.sub,
447
+ { family: ctx.F.body, size: 11, color: ctx.C.soft, line: 120 });
448
+ }
449
+ const rx = 368;
450
+ const ry = 78;
451
+ const rw = 304;
452
+ const fields = arr(s.fields).slice(0, 6);
453
+ const rh = 48 + fields.length * 36 + (s.stamp ? 44 : 16);
454
+ ctx.fill(ctx.shape('ROUND_RECTANGLE', slide, rx, ry, rw, rh), ctx.C.panel, ctx.C.line, 1);
455
+ ctx.box(slide, rx + 16, ry + 12, rw - 32, 18, s.receiptTitle || 'Judgment packet',
456
+ { family: ctx.F.mono, size: 9, color: ctx.C.faint, line: 110 });
457
+ ctx.fill(ctx.shape('RECTANGLE', slide, rx + 16, ry + 34, rw - 32, 0.75), ctx.C.line);
458
+ fields.forEach((f, i) => {
459
+ const fy = ry + 44 + i * 36;
460
+ ctx.box(slide, rx + 16, fy, 90, 18, f.k || '',
461
+ { family: ctx.F.mono, size: 9, color: ctx.C.faint, line: 110 });
462
+ ctx.box(slide, rx + 108, fy, rw - 124, 32, f.v || '',
463
+ { family: ctx.F.body, size: 10.5, color: ctx.C.ink, line: 116 });
464
+ });
465
+ if (s.stamp) {
466
+ const sy = ry + rh - 36;
467
+ ctx.fill(ctx.shape('ROUND_RECTANGLE', slide, rx + 16, sy, rw - 32, 24), ctx.C.panelAlt, ctx.C.accent, 1);
468
+ ctx.box(slide, rx + 16, sy, rw - 32, 24, s.stamp,
469
+ { family: ctx.F.mono, size: 10, color: ctx.C.accent2, align: 'CENTER', vmid: true });
470
+ }
471
+ },
472
+ stack(ctx, slide, s, spec) {
473
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
474
+ if (s.heading) {
475
+ ctx.box(slide, M, 88, 580, 32, s.heading,
476
+ { family: ctx.F.display, size: 24, color: ctx.C.ink });
477
+ }
478
+ const layers = arr(s.layers).slice(0, 4);
479
+ const cardW = 460;
480
+ const cardH = 56;
481
+ const gap = 10;
482
+ const baseX = (W - cardW) / 2;
483
+ const baseY = s.heading ? 128 : 108;
484
+ layers.forEach((layer, i) => {
485
+ const x = baseX;
486
+ const y = baseY + i * (cardH + gap);
487
+ ctx.fill(ctx.shape('ROUND_RECTANGLE', slide, x, y, cardW, cardH),
488
+ ctx.C.panel, ctx.C.line, 1);
489
+ ctx.box(slide, x + 18, y + 12, cardW - 36, 22, layer.title || '',
490
+ { family: ctx.F.display, size: 15, color: ctx.C.ink, line: 110 });
491
+ if (layer.sub) {
492
+ ctx.box(slide, x + 18, y + 34, cardW - 36, 20, layer.sub,
493
+ { family: ctx.F.body, size: 10, color: ctx.C.soft, line: 116 });
494
+ }
495
+ });
496
+ const totalH = layers.length * cardH + (layers.length - 1) * gap;
497
+ if (s.sub) {
498
+ ctx.box(slide, M, baseY + totalH + 18, W - M * 2, 40, s.sub,
499
+ { family: ctx.F.body, size: 11.5, color: ctx.C.soft, align: 'CENTER', line: 122 });
500
+ }
501
+ },
502
+ quote(ctx, slide, s, spec) {
503
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
504
+ ctx.rule(slide, W / 2 - 24, 118, 48);
505
+ const qText = s.text || '';
506
+ ctx.box(slide, 64, 138, W - 128, 120, qText,
507
+ { family: ctx.F.display, size: fitSize(qText, W - 128, 120, 28, 16, 118), color: ctx.C.ink, align: 'CENTER', line: 118 });
508
+ const author = [s.author, s.role].filter(Boolean).join(' · ');
509
+ if (author) {
510
+ ctx.box(slide, 64, 278, W - 128, 28, author,
511
+ { family: ctx.F.body, size: 11.5, color: ctx.C.faint, align: 'CENTER', line: 118 });
512
+ }
513
+ },
514
+ hero(ctx, slide, s, spec) {
515
+ ctx.rule(slide, (W - 56) / 2, 108, 56);
516
+ ctx.box(slide, 48, 124, W - 96, 88, s.headline || s.text || '',
517
+ { family: ctx.F.display, size: 36, color: ctx.C.ink, align: 'CENTER', line: 104 });
518
+ if (s.sub) {
519
+ ctx.box(slide, 72, 220, W - 144, 56, s.sub,
520
+ { family: ctx.F.body, size: 13, color: ctx.C.soft, align: 'CENTER', line: 122 });
521
+ }
522
+ if (s.mono) {
523
+ const monoSize = s.mono.length > 52 ? 8.5 : 10.5;
524
+ const monoH = s.mono.length > 52 ? 44 : 28;
525
+ ctx.box(slide, 36, 292, W - 72, monoH, s.mono,
526
+ { family: ctx.F.mono, size: monoSize, color: ctx.C.accent2, align: 'CENTER', line: 118 });
527
+ }
528
+ if (s.footer) {
529
+ ctx.box(slide, 0, 352, W, 20, s.footer,
530
+ { family: ctx.F.body, size: 10, color: ctx.C.faint, align: 'CENTER' });
531
+ }
532
+ },
533
+ // --- narrative archetypes (typography-first, no widget boxes) ---
534
+ interstitial(ctx, slide, s) {
535
+ const align = s.align === 'CENTER' ? 'CENTER' : 'START';
536
+ const x = align === 'CENTER' ? M : M;
537
+ const w = W - M * 2;
538
+ if (s.kicker) {
539
+ ctx.box(slide, x, 118, w, 22, s.kicker,
540
+ { family: ctx.F.mono, size: 10, color: ctx.C.faint, align, line: 110 });
541
+ }
542
+ const txt = s.text || s.headline || '';
543
+ const size = s.size || fitSize(txt, w, 150, 56, 26, 96);
544
+ const textY = s.kicker ? 152 : 128;
545
+ ctx.box(slide, x, textY, w, 150, txt,
546
+ { family: ctx.F.display, size, color: ctx.C.ink, align, line: 96 });
547
+ if (s.sub) {
548
+ ctx.box(slide, x, textY + 158, Math.min(520, w), 56, s.sub,
549
+ { family: ctx.F.body, size: 13, color: ctx.C.soft, align, line: 124 });
550
+ }
551
+ },
552
+ lede(ctx, slide, s, spec) {
553
+ ctx.wordmark(slide, M, 28, 14, spec.brand);
554
+ const head = s.headline || s.title || '';
555
+ ctx.box(slide, M, 92, 640, 128, head,
556
+ { family: ctx.F.display, size: fitSize(head, 640, 128, 40, 22, 100), color: ctx.C.ink, line: 100 });
557
+ if (s.dek) {
558
+ ctx.box(slide, M, 232, 580, 72, s.dek,
559
+ { family: ctx.F.body, size: fitSize(s.dek, 580, 72, 14, 10, 128), color: ctx.C.soft, line: 128 });
560
+ }
561
+ if (s.byline) {
562
+ ctx.box(slide, M, 318, 580, 24, s.byline,
563
+ { family: ctx.F.mono, size: 10, color: ctx.C.faint, line: 118 });
564
+ }
565
+ },
566
+ prose(ctx, slide, s, spec) {
567
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
568
+ let y = 86;
569
+ if (s.heading) {
570
+ const headSize = s.heading.length > 52 ? 22 : 26;
571
+ const headH = s.heading.length > 52 ? 72 : 52;
572
+ ctx.box(slide, M, y, 580, headH, s.heading,
573
+ { family: ctx.F.display, size: headSize, color: ctx.C.ink, line: 108 });
574
+ y += headH + 12;
575
+ }
576
+ const paras = (Array.isArray(s.paragraphs) ? s.paragraphs : (s.body ? [s.body] : [])).slice(0, 3);
577
+ const np = paras.length || 1;
578
+ // Distribute paragraphs across the space left to the slide floor, then pick
579
+ // one font size that fits all of them in their slots (3 long paras shrink).
580
+ const slotH = Math.floor((388 - y) / np);
581
+ const size = fitSizeAll(paras, 580, slotH - 10, 14, 10.5, 132);
582
+ paras.forEach((p, i) => {
583
+ ctx.box(slide, M, y + i * slotH, 580, slotH, p,
584
+ { family: ctx.F.body, size, color: ctx.C.soft, line: 132 });
585
+ });
586
+ },
587
+ bullets(ctx, slide, s, spec) {
588
+ // A real bullet list: accent dot + text, typography only, no card boxes.
589
+ // Font and row height shrink as the list grows so it never runs off-slide.
590
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
591
+ let y = 96;
592
+ if (s.heading) {
593
+ ctx.box(slide, M, y, W - M * 2, 40, s.heading,
594
+ { family: ctx.F.display, size: 26, color: ctx.C.ink, line: 110 });
595
+ y += 54;
596
+ }
597
+ const items = arr(s.items).slice(0, 7);
598
+ const n = items.length || 1;
599
+ const hasSub = items.some((it) => it && typeof it === 'object' && (it.sub || it.b));
600
+ const bottom = s.sub ? H - 76 : H - 36;
601
+ const rowH = Math.max(hasSub ? 44 : 30, Math.min(hasSub ? 70 : 54, Math.floor((bottom - y) / n)));
602
+ const size = hasSub ? (n > 4 ? 13 : 15) : (n > 5 ? 13.5 : n > 3 ? 15.5 : 18);
603
+ items.forEach((item, i) => {
604
+ const iy = y + i * rowH;
605
+ const isObj = item && typeof item === 'object';
606
+ const txt = isObj ? (item.text || item.h || item.title || '') : item;
607
+ const sub = isObj ? (item.sub || item.b || '') : '';
608
+ ctx.fill(ctx.shape('ELLIPSE', slide, M + 1, iy + size * 0.42, 6, 6), ctx.C.accent);
609
+ ctx.box(slide, M + 22, iy, W - M * 2 - 22, sub ? 24 : rowH, txt,
610
+ { family: ctx.F.body, size, color: ctx.C.ink, line: 116 });
611
+ if (sub) {
612
+ ctx.box(slide, M + 22, iy + size + 9, W - M * 2 - 22, 24, sub,
613
+ { family: ctx.F.body, size: size - 3, color: ctx.C.faint, line: 116 });
614
+ }
615
+ });
616
+ if (s.sub) {
617
+ ctx.box(slide, M, bottom + 6, W - M * 2, 32, s.sub,
618
+ { family: ctx.F.body, size: 11.5, color: ctx.C.soft, line: 122 });
619
+ }
620
+ },
621
+ split(ctx, slide, s, spec) {
622
+ ctx.wordmark(slide, M, 28, 13, spec.brand);
623
+ const leftW = 372;
624
+ const topY = 88;
625
+ if (s.heading) {
626
+ ctx.box(slide, M, topY, leftW, 44, s.heading,
627
+ { family: ctx.F.display, size: 24, color: ctx.C.ink, line: 108 });
628
+ }
629
+ const bodyY = s.heading ? topY + 52 : topY;
630
+ const body = s.body || s.text || '';
631
+ ctx.box(slide, M, bodyY, leftW, 405 - bodyY - 24, body,
632
+ { family: ctx.F.body, size: fitSize(body, leftW, 405 - bodyY - 24, 13, 9.5, 132), color: ctx.C.soft, line: 132 });
633
+ const rx = 448;
634
+ if (s.accent) {
635
+ ctx.box(slide, rx, 108, 224, 80, s.accent,
636
+ { family: ctx.F.display, size: 38, color: ctx.C.accent2, line: 96 });
637
+ }
638
+ if (s.accentSub) {
639
+ ctx.box(slide, rx, 196, 224, 96, s.accentSub,
640
+ { family: ctx.F.body, size: 11.5, color: ctx.C.faint, line: 122 });
641
+ }
219
642
  },
220
643
  };
221
644
 
645
+ // ---------- speaker notes ----------
646
+ // Notes live on a slide's auto-created notesPage, whose body placeholder
647
+ // objectId is only known after the deck exists. So notes are a second pass:
648
+ // GET the presentation, find each slide's notes body, then insert text.
649
+ function findNotesBodyId(apiSlide) {
650
+ const els = (apiSlide
651
+ && apiSlide.slideProperties
652
+ && apiSlide.slideProperties.notesPage
653
+ && apiSlide.slideProperties.notesPage.pageElements) || [];
654
+ const body = els.find((e) => e.shape && e.shape.placeholder && e.shape.placeholder.type === 'BODY');
655
+ return body ? body.objectId : null;
656
+ }
657
+
658
+ // Pure: GET-response slides + the spec's slides -> insertText requests for the
659
+ // notes body of every slide that declares a `notes` string. Slides are matched
660
+ // by index (build creates them in spec order). Notes text is left raw (it is
661
+ // private speaker context, not on-canvas copy, so the AI-tell sanitizer that
662
+ // guards visible text does not apply).
663
+ function notesRequests(apiSlides, specSlides) {
664
+ const reqs = [];
665
+ (apiSlides || []).forEach((s, i) => {
666
+ const note = specSlides && specSlides[i] && specSlides[i].notes;
667
+ if (typeof note !== 'string' || !note) return; // ignore non-string notes (no "[object Object]")
668
+ const bodyId = findNotesBodyId(s);
669
+ if (!bodyId) return;
670
+ reqs.push({ insertText: { objectId: bodyId, text: note } });
671
+ });
672
+ return reqs;
673
+ }
674
+
222
675
  // ---------- public: spec -> requests ----------
223
- function buildDeck(spec, opts = {}) {
224
- const themes = opts.themes || THEMES;
225
- const theme = themes[spec.theme] || THEMES.terminal;
676
+ function buildDeck(spec) {
677
+ const safe = spec && typeof spec === 'object' ? spec : {};
678
+ const theme = THEMES[safe.theme] || THEMES.terminal;
226
679
  const ctx = makeCtx(theme);
227
680
  const slideIds = [];
228
- (spec.slides || []).forEach((s, i) => {
681
+ (Array.isArray(safe.slides) ? safe.slides : []).forEach((slide, i) => {
682
+ const s = slide && typeof slide === 'object' ? slide : {};
229
683
  const sid = `deck_slide_${i + 1}`; slideIds.push(sid);
230
684
  ctx.createSlide(sid);
231
685
  ctx.bg(sid, theme.color.bg);
232
- (ARCHE[s.type] || ARCHE.statement)(ctx, sid, s, spec);
686
+ (ARCHE[s.type] || ARCHE.statement)(ctx, sid, s, safe);
233
687
  });
234
688
  return { requests: ctx.requests, slideIds };
235
689
  }
236
690
 
237
- module.exports = { buildDeck, THEMES, ARCHE, sanitize, parseEmph, rgb, COLOR_ROLES };
691
+ module.exports = { buildDeck, THEMES, ARCHE, sanitize, parseEmph, rgb, COLOR_ROLES, fitSize, estLines, findNotesBodyId, notesRequests };