amalgm 0.1.98 → 0.1.99

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.98",
3
+ "version": "0.1.99",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -7,9 +7,37 @@
7
7
  * even without blank-line separators.
8
8
  *
9
9
  * Output is inline-styled HTML suitable for email clients that strip <style>
10
- * tags. Colors match the amalgm dark theme.
10
+ * tags. All values come from EMAIL_TOKENS below, which mirrors the shared
11
+ * rendering ruleset in amalgm-ui/lib/rendering/tokens.ts — keep both in sync:
12
+ * body 14px, h1 caps at 18px (the amalgm section-header size), Notion-style
13
+ * table grid, black chat-style code blocks, blue hyperlinks/citations.
11
14
  */
12
15
 
16
+ const EMAIL_TOKENS = {
17
+ font: {
18
+ text: "'SF Pro Text',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif",
19
+ mono: "'JetBrains Mono','SF Mono',SFMono-Regular,Menlo,Consolas,monospace",
20
+ },
21
+ type: {
22
+ body: 14,
23
+ // h1 caps at the section-header size; h4-h6 share the body size.
24
+ headings: { 1: 18, 2: 16, 3: 15, 4: 14, 5: 14, 6: 14 },
25
+ code: 12,
26
+ table: 14,
27
+ },
28
+ color: {
29
+ text: '#ffffff',
30
+ textSecondary: '#d6d3d1',
31
+ border: '#1c1917', // stone-900 — container borders
32
+ tableBorder: '#292524', // stone-800 — table grid (Notion editor standard)
33
+ tableHeaderBg: '#141210', // stone-925 — Notion editor header row
34
+ bg: '#000000',
35
+ bgInset: '#0a0a0a', // inline-code fill
36
+ link: '#60a5fa', // blue-400 — hyperlinks + citations everywhere
37
+ },
38
+ radius: { inline: 6, block: 12 },
39
+ };
40
+
13
41
  function escapeHtmlAttr(value) {
14
42
  return String(value)
15
43
  .replace(/&/g, '&amp;')
@@ -25,27 +53,18 @@ function escapeHtmlText(value) {
25
53
  .replace(/>/g, '&gt;');
26
54
  }
27
55
 
28
- // Inline citation chip for amalgm:// references (files, folders, skills, agents).
29
- // Non-clickable in email local resources can't be opened from a mail client —
30
- // but the chip preserves visual continuity with the in-app render.
31
- const AMALGM_KIND_ICONS = {
32
- file: '\u{1F4C4}', // 📄
33
- folder: '\u{1F4C1}', // 📁
34
- skill: '\u{2728}', // ✨
35
- agent: '@',
36
- };
37
-
38
- function renderAmalgmCitation(label, kindAndPath, description) {
39
- const slashIdx = kindAndPath.indexOf('/');
40
- const kind = slashIdx === -1 ? kindAndPath : kindAndPath.slice(0, slashIdx);
41
- const icon = AMALGM_KIND_ICONS[kind] || '\u{1F4CE}'; // 📎 fallback
56
+ // Inline citation for amalgm:// references (files, folders, skills, agents).
57
+ // Codex-style: plain blue text at the surrounding font size no chip.
58
+ // Non-clickable in email (local resources can't open from a mail client);
59
+ // the optional description survives as a title tooltip.
60
+ function renderAmalgmCitation(label, _kindAndPath, description) {
42
61
  const titleAttr = description ? ` title="${escapeHtmlAttr(description)}"` : '';
43
62
  const safeLabel = escapeHtmlText(label);
44
- return `<span style="display:inline-block;background:#0c0a09;border:1px solid #1c1917;border-radius:9999px;padding:1px 8px;font-size:12px;color:#ffffff;line-height:1.5;white-space:nowrap"${titleAttr}>${icon}&nbsp;${safeLabel}</span>`;
63
+ return `<span style="color:${EMAIL_TOKENS.color.link};white-space:nowrap"${titleAttr}>${safeLabel}</span>`;
45
64
  }
46
65
 
47
66
  /**
48
- * Inline format: bold, italic, code, links, images, amalgm:// citation chips.
67
+ * Inline format: bold, italic, code, links, images, amalgm:// citations.
49
68
  */
50
69
  function inlineFormat(text) {
51
70
  return (
@@ -53,7 +72,7 @@ function inlineFormat(text) {
53
72
  // Images: ![alt](url)
54
73
  .replace(
55
74
  /!\[([^\]]*)\]\(([^)]+)\)/g,
56
- '<img src="$2" alt="$1" style="max-width:100%;height:auto;border-radius:8px;margin:8px 0">',
75
+ '<img src="$2" alt="$1" style="max-width:100%;height:auto;border-radius:16px;margin:8px 0">',
57
76
  )
58
77
  // amalgm:// citations (run before generic link regex so the optional
59
78
  // title attribute doesn't get swallowed into the href). Format:
@@ -64,10 +83,10 @@ function inlineFormat(text) {
64
83
  (_, label, kindAndPath, description) =>
65
84
  renderAmalgmCitation(label, kindAndPath, description),
66
85
  )
67
- // Links: [text](url)
86
+ // Links: [text](url) — blue hyperlink per the rendering ruleset.
68
87
  .replace(
69
88
  /\[([^\]]+)\]\(([^)]+)\)/g,
70
- '<a href="$2" style="color:#ffffff;text-decoration:underline;text-underline-offset:2px" target="_blank">$1</a>',
89
+ `<a href="$2" style="color:${EMAIL_TOKENS.color.link};text-decoration:none" target="_blank">$1</a>`,
71
90
  )
72
91
  // Bold + italic: ***text***
73
92
  .replace(
@@ -82,7 +101,7 @@ function inlineFormat(text) {
82
101
  // Inline code: `text`
83
102
  .replace(
84
103
  /`([^`]+)`/g,
85
- '<code style="background:#1c1917;padding:2px 6px;border-radius:4px;font-size:13px;font-family:\'SF Mono\',SFMono-Regular,Menlo,Consolas,monospace;color:#a8a29e">$1</code>',
104
+ `<code style="background:${EMAIL_TOKENS.color.bgInset};border:1px solid ${EMAIL_TOKENS.color.border};padding:2px 6px;border-radius:${EMAIL_TOKENS.radius.inline}px;font-size:${EMAIL_TOKENS.type.code}px;font-family:${EMAIL_TOKENS.font.mono};color:#f5f5f4">$1</code>`,
86
105
  )
87
106
  );
88
107
  }
@@ -98,7 +117,7 @@ function markdownToEmailHtml(md) {
98
117
  const escaped = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
99
118
  const placeholder = `%%CODEBLOCK_${codeBlocks.length}%%`;
100
119
  codeBlocks.push(
101
- `<pre style="background:#0a0a0a;border:1px solid #1c1917;border-radius:8px;padding:12px 16px;font-size:13px;font-family:'SF Mono',SFMono-Regular,Menlo,Consolas,monospace;color:#f5f5f4;overflow-x:auto;margin:16px 0;line-height:1.5"><code>${escaped.trimEnd()}</code></pre>`,
120
+ `<pre style="background:${EMAIL_TOKENS.color.bg};border:1px solid ${EMAIL_TOKENS.color.border};border-radius:${EMAIL_TOKENS.radius.block}px;padding:16px;font-size:${EMAIL_TOKENS.type.code}px;font-family:${EMAIL_TOKENS.font.mono};color:#f5f5f4;overflow-x:auto;margin:16px 0;line-height:1.6"><code>${escaped.trimEnd()}</code></pre>`,
102
121
  );
103
122
  return placeholder;
104
123
  });
@@ -136,29 +155,37 @@ function markdownToEmailHtml(md) {
136
155
  tableRows = [];
137
156
  return;
138
157
  }
158
+ // Notion-style grid per the rendering ruleset: stone-800 borders,
159
+ // stone-925 header row, px-3 py-1.5 cells, 14px text, wrapped in the
160
+ // same rounded-xl (12px) chrome as code blocks. The wrapper draws the
161
+ // outer border, so cells only draw their inner right/bottom edges.
162
+ const cellBase = `padding:6px 12px;text-align:left;vertical-align:top;font-size:${EMAIL_TOKENS.type.table}px`;
163
+ const edge = `1px solid ${EMAIL_TOKENS.color.tableBorder}`;
164
+ const cellBorders = (isLastCol, isLastRow) =>
165
+ `${isLastCol ? '' : `border-right:${edge};`}${isLastRow ? '' : `border-bottom:${edge};`}`;
139
166
  const headerCells = tableRows[0].cells;
140
167
  const bodyRows = tableRows.slice(1);
141
168
  const hdr = headerCells
142
169
  .map(
143
- (c) =>
144
- `<th style="background:#292524;padding:8px 12px;text-align:left;font-size:13px;font-weight:600;color:#ffffff;border:1px solid #44403c">${inlineFormat(c)}</th>`,
170
+ (c, i) =>
171
+ `<th style="${cellBase};${cellBorders(i === headerCells.length - 1, false)}background:${EMAIL_TOKENS.color.tableHeaderBg};font-weight:500;color:#ffffff">${inlineFormat(c)}</th>`,
145
172
  )
146
173
  .join('');
147
174
  const body = bodyRows
148
175
  .map(
149
- (r) =>
176
+ (r, rowIdx) =>
150
177
  '<tr>' +
151
178
  r.cells
152
179
  .map(
153
- (c) =>
154
- `<td style="padding:8px 12px;font-size:13px;color:#d6d3d1;border:1px solid #44403c">${inlineFormat(c)}</td>`,
180
+ (c, i) =>
181
+ `<td style="${cellBase};${cellBorders(i === r.cells.length - 1, rowIdx === bodyRows.length - 1)}color:#ffffff">${inlineFormat(c)}</td>`,
155
182
  )
156
183
  .join('') +
157
184
  '</tr>',
158
185
  )
159
186
  .join('');
160
187
  out.push(
161
- `<table style="width:100%;border-collapse:collapse;margin:16px 0;background:#1c1917;border-radius:8px;overflow:hidden"><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table>`,
188
+ `<div style="border:${edge};border-radius:12px;overflow:hidden;margin:16px 0"><table style="width:100%;border-collapse:collapse"><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table></div>`,
162
189
  );
163
190
  tableRows = [];
164
191
  }
@@ -166,7 +193,7 @@ function markdownToEmailHtml(md) {
166
193
  if (!quoteLines.length) return;
167
194
  const content = inlineFormat(quoteLines.join('\n')).replace(/\n/g, '<br>');
168
195
  out.push(
169
- `<blockquote style="margin:16px 0;padding:8px 16px;border-left:3px solid #44403c;color:#d6d3d1;font-style:italic">${content}</blockquote>`,
196
+ `<blockquote style="margin:16px 0;padding:8px 16px;border-left:3px solid ${EMAIL_TOKENS.color.tableBorder};color:${EMAIL_TOKENS.color.textSecondary};font-style:italic">${content}</blockquote>`,
170
197
  );
171
198
  quoteLines = [];
172
199
  }
@@ -200,16 +227,16 @@ function markdownToEmailHtml(md) {
200
227
  }
201
228
  if (/^(-{3,}|_{3,}|\*{3,})$/.test(trimmed)) {
202
229
  flushAll();
203
- out.push('<hr style="border:none;border-top:1px solid #292524;margin:24px 0">');
230
+ out.push(`<hr style="border:none;border-top:1px solid ${EMAIL_TOKENS.color.border};margin:24px 0">`);
204
231
  continue;
205
232
  }
206
233
  const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
207
234
  if (headingMatch) {
208
235
  flushAll();
209
236
  const level = headingMatch[1].length;
210
- const sizes = { 1: '22px', 2: '18px', 3: '16px', 4: '15px', 5: '14px', 6: '13px' };
237
+ const size = EMAIL_TOKENS.type.headings[level];
211
238
  out.push(
212
- `<h${level} style="margin:20px 0 8px;font-size:${sizes[level]};font-weight:600;color:#ffffff;line-height:1.3">${inlineFormat(headingMatch[2])}</h${level}>`,
239
+ `<h${level} style="margin:20px 0 8px;font-size:${size}px;font-weight:650;color:#ffffff;line-height:1.35">${inlineFormat(headingMatch[2])}</h${level}>`,
213
240
  );
214
241
  continue;
215
242
  }
@@ -270,14 +297,14 @@ function markdownToEmailHtml(md) {
270
297
  function formatNotificationEmail(message, link) {
271
298
  const htmlMessage = markdownToEmailHtml(message);
272
299
  const linkHtml = link
273
- ? `<p style="margin:24px 0 0"><a href="${link}" style="color:#ffffff;text-decoration:underline;font-size:14px">View details &rarr;</a></p>`
300
+ ? `<p style="margin:24px 0 0"><a href="${link}" style="color:${EMAIL_TOKENS.color.link};text-decoration:none;font-size:14px">View details &rarr;</a></p>`
274
301
  : '';
275
302
  return `<!DOCTYPE html>
276
303
  <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
277
- <body style="margin:0;padding:0;background:#000000;font-family:'SF Pro Text',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif;-webkit-font-smoothing:antialiased">
304
+ <body style="margin:0;padding:0;background:#000000;font-family:${EMAIL_TOKENS.font.text};-webkit-font-smoothing:antialiased">
278
305
  <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#000000"><tr><td align="center" style="padding:0">
279
306
  <table role="presentation" width="520" cellpadding="0" cellspacing="0" style="max-width:520px;width:100%"><tr><td style="padding:40px 28px 48px">
280
- <div style="font-size:15px;color:#ffffff">${htmlMessage}</div>
307
+ <div style="font-size:${EMAIL_TOKENS.type.body}px;color:#ffffff">${htmlMessage}</div>
281
308
  ${linkHtml}
282
309
  <p style="margin:48px 0 0;font-size:12px;color:#ffffff">amalgm.ai</p>
283
310
  </td></tr></table>