atris 3.22.0 → 3.23.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/atris.md +1 -1
- package/bin/atris.js +7 -1
- package/commands/card.js +121 -0
- package/lib/card.js +120 -0
- package/package.json +1 -1
package/atris.md
CHANGED
|
@@ -67,7 +67,7 @@ What you ship should not read as generated. The test: if someone said "an AI mad
|
|
|
67
67
|
- **Refuse the wells** (named so you can): purple/indigo gradients, gradient-filled text, glassmorphism, Inter/Roboto defaults, claude-beige, neon-on-dark, hero-metric rows, identical card grids, eyebrow/tracked-caps labels, pulsing live-dots, em dashes.
|
|
68
68
|
- **Commit to constraints.** One distinctive font, one accent hue, a small spacing scale. Taste is subtraction, not addition.
|
|
69
69
|
- **Generate it right.** `atris deck` (slides), `atris deck from <doc.md> --html` (a web page from a plain doc, in the web app's design tokens), `atris site <dir>` (a whole markdown folder into a navigable site), and `atris recap --html` (a memory-updates page) all apply the system by default: own backgrounds and fonts, never the tool's stock template. Output as an AppBlock with `--block` to drop into a web app.
|
|
70
|
-
- **Compound it.** A new tell becomes a project rule in `.atris/slop.rules.json` (`atris slop rules --add`), and a project's brand lives in `.atris/theme.json` (`atris theme create` builds your own by feel, or `atris theme init` scaffolds one) so every deck, page, and site is on-brand by default
|
|
70
|
+
- **Compound it.** A new tell becomes a project rule in `.atris/slop.rules.json` (`atris slop rules --add`), and a project's brand lives in `.atris/theme.json` (`atris theme create` builds your own by feel, or `atris theme init` scaffolds one) so every deck, page, and site is on-brand by default, and one line of text becomes an on-brand image with `atris card`. The gate and the look grow per project instead of leaning on memory. Taste lives in code, not vibes.
|
|
71
71
|
|
|
72
72
|
## voice
|
|
73
73
|
|
package/bin/atris.js
CHANGED
|
@@ -365,6 +365,7 @@ function showHelp() {
|
|
|
365
365
|
console.log(' deck - Generate a premium Google Slides deck from a content spec');
|
|
366
366
|
console.log(' site - Build a beautiful static site from a folder of markdown');
|
|
367
367
|
console.log(' theme - Brand themes: "theme create" builds your own by feel (deck/html/site)');
|
|
368
|
+
console.log(' card - One line of text into an on-brand image (uses your theme)');
|
|
368
369
|
console.log(' run - Auto-chain plan→do→review (autonomous loop, auto-pushes)');
|
|
369
370
|
console.log(' run logs - Browse glass run logs (phase reasoning persisted to disk)');
|
|
370
371
|
console.log(' run search - Search phase reasoning across all run logs');
|
|
@@ -830,7 +831,7 @@ if (command === '2' && ['fast', 'pro'].includes(String(firstCommandArg || '').to
|
|
|
830
831
|
const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
831
832
|
'activate', '_activate', 'agent', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
832
833
|
'clean', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
|
|
833
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'deck', 'site', 'theme', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
|
|
834
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'deck', 'site', 'theme', 'card', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
|
|
834
835
|
'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
835
836
|
'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship'];
|
|
836
837
|
|
|
@@ -1364,6 +1365,11 @@ if (command === 'init') {
|
|
|
1364
1365
|
Promise.resolve(require('../commands/theme').run(process.argv.slice(3)))
|
|
1365
1366
|
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
1366
1367
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1368
|
+
} else if (command === 'card') {
|
|
1369
|
+
// Card: one line of text into an on-brand image (uses your theme + the design system).
|
|
1370
|
+
Promise.resolve(require('../commands/card').run(process.argv.slice(3)))
|
|
1371
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
1372
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1367
1373
|
} else if (command === 'aeo') {
|
|
1368
1374
|
// AEO: AI Engine Optimization — credit-metered citation drafting against the customer workspace.
|
|
1369
1375
|
Promise.resolve(require('../commands/aeo').run(process.argv.slice(3)))
|
package/commands/card.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// atris card — one line of text -> a beautiful, on-brand image (your theme).
|
|
2
|
+
//
|
|
3
|
+
// atris card "Ship faster" --kind statement --theme brand --size og
|
|
4
|
+
// atris card "It just works" --kind quote --by "a happy user"
|
|
5
|
+
// atris card --kind stat --number "10x" --label "faster reviews"
|
|
6
|
+
//
|
|
7
|
+
// Writes an .html (always) and a .png (when headless Chrome is available).
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
13
|
+
const { buildCard, SIZES, KINDS } = require('../lib/card');
|
|
14
|
+
|
|
15
|
+
function parseFlags(argv) {
|
|
16
|
+
const flags = {}; const pos = [];
|
|
17
|
+
for (let i = 0; i < argv.length; i++) {
|
|
18
|
+
const a = argv[i];
|
|
19
|
+
if (a === '--html-only') { flags.htmlOnly = true; continue; }
|
|
20
|
+
if (a.startsWith('--')) {
|
|
21
|
+
const key = a.slice(2);
|
|
22
|
+
const next = argv[i + 1];
|
|
23
|
+
if (next != null && !next.startsWith('--')) { flags[key] = next; i++; } else flags[key] = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
pos.push(a);
|
|
27
|
+
}
|
|
28
|
+
return { flags, pos };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// find an installed Chrome/Chromium without adding a dependency
|
|
32
|
+
function findChrome() {
|
|
33
|
+
if (process.env.CHROME_PATH && fs.existsSync(process.env.CHROME_PATH)) return process.env.CHROME_PATH;
|
|
34
|
+
const macApps = [
|
|
35
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
36
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
37
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
38
|
+
];
|
|
39
|
+
for (const p of macApps) if (fs.existsSync(p)) return p;
|
|
40
|
+
for (const name of ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']) {
|
|
41
|
+
const r = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8' });
|
|
42
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderPng(chrome, html, width, height, outPng) {
|
|
48
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-card-'));
|
|
49
|
+
const htmlFile = path.join(tmp, 'card.html');
|
|
50
|
+
fs.writeFileSync(htmlFile, html);
|
|
51
|
+
const args = [
|
|
52
|
+
'--headless=new', '--disable-gpu', '--hide-scrollbars',
|
|
53
|
+
'--force-device-scale-factor=2',
|
|
54
|
+
`--window-size=${width},${height}`,
|
|
55
|
+
'--virtual-time-budget=4000',
|
|
56
|
+
`--screenshot=${outPng}`,
|
|
57
|
+
`file://${htmlFile}`,
|
|
58
|
+
];
|
|
59
|
+
execFileSync(chrome, args, { stdio: 'ignore', timeout: 60000 });
|
|
60
|
+
return fs.existsSync(outPng);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function run(argv) {
|
|
64
|
+
const { flags, pos } = parseFlags(argv);
|
|
65
|
+
|
|
66
|
+
if (pos[0] === 'help' || flags.help) {
|
|
67
|
+
console.log(`\n atris card — one line of text into an on-brand image\n
|
|
68
|
+
atris card "Your headline" [--kind statement|quote|stat] [--theme <name>] [--size og|wide|square|story]
|
|
69
|
+
flags: --sub --kicker --by --number --label --brand --version --out <file.png> --html-only\n
|
|
70
|
+
examples:
|
|
71
|
+
atris card "Design that builds itself" --kicker "Atris v3.23.0" --theme brand
|
|
72
|
+
atris card "It just works." --kind quote --by "a founder" --size square
|
|
73
|
+
atris card --kind stat --number "1260" --label "tests, all green"\n`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const text = pos.join(' ').trim();
|
|
78
|
+
const kind = flags.kind || 'statement';
|
|
79
|
+
if (!KINDS.includes(kind)) { console.error(` unknown kind "${kind}". try: ${KINDS.join(', ')}`); return 2; }
|
|
80
|
+
if (flags.size && !SIZES[flags.size]) { console.error(` unknown size "${flags.size}". try: ${Object.keys(SIZES).join(', ')}`); return 2; }
|
|
81
|
+
if (kind === 'stat' && !flags.number && !text) { console.error(' stat cards need --number (e.g. --number "10x")'); return 2; }
|
|
82
|
+
if (kind !== 'stat' && !text) { console.error(' give the card some text: atris card "Your headline"'); return 2; }
|
|
83
|
+
|
|
84
|
+
const spec = {
|
|
85
|
+
kind, text, headline: text,
|
|
86
|
+
theme: flags.theme, size: flags.size,
|
|
87
|
+
sub: flags.sub, kicker: flags.kicker, by: flags.by,
|
|
88
|
+
number: flags.number, label: flags.label,
|
|
89
|
+
brand: flags.brand, version: flags.version,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
let card;
|
|
93
|
+
try { card = buildCard(spec); }
|
|
94
|
+
catch (e) { console.error(` could not build card: ${e.message}`); return 1; }
|
|
95
|
+
|
|
96
|
+
const base = (flags.out ? String(flags.out).replace(/\.png$/i, '') : `card-${kind}-${card.theme}-${card.size}`);
|
|
97
|
+
const outPng = path.resolve(`${base}.png`);
|
|
98
|
+
const outHtml = path.resolve(`${base}.html`);
|
|
99
|
+
fs.writeFileSync(outHtml, card.html);
|
|
100
|
+
|
|
101
|
+
if (flags.htmlOnly) {
|
|
102
|
+
console.log(`\n ✓ ${card.kind} card (${card.width}x${card.height}, theme ${card.theme})\n html: ${outHtml}\n open it, or render a png with Chrome installed.\n`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const chrome = findChrome();
|
|
107
|
+
if (!chrome) {
|
|
108
|
+
console.log(`\n ✓ wrote html: ${outHtml}\n no Chrome found for png. open the html, or set CHROME_PATH and re-run.\n`);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
renderPng(chrome, card.html, card.width, card.height, outPng);
|
|
113
|
+
console.log(`\n ✓ ${card.kind} card, ${card.width}x${card.height}, theme ${card.theme}\n ${outPng}\n`);
|
|
114
|
+
return 0;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.log(`\n ! png render failed (${e.message.split('\n')[0]})\n html is ready: ${outHtml}\n`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { run };
|
package/lib/card.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// atris card — turn one line of text into a beautiful, on-brand image.
|
|
2
|
+
// Pure: spec -> self-contained HTML string. Reuses the design system: the same
|
|
3
|
+
// themes as deck/site/html (incl. your .atris/theme.json brand), one accent,
|
|
4
|
+
// Fraunces display, restraint. Render to PNG with headless Chrome (commands/card.js).
|
|
5
|
+
//
|
|
6
|
+
// Kinds: statement (kicker + headline + sub), quote (quote + attribution),
|
|
7
|
+
// stat (big number + label). Sizes: og, wide, square, story.
|
|
8
|
+
|
|
9
|
+
const { mergedThemes } = require('./theme');
|
|
10
|
+
const { THEMES: HTML_THEMES, rich, esc } = require('./html-render');
|
|
11
|
+
|
|
12
|
+
const SIZES = {
|
|
13
|
+
og: { w: 1200, h: 630 }, // OpenGraph / link card
|
|
14
|
+
wide: { w: 1920, h: 1080 }, // slide / banner
|
|
15
|
+
square: { w: 1080, h: 1080 }, // feed
|
|
16
|
+
story: { w: 1080, h: 1920 }, // story / reel
|
|
17
|
+
};
|
|
18
|
+
const KINDS = ['statement', 'quote', 'stat'];
|
|
19
|
+
|
|
20
|
+
const plainLen = (s) => String(s == null ? '' : s).replace(/\*\*/g, '').length;
|
|
21
|
+
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
22
|
+
|
|
23
|
+
// shrink a display line so long text still fits the card
|
|
24
|
+
function fit(base, text, refChars) {
|
|
25
|
+
return Math.round(base * clamp(refChars / Math.max(plainLen(text), 1), 0.5, 1));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fontStack(name, fallback) {
|
|
29
|
+
return `'${String(name || fallback).replace(/'/g, '')}', '${fallback}', ${fallback === 'Fraunces' ? 'Georgia, serif' : 'system-ui, sans-serif'}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildCard(spec = {}, opts = {}) {
|
|
33
|
+
const size = SIZES[spec.size] || SIZES.og;
|
|
34
|
+
const themes = opts.themes || mergedThemes(HTML_THEMES, opts.root || process.cwd());
|
|
35
|
+
const theme = themes[spec.theme] || HTML_THEMES.atris;
|
|
36
|
+
const c = theme.color;
|
|
37
|
+
const f = theme.fonts || {};
|
|
38
|
+
const kind = KINDS.includes(spec.kind) ? spec.kind : 'statement';
|
|
39
|
+
const u = Math.min(size.w, size.h);
|
|
40
|
+
|
|
41
|
+
const pad = Math.round(u * 0.088);
|
|
42
|
+
const px = {
|
|
43
|
+
kicker: Math.round(u * 0.030),
|
|
44
|
+
sub: Math.round(u * 0.040),
|
|
45
|
+
foot: Math.round(u * 0.028),
|
|
46
|
+
rule: Math.round(u * 0.010),
|
|
47
|
+
gap: Math.round(u * 0.030),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const brand = spec.brand || 'Atris';
|
|
51
|
+
const version = spec.version || '';
|
|
52
|
+
const display = fontStack(f.display, 'Fraunces');
|
|
53
|
+
const body = fontStack(f.body, 'Outfit');
|
|
54
|
+
const grotesk = "'Space Grotesk', system-ui, sans-serif";
|
|
55
|
+
const monoFont = "'IBM Plex Mono', ui-monospace, monospace";
|
|
56
|
+
|
|
57
|
+
const foot = `<div class="foot"><span class="mark">${esc(brand)}</span>${version ? `<span class="ver">${esc(version)}</span>` : ''}</div>`;
|
|
58
|
+
const kickerHtml = spec.kicker ? `<div class="kicker">${rich(spec.kicker)}</div>` : '';
|
|
59
|
+
const subHtml = spec.sub ? `<p class="sub">${rich(spec.sub)}</p>` : '';
|
|
60
|
+
|
|
61
|
+
let main = '';
|
|
62
|
+
let extraCss = '';
|
|
63
|
+
if (kind === 'quote') {
|
|
64
|
+
const text = spec.text || spec.headline || 'Your quote here';
|
|
65
|
+
const qSize = fit(Math.round(u * 0.090), text, 64);
|
|
66
|
+
main = `<div class="rule"></div>
|
|
67
|
+
<div class="qmark">“</div>
|
|
68
|
+
<blockquote class="qtext">${rich(text)}</blockquote>
|
|
69
|
+
${spec.by ? `<div class="by">${rich(spec.by)}</div>` : ''}`;
|
|
70
|
+
extraCss = `
|
|
71
|
+
.qmark{font-family:${display};color:${c.accent};font-size:${Math.round(u * 0.20)}px;line-height:.6;height:${Math.round(u * 0.10)}px}
|
|
72
|
+
.qtext{font-family:${display};font-weight:600;font-size:${qSize}px;line-height:1.08;letter-spacing:-1px;margin:${px.gap}px 0}
|
|
73
|
+
.by{font-family:${monoFont};font-size:${px.foot}px;color:${c.soft}}`;
|
|
74
|
+
} else if (kind === 'stat') {
|
|
75
|
+
const number = spec.number || spec.headline || '42';
|
|
76
|
+
const numSize = fit(Math.round(u * 0.34), number, 6);
|
|
77
|
+
main = `<div class="rule"></div>
|
|
78
|
+
${kickerHtml}
|
|
79
|
+
<div class="big">${rich(number)}</div>
|
|
80
|
+
${spec.label ? `<div class="statlabel">${rich(spec.label)}</div>` : ''}
|
|
81
|
+
${subHtml}`;
|
|
82
|
+
extraCss = `
|
|
83
|
+
.big{font-family:${grotesk};font-weight:600;color:${c.accent};font-size:${numSize}px;line-height:.92;letter-spacing:-2px}
|
|
84
|
+
.statlabel{font-family:${display};font-weight:600;font-size:${Math.round(u * 0.058)}px;color:${c.ink};margin-top:${Math.round(px.gap * 0.5)}px}`;
|
|
85
|
+
} else {
|
|
86
|
+
const headline = spec.headline || spec.text || 'Your headline here';
|
|
87
|
+
const hSize = fit(Math.round(u * 0.135), headline, 34);
|
|
88
|
+
main = `<div class="rule"></div>
|
|
89
|
+
${kickerHtml}
|
|
90
|
+
<h1 class="headline">${rich(headline)}</h1>
|
|
91
|
+
${subHtml}`;
|
|
92
|
+
extraCss = `
|
|
93
|
+
.headline{font-family:${display};font-weight:600;font-size:${hSize}px;line-height:1.02;letter-spacing:-1.5px}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const html = `<!doctype html>
|
|
97
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
98
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
99
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
100
|
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Outfit:wght@400;500;600&family=Space+Grotesk:wght@500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
101
|
+
<style>
|
|
102
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
103
|
+
html,body{width:${size.w}px;height:${size.h}px;overflow:hidden}
|
|
104
|
+
body{background:${c.bg};color:${c.ink};font-family:${body};-webkit-font-smoothing:antialiased}
|
|
105
|
+
.card{width:${size.w}px;height:${size.h}px;padding:${pad}px;display:flex;flex-direction:column;justify-content:center;position:relative}
|
|
106
|
+
.rule{width:${Math.round(u * 0.085)}px;height:${px.rule}px;background:${c.accent};border-radius:${px.rule}px;margin-bottom:${Math.round(px.gap * 0.9)}px}
|
|
107
|
+
.kicker{font-family:${monoFont};font-size:${px.kicker}px;letter-spacing:2px;color:${c.soft};margin-bottom:${Math.round(px.gap * 0.6)}px}
|
|
108
|
+
.sub{font-size:${px.sub}px;line-height:1.4;color:${c.soft};margin-top:${px.gap}px;max-width:88%}
|
|
109
|
+
.foot{position:absolute;left:${pad}px;right:${pad}px;bottom:${pad}px;display:flex;align-items:baseline;justify-content:space-between}
|
|
110
|
+
.mark{font-family:${display};font-weight:600;font-size:${Math.round(u * 0.034)}px}
|
|
111
|
+
.ver{font-family:${monoFont};font-size:${px.foot}px;color:${c.soft};letter-spacing:1px}
|
|
112
|
+
.accent{color:${c.accent}}
|
|
113
|
+
${extraCss}
|
|
114
|
+
</style></head>
|
|
115
|
+
<body><div class="card">${main}${foot}</div></body></html>`;
|
|
116
|
+
|
|
117
|
+
return { html, width: size.w, height: size.h, kind, theme: spec.theme || 'atris', size: spec.size || 'og' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { buildCard, SIZES, KINDS };
|