@ammduncan/easel 0.2.11 → 0.2.13

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 0.2.13 — 2026-05-23
6
+
7
+ ### Added
8
+ - **`kind: "mockup"` and `kind: "app"` switch the iframe into app-fidelity mode.** The wrapper skips its presentation defaults (Inter body font, design-token CSS, semantic chips, body bg/color, prose width constraints) and only keeps the box-sizing reset and the html-to-image bridge. Agent paints everything. Makes the existing "App/UI recreations are always locked-mode" rule structural — for a true mockup, opt in via `kind` and the wrapper stops fighting you instead of relying on the agent remembering to override every default. Presentation pushes (explanations, comparisons, status reports) keep the existing wrapper as before.
9
+
10
+ ## 0.2.12 — 2026-05-23
11
+
12
+ ### Docs
13
+ - **Locked-mode guidance now ships a paired light example next to the dark one.** The existing rule — "background and text are a pair, commit both, re-scope `color: inherit` to children" — was illustrated only with a dark `.terminal` block. Agents (and people) generalized the rule to "lock your dark containers" and missed the equally-common inverse: a white `.card` on the host canvas with no `color:` of its own, which in dark host mode inherits `.wrap`'s `light-dark()` and resolves to light gray → invisible titles on white. The skill and inline `push` tool description now show both shapes side by side so agents see the rule is direction-agnostic.
14
+
5
15
  ## 0.2.11 — 2026-05-22
6
16
 
7
17
  ### Docs
@@ -585,7 +585,7 @@
585
585
  iframe.setAttribute("scrolling", "no");
586
586
  iframe.setAttribute("title", push.title || "push " + push.index);
587
587
  iframe.dataset.pushId = push.id;
588
- iframe.srcdoc = wrapPushedHtml(push.html, currentTheme(), push.id);
588
+ iframe.srcdoc = wrapPushedHtml(push.html, currentTheme(), push.id, push.kind);
589
589
  iframe.addEventListener("load", () => {
590
590
  iframes.add(iframe);
591
591
  // Primary path: the iframe self-measures and posts back size via
@@ -607,7 +607,7 @@
607
607
  - write plain HTML (<h1>, <h2>, <p>, etc.) — gets styled for free, OR
608
608
  - write their own <style> and override anything.
609
609
  ============================================================ */
610
- function wrapPushedHtml(html, theme, pushId) {
610
+ function wrapPushedHtml(html, theme, pushId, kind) {
611
611
  // Authors sometimes wrap payloads in <![CDATA[ ... ]]> (treating html
612
612
  // like CDATA-in-XML). Strip the XML-ism before doing anything else —
613
613
  // otherwise the iframe renders the CDATA tags as visible text.
@@ -620,7 +620,8 @@
620
620
  if (lower.startsWith("<!doctype") || lower.startsWith("<html")) {
621
621
  return injectBridge(cleaned, theme, preset, pushId);
622
622
  }
623
- return buildDefaultWrapper(cleaned, theme, preset, pushId);
623
+ const isAppFidelity = kind === "mockup" || kind === "app";
624
+ return buildDefaultWrapper(cleaned, theme, preset, pushId, isAppFidelity);
624
625
  }
625
626
 
626
627
  function selfMeasureScript(pushId) {
@@ -631,8 +632,29 @@
631
632
  );
632
633
  }
633
634
 
634
- function buildDefaultWrapper(body, theme, preset, pushId) {
635
+ function buildDefaultWrapper(body, theme, preset, pushId, appFidelity) {
635
636
  const density = currentDensity();
637
+ // app-fidelity mode: skip presentation defaults (presets, semantic chips,
638
+ // body font/bg/color, prose constraints). Agent paints everything. Only
639
+ // keeps box-sizing reset + the html-to-image bridge script.
640
+ if (appFidelity) {
641
+ return `<!DOCTYPE html>
642
+ <html data-theme="${theme}" data-preset="${preset}" data-density="${density}" data-app-fidelity="true">
643
+ <head>
644
+ <meta charset="utf-8" />
645
+ <base target="_blank" />
646
+ <script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.js"></script>
647
+ <style>
648
+ *, *::before, *::after { box-sizing: border-box; }
649
+ html, body { margin: 0; padding: 0; }
650
+ </style>
651
+ </head>
652
+ <body>
653
+ ${body}
654
+ <script>${selfMeasureScript(pushId)}</script>
655
+ </body>
656
+ </html>`;
657
+ }
636
658
  return `<!DOCTYPE html>
637
659
  <html data-theme="${theme}" data-preset="${preset}" data-density="${density}">
638
660
  <head>
package/dist/mcp.js CHANGED
@@ -82,7 +82,7 @@ const inputSchema = {
82
82
  },
83
83
  kind: {
84
84
  type: "string",
85
- description: "Freeform tag: mockup, diff, explanation, comparison, diagram, status, progress, etc.",
85
+ description: "Freeform tag: mockup, app, diff, explanation, comparison, diagram, status, progress, etc. SPECIAL: 'mockup' and 'app' switch the iframe into APP-FIDELITY mode — the wrapper skips its presentation defaults (Inter body font, design-token CSS, semantic chips, prose width constraints, body bg/color). Only the box-sizing reset and the html-to-image bridge stay. Use this when the push is a recreation of real UI (app screen, component instance, embedded preview) and you want full control over every pixel without the host theme leaking in. For presentation content (explanations, comparisons, status reports), omit kind or use a non-fidelity value.",
86
86
  },
87
87
  },
88
88
  required: ["html"],
@@ -128,6 +128,9 @@ export async function main() {
128
128
  " .terminal *, .terminal span, .terminal pre { color: inherit; }\n" +
129
129
  " .terminal .muted { color: #94a3b8; }\n" +
130
130
  " .terminal .accent { color: #6ee7b7; }\n" +
131
+ "• Same pairing applies in the OPPOSITE direction — locked-LIGHT containers (e.g. a white card on the host canvas). A `.card { background: #fff }` with no `color:` inherits `.wrap`'s light-dark() text, which in dark host mode resolves to a light cream/gray → invisible titles on a white card. Commit text too AND re-scope inherit on children. This bites just as often as the dark case.\n" +
132
+ " .card { background: #ffffff; color: #111111; border: 1px solid #e5e5e5; border-radius: 12px; padding: 24px 32px; }\n" +
133
+ " .card * { color: inherit; }\n" +
131
134
  "• Syntax-highlighted code in a locked-bg block: EVERY token color must be verified readable against the bg, not just the body color. Recurring bug: locking to #0f172a then giving 'property' / 'punctuation' / 'comment' tokens something like #2c2c40 because it 'looked subtle' — against #0f172a it's nearly invisible and identifiers disappear. Either use a tested theme designed for your bg (Shiki github-dark / vitesse-dark / one-dark-pro for #0f172a-ish, github-light / vitesse-light for #f5f7fa-ish), or pick from this verified palette for #0f172a: keyword #ff7b72, string #a5d6ff, function #d2a8ff, property #79c0ff, number #ffa657, comment #8b949e, default text #e6edf3. If you can't articulate why each token reads against the bg, drop highlighting and use single-color monospace — that always works.\n\n" +
132
135
  "═══ TYPOGRAPHY (presentation scale, NOT dashboard) ═══\n" +
133
136
  "• Hero title: 44–52px, weight 500, letter-spacing -0.025em\n" +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ammduncan/easel",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "A live browser tab for every Claude Code (and MCP) session. The push MCP tool appends HTML cards to a scrolling feed you keep open in split-screen.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -189,14 +189,28 @@ When the mockup references a real thing — a real app, a real component, a real
189
189
  **If you can't reach the actuals**, say so explicitly in the chat reply (e.g. *"Couldn't find the project's theme file — colors and sizing in this mock are estimates"*) and skip the mock if it would mislead. A recreation labelled "approximation" is fine; one passed off as accurate is a trap.
190
190
 
191
191
  ```css
192
+ /* Locked-dark container (terminal, dark code block, dark callout). */
192
193
  .terminal {
193
194
  background: #0f172a; /* locked dark, ignores host mode */
194
195
  color: #e6edf3; /* MUST set text too */
195
196
  }
196
197
  .terminal * { color: inherit; } /* re-scope so .wrap's light-dark() doesn't leak in */
198
+
199
+ /* Locked-LIGHT container (white card on the host canvas). Just as common a
200
+ * failure as the dark case: a white `.card` with no `color:` of its own
201
+ * inherits `.wrap`'s light-dark() text → in dark host mode that resolves to
202
+ * light gray → invisible titles on a white card. Commit the same way. */
203
+ .card {
204
+ background: #ffffff; /* locked white, ignores host mode */
205
+ color: #111111; /* MUST set text too */
206
+ border: 1px solid #e5e5e5;
207
+ border-radius: 12px;
208
+ padding: 24px 32px;
209
+ }
210
+ .card * { color: inherit; }
197
211
  ```
198
212
 
199
- The rule of thumb: background and text are a pair — commit one, commit the other.
213
+ The rule of thumb: background and text are a pair — commit one, commit the other. The direction (dark or light) doesn't matter; the pairing does.
200
214
 
201
215
  **Syntax highlighting in locked-bg code blocks needs *every token* verified.** "Bg + text are a pair" extends to every token color you use. The recurring failure: lock a code block to `#0f172a`, then layer syntax tokens where one (usually `property`, `punctuation`, or `comment`) is colored `#2c2c40` or `#3b4252` because it "looked subtle" — against `#0f172a` it's nearly invisible and whole identifiers disappear from the block. Two ways out:
202
216