@ammduncan/easel 0.2.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.
@@ -0,0 +1,315 @@
1
+ ---
2
+ name: using-easel
3
+ description: Use whenever a response is about to include a long explanation (>2 paragraphs), a UI mockup, a diagram, a side-by-side comparison, ≥3 options, a code diff / before-after, or a multi-step status / progress view — push it to the easel display instead of dumping a wall of text in the terminal. Trigger on content shape, not on whether the user mentioned "easel" or "display".
4
+ ---
5
+
6
+ # using-easel
7
+
8
+ The `easel` MCP gives this session a live browser tab the user keeps open in split-screen. The `push` tool appends a card to a single scrolling page. You push **proactively**. You do not ask.
9
+
10
+ ## When to push
11
+
12
+ - Explanation that would run >2 paragraphs in the terminal.
13
+ - Any UI mockup, wireframe, or layout proposal.
14
+ - Side-by-side comparisons, ≥3 options, decision matrices.
15
+ - Architecture / sequence / flow / state diagrams.
16
+ - Code diffs or before-after snippets where scanning visually helps.
17
+ - Multi-step progress, status snapshots, run summaries.
18
+ - Anything you'd otherwise format as a long markdown block with multiple headings.
19
+
20
+ Triggers are **content-shape**, not user-phrasing. If the answer would be a wall of text or a visual, push it — regardless of whether the user said "show me" or named the tool.
21
+
22
+ ## When NOT to push (stay terminal)
23
+
24
+ - 1–2 lines.
25
+ - Clarifying questions or yes/no acknowledgements.
26
+ - A single short code block the user is about to copy.
27
+ - Direct answers to "what's the value of X" style queries.
28
+
29
+ ## Tools
30
+
31
+ - **`push({ html, title?, kind? })`** — appends a card. `html` is required; the wrapper sandboxes it in an iframe and injects a baseline design system. `title` is shown in the card header. `kind` is a freeform tag for the chip (`mockup`, `diff`, `explanation`, `comparison`, `diagram`, `progress`, `status`, etc.).
32
+ - **`open()`** — force-open a fresh browser tab for this session.
33
+ - **`config({ preset?, theme?, density? })`** — switch palette, mode, or layout live. Presets: `paper` | `aurora` | `slate`. Themes: `light` | `dark`. Density: `carded` | `flat`.
34
+ - **`label({ label })`** — name the session ("Roadworthy 401 fix"). Call early once the task focus is clear; re-call when the theme shifts. Pass `""` to clear.
35
+
36
+ Reply in chat with **one line**: `pushed to easel ↗ — #N`. Do not restate the content.
37
+
38
+ ### Tab presence
39
+
40
+ Every `push` response tells you how many viewer tabs are currently open for this session. The response text includes `· NO TAB OPEN for this session — ask the user if you should open one (call \`open\`)` when the count is zero.
41
+
42
+ When you see that hint, in the SAME reply ask the user a one-liner: *"No window is open for this session — want me to open one?"*. If they say yes, call `open()`. If they say no or ignore it, don't ask again unless they explicitly want the visual.
43
+
44
+ Don't poll. Just react to the hint when it appears.
45
+
46
+ ---
47
+
48
+ ## Style — presentation scale, not dashboard scale
49
+
50
+ Pushed cards are **presentations**, not UI dashboards. Read each rule and apply it; the wrapper gives you good defaults but they only carry so far.
51
+
52
+ ### 1. Typography
53
+
54
+ - **Page lede**: 40–52 px, weight 500, letter-spacing ≈ -0.025em
55
+ - **Section / slide titles**: 28–36 px, weight 500
56
+ - **Body text and decks**: 18–22 px, weight 400, line-height ≥ 1.55
57
+ - **Eyebrow / kicker** (the small label above a title): 13–14 px UPPERCASE, letter-spacing ≈ 0.14em, weight 500
58
+ - **Captions, meta**: 13 px minimum — never go below
59
+
60
+ Use Inter (the wrapper preconnects to `rsms.me/inter`) or system sans. Never use a serif unless the brief explicitly calls for one.
61
+
62
+ ### 2. Whitespace
63
+
64
+ - **Page padding** inside the wrapper: 56–80 px vertical, 32–48 px horizontal (the wrapper sets ~40 px / clamp ~28–64 px by default — respect that)
65
+ - **Between major sections**: 96–120 px
66
+ - **Between the deck/intro and the first content block**: 40–48 px
67
+ - **Inside cards**: 24–32 px padding minimum
68
+
69
+ Don't pack content. Space carries the rhythm of a presentation.
70
+
71
+ ### 3. Color
72
+
73
+ - **The host iframe owns the canvas.** Don't paint `background` on `body` — it fights the host and shows up as a wrong-shade block when the user is in the opposite mode. *Exception:* if a particular push really needs to be dark regardless of host (a code-heavy push, say), then OWN the canvas — `background: #0b0f17; color: #e5e7eb;` on `.wrap` — and commit fully to dark.
74
+ - **One accent only.** Use the wrapper's `var(--ds-accent)` token (it adapts to whichever preset the user picked). Limit accent uses to **3–4 per push**.
75
+ - **Status colors** (red / amber / green) only when content actually maps to status — not as decoration.
76
+ - **Page background is never pure white.** The wrapper uses an off-white (`#fafafa`-ish) or a deep dark depending on the host. If you must paint a card surface inside the push, use `var(--ds-surface)`.
77
+
78
+ ### 4. Light / dark — make EVERYTHING adaptive
79
+
80
+ Because the host owns the canvas color, **text color and any local card backgrounds must also respond to the user's light/dark mode**. Hardcoding `color: #111` puts black text on a dark canvas and the whole push goes invisible — this has bitten us repeatedly.
81
+
82
+ Use CSS `light-dark()` to swap:
83
+
84
+ ```html
85
+ <style>
86
+ :root { color-scheme: light dark; }
87
+ .wrap {
88
+ color: light-dark(#111, #e5e7eb);
89
+ padding: 56px 40px 96px;
90
+ font-family: -apple-system, 'Inter', system-ui, sans-serif;
91
+ }
92
+ /* Scope inheritance so the wrapper's text color reaches every child */
93
+ .wrap *, .wrap h1, .wrap h2, .wrap p, .wrap li, .wrap span,
94
+ .wrap div, .wrap b, .wrap em { color: inherit; }
95
+
96
+ /* Cards float above whatever canvas the tool gives us — also adapt */
97
+ .card {
98
+ background: light-dark(#fff, #111827);
99
+ border: 1px solid light-dark(#e5e5e5, #1f2937);
100
+ border-radius: 12px;
101
+ padding: 24px;
102
+ }
103
+
104
+ /* Same treatment for any badge, chip, accent shade you'd previously hardcode */
105
+ .badge {
106
+ background: light-dark(#f0fdf4, #052e16);
107
+ color: light-dark(#028043, #6ee7b7);
108
+ }
109
+ </style>
110
+ <div class="wrap">…</div>
111
+ ```
112
+
113
+ **Locked-mode containers must lock their own text color too.** Any container that paints a *fixed*, non-adaptive background — a terminal/code block locked to dark, an always-dark callout, a hero filled with a brand color — MUST also set its own text color AND re-scope `color: inherit` to its children. Otherwise it inherits `.wrap`'s `light-dark()` and the text flips to the wrong shade for that container's background in one of the two modes.
114
+
115
+ #### App / UI recreations are *always* locked-mode
116
+
117
+ When you're rendering a recreation of a real piece of UI — a mock of an app screen, a component instance, an embedded preview of what the user will actually see — **that mockup owns its theme completely. Don't make it adapt to the host.**
118
+
119
+ It's a screenshot-equivalent. If the real app is a dark cobalt dashboard with cyan accents, the mockup should be dark cobalt + cyan regardless of whether the user has easel in light or dark mode. If the real app is a warm cream marketing page, the mockup stays warm cream. The host toggle changes the *surrounding explanation*, not the embedded app preview.
120
+
121
+ Two reasons:
122
+ 1. **Visual fidelity.** A mockup of the app in dark mode looks wrong when paper-light leaks into it. The user is trying to evaluate the implementation, not a translated version of it.
123
+ 2. **It removes a class of bugs.** App previews have lots of nested elements (buttons, chips, table cells, modal overlays) — getting every layer to adapt correctly via `light-dark()` is fragile. Locking the whole island is simpler and more faithful.
124
+
125
+ How to do it: paint the mockup's outer container with the app's actual `background` and `color`, and re-scope `color: inherit` to every descendant so the host's adaptive text doesn't leak in:
126
+
127
+ ```html
128
+ <style>
129
+ .wrap {
130
+ /* host-adaptive surrounding prose */
131
+ color: light-dark(#111, #e5e7eb);
132
+ }
133
+ .wrap *, .wrap h1, .wrap h2, .wrap p, .wrap li, .wrap span,
134
+ .wrap div, .wrap b, .wrap em { color: inherit; }
135
+
136
+ /* The app mock — LOCKED to the real app's colors, ignores host mode */
137
+ .app-mock {
138
+ background: #0a0e1a; /* the app's actual canvas */
139
+ color: #e5edff; /* the app's actual ink */
140
+ border-radius: 12px;
141
+ padding: 32px;
142
+ }
143
+ .app-mock * { color: inherit; } /* re-scope so .wrap's light-dark() can't leak in */
144
+ .app-mock .btn-primary {
145
+ background: #3b82f6;
146
+ color: #fff;
147
+ }
148
+ .app-mock .card {
149
+ background: #131933;
150
+ border: 1px solid #1f2647;
151
+ }
152
+ </style>
153
+ <div class="wrap">
154
+ <h2>Here's the new payments modal</h2>
155
+ <p>Locked dark, just like the app.</p>
156
+
157
+ <div class="app-mock">
158
+ <!-- self-contained app preview; doesn't care about host theme -->
159
+ </div>
160
+ </div>
161
+ ```
162
+
163
+ Same principle for light-themed apps: lock to the app's cream/white surface, lock the ink, lock every accent. The host toggle moves the prose around the mock; the mock stays put.
164
+
165
+ #### Use the actual values, not approximations
166
+
167
+ When the mockup references a real thing — a real app, a real component, a real Figma — **every visual value must come from the source**. Don't ballpark anything: not colors, not sizing, not spacing, not typography, not radii. A close-but-wrong recreation is more misleading than no recreation at all.
168
+
169
+ **Where to find the actual values** (in this order of preference):
170
+
171
+ 1. The component's own CSS / styled-components / template (`components/.../Foo.vue`, `Foo.tsx`, `Foo.module.css`).
172
+ 2. The project's theme / token file — `config/theme.ts`, `tailwind.config.ts`, `app.css` `@theme`, design-system CSS vars.
173
+ 3. The Figma node (`get_design_context` in the figma MCP) — exact px, font, weight, fill.
174
+ 4. Computed styles in the browser — open DevTools on the running app, inspect, copy.
175
+ 5. Brand-asset SVGs for logos/marks.
176
+
177
+ **What to copy literally**:
178
+
179
+ - **Colors** — the actual hex / oklch / token. `#2f5fd1`, not "blue-600 ish". Pull the project's primary, status palette (success/danger/warning), and surface tones — many apps use unusual values that don't match Tailwind defaults.
180
+ - **Spacing** — paddings, margins, gaps. If the source uses a 4-px scale (`p-4`, `gap-6`), use those literal values. If it uses arbitrary px (`padding: 22px`), use 22, not 24 because it "looked close enough".
181
+ - **Sizing** — heights, widths, max-widths, min-heights of buttons, inputs, rows, modals, sidebars. A button that's 36 px tall in the app should be 36 px in the mock. The "Tailwind h-10 / h-11 / h-12" guess is wrong if the app actually uses 40 or 38.
182
+ - **Border radii** — components vary wildly (4 / 6 / 8 / 10 / 12 / 14 / 16 px). Pull the exact one.
183
+ - **Borders** — thickness, color, dashed vs solid. A 1 px line and a 1.5 px line read differently; copy what's there.
184
+ - **Shadows** — drop the literal `box-shadow` value from the source, not a generic `0 1px 3px rgba(0,0,0,0.1)`.
185
+ - **Typography** — font stack, weight, size, line-height, letter-spacing. Don't substitute "system-ui" for an Inter app, and don't render a 13 px label as 14. Inter ≠ Manrope ≠ Geist ≠ SF Pro.
186
+ - **Layout** — gap, grid-template-columns, flex-direction, justify-content alignment. If the app's table has 6 columns at specific min-widths, your mock should have 6 columns at those widths.
187
+ - **States** — hover, active, focused, disabled visuals. If you're showing a focused input, render the actual focus ring color and offset from the source.
188
+
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
+
191
+ ```css
192
+ .terminal {
193
+ background: #0f172a; /* locked dark, ignores host mode */
194
+ color: #e6edf3; /* MUST set text too */
195
+ }
196
+ .terminal * { color: inherit; } /* re-scope so .wrap's light-dark() doesn't leak in */
197
+ ```
198
+
199
+ The rule of thumb: background and text are a pair — commit one, commit the other.
200
+
201
+ ### 5. Visualizations — tangible over abstract
202
+
203
+ When something can be represented as a real-world object — browser-chrome tab cards with red/yellow/green dots, proportional horizontal timeline bars with marked phases, device frames, code editor windows, terminal windows, merging-pipe shapes for funnels — **do that**, not abstract labeled rectangles with arrows.
204
+
205
+ **The test:** "Could a bullet list communicate this just as well?" If yes, the visual is decoration not explanation — rebuild it as something tangible or drop it for bullets.
206
+
207
+ Bad shapes you should never push:
208
+ - A coloured rectangle with text inside, captioned with what it represents.
209
+ - Five boxes in a row connected by arrows, each box being just a number + title + one-line description.
210
+ - "Sequence diagrams" rendered as text labels under horizontal lines.
211
+
212
+ Good shapes:
213
+ - A mock browser window with a real address bar, dots, and the actual UI inside.
214
+ - A terminal block with a green dot, a username prompt, and a code session.
215
+ - A device frame around a screen mockup.
216
+ - Genuine proportional bars / pie / arc when you're showing real ratios.
217
+
218
+ ### 6. Layout patterns that work
219
+
220
+ - **Hero section** (kicker + lede + deck) at the top, with generous breathing room before the first slide.
221
+ - **Each slide**: section number → slide title → deck → then the visual.
222
+ - **3-up grids** for "actors" or "options" — never more than 4, and only when each tile is genuinely small (icon + label + one-line description).
223
+
224
+ ### 7. Layout patterns to avoid
225
+
226
+ - Dense info-graphics packed into a small area.
227
+ - Side-by-side text columns (companions are scrolled, not read like newspapers).
228
+ - **Side-by-side desktop mockups.** The iframe is ~900 px wide. Two desktop screens in a 2-col grid each get ~430 px, which crushes columns, wraps headings to 3 lines, and turns matrix/table cells unreadable. **Stack vertically** with a clear label above each ("**Now**", "**Proposed**"). Side-by-side is fine only for narrow mobile mockups, small cards, or short text columns that genuinely fit in half-width.
229
+ - Tiny captions (sub-13 px).
230
+ - Stacked colored badges/pills used as decoration.
231
+
232
+ ---
233
+
234
+ ## Built-in helpers
235
+
236
+ ### Semantic chips
237
+
238
+ ```html
239
+ <span class="chip bug">BUG</span> <!-- red, soft glow -->
240
+ <span class="chip ux">UX</span> <!-- blue -->
241
+ <span class="chip polish">POLISH</span><!-- violet -->
242
+ <span class="chip ok">OK</span> <!-- green -->
243
+ <span class="chip info">INFO</span> <!-- cyan -->
244
+ <span class="chip accent">FOCUS</span> <!-- the active preset's accent -->
245
+ ```
246
+
247
+ All include a soft outer glow tuned for the host preset/mode.
248
+
249
+ ### Design tokens
250
+
251
+ Use these on every property where you'd reach for a hex code — they adapt to whichever preset / theme the user picked:
252
+
253
+ | Token | What it's for |
254
+ |---|---|
255
+ | `--ds-bg` | page canvas |
256
+ | `--ds-bg-elev` | one step up from canvas |
257
+ | `--ds-surface` | card surface |
258
+ | `--ds-surface-soft` | recessed surface (rows, code chips) |
259
+ | `--ds-ink` | primary text |
260
+ | `--ds-ink-soft` | secondary text |
261
+ | `--ds-muted` | tertiary / labels |
262
+ | `--ds-line` | default border |
263
+ | `--ds-line-soft` | subtle border |
264
+ | `--ds-accent` | single highlight color |
265
+ | `--ds-accent-soft` | accent background |
266
+ | `--ds-accent-ink` | text on accent fill |
267
+ | `--ds-code-bg` / `--ds-code-ink` | code block bg/text |
268
+ | `--ds-shadow-md` | layered shadow |
269
+
270
+ Default to tokens. Reach for `light-dark()` only when the design needs a value the tokens don't cover.
271
+
272
+ ### Session names
273
+
274
+ By default sessions show as the cwd basename — `dvla`, `harmony-platform`. With multiple tabs open that's not enough to tell them apart.
275
+
276
+ **Hard convention: every chat must call `label` no later than its first `push` call.** If the chat ends without a label, the session is unfindable in the switcher.
277
+
278
+ Practical rule:
279
+ 1. As soon as the user describes what they want (first non-trivial message), call `label`.
280
+ 2. If you forgot to and you're about to call `push` for the first time, call `label` first.
281
+ 3. Re-call when the work's theme shifts meaningfully (started on a bug, now moved on to a refactor).
282
+ 4. Pass `""` to clear back to cwd basename.
283
+
284
+ ```
285
+ label({ label: "Roadworthy 401 investigation" })
286
+ label({ label: "Roadworthy 401 — fixed, ready to PR" })
287
+ label({ label: "" }) # clear back to cwd basename
288
+ ```
289
+
290
+ Format: 1–8 words, sentence case, no trailing punctuation. Mention the **artefact** (file/feature/bug) — not the verb. Good: `RegistrationNumberInput extraction`. Bad: `Extracting RegistrationNumberInput`.
291
+
292
+ ### Presets
293
+
294
+ `paper` (warm pitstop-style, amber accent — default) · `aurora` (deep canvas + violet/blue glow halos) · `slate` (cool neutral, cyan accent). Two themes (`light`, `dark`) and two densities (`carded`, `flat`).
295
+
296
+ When the user asks to change the look ("more glowy", "warmer", "flat", "back to dark"):
297
+
298
+ ```
299
+ config({ preset: "aurora" })
300
+ config({ theme: "light" })
301
+ config({ density: "flat" })
302
+ config({ preset: "paper", theme: "dark", density: "carded" })
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Failure mode
308
+
309
+ If `push` errors with a missing session id, the SessionStart hook hasn't fired for this session yet. Mention it once in the terminal (`easel unavailable — continuing without it`) and proceed. Do not retry every push.
310
+
311
+ ## Misc
312
+
313
+ - No `<script>` tags that try to mutate the parent window — the sandbox blocks it anyway. Self-contained `<style>` is fine.
314
+ - Pushed HTML is sandboxed with `allow-scripts`. `<base target="_blank">` is set so links open in a new tab.
315
+ - The iframe self-measures its body and tells the parent how tall to render it. You don't need to set heights.