@ammduncan/easel 0.2.29 → 0.3.1

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,24 @@
2
2
 
3
3
  All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 0.3.1 — 2026-05-26
6
+
7
+ ### Fixed
8
+ - **Prose measure tightened from ~90 to ~66 characters per line.** The reading-column cap was `max-width: 880px`, which at the 18px body font produces ~90-character lines — past WCAG 1.4.8's 80-char ceiling and well past Bringhurst's 45–75 comfortable range. Changed to `max-width: 56ch`. The `ch` unit is the width of the "0" glyph and proportional Inter averages narrower, so 56ch renders ~66 actual characters — the reading-measure sweet spot. (`ch` scales with font-size, so headings stay proportional; short headings never hit the cap, so it's load-bearing only on body paragraphs, which is correct.)
9
+ - **`.full-bleed` now has vertical breathing room.** It only set horizontal margins, so a paragraph after an embedded mockup hugged the frame with no gap. Added `margin: 32px 0`; it collapses correctly against adjacent prose margins (32px, not 64) and the existing first/last-child margin resets still zero it at the card's top/bottom edge.
10
+
11
+ ## 0.3.0 — 2026-05-26
12
+
13
+ ### Fixed
14
+ - **App-fidelity mode (`kind:"mockup"`/`"app"`) no longer strips the structural primitives — `.window`/`.code`/`.terminal` now render in mockups.** The wrapper has two branches: a normal presentation branch and an app-fidelity branch for UI recreations (which skips presets/tokens/chips/prose-caps/body-bg so the agent controls every pixel). But app-fidelity skipped the *entire* built-in stylesheet — including the self-contained `.window` window chrome and `.code`/`.terminal` code blocks — while the skill and the `push` tool description told agents to use `.window` *for mockups*. So a `kind:"mockup"` push with `<div class="window">` rendered as unstyled serif text with no chrome (the same "I shipped a fix but it doesn't apply here" surprise as the 0.2.29 `.window` washout). Extracted the primitives — which use fixed, theme-independent colours and can't leak the host theme — into a shared `STRUCTURAL_PRIMITIVES_CSS` constant injected into BOTH wrapper branches (single source, no duplication; the normal branch's inline copies and their `@media print` overrides were removed). A mockup can now reach for `.window`/`.code`/`.terminal` and they render, in either kind.
15
+ - **App-fidelity mode now sets a system-sans default font instead of falling back to serif.** It deliberately omits the Inter webfont (so the agent controls typography with no CDN dependency), but it also left no `font-family` at all, so any mockup that didn't set its own font rendered in Times serif. Added a `system-ui, -apple-system, "Segoe UI", sans-serif` floor; the pushed HTML's own `font-family` still wins the cascade.
16
+
17
+ ### Added
18
+ - **Visual-regression test suite (`tests/visual/`).** A fixture battery covering every built-in primitive plus realistic composites, each carrying an injected contrast-audit that walks text nodes, composites translucent backgrounds over the host backdrop, computes WCAG contrast, and reports washout failures. A driver pushes them to a running server; the suite is rendered across the full preset × theme × density matrix (+ print) to catch surface-vs-ink regressions. The two fixes above were both found by this suite. See `tests/visual/README.md`.
19
+
20
+ ### Docs
21
+ - **`push` tool `kind` description and the using-easel skill now state what app-fidelity keeps vs strips** — structural primitives + sans default kept; presentation defaults (preset tokens, chips, prose caps, body bg/color, Inter) stripped — so the "use `.window` for mockups" guidance and the engine no longer contradict each other.
22
+
5
23
  ## 0.2.29 — 2026-05-25
6
24
 
7
25
  ### Fixed
@@ -110,6 +110,127 @@
110
110
  :root[data-theme="light"] .chip.info { background:#ecfeff; color:#0e7490; border-color:#a5f3fc; box-shadow:0 0 12px -6px rgba(14,116,144,.45); }
111
111
  :root[data-theme="dark"] .chip.info { background:#0a1c22; color:#67e8f9; border-color:#155060; box-shadow:0 0 12px -4px rgba(103,232,249,.4); }
112
112
  .chip.accent { background:var(--ds-accent-soft); color:var(--ds-accent); border-color:transparent; box-shadow:0 0 12px -4px color-mix(in srgb, var(--ds-accent) 40%, transparent); }
113
+ `;
114
+
115
+ /* Self-contained structural primitives — window chrome + locked code/terminal.
116
+ These pin their own background and ink with fixed (theme-independent) colours,
117
+ so unlike the preset-token presentation styles they CAN'T leak the host theme.
118
+ Injected in BOTH the normal wrapper AND app-fidelity (mockup/app) mode: a UI
119
+ recreation is exactly where an agent reaches for a window frame or a code
120
+ block, so stripping these in fidelity mode left the skill's own guidance
121
+ ("wrap a mockup in .window") producing unstyled output. */
122
+ const STRUCTURAL_PRIMITIVES_CSS = `
123
+ /* Skeuomorphic macOS-style window chrome for UI mockups. Usage:
124
+ <div class="window" data-title="App name"> …mockup content… </div>
125
+ Draws a 40px title bar with the three traffic-light dots (red/yellow/green)
126
+ and an optional centred title from data-title. Content sits below the bar.
127
+ Add the desktop class for a full desktop-screen canvas — min-height 900px,
128
+ i.e. the 1440x900 (16:10) standard design canvas — so a screen mockup looks
129
+ like a real window with viewport breathing room rather than a short strip.
130
+ Omit desktop for dialogs / small components so they stay content-sized.
131
+
132
+ A mockup renders an app's own UI, so it owns a STABLE surface that does NOT
133
+ flip with the host theme — otherwise a light dashboard mockup renders on a dark
134
+ window in a dark-mode viewer and every subtle gray label washes out (same
135
+ surface-vs-ink mismatch the .code primitive locks against). Default is a light
136
+ canvas with pinned dark ink and color:inherit re-scoped to every child so the
137
+ host's light-dark() ink can never leak in. Add the dark class
138
+ (class="window dark") for a genuinely dark-UI mockup. */
139
+ .window {
140
+ position: relative;
141
+ padding-top: 40px;
142
+ border-radius: 12px;
143
+ border: 1px solid #e2e2e2;
144
+ box-shadow: 0 14px 48px rgba(0, 0, 0, 0.16);
145
+ overflow: hidden;
146
+ background: #ffffff;
147
+ color: #1a1a1a;
148
+ }
149
+ .window * { color: inherit; }
150
+ .window::before {
151
+ content: "";
152
+ position: absolute;
153
+ top: 0;
154
+ left: 0;
155
+ right: 0;
156
+ height: 40px;
157
+ background-color: #f1f1f1;
158
+ border-bottom: 1px solid #e2e2e2;
159
+ background-image:
160
+ radial-gradient(circle at 19px 20px, #ff5f57 6px, transparent 6.5px),
161
+ radial-gradient(circle at 39px 20px, #febc2e 6px, transparent 6.5px),
162
+ radial-gradient(circle at 59px 20px, #28c840 6px, transparent 6.5px);
163
+ background-repeat: no-repeat;
164
+ }
165
+ .window::after {
166
+ content: attr(data-title);
167
+ position: absolute;
168
+ top: 0;
169
+ left: 0;
170
+ right: 0;
171
+ height: 40px;
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ font-size: 13px;
176
+ font-weight: 500;
177
+ color: #6b6b6b;
178
+ pointer-events: none;
179
+ }
180
+ .window.dark {
181
+ border-color: #2a2a2a;
182
+ background: #161616;
183
+ color: #e6edf3;
184
+ box-shadow: 0 14px 48px rgba(0, 0, 0, 0.4);
185
+ }
186
+ .window.dark::before {
187
+ background-color: #1f1f1f;
188
+ border-bottom-color: #2a2a2a;
189
+ }
190
+ .window.dark::after { color: #9b9b9b; }
191
+ .window.desktop {
192
+ min-height: 900px;
193
+ }
194
+ /* Locked-dark code / terminal primitive. Reach for this instead of hand-rolling
195
+ a dark code container — the recurring failure is a custom dark <div> that sets
196
+ its own background but lets base text inherit .wrap's light-dark() ink, which
197
+ resolves to near-black in light host mode and vanishes against the dark panel.
198
+ This class locks BOTH background and ink, and re-scopes color:inherit to every
199
+ child so the host theme can never leak in. Ships the verified github-dark token
200
+ palette so syntax highlighting reads against #0f172a without per-token tuning.
201
+ Usage: <div class="code"><span class="kw">gcloud</span> services enable …</div>
202
+ .terminal is an alias; add .terminal for a prompt feel (same colors). */
203
+ .code, .terminal {
204
+ background: #0f172a;
205
+ color: #e6edf3;
206
+ border-radius: 12px;
207
+ padding: 18px 22px;
208
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
209
+ font-size: 13.5px;
210
+ line-height: 1.7;
211
+ overflow: auto;
212
+ margin: 16px 0 24px;
213
+ }
214
+ .code *, .terminal * { color: inherit; }
215
+ .code .kw, .terminal .kw { color: #ff7b72; } /* keywords, control flow */
216
+ .code .string, .terminal .string { color: #a5d6ff; } /* strings, attr values */
217
+ .code .fn, .terminal .fn { color: #d2a8ff; } /* function names */
218
+ .code .prop, .terminal .prop { color: #79c0ff; } /* identifiers, properties */
219
+ .code .num, .terminal .num { color: #ffa657; } /* numbers, constants */
220
+ .code .comment, .terminal .comment { color: #8b949e; } /* comments */
221
+ .code .muted, .terminal .muted { color: #94a3b8; } /* dim / secondary */
222
+ .code .accent, .terminal .accent { color: #6ee7b7; } /* highlight / success */
223
+ @media print {
224
+ /* Force the locked-dark primitives light for print — browsers drop background
225
+ colours by default, which would otherwise strand their light ink on white
226
+ paper. Applies in both normal and app-fidelity mode. The !important here
227
+ also (intentionally) overrides the normal branch's non-print-gated pre/code
228
+ theming, so code reads as dark-on-light on paper regardless of host theme. */
229
+ pre, code, .code, .terminal { background: #f4f3ed !important; color: #111 !important; border: 1px solid #ddd; }
230
+ .code *, .terminal * { color: #111 !important; }
231
+ .window.dark { background: #ffffff !important; color: #111 !important; }
232
+ .window.dark * { color: #111 !important; }
233
+ }
113
234
  `;
114
235
 
115
236
  const unreadIds = new Set();
@@ -653,7 +774,11 @@
653
774
  <script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.js"></script>
654
775
  <style>
655
776
  *, *::before, *::after { box-sizing: border-box; }
656
- html, body { margin: 0; padding: 0; }
777
+ /* Sane sans-serif floor so an unstyled mockup doesn't fall back to Times serif.
778
+ No CDN font here (that's the agent's call in fidelity mode) — the pushed HTML
779
+ can override font-family inline / in its own <style>, which wins the cascade. */
780
+ html, body { margin: 0; padding: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
781
+ ${STRUCTURAL_PRIMITIVES_CSS}
657
782
  </style>
658
783
  </head>
659
784
  <body>
@@ -671,9 +796,10 @@ ${body}
671
796
  <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
672
797
  <script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.js"></script>
673
798
  <style>
799
+ *, *::before, *::after { box-sizing: border-box; }
674
800
  ${PRESET_TOKENS_CSS}
675
801
  ${SEMANTIC_CHIPS_CSS}
676
- *, *::before, *::after { box-sizing: border-box; }
802
+ ${STRUCTURAL_PRIMITIVES_CSS}
677
803
  html, body {
678
804
  margin: 0;
679
805
  background: var(--ds-bg-elev);
@@ -703,7 +829,10 @@ body > h1, body > h2, body > h3, body > h4,
703
829
  body > .wrap > p, body > .wrap > .deck, body > .wrap > .lede,
704
830
  body > .wrap > ul, body > .wrap > ol, body > .wrap > blockquote,
705
831
  body > .wrap > h1, body > .wrap > h2, body > .wrap > h3, body > .wrap > h4 {
706
- max-width: 880px;
832
+ /* ~56ch of "0"-width lands ~66 actual characters in proportional Inter
833
+ (avg glyph is narrower than "0"), i.e. Bringhurst's reading-measure sweet
834
+ spot — not 56 literal characters. */
835
+ max-width: 56ch;
707
836
  }
708
837
  body > *:first-child { margin-top: 0 !important; }
709
838
  body > *:last-child { margin-bottom: 0 !important; }
@@ -714,83 +843,13 @@ body > *:last-child { margin-bottom: 0 !important; }
714
843
  stays as a gutter, so neither the mockup nor the surrounding text ever touches
715
844
  the card border. (The name is historical — it's "full content width", not
716
845
  "bleed to the card edge".) Capped at 100% of the content column, which the
717
- body's max-width already limits to desktop-realistic proportions. */
846
+ body's max-width already limits to desktop-realistic proportions.
847
+ Vertical margin gives an embedded mockup breathing room from the prose above
848
+ and below it (without it, the next paragraph hugs the frame). */
718
849
  .full-bleed {
719
850
  width: 100%;
720
851
  max-width: 100% !important;
721
- margin-left: 0;
722
- margin-right: 0;
723
- }
724
- /* Skeuomorphic macOS-style window chrome for UI mockups. Usage:
725
- <div class="window" data-title="App name"> …mockup content… </div>
726
- Draws a 40px title bar with the three traffic-light dots (red/yellow/green)
727
- and an optional centred title from data-title. Content sits below the bar.
728
- Add the desktop class for a full desktop-screen canvas — min-height 900px,
729
- i.e. the 1440x900 (16:10) standard design canvas — so a screen mockup looks
730
- like a real window with viewport breathing room rather than a short strip.
731
- Omit desktop for dialogs / small components so they stay content-sized.
732
-
733
- A mockup renders an app's own UI, so it owns a STABLE surface that does NOT
734
- flip with the host theme — otherwise a light dashboard mockup renders on a dark
735
- window in a dark-mode viewer and every subtle gray label washes out (same
736
- surface-vs-ink mismatch the .code primitive locks against). Default is a light
737
- canvas with pinned dark ink and color:inherit re-scoped to every child so the
738
- host's light-dark() ink can never leak in. Add the dark class
739
- (class="window dark") for a genuinely dark-UI mockup. */
740
- .window {
741
- position: relative;
742
- padding-top: 40px;
743
- border-radius: 12px;
744
- border: 1px solid #e2e2e2;
745
- box-shadow: 0 14px 48px rgba(0, 0, 0, 0.16);
746
- overflow: hidden;
747
- background: #ffffff;
748
- color: #1a1a1a;
749
- }
750
- .window * { color: inherit; }
751
- .window::before {
752
- content: "";
753
- position: absolute;
754
- top: 0;
755
- left: 0;
756
- right: 0;
757
- height: 40px;
758
- background-color: #f1f1f1;
759
- border-bottom: 1px solid #e2e2e2;
760
- background-image:
761
- radial-gradient(circle at 19px 20px, #ff5f57 6px, transparent 6.5px),
762
- radial-gradient(circle at 39px 20px, #febc2e 6px, transparent 6.5px),
763
- radial-gradient(circle at 59px 20px, #28c840 6px, transparent 6.5px);
764
- background-repeat: no-repeat;
765
- }
766
- .window::after {
767
- content: attr(data-title);
768
- position: absolute;
769
- top: 0;
770
- left: 0;
771
- right: 0;
772
- height: 40px;
773
- display: flex;
774
- align-items: center;
775
- justify-content: center;
776
- font-size: 13px;
777
- font-weight: 500;
778
- color: #6b6b6b;
779
- pointer-events: none;
780
- }
781
- .window.dark {
782
- border-color: #2a2a2a;
783
- background: #161616;
784
- color: #e6edf3;
785
- box-shadow: 0 14px 48px rgba(0, 0, 0, 0.4);
786
- }
787
- .window.dark::before {
788
- background-color: #1f1f1f;
789
- border-bottom-color: #2a2a2a;
790
- }
791
- .window.dark::after { color: #9b9b9b; }
792
- .window.desktop {
793
- min-height: 900px;
852
+ margin: 32px 0;
794
853
  }
795
854
  .wrap { display: block; }
796
855
  .kicker {
@@ -858,35 +917,6 @@ pre {
858
917
  margin: 16px 0 24px;
859
918
  }
860
919
  pre code { background: transparent; padding: 0; color: inherit; font-size: inherit; }
861
- /* Locked-dark code / terminal primitive. Reach for this instead of hand-rolling
862
- a dark code container — the recurring failure is a custom dark <div> that sets
863
- its own background but lets base text inherit .wrap's light-dark() ink, which
864
- resolves to near-black in light host mode and vanishes against the dark panel.
865
- This class locks BOTH background and ink, and re-scopes color:inherit to every
866
- child so the host theme can never leak in. Ships the verified github-dark token
867
- palette so syntax highlighting reads against #0f172a without per-token tuning.
868
- Usage: <div class="code"><span class="kw">gcloud</span> services enable …</div>
869
- .terminal is an alias; add .terminal for a prompt feel (same colors). */
870
- .code, .terminal {
871
- background: #0f172a;
872
- color: #e6edf3;
873
- border-radius: 12px;
874
- padding: 18px 22px;
875
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
876
- font-size: 13.5px;
877
- line-height: 1.7;
878
- overflow: auto;
879
- margin: 16px 0 24px;
880
- }
881
- .code *, .terminal * { color: inherit; }
882
- .code .kw, .terminal .kw { color: #ff7b72; } /* keywords, control flow */
883
- .code .string, .terminal .string { color: #a5d6ff; } /* strings, attr values */
884
- .code .fn, .terminal .fn { color: #d2a8ff; } /* function names */
885
- .code .prop, .terminal .prop { color: #79c0ff; } /* identifiers, properties */
886
- .code .num, .terminal .num { color: #ffa657; } /* numbers, constants */
887
- .code .comment, .terminal .comment { color: #8b949e; } /* comments */
888
- .code .muted, .terminal .muted { color: #94a3b8; } /* dim / secondary */
889
- .code .accent, .terminal .accent { color: #6ee7b7; } /* highlight / success */
890
920
  blockquote {
891
921
  border-left: 3px solid var(--ds-accent);
892
922
  margin: 18px 0;
@@ -933,13 +963,7 @@ img { max-width: 100%; height: auto; border-radius: 10px; }
933
963
  body { padding: 24px !important; max-width: none !important; }
934
964
  body > p, body > .deck, body > .lede, body > ul, body > ol, body > blockquote,
935
965
  body > h1, body > h2, body > h3, body > h4 { max-width: none !important; }
936
- pre, code, .code, .terminal { background: #f4f3ed !important; color: #111 !important; border: 1px solid #ddd; }
937
- .code *, .terminal * { color: #111 !important; }
938
- /* Force the dark window variant light for print — browsers drop background
939
- colors by default, which would leave its light ink invisible on white paper
940
- (same reason .code/.terminal are forced light above). */
941
- .window.dark { background: #ffffff !important; color: #111 !important; }
942
- .window.dark * { color: #111 !important; }
966
+ /* .code/.terminal/.window.dark print overrides live in STRUCTURAL_PRIMITIVES_CSS. */
943
967
  .card, .panel { background: #fff !important; border: 1px solid #ddd !important; box-shadow: none !important; }
944
968
  a { color: #111 !important; text-decoration: underline; border-bottom: 0 !important; }
945
969
  }
package/dist/mcp.js CHANGED
@@ -72,7 +72,7 @@ const inputSchema = {
72
72
  },
73
73
  kind: {
74
74
  type: "string",
75
- 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.",
75
+ 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 (preset design-token CSS, semantic chips, prose width constraints, body bg/color, the Inter webfont) so the host theme can't leak in and you control every pixel. It KEEPS the self-contained structural primitives (.window/.window.dark window chrome, .code/.terminal code blocks — all fixed-colour, theme-independent) and a neutral system-sans default font, so you can still reach for <div class=\"window\"> in a mockup and it renders. Set your own font-family/colours in the pushed HTML to override the sans default. Use this kind when the push is a recreation of real UI (app screen, component instance, embedded preview). For presentation content (explanations, comparisons, status reports), omit kind or use a non-fidelity value.",
76
76
  },
77
77
  },
78
78
  required: ["html"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ammduncan/easel",
3
- "version": "0.2.29",
3
+ "version": "0.3.1",
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",
@@ -299,7 +299,7 @@ Most mockups appear *inside* an explanation push — prose intro, embedded UI mo
299
299
  `.full-bleed` is injected into every presentation push. Prose is left-aligned and capped at ~880px; `.full-bleed` fills the **content column's full width from the same left edge** — wider than the prose, sharing one left margin down the card. It does *not* bleed to the card's physical edge: the body padding stays as a gutter, so neither the mockup nor the text ever touches the card border.
300
300
 
301
301
  Two cases, two tools:
302
- - **Whole push is a mockup / app recreation** → `kind: "mockup"` (or `"app"`) on the push. Strips the entire presentation frame; content owns the canvas.
302
+ - **Whole push is a mockup / app recreation** → `kind: "mockup"` (or `"app"`) on the push. Strips the *presentation* frame (preset tokens, semantic chips, prose-width caps, body bg/color, the Inter webfont) so the content owns the canvas — but **keeps the structural primitives** (`.window`/`.window.dark`, `.code`/`.terminal`) and a neutral system-sans default font, so `.window` and friends still render in a mockup. To match the real app's typeface, **inject its webfont right in the pushed HTML** — a `<link rel="stylesheet" href="…">` or an `@font-face` block loads fine in the sandbox — then set `font-family` on the content; that wins over the sans default.
303
303
  - **Mockup embedded in an explanation** → leave the push as-is and wrap the mockup section in `<div class="full-bleed">`.
304
304
 
305
305
  ### Window chrome for UI mockups