amalgm 0.1.101 → 0.1.102

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.101",
3
+ "version": "0.1.102",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -115,30 +115,68 @@ function isEmbeddableImageSrc(url) {
115
115
 
116
116
  // --- EmbedChrome (email form) ------------------------------------------------
117
117
 
118
- function chromePill(content) {
119
- return `<span style="display:inline-block;background:${COLOR.bgChip};border:1px solid ${COLOR.pillBorder};border-radius:999px;padding:4px 12px;font-size:12px;line-height:16px;color:${COLOR.text};white-space:nowrap">${content}</span>`;
118
+ /**
119
+ * Embed-type glyph shown before the name — Gmail-safe geometric unicode
120
+ * (inline SVG is stripped by Gmail), the email stand-in for the UI's
121
+ * FileTypeIcon / app icon.
122
+ */
123
+ // Codepoints with NO emoji variant — Gmail swaps U+25B6 (▶) for a colored
124
+ // emoji even with the U+FE0E text selector, so we use the small triangle
125
+ // U+25B8 (▸) instead. ◆ / ▣ / ♪ have no emoji form and render monochrome.
126
+ const EMBED_GLYPH = {
127
+ video: '▸', // ▸
128
+ audio: '♪', // ♪
129
+ app: '◆', // ◆
130
+ entity: '◆', // ◆
131
+ document: '▣', // ▣
132
+ file: '▣', // ▣
133
+ };
134
+
135
+ function glyphSpan(glyph) {
136
+ return glyph
137
+ ? `<span style="font-size:12px;line-height:16px;color:${COLOR.text};padding-right:7px">${glyph}</span>`
138
+ : '';
139
+ }
140
+
141
+ /**
142
+ * One header pill, the email form of EmbedChrome's floating pill: a pure-black
143
+ * fill (Gmail dark mode recolors near-blacks like stone-950 — which is why the
144
+ * name used to flip to a white pill — but leaves #000 alone) with a stone-800
145
+ * border carrying the rounded shape and white text.
146
+ */
147
+ function headerPill(inner) {
148
+ return `<span style="display:inline-block;background:${COLOR.bg};border:1px solid ${COLOR.pillBorder};border-radius:12px;padding:5px 11px;font-size:13px;line-height:16px;color:${COLOR.text};white-space:nowrap">${inner}</span>`;
149
+ }
150
+
151
+ /** `[glyph] name` — name clipped with an ellipsis (email's stand-in for the UI's edge fade). */
152
+ function namePill(glyph, name) {
153
+ const label =
154
+ `<span style="display:inline-block;max-width:220px;overflow:hidden;text-overflow:ellipsis;` +
155
+ `white-space:nowrap;vertical-align:bottom;color:${COLOR.text}">${escapeText(name)}</span>`;
156
+ return headerPill(`${glyphSpan(glyph)}${label}`);
157
+ }
158
+
159
+ /** The single "Open ↗" affordance — a link when href is given. */
160
+ function openPill(href) {
161
+ const inner = `Open<span style="color:${COLOR.textMuted};padding-left:5px">&#8599;</span>`;
162
+ return href
163
+ ? `<a href="${escapeAttr(href)}" target="_blank" style="text-decoration:none">${headerPill(inner)}</a>`
164
+ : headerPill(inner);
120
165
  }
121
166
 
122
167
  /**
123
168
  * The expanded-embed card: rounded-16 stone-900 border, black fill, a header
124
- * row carrying the name pill (left) and an action pill (right), media below.
125
- * actionLabel+actionHref make the right pill a link; actionText makes it a
126
- * plain label (e.g. "Attachment").
169
+ * row carrying the `[glyph] name` pill (left) and a single Open pill (right),
170
+ * media below. `action.href` points the Open pill; pass action:null for none.
127
171
  */
128
- function embedShell({ name, actionLabel, actionHref, actionText, body }) {
129
- const namePill = name ? chromePill(escapeText(name)) : '';
130
- const actionPill = actionLabel && actionHref
131
- ? `<a href="${escapeAttr(actionHref)}" target="_blank" style="text-decoration:none">${chromePill(
132
- `<span style="color:${COLOR.link}">${escapeText(actionLabel)}</span>`,
133
- )}</a>`
134
- : actionText
135
- ? chromePill(`<span style="color:${COLOR.textMuted}">${escapeText(actionText)}</span>`)
136
- : '';
172
+ function embedShell({ glyph, name, action, body }) {
173
+ const left = name ? namePill(glyph, name) : '';
174
+ const right = action ? openPill(action.href) : '';
137
175
  return (
138
176
  `<div style="border:1px solid ${COLOR.border};border-radius:16px;background:${COLOR.bg};margin:16px 0;overflow:hidden">` +
139
177
  `<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>` +
140
- `<td align="left" style="padding:10px 10px 8px">${namePill}</td>` +
141
- `<td align="right" style="padding:10px 10px 8px">${actionPill}</td>` +
178
+ `<td align="left" style="padding:10px 10px 8px">${left}</td>` +
179
+ `<td align="right" style="padding:10px 10px 8px;white-space:nowrap">${right}</td>` +
142
180
  `</tr></table>` +
143
181
  body +
144
182
  `</div>`
@@ -147,9 +185,19 @@ function embedShell({ name, actionLabel, actionHref, actionText, body }) {
147
185
 
148
186
  // --- Media embeds -------------------------------------------------------------
149
187
 
150
- /** Bare rounded image per the ruleset — images never wear chrome. */
151
- function renderImageEmbed(alt, url) {
152
- return `<img src="${escapeAttr(url)}" alt="${escapeAttr(alt || '')}" style="display:block;max-width:100%;height:auto;border-radius:16px;border:1px solid ${COLOR.border};margin:16px 0">`;
188
+ /**
189
+ * Bare rounded image per the ruleset — images never wear chrome. The <img> is
190
+ * wrapped in a link so Gmail treats it as a clickable image rather than a
191
+ * savable attachment, which suppresses Gmail's Download / Save-to-Photos /
192
+ * Save-to-Drive hover overlay. http images link to themselves; inline (cid)
193
+ * images take the passed href (the "open in amalgm" target).
194
+ */
195
+ function renderImageEmbed(alt, url, href) {
196
+ const img = `<img src="${escapeAttr(url)}" alt="${escapeAttr(alt || '')}" style="display:block;max-width:100%;height:auto;border-radius:16px;border:1px solid ${COLOR.border};margin:16px 0">`;
197
+ const link = href || (isHttpUrl(url) ? url : null);
198
+ return link
199
+ ? `<a href="${escapeAttr(link)}" target="_blank" style="text-decoration:none;display:block">${img}</a>`
200
+ : img;
153
201
  }
154
202
 
155
203
  const PLAY_PILL =
@@ -185,7 +233,7 @@ function renderVideoEmbed(label, url, poster) {
185
233
  `<video controls preload="metadata" src="${safeUrl}"${posterSrc ? ` poster="${escapeAttr(posterSrc)}"` : ''} style="display:block;width:100%;max-height:480px;background:${COLOR.bg}">` +
186
234
  fallback +
187
235
  `</video>`;
188
- return embedShell({ name, actionLabel: 'Open ↗', actionHref: url, body });
236
+ return embedShell({ glyph: EMBED_GLYPH.video, name, action: { href: url }, body });
189
237
  }
190
238
 
191
239
  /**
@@ -197,16 +245,15 @@ function renderVideoEmbed(label, url, poster) {
197
245
  function renderAttachedVideoCard(label, poster) {
198
246
  const name = label || 'Video';
199
247
  const posterSrc = poster && isEmbeddableImageSrc(poster) ? poster : null;
200
- const note =
201
- `<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>` +
202
- `<td align="center" style="padding:${posterSrc ? '0 16px 16px' : '40px 16px 16px'};background:${COLOR.bg}">` +
203
- (posterSrc ? '' : `${PLAY_PILL}<br>`) +
204
- `<span style="display:inline-block;padding-top:8px;font-size:12px;color:${COLOR.textMuted}">Attached to this email &mdash; open the attachment to play</span>` +
205
- `</td></tr></table>`;
206
- const body = (posterSrc
207
- ? `<img src="${escapeAttr(posterSrc)}" alt="${escapeAttr(name)}" style="display:block;width:100%;height:auto;background:${COLOR.bg}">`
208
- : '') + note;
209
- return embedShell({ name, actionText: 'Attachment', body });
248
+ // Poster is the animating GIF in Gmail; the real video rides in the
249
+ // attachment strip. No in-body note — the header + strip carry it.
250
+ // Poster <img> wrapped in a link so Gmail doesn't slap its save/download
251
+ // hover overlay on it (it treats linked images as clickable, not savable).
252
+ const body = posterSrc
253
+ ? `<a href="https://amalgm.ai" target="_blank" style="text-decoration:none;display:block"><img src="${escapeAttr(posterSrc)}" alt="${escapeAttr(name)}" style="display:block;width:100%;height:auto;background:${COLOR.bg}"></a>`
254
+ : `<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>` +
255
+ `<td align="center" style="padding:44px 16px;background:${COLOR.bg}">${PLAY_PILL}</td></tr></table>`;
256
+ return embedShell({ glyph: EMBED_GLYPH.video, name, action: { href: 'https://amalgm.ai' }, body });
210
257
  }
211
258
 
212
259
  /**
@@ -216,18 +263,19 @@ function renderAttachedVideoCard(label, poster) {
216
263
  function renderFileCardEmbed(label, url) {
217
264
  const name = label || mediaNameOf(url) || 'File';
218
265
  const ext = mediaExtensionOf(name).toUpperCase();
219
- const inner =
220
- `<span style="font-size:13px;color:${COLOR.text}">${escapeText(name)}</span>` +
221
- (ext ? `<span style="font-size:10px;color:${COLOR.textMuted};padding-left:8px">${escapeText(ext)}</span>` : '');
222
- const body = isHttpUrl(url)
223
- ? `<a href="${escapeAttr(url)}" target="_blank" style="text-decoration:none;display:block;padding:4px 14px 14px">${inner}<span style="font-size:12px;color:${COLOR.link};padding-left:10px">Open &#8599;</span></a>`
224
- : `<div style="padding:4px 14px 14px">${inner}<span style="font-size:12px;color:${COLOR.textMuted};padding-left:10px">On your computer &mdash; open in amalgm</span></div>`;
225
- return embedShell({
226
- name,
227
- actionLabel: isHttpUrl(url) ? 'Open ↗' : 'amalgm ↗',
228
- actionHref: isHttpUrl(url) ? url : 'https://amalgm.ai',
229
- body,
230
- });
266
+ const http = isHttpUrl(url);
267
+ const glyph = mediaKindOf(url) === 'audio' ? EMBED_GLYPH.audio : EMBED_GLYPH.document;
268
+ const extChip = ext
269
+ ? `<span style="font-size:10px;color:${COLOR.textMuted};padding-left:8px">${escapeText(ext)}</span>`
270
+ : '';
271
+ const meta = http
272
+ ? ''
273
+ : `<span style="font-size:12px;color:${COLOR.textMuted};padding-left:10px">On your computer</span>`;
274
+ const body =
275
+ `<div style="padding:6px 16px 16px">` +
276
+ `<span style="font-size:13px;color:${COLOR.text}">${escapeText(name)}</span>${extChip}${meta}` +
277
+ `</div>`;
278
+ return embedShell({ glyph, name, action: { href: http ? url : 'https://amalgm.ai' }, body });
231
279
  }
232
280
 
233
281
  /**
@@ -243,7 +291,7 @@ function renderFileCardEmbed(label, url) {
243
291
  */
244
292
  function renderMediaEmbed(alt, url, poster) {
245
293
  if (isCidUrl(url)) {
246
- return renderImageEmbed(alt, url);
294
+ return renderImageEmbed(alt, url, 'https://amalgm.ai');
247
295
  }
248
296
  const attachedMatch = url.match(/^attached:(.*)$/i);
249
297
  if (attachedMatch) {
@@ -313,13 +361,11 @@ function renderEntityExpanded(label, type) {
313
361
  const display = label || entityTypeLabel(type);
314
362
  const body =
315
363
  `<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr>` +
316
- `<td align="center" style="padding:36px 16px 40px">` +
364
+ `<td align="center" style="padding:40px 16px 44px">` +
317
365
  `<span style="display:block;font-size:11px;line-height:14px;color:${COLOR.textMuted};text-transform:uppercase;letter-spacing:0.04em;padding-bottom:6px">${escapeText(entityTypeLabel(type))}</span>` +
318
- `<span style="display:block;font-size:16px;font-weight:650;color:${COLOR.text};padding-bottom:16px">${escapeText(display)}</span>` +
319
- `<a href="https://amalgm.ai" target="_blank" style="text-decoration:none">` +
320
- `<span style="display:inline-block;background:${COLOR.bgChip};border:1px solid ${COLOR.pillBorder};border-radius:999px;padding:8px 18px;font-size:13px;color:${COLOR.link}">Open in amalgm &#8599;</span>` +
321
- `</a></td></tr></table>`;
322
- return embedShell({ name: display, actionLabel: 'Open ↗', actionHref: 'https://amalgm.ai', body });
366
+ `<span style="display:block;font-size:16px;font-weight:650;color:${COLOR.text}">${escapeText(display)}</span>` +
367
+ `</td></tr></table>`;
368
+ return embedShell({ glyph: EMBED_GLYPH.entity, name: display, action: { href: 'https://amalgm.ai' }, body });
323
369
  }
324
370
 
325
371
  function renderEntityEmbed(mode, type, label) {