@ammduncan/easel 0.6.2 → 0.7.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.
- package/CHANGELOG.md +18 -0
- package/dist/client/viewer.js +82 -235
- package/dist/http-server.js +2 -2
- package/dist/mcp.js +41 -13
- package/dist/session-store.js +2 -0
- package/package.json +1 -1
- package/scripts/easel-session-id.mjs +8 -0
- package/skills/using-easel/SKILL.md +29 -30
- package/skills/using-easel/kit/EASEL-GUIDE.md +161 -0
- package/skills/using-easel/kit/easel-base.css +176 -0
- package/skills/using-easel/kit/easel-icons.svg +28 -0
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
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.7.0 — 2026-06-27
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Pushed cards are now immutable snapshots — each is sealed to one light/dark mode at push time and never reflows when the global theme is toggled.** Previously every rendered card live-tracked the global theme: a config/theme message was broadcast into every iframe, so toggling Easel light↔dark flipped *all* existing cards — and forced authors into adaptive `light-dark()` styling that frequently produced dark-on-dark / washed-out text. Now the iframe-config broadcast and the in-iframe config/theme listeners are removed (print is preserved); a card's `data-theme`/`preset`/`density` are baked once at render. The global toggle still restyles Easel's own chrome and sets the default for *future* pushes — it just never touches existing cards. Verified live: with two cards pushed under different modes, toggling the global light↔dark left both sealed (the light card stayed light, the dark stayed dark). The convention across the `push` tool description + `using-easel` SKILL/kit inverts accordingly: "own your canvas, commit to one mode" replaces "adapt to both modes via `light-dark()`".
|
|
11
|
+
- **The default push wrapper no longer injects the design-system token layer — authors own the canvas.** With cards now frozen to one mode, the six-combo `--ds-*` preset token block, the presentation type scale, and the Inter webfont are dropped from the default wrapper. What remains is a minimal floor: a box-sizing reset, a system-sans default, a committed base surface + ink for the sealed mode, and the prose-width reading cap (plus the `.full-bleed` escape). The structural primitives (`.window`/`.code`/`.terminal`/`.easel-*`) and semantic chips are **retained**, and the `using-easel` kit (`easel-base.css`) re-supplies the full presentation scaffold on demand via its `light-dark()` token fallbacks — so kit-based pushes are unchanged, while bare semantic HTML now renders on the clean floor instead of the auto-injected design system. Verified by rendering bare-HTML, kit-inlined, and primitive (`.window`/`.code`/`.chip`) pushes against the live build.
|
|
12
|
+
|
|
13
|
+
### Removed
|
|
14
|
+
- **The injected `--ds-*` preset token block, presentation type scale, and Inter webfont** from the default push wrapper (the "own the canvas" change above). Pushes that relied on the auto-injected tokens/type *without* inlining the kit render plainer; inline `easel-base.css` for the presentation scaffold.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Reserved primitive class names (`code`/`terminal`/`window`) no longer render invisible dark-on-dark when an author reuses them.** Two correct-in-isolation rules were mutually destructive: the structural primitives set a fixed dark background + light ink at specificity `(0,1,0)`, while the documented `.wrap * { color: inherit }` guard is *also* `(0,1,0)` but lives later in source order (author `<body>` vs injected `<head>`), so it won the tie and flipped the primitive's ink back to the author's near-black `.wrap` colour → near-black text on the primitive's dark fill, invisible. Because the names are generic English words, author markup naturally reused them (`<td class="code">`, `<span class="code">`) and inherited the dark fill unintentionally. Fixes: (1) **Hardened** — each primitive's `background` + `color` are now committed with `!important` on the *container only* (not on `*`/token rules), so `.wrap * { color: inherit }` can't flip the ink; syntax-highlight tokens still win by specificity and are untouched. Existing content becomes readable with no author change. (2) **Namespaced** — `.easel-code` / `.easel-terminal` / `.easel-window` (and `[data-easel="code|terminal|window"]`) are the canonical collision-free forms; bare `code`/`terminal`/`window` remain as deprecated aliases. (3) **Warned** — the in-iframe guard now logs a console warning when a reserved primitive name lands on an inline/table element (`span`/`td`/…), the accidental-collision signature; the existing low-contrast warning points at the same cause. `using-easel` SKILL + kit guide document the reserved names, the `.easel-*` forms, and the `mono`/`<code>` workaround. Verified by rendering the exact `<td class="code">` / `<span class="code">` / `.code` / `.easel-code` repro against the real injected CSS in light mode.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`theme: 'light' | 'dark'` param on the `push` tool — declares the card's sealed canvas mode.** Frozen at push time; never flips with the global toggle. Omit to snapshot the current global theme (also frozen). Pairs with the immutable-snapshot change above: an author told "present in dark" passes `theme:'dark'` and gets a card that stays dark regardless of the viewer's global setting.
|
|
21
|
+
- **`EASEL_SUPPRESS_SESSION=1` env var suppresses switcher-session registration.** When set on a `claude` (or any client) invocation, easel still loads as an MCP but every tool (`push`/`label`/`open`/`config`) short-circuits to a no-op — the MCP never contacts the HTTP server and the session never appears in the switcher. The SessionStart hook also skips its convention reminder so the agent isn't nagged to label a session that can't register. Built for automated/headless consumers that run easel-registered Claude on a tight cadence (e.g. the ammiels-bot dispatcher tick fired every ~60s by launchd), which otherwise pile up churny "Bot watcher tick" entries in the switcher. Deliberately MCP-local: it leaves the Slack connector and every other MCP/account connector fully intact, unlike `claude --strict-mcp-config`, which also strips the claude.ai account connectors.
|
|
22
|
+
|
|
5
23
|
## 0.6.2 — 2026-06-04
|
|
6
24
|
|
|
7
25
|
### Fixed
|
package/dist/client/viewer.js
CHANGED
|
@@ -47,65 +47,6 @@
|
|
|
47
47
|
const PRESETS = ["paper", "aurora", "slate"];
|
|
48
48
|
const DENSITIES = ["carded", "flat"];
|
|
49
49
|
|
|
50
|
-
/* The token block injected into every iframe wrapper — six combos so
|
|
51
|
-
pushed HTML themes correctly regardless of host preset/mode. */
|
|
52
|
-
const PRESET_TOKENS_CSS = `
|
|
53
|
-
:root[data-preset="paper"][data-theme="light"] {
|
|
54
|
-
--ds-bg:#f4efe2;--ds-bg-elev:#f8f3e6;--ds-surface:#faf6ee;--ds-surface-soft:#f0ead9;
|
|
55
|
-
--ds-ink:#2a261e;--ds-ink-soft:#524b3c;--ds-muted:#756c57;
|
|
56
|
-
--ds-line:#d5cdb6;--ds-line-soft:#e3dcc6;
|
|
57
|
-
--ds-accent:#c97a1c;--ds-accent-soft:#f7e8c0;--ds-accent-ink:#fff;
|
|
58
|
-
--ds-code-bg:#2a261e;--ds-code-ink:#eae5d5;
|
|
59
|
-
--ds-shadow-md:0 1px 2px rgba(70,50,10,.06),0 18px 36px rgba(70,50,10,.1);
|
|
60
|
-
color-scheme:light;
|
|
61
|
-
}
|
|
62
|
-
:root[data-preset="paper"][data-theme="dark"] {
|
|
63
|
-
--ds-bg:#1c1b18;--ds-bg-elev:#25241f;--ds-surface:#25241f;--ds-surface-soft:#20201c;
|
|
64
|
-
--ds-ink:#ede9e0;--ds-ink-soft:#bbb5a8;--ds-muted:#888273;
|
|
65
|
-
--ds-line:#423f37;--ds-line-soft:#312f29;
|
|
66
|
-
--ds-accent:#f4bf5e;--ds-accent-soft:#3d3322;--ds-accent-ink:#1f1d18;
|
|
67
|
-
--ds-code-bg:#161514;--ds-code-ink:#eae5d5;
|
|
68
|
-
--ds-shadow-md:inset 0 1px 0 rgba(255,255,255,.045),0 1px 2px rgba(0,0,0,.55),0 18px 38px rgba(0,0,0,.45);
|
|
69
|
-
color-scheme:dark;
|
|
70
|
-
}
|
|
71
|
-
:root[data-preset="aurora"][data-theme="light"] {
|
|
72
|
-
--ds-bg:#f5f3fa;--ds-bg-elev:#fafaff;--ds-surface:#fff;--ds-surface-soft:#f0eef7;
|
|
73
|
-
--ds-ink:#1c1d24;--ds-ink-soft:#4a4d5a;--ds-muted:#7a7d8c;
|
|
74
|
-
--ds-line:#e1dff0;--ds-line-soft:#ebe9f5;
|
|
75
|
-
--ds-accent:#6d4eff;--ds-accent-soft:#ebe7ff;--ds-accent-ink:#fff;
|
|
76
|
-
--ds-code-bg:#1c1d24;--ds-code-ink:#ebe7ff;
|
|
77
|
-
--ds-shadow-md:0 1px 2px rgba(60,50,120,.05),0 18px 36px rgba(60,50,120,.08);
|
|
78
|
-
color-scheme:light;
|
|
79
|
-
}
|
|
80
|
-
:root[data-preset="aurora"][data-theme="dark"] {
|
|
81
|
-
--ds-bg:#0d0f14;--ds-bg-elev:#14171f;--ds-surface:#161a23;--ds-surface-soft:#11141a;
|
|
82
|
-
--ds-ink:#e7e9ee;--ds-ink-soft:#b9bdc6;--ds-muted:#8b909a;
|
|
83
|
-
--ds-line:rgba(143,160,200,.14);--ds-line-soft:rgba(143,160,200,.08);
|
|
84
|
-
--ds-accent:#b8c8ff;--ds-accent-soft:rgba(140,170,255,.12);--ds-accent-ink:#0d0f14;
|
|
85
|
-
--ds-code-bg:#07080a;--ds-code-ink:#e7e9ee;
|
|
86
|
-
--ds-shadow-md:inset 0 1px 0 rgba(255,255,255,.045),0 0 0 1px rgba(123,97,255,.06),0 24px 60px rgba(0,0,0,.55),0 0 80px -20px rgba(123,97,255,.25);
|
|
87
|
-
color-scheme:dark;
|
|
88
|
-
}
|
|
89
|
-
:root[data-preset="slate"][data-theme="light"] {
|
|
90
|
-
--ds-bg:#ecebe5;--ds-bg-elev:#f6f4ee;--ds-surface:#f6f4ee;--ds-surface-soft:#ecebe3;
|
|
91
|
-
--ds-ink:#1a1916;--ds-ink-soft:#34322d;--ds-muted:#76746c;
|
|
92
|
-
--ds-line:#d8d5cb;--ds-line-soft:#e1ddd2;
|
|
93
|
-
--ds-accent:#2f5fd1;--ds-accent-soft:#e4ebfb;--ds-accent-ink:#fff;
|
|
94
|
-
--ds-code-bg:#1c1b18;--ds-code-ink:#f1ede1;
|
|
95
|
-
--ds-shadow-md:0 1px 2px rgba(40,30,10,.05),0 16px 32px rgba(40,30,10,.08);
|
|
96
|
-
color-scheme:light;
|
|
97
|
-
}
|
|
98
|
-
:root[data-preset="slate"][data-theme="dark"] {
|
|
99
|
-
--ds-bg:#0c0d10;--ds-bg-elev:#15171c;--ds-surface:#15171c;--ds-surface-soft:#1c1f25;
|
|
100
|
-
--ds-ink:#f5f5f5;--ds-ink-soft:#d4d4d8;--ds-muted:#9ca3af;
|
|
101
|
-
--ds-line:#23262d;--ds-line-soft:#1c1f25;
|
|
102
|
-
--ds-accent:#7dd3fc;--ds-accent-soft:rgba(125,211,252,.16);--ds-accent-ink:#07242e;
|
|
103
|
-
--ds-code-bg:#07080a;--ds-code-ink:#f5f5f5;
|
|
104
|
-
--ds-shadow-md:0 1px 2px rgba(0,0,0,.4),0 12px 28px rgba(0,0,0,.45);
|
|
105
|
-
color-scheme:dark;
|
|
106
|
-
}
|
|
107
|
-
`;
|
|
108
|
-
|
|
109
50
|
/* Semantic chips — universal across presets. Authors use:
|
|
110
51
|
<span class="chip bug">BUG</span> / .ux / .polish / .ok / .info
|
|
111
52
|
to get accessible, glow-haloed badges that work in both modes. */
|
|
@@ -142,12 +83,11 @@
|
|
|
142
83
|
block, so stripping these in fidelity mode left the skill's own guidance
|
|
143
84
|
("wrap a mockup in .window") producing unstyled output. */
|
|
144
85
|
const STRUCTURAL_PRIMITIVES_CSS = `
|
|
145
|
-
/* Bind the CSS color-scheme to the
|
|
146
|
-
light-dark() (text ink, surfaces, borders)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
scheme and washes out whenever the OS disagrees with the easel toggle. */
|
|
86
|
+
/* Bind the CSS color-scheme to the card's sealed theme so any author CSS that
|
|
87
|
+
uses light-dark() (text ink, surfaces, borders) — including the kit's token
|
|
88
|
+
fallbacks — resolves against the frozen mode rather than the OS preference.
|
|
89
|
+
Injected in every wrapper (default + app-fidelity), the only place the canvas
|
|
90
|
+
mode is now declared since the preset token block was removed. */
|
|
151
91
|
:root[data-theme="light"] { color-scheme: light; }
|
|
152
92
|
:root[data-theme="dark"] { color-scheme: dark; }
|
|
153
93
|
|
|
@@ -167,18 +107,19 @@
|
|
|
167
107
|
canvas with pinned dark ink and color:inherit re-scoped to every child so the
|
|
168
108
|
host's light-dark() ink can never leak in. Add the dark class
|
|
169
109
|
(class="window dark") for a genuinely dark-UI mockup. */
|
|
170
|
-
.window {
|
|
110
|
+
.window, .easel-window, [data-easel="window"] {
|
|
171
111
|
position: relative;
|
|
172
112
|
padding-top: 40px;
|
|
173
113
|
border-radius: 12px;
|
|
174
114
|
border: 1px solid #e2e2e2;
|
|
175
115
|
box-shadow: 0 14px 48px rgba(0, 0, 0, 0.16);
|
|
176
116
|
overflow: hidden;
|
|
177
|
-
|
|
178
|
-
|
|
117
|
+
/* Locked surface + ink, !important so .wrap * { color: inherit } can't flip it. */
|
|
118
|
+
background: #ffffff !important;
|
|
119
|
+
color: #1a1a1a !important;
|
|
179
120
|
}
|
|
180
|
-
.window * { color: inherit; }
|
|
181
|
-
.window::before {
|
|
121
|
+
.window *, .easel-window *, [data-easel="window"] * { color: inherit; }
|
|
122
|
+
.window::before, .easel-window::before, [data-easel="window"]::before {
|
|
182
123
|
content: "";
|
|
183
124
|
position: absolute;
|
|
184
125
|
top: 0;
|
|
@@ -193,7 +134,7 @@
|
|
|
193
134
|
radial-gradient(circle at 59px 20px, #28c840 6px, transparent 6.5px);
|
|
194
135
|
background-repeat: no-repeat;
|
|
195
136
|
}
|
|
196
|
-
.window::after {
|
|
137
|
+
.window::after, .easel-window::after, [data-easel="window"]::after {
|
|
197
138
|
content: attr(data-title);
|
|
198
139
|
position: absolute;
|
|
199
140
|
top: 0;
|
|
@@ -208,18 +149,18 @@
|
|
|
208
149
|
color: #6b6b6b;
|
|
209
150
|
pointer-events: none;
|
|
210
151
|
}
|
|
211
|
-
.window.dark {
|
|
152
|
+
.window.dark, .easel-window.dark, [data-easel="window"].dark {
|
|
212
153
|
border-color: #2a2a2a;
|
|
213
|
-
background: #161616;
|
|
214
|
-
color: #e6edf3;
|
|
154
|
+
background: #161616 !important;
|
|
155
|
+
color: #e6edf3 !important;
|
|
215
156
|
box-shadow: 0 14px 48px rgba(0, 0, 0, 0.4);
|
|
216
157
|
}
|
|
217
|
-
.window.dark::before {
|
|
158
|
+
.window.dark::before, .easel-window.dark::before, [data-easel="window"].dark::before {
|
|
218
159
|
background-color: #1f1f1f;
|
|
219
160
|
border-bottom-color: #2a2a2a;
|
|
220
161
|
}
|
|
221
|
-
.window.dark::after { color: #9b9b9b; }
|
|
222
|
-
.window.desktop {
|
|
162
|
+
.window.dark::after, .easel-window.dark::after, [data-easel="window"].dark::after { color: #9b9b9b; }
|
|
163
|
+
.window.desktop, .easel-window.desktop, [data-easel="window"].desktop {
|
|
223
164
|
min-height: 900px;
|
|
224
165
|
}
|
|
225
166
|
/* Locked-dark code / terminal primitive. Reach for this instead of hand-rolling
|
|
@@ -231,9 +172,18 @@
|
|
|
231
172
|
palette so syntax highlighting reads against #0f172a without per-token tuning.
|
|
232
173
|
Usage: <div class="code"><span class="kw">gcloud</span> services enable …</div>
|
|
233
174
|
.terminal is an alias; add .terminal for a prompt feel (same colors). */
|
|
234
|
-
.code, .terminal
|
|
235
|
-
|
|
236
|
-
|
|
175
|
+
.code, .terminal, .easel-code, .easel-terminal,
|
|
176
|
+
[data-easel="code"], [data-easel="terminal"] {
|
|
177
|
+
/* Locked dark surface + ink. Committed with !important so the documented
|
|
178
|
+
.wrap * { color: inherit } can't flip the ink onto this fixed background —
|
|
179
|
+
that tie (both (0,1,0); author rule later in source) is the recurring
|
|
180
|
+
invisible dark-on-dark bug. !important is on the CONTAINER only, not on the
|
|
181
|
+
descendant rules below, so syntax tokens still win by specificity.
|
|
182
|
+
Generic names collide with author markup like <td class="code"> — prefer the
|
|
183
|
+
namespaced .easel-code / [data-easel="code"]; bare code/terminal are kept as
|
|
184
|
+
deprecated aliases. */
|
|
185
|
+
background: #0f172a !important;
|
|
186
|
+
color: #e6edf3 !important;
|
|
237
187
|
border-radius: 12px;
|
|
238
188
|
padding: 18px 22px;
|
|
239
189
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
@@ -242,25 +192,26 @@
|
|
|
242
192
|
overflow: auto;
|
|
243
193
|
margin: 16px 0 24px;
|
|
244
194
|
}
|
|
245
|
-
.code
|
|
246
|
-
.code .
|
|
247
|
-
.code .
|
|
248
|
-
.code .
|
|
249
|
-
.code .
|
|
250
|
-
.code .
|
|
251
|
-
.code .
|
|
252
|
-
.code .
|
|
253
|
-
.code .
|
|
195
|
+
:is(.code, .terminal, .easel-code, .easel-terminal, [data-easel="code"], [data-easel="terminal"]) * { color: inherit; }
|
|
196
|
+
:is(.code, .terminal, .easel-code, .easel-terminal) .kw { color: #ff7b72; } /* keywords, control flow */
|
|
197
|
+
:is(.code, .terminal, .easel-code, .easel-terminal) .string { color: #a5d6ff; } /* strings, attr values */
|
|
198
|
+
:is(.code, .terminal, .easel-code, .easel-terminal) .fn { color: #d2a8ff; } /* function names */
|
|
199
|
+
:is(.code, .terminal, .easel-code, .easel-terminal) .prop { color: #79c0ff; } /* identifiers, properties */
|
|
200
|
+
:is(.code, .terminal, .easel-code, .easel-terminal) .num { color: #ffa657; } /* numbers, constants */
|
|
201
|
+
:is(.code, .terminal, .easel-code, .easel-terminal) .comment { color: #8b949e; } /* comments */
|
|
202
|
+
:is(.code, .terminal, .easel-code, .easel-terminal) .muted { color: #94a3b8; } /* dim / secondary */
|
|
203
|
+
:is(.code, .terminal, .easel-code, .easel-terminal) .accent { color: #6ee7b7; } /* highlight / success */
|
|
254
204
|
@media print {
|
|
255
205
|
/* Force the locked-dark primitives light for print — browsers drop background
|
|
256
206
|
colours by default, which would otherwise strand their light ink on white
|
|
257
207
|
paper. Applies in both normal and app-fidelity mode. The !important here
|
|
258
208
|
also (intentionally) overrides the normal branch's non-print-gated pre/code
|
|
259
209
|
theming, so code reads as dark-on-light on paper regardless of host theme. */
|
|
260
|
-
pre, code, .code, .terminal
|
|
261
|
-
|
|
262
|
-
.
|
|
263
|
-
.window.dark
|
|
210
|
+
pre, code, .code, .terminal, .easel-code, .easel-terminal,
|
|
211
|
+
[data-easel="code"], [data-easel="terminal"] { background: #f4f3ed !important; color: #111 !important; border: 1px solid #ddd; }
|
|
212
|
+
:is(.code, .terminal, .easel-code, .easel-terminal, [data-easel="code"], [data-easel="terminal"]) * { color: #111 !important; }
|
|
213
|
+
.window.dark, .easel-window.dark, [data-easel="window"].dark { background: #ffffff !important; color: #111 !important; }
|
|
214
|
+
:is(.window, .easel-window, [data-easel="window"]).dark * { color: #111 !important; }
|
|
264
215
|
}
|
|
265
216
|
`;
|
|
266
217
|
|
|
@@ -514,7 +465,11 @@
|
|
|
514
465
|
/* ignore */
|
|
515
466
|
}
|
|
516
467
|
}
|
|
517
|
-
|
|
468
|
+
// NOTE: we deliberately do NOT push theme/preset/density into rendered
|
|
469
|
+
// iframes. A pushed card is an immutable snapshot — its canvas is sealed at
|
|
470
|
+
// render time. applyConfig only restyles Easel's own chrome (host <html>
|
|
471
|
+
// tokens) and the default for FUTURE pushes; it never reaches back into
|
|
472
|
+
// existing cards.
|
|
518
473
|
if (!opts || !opts.skipServer) {
|
|
519
474
|
pushConfigToServer({ preset, theme, density });
|
|
520
475
|
}
|
|
@@ -531,20 +486,6 @@
|
|
|
531
486
|
});
|
|
532
487
|
}
|
|
533
488
|
|
|
534
|
-
function broadcastConfigToIframes(cfg) {
|
|
535
|
-
iframes.forEach((iframe) => {
|
|
536
|
-
try {
|
|
537
|
-
iframe.contentWindow &&
|
|
538
|
-
iframe.contentWindow.postMessage(
|
|
539
|
-
{ type: "easel:config", ...cfg },
|
|
540
|
-
"*",
|
|
541
|
-
);
|
|
542
|
-
} catch (e) {
|
|
543
|
-
/* ignore */
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
|
|
548
489
|
async function pushConfigToServer(cfg) {
|
|
549
490
|
try {
|
|
550
491
|
await fetch("/api/config", {
|
|
@@ -831,7 +772,12 @@
|
|
|
831
772
|
iframe.setAttribute("scrolling", "no");
|
|
832
773
|
iframe.setAttribute("title", push.title || "push " + push.index);
|
|
833
774
|
iframe.dataset.pushId = push.id;
|
|
834
|
-
|
|
775
|
+
// Explicit push.theme wins; absent → snapshot the global at render. Either
|
|
776
|
+
// way it's frozen — the card never flips on a later global toggle.
|
|
777
|
+
const sealedTheme = push.theme === "light" || push.theme === "dark"
|
|
778
|
+
? push.theme
|
|
779
|
+
: currentTheme();
|
|
780
|
+
iframe.srcdoc = wrapPushedHtml(push.html, sealedTheme, push.id, push.kind);
|
|
835
781
|
iframe.addEventListener("load", () => {
|
|
836
782
|
iframes.add(iframe);
|
|
837
783
|
// Primary path: the iframe self-measures and posts back size via
|
|
@@ -1051,8 +997,13 @@ window.htmlToImage.toSvg(de,{width:w,height:h,cacheBust:true})
|
|
|
1051
997
|
"function hasDirectText(el){for(var i=0;i<el.childNodes.length;i++){var n=el.childNodes[i];if(n.nodeType===3&&n.nodeValue.trim().length>0)return true}return false}" +
|
|
1052
998
|
"function fmt(c){return'rgb('+Math.round(c.r)+','+Math.round(c.g)+','+Math.round(c.b)+')'}" +
|
|
1053
999
|
"function scan(){if(!document.body)return;var offenders=[];var seen=0;var all=document.body.querySelectorAll('*');for(var i=0;i<all.length&&seen<2000;i++){var el=all[i];if(!hasDirectText(el))continue;seen++;var cs=getComputedStyle(el);if(cs.visibility==='hidden'||cs.display==='none')continue;var fg=parseColor(cs.color);if(!fg||fg.a<0.05)continue;var bg=effBg(el);var ratio=contrast(fg,bg);if(ratio<3){offenders.push({tag:el.tagName.toLowerCase(),cls:(el.className&&el.className.toString?el.className.toString():'').slice(0,80),text:(el.textContent||'').trim().slice(0,60),ratio:Math.round(ratio*100)/100,fg:fmt(fg),bg:fmt(bg)})}}" +
|
|
1054
|
-
"if(offenders.length){console.warn('[easel] low-contrast text detected ('+offenders.length+' element(s), threshold 3:1).
|
|
1055
|
-
|
|
1000
|
+
"if(offenders.length){console.warn('[easel] low-contrast text detected ('+offenders.length+' element(s), threshold 3:1). Common causes: (1) a reserved primitive class (code/terminal/window) on your own element inheriting its locked dark fill — see the reserved-class warning below; (2) a hand-rolled dark container — use <div class=\"easel-code\"> instead (locks bg AND ink). Offenders:',offenders.slice(0,10));try{parent.postMessage({type:'easel:contrast-warn',pushId:ID,count:offenders.length,samples:offenders.slice(0,5)},'*')}catch(e){}}}" +
|
|
1001
|
+
// Reserved-class collision guard: the primitive names code/terminal/window paint a
|
|
1002
|
+
// locked dark background, so finding one on an inline/table element (span/td/…) is a
|
|
1003
|
+
// strong signal the author reused a generic name and got an unintended dark block.
|
|
1004
|
+
"function scanReserved(){if(!document.body)return;var R={code:1,terminal:1,window:1};var inl={SPAN:1,TD:1,TH:1,A:1,LI:1,LABEL:1,B:1,I:1,EM:1,STRONG:1,SMALL:1};var bad=[];var all=document.body.querySelectorAll('*');for(var i=0;i<all.length;i++){var el=all[i];if(!inl[el.tagName])continue;var cl=(el.className&&el.className.toString?el.className.toString():'').split(/\\s+/);for(var j=0;j<cl.length;j++){if(R[cl[j]]){bad.push({tag:el.tagName.toLowerCase(),cls:cl[j],text:(el.textContent||'').trim().slice(0,40)});break}}}" +
|
|
1005
|
+
"if(bad.length){console.warn('[easel] reserved primitive class on '+bad.length+' inline/table element(s). code/terminal/window are RESERVED easel primitives that paint a locked dark background — on a <span>/<td>/etc. that yields an unintended dark block. Rename your class (e.g. mono, codecell), or use the namespaced .easel-code/.easel-terminal/.easel-window form if you DO want the primitive. Offenders:',bad.slice(0,10))}}" +
|
|
1006
|
+
"function run(){setTimeout(function(){scan();scanReserved()},400)}" +
|
|
1056
1007
|
"if(document.fonts&&document.fonts.ready){document.fonts.ready.then(run).catch(run)}else{run()}" +
|
|
1057
1008
|
"})();"
|
|
1058
1009
|
);
|
|
@@ -1092,23 +1043,30 @@ ${body}
|
|
|
1092
1043
|
<head>
|
|
1093
1044
|
<meta charset="utf-8" />
|
|
1094
1045
|
<base target="_blank" />
|
|
1095
|
-
<link rel="preconnect" href="https://rsms.me/" />
|
|
1096
|
-
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
|
1097
1046
|
<script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.js"></script>
|
|
1098
1047
|
<style>
|
|
1099
1048
|
*, *::before, *::after { box-sizing: border-box; }
|
|
1100
|
-
${PRESET_TOKENS_CSS}
|
|
1101
1049
|
${SEMANTIC_CHIPS_CSS}
|
|
1102
1050
|
${STRUCTURAL_PRIMITIVES_CSS}
|
|
1051
|
+
/* MINIMAL FLOOR (layer 3 removed): no --ds token block, no Inter webfont, no
|
|
1052
|
+
presentation type scale. The wrapper only commits a base surface + ink for
|
|
1053
|
+
the frozen mode and a system-sans default — the agent owns everything else
|
|
1054
|
+
(or inlines the kit for the presentation scaffold). Primitives + chips +
|
|
1055
|
+
prose-width cap are retained. */
|
|
1103
1056
|
html, body {
|
|
1104
1057
|
margin: 0;
|
|
1105
|
-
background:
|
|
1106
|
-
color:
|
|
1107
|
-
font-family:
|
|
1108
|
-
font-feature-settings: "cv11", "ss01";
|
|
1058
|
+
background: ${theme === "dark" ? "#0e1116" : "#faf7f0"};
|
|
1059
|
+
color: ${theme === "dark" ? "#e8e8e8" : "#1a1a1a"};
|
|
1060
|
+
font-family: -apple-system, "SF Pro Text", system-ui, "Segoe UI", sans-serif;
|
|
1109
1061
|
line-height: 1.55;
|
|
1110
1062
|
-webkit-font-smoothing: antialiased;
|
|
1111
|
-
|
|
1063
|
+
}
|
|
1064
|
+
:where(body) :where(*) { color: inherit; }
|
|
1065
|
+
/* keep the accent chip working without the dropped --ds-accent token */
|
|
1066
|
+
.chip.accent {
|
|
1067
|
+
background: ${theme === "dark" ? "#10241c" : "#eafaf0"};
|
|
1068
|
+
color: ${theme === "dark" ? "#6ee7a8" : "#15803d"};
|
|
1069
|
+
border-color: transparent;
|
|
1112
1070
|
}
|
|
1113
1071
|
body {
|
|
1114
1072
|
padding: 40px clamp(28px, 4vw, 64px) 48px;
|
|
@@ -1152,104 +1110,7 @@ body > *:last-child { margin-bottom: 0 !important; }
|
|
|
1152
1110
|
margin: 32px 0;
|
|
1153
1111
|
}
|
|
1154
1112
|
.wrap { display: block; }
|
|
1155
|
-
|
|
1156
|
-
display: block;
|
|
1157
|
-
font-size: 13px;
|
|
1158
|
-
letter-spacing: 0.14em;
|
|
1159
|
-
text-transform: uppercase;
|
|
1160
|
-
color: var(--ds-muted);
|
|
1161
|
-
font-weight: 500;
|
|
1162
|
-
margin-bottom: 14px;
|
|
1163
|
-
}
|
|
1164
|
-
h1 {
|
|
1165
|
-
font-size: 40px;
|
|
1166
|
-
font-weight: 500;
|
|
1167
|
-
letter-spacing: -0.025em;
|
|
1168
|
-
line-height: 1.08;
|
|
1169
|
-
margin: 0 0 18px;
|
|
1170
|
-
}
|
|
1171
|
-
.deck, .lede {
|
|
1172
|
-
font-size: 19px;
|
|
1173
|
-
line-height: 1.55;
|
|
1174
|
-
color: var(--ds-ink-soft);
|
|
1175
|
-
margin: 0 0 28px;
|
|
1176
|
-
max-width: 720px;
|
|
1177
|
-
}
|
|
1178
|
-
h2 {
|
|
1179
|
-
font-size: 26px;
|
|
1180
|
-
font-weight: 500;
|
|
1181
|
-
letter-spacing: -0.02em;
|
|
1182
|
-
margin: 36px 0 12px;
|
|
1183
|
-
}
|
|
1184
|
-
h3 {
|
|
1185
|
-
font-size: 19px;
|
|
1186
|
-
font-weight: 600;
|
|
1187
|
-
letter-spacing: -0.005em;
|
|
1188
|
-
margin: 24px 0 8px;
|
|
1189
|
-
}
|
|
1190
|
-
h4 { font-size: 15px; font-weight: 600; margin: 20px 0 6px; }
|
|
1191
|
-
p {
|
|
1192
|
-
font-size: 18px;
|
|
1193
|
-
margin: 0 0 14px;
|
|
1194
|
-
color: var(--ds-ink-soft);
|
|
1195
|
-
}
|
|
1196
|
-
a { color: var(--ds-accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--ds-accent) 40%, transparent); }
|
|
1197
|
-
a:hover { border-bottom-color: var(--ds-accent); }
|
|
1198
|
-
ul, ol { padding-left: 22px; margin: 0 0 18px; }
|
|
1199
|
-
li { font-size: 18px; margin-bottom: 6px; color: var(--ds-ink-soft); }
|
|
1200
|
-
code {
|
|
1201
|
-
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
1202
|
-
font-size: 0.92em;
|
|
1203
|
-
background: var(--ds-surface-soft);
|
|
1204
|
-
color: var(--ds-ink);
|
|
1205
|
-
padding: 2px 6px;
|
|
1206
|
-
border-radius: 5px;
|
|
1207
|
-
}
|
|
1208
|
-
pre {
|
|
1209
|
-
background: var(--ds-code-bg);
|
|
1210
|
-
color: var(--ds-code-ink);
|
|
1211
|
-
padding: 18px 22px;
|
|
1212
|
-
border-radius: 12px;
|
|
1213
|
-
overflow: auto;
|
|
1214
|
-
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
1215
|
-
font-size: 13.5px;
|
|
1216
|
-
line-height: 1.7;
|
|
1217
|
-
margin: 16px 0 24px;
|
|
1218
|
-
}
|
|
1219
|
-
pre code { background: transparent; padding: 0; color: inherit; font-size: inherit; }
|
|
1220
|
-
blockquote {
|
|
1221
|
-
border-left: 3px solid var(--ds-accent);
|
|
1222
|
-
margin: 18px 0;
|
|
1223
|
-
padding: 4px 0 4px 20px;
|
|
1224
|
-
color: var(--ds-ink-soft);
|
|
1225
|
-
font-size: 18px;
|
|
1226
|
-
}
|
|
1227
|
-
.card, .panel {
|
|
1228
|
-
background: var(--ds-surface);
|
|
1229
|
-
border: 1px solid var(--ds-line);
|
|
1230
|
-
border-radius: 14px;
|
|
1231
|
-
padding: 24px 28px;
|
|
1232
|
-
margin: 0 0 20px;
|
|
1233
|
-
box-shadow: 0 1px 2px rgba(0,0,0,0.04), 0 8px 24px rgba(0,0,0,0.04);
|
|
1234
|
-
}
|
|
1235
|
-
hr {
|
|
1236
|
-
border: 0;
|
|
1237
|
-
border-top: 1px solid var(--ds-line);
|
|
1238
|
-
margin: 36px 0;
|
|
1239
|
-
}
|
|
1240
|
-
table {
|
|
1241
|
-
width: 100%;
|
|
1242
|
-
border-collapse: collapse;
|
|
1243
|
-
font-size: 15px;
|
|
1244
|
-
margin: 18px 0 24px;
|
|
1245
|
-
}
|
|
1246
|
-
th, td {
|
|
1247
|
-
text-align: left;
|
|
1248
|
-
padding: 10px 12px;
|
|
1249
|
-
border-bottom: 1px solid var(--ds-line);
|
|
1250
|
-
}
|
|
1251
|
-
th { color: var(--ds-muted); font-weight: 500; font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
1252
|
-
img { max-width: 100%; height: auto; border-radius: 10px; }
|
|
1113
|
+
img { max-width: 100%; height: auto; }
|
|
1253
1114
|
:root[data-density="flat"] html,
|
|
1254
1115
|
:root[data-density="flat"] body { background: transparent; }
|
|
1255
1116
|
|
|
@@ -1272,26 +1133,12 @@ img { max-width: 100%; height: auto; border-radius: 10px; }
|
|
|
1272
1133
|
<body>
|
|
1273
1134
|
${body}
|
|
1274
1135
|
<script>
|
|
1136
|
+
// Canvas is sealed at render (data-theme/preset/density are baked into <html>
|
|
1137
|
+
// above and never change). The only live message this card honours is print —
|
|
1138
|
+
// config/theme broadcasts are intentionally ignored so the snapshot is immutable.
|
|
1275
1139
|
(function(){
|
|
1276
|
-
function apply(cfg){
|
|
1277
|
-
if (!cfg) return;
|
|
1278
|
-
if (cfg.theme === "light" || cfg.theme === "dark") {
|
|
1279
|
-
document.documentElement.setAttribute("data-theme", cfg.theme);
|
|
1280
|
-
window.__claudeDisplayTheme = cfg.theme;
|
|
1281
|
-
}
|
|
1282
|
-
if (cfg.preset === "paper" || cfg.preset === "aurora" || cfg.preset === "slate") {
|
|
1283
|
-
document.documentElement.setAttribute("data-preset", cfg.preset);
|
|
1284
|
-
window.__claudeDisplayPreset = cfg.preset;
|
|
1285
|
-
}
|
|
1286
|
-
if (cfg.density === "carded" || cfg.density === "flat") {
|
|
1287
|
-
document.documentElement.setAttribute("data-density", cfg.density);
|
|
1288
|
-
window.__claudeDisplayDensity = cfg.density;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
1140
|
window.addEventListener("message", function(e){
|
|
1292
1141
|
if (!e || !e.data) return;
|
|
1293
|
-
if (e.data.type === "easel:config") apply(e.data);
|
|
1294
|
-
if (e.data.type === "easel:theme") apply({ theme: e.data.theme });
|
|
1295
1142
|
if (e.data.type === "easel:print") {
|
|
1296
1143
|
try { window.print(); } catch(_) {}
|
|
1297
1144
|
}
|
|
@@ -1310,7 +1157,7 @@ ${body}
|
|
|
1310
1157
|
const configScript =
|
|
1311
1158
|
"<script src='https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.js'></script><script>(function(){function a(c){if(!c)return;if(c.theme==='light'||c.theme==='dark'){document.documentElement.setAttribute('data-theme',c.theme);window.__claudeDisplayTheme=c.theme}if(c.preset==='paper'||c.preset==='aurora'||c.preset==='slate'){document.documentElement.setAttribute('data-preset',c.preset);window.__claudeDisplayPreset=c.preset}if(c.density==='carded'||c.density==='flat'){document.documentElement.setAttribute('data-density',c.density);window.__claudeDisplayDensity=c.density}}a(" +
|
|
1312
1159
|
JSON.stringify({ theme, preset, density }) +
|
|
1313
|
-
");window.addEventListener('message',function(e){if(!e||!e.data)return;if(e.data.type==='easel:
|
|
1160
|
+
");window.addEventListener('message',function(e){if(!e||!e.data)return;if(e.data.type==='easel:print'){try{window.print()}catch(_){}}})})();</script>";
|
|
1314
1161
|
const imageScript = "<script>" + imageExportScript() + "</script>";
|
|
1315
1162
|
const guardScript = "<script>" + contrastGuardScript(pushId) + "</script>";
|
|
1316
1163
|
const measureScript = "<script>" + selfMeasureScript(pushId) + "</script>";
|
package/dist/http-server.js
CHANGED
|
@@ -159,7 +159,7 @@ export function startHttpServer() {
|
|
|
159
159
|
res.json({ ok });
|
|
160
160
|
});
|
|
161
161
|
app.post("/api/push", async (req, res) => {
|
|
162
|
-
const { sessionId, html, title, kind } = req.body ?? {};
|
|
162
|
+
const { sessionId, html, title, kind, theme } = req.body ?? {};
|
|
163
163
|
if (typeof sessionId !== "string" || !sessionId.trim()) {
|
|
164
164
|
res.status(400).json({ error: "sessionId required" });
|
|
165
165
|
return;
|
|
@@ -185,7 +185,7 @@ export function startHttpServer() {
|
|
|
185
185
|
console.warn("[easel] image inlining failed; storing original html:", err);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
-
const push = appendPush(sessionId, { html: storedHtml, title, kind });
|
|
188
|
+
const push = appendPush(sessionId, { html: storedHtml, title, kind, theme });
|
|
189
189
|
touchSession(sessionId);
|
|
190
190
|
broadcast(sessionId, "push", push);
|
|
191
191
|
if (Math.random() < 0.05) {
|
package/dist/mcp.js
CHANGED
|
@@ -21,6 +21,14 @@ const TOOL_PUSH = "push";
|
|
|
21
21
|
const TOOL_OPEN = "open";
|
|
22
22
|
const TOOL_CONFIG = "config";
|
|
23
23
|
const TOOL_LABEL = "label";
|
|
24
|
+
// When EASEL_SUPPRESS_SESSION=1 (set by automated/headless consumers like the
|
|
25
|
+
// ammiels-bot dispatcher tick), easel loads normally but registers NO switcher
|
|
26
|
+
// session — every tool short-circuits to a no-op, so the MCP never contacts the
|
|
27
|
+
// HTTP server and the session never appears in the switcher. This keeps churny
|
|
28
|
+
// background sessions out of the switcher WITHOUT disabling the MCP or any OTHER
|
|
29
|
+
// connector. (Contrast `claude --strict-mcp-config`, which also strips the
|
|
30
|
+
// claude.ai account connectors — Slack etc. — and so can't be used for this.)
|
|
31
|
+
const SUPPRESS_SESSION = process.env.EASEL_SUPPRESS_SESSION === "1";
|
|
24
32
|
// One-shot guard: only auto-open once per MCP-child lifetime. If the user
|
|
25
33
|
// closes the tab afterwards, subsequent pushes won't re-open it — the user
|
|
26
34
|
// closing the tab is treated as an explicit dismissal we should respect.
|
|
@@ -74,6 +82,11 @@ const inputSchema = {
|
|
|
74
82
|
type: "string",
|
|
75
83
|
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
84
|
},
|
|
85
|
+
theme: {
|
|
86
|
+
type: "string",
|
|
87
|
+
enum: ["light", "dark"],
|
|
88
|
+
description: "Canvas mode for THIS push — light or dark. SEALED at push time and never flips with the global Easel toggle: a pushed card is an immutable snapshot, like a screenshot. You OWN this canvas — set your own background + text colours in the HTML for the mode you pick here; do NOT write light-dark() / prefers-color-scheme for the page surface (the card is frozen to one mode, so adaptive CSS is dead weight). Omit to snapshot whatever the global theme is at push time (also frozen). Pass 'light' or 'dark' explicitly when you've been told which mode to present in.",
|
|
89
|
+
},
|
|
77
90
|
},
|
|
78
91
|
required: ["html"],
|
|
79
92
|
additionalProperties: false,
|
|
@@ -87,6 +100,7 @@ async function pushToServer(args) {
|
|
|
87
100
|
html: args.html,
|
|
88
101
|
title: args.title,
|
|
89
102
|
kind: args.kind,
|
|
103
|
+
theme: args.theme,
|
|
90
104
|
}),
|
|
91
105
|
});
|
|
92
106
|
if (!r.ok) {
|
|
@@ -101,7 +115,9 @@ export async function main() {
|
|
|
101
115
|
tools: [
|
|
102
116
|
{
|
|
103
117
|
name: TOOL_PUSH,
|
|
104
|
-
description: "Push an HTML card to this session's live browser tab. Renders in a sandboxed iframe
|
|
118
|
+
description: "Push an HTML card to this session's live browser tab. Renders in a sandboxed iframe. Treat each card as a presentation slide — generous whitespace, presentation-scale type, tangible visuals.\n\n" +
|
|
119
|
+
"═══ YOU OWN THE CANVAS — COMMIT TO ONE MODE ═══\n" +
|
|
120
|
+
"A pushed card is an IMMUTABLE SNAPSHOT, like a screenshot: it's sealed at push time and NEVER reflows when the user toggles Easel's global light/dark theme. So pick ONE mode for this push and own every colour in it — set your own `background` AND `color` on `.wrap`/`body`, and choose surfaces/borders/ink for that single mode. Do NOT write `light-dark()` or `prefers-color-scheme` for the page surface; the card won't flip, so adaptive CSS is dead weight that just risks the wrong branch winning. Use the `theme` param to declare the mode ('light'/'dark') when you've been told which to present in; omit it to snapshot the current global theme (also frozen). Still scope `color: inherit` to descendants so a stray default ink can't leak in, and the locked primitives (.code/.terminal/.window) work exactly as before — the whole surface now just behaves like they already do: one committed mode, fully self-contained.\n\n" +
|
|
105
121
|
"═══ FIDELITY BAR — SHIP HIGH-FIDELITY BY DEFAULT ═══\n" +
|
|
106
122
|
"Default to polished, production-grade output that looks like a real screenshot of shipped software or a finished design — NOT a rough sketch, wireframe, or grey-box placeholder. This is the default for EVERY push; you do not need to be asked for quality. Only drop to low-fidelity (wireframe, ASCII-ish boxes, lorem-ipsum, unstyled) when the user EXPLICITLY says rough/lo-fi/wireframe/sketch/quick-and-dirty is fine, or asks for a thumbnail/napkin idea. When in doubt, go high-fidelity.\n" +
|
|
107
123
|
"What high-fidelity means concretely:\n" +
|
|
@@ -111,18 +127,19 @@ export async function main() {
|
|
|
111
127
|
"• Visual craft: deliberate hierarchy, aligned grids, consistent spacing scale, real iconography (inline SVG, not emoji-as-icon), proper empty/hover/active states where they matter. Avoid the generic-AI look (one purple gradient, evenly-sized boxes, centered everything).\n" +
|
|
112
128
|
"• Tangible over abstract (see VISUALS): a mock should read as the actual thing, not labeled rectangles.\n" +
|
|
113
129
|
"If you genuinely can't reach the bar (missing real values, ambiguous source), say so in ONE line in chat and push your best honest attempt — don't pass a rough draft off as final, and don't silently ship a grey-box.\n\n" +
|
|
114
|
-
"═══
|
|
115
|
-
"
|
|
116
|
-
"•
|
|
117
|
-
"•
|
|
118
|
-
"•
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
" .wrap {
|
|
130
|
+
"═══ COMMITTED COLOR — PICK ONE MODE, PAINT IT FULLY ═══\n" +
|
|
131
|
+
"The card is frozen to one mode (the `theme` param, or the global theme snapshotted at push time). So:\n" +
|
|
132
|
+
"• DO set `background` AND `color` explicitly on your root wrapper. You own the canvas — paint it. (Old guidance said 'leave bg to the host'; that's gone — the host no longer reflows your card, so an unpainted surface just inherits chrome you don't control.)\n" +
|
|
133
|
+
"• Pick concrete colours for the ONE mode you chose — `color: #111` for a light card, `color: #e8e8e8` for a dark one. Do NOT use `light-dark()` / `prefers-color-scheme` for the surface; the card won't flip, so the adaptive branch is dead weight and risks resolving to the wrong half.\n" +
|
|
134
|
+
"• Still re-scope `color: inherit` to every descendant so a stray default ink can't leak in.\n" +
|
|
135
|
+
"• Pairing rule (unchanged): any container with a fixed background (a dark code block, a brand-color hero, a white card) MUST set its own text color and re-scope `color: inherit` to its children. Background and ink are always a pair.\n\n" +
|
|
136
|
+
"═══ COPY-PASTE STARTER (light card) ═══\n" +
|
|
137
|
+
" .wrap { background: #faf7f0; color: #1a1a1a; padding: 56px 48px; font-family: -apple-system, 'Inter', system-ui, sans-serif; max-width: 820px; }\n" +
|
|
122
138
|
" .wrap *, .wrap h1, .wrap h2, .wrap h3, .wrap p, .wrap li, .wrap span { color: inherit; }\n" +
|
|
123
|
-
" .card { background:
|
|
139
|
+
" .card { background: #ffffff; border: 1px solid #e0d9c3; border-radius: 12px; padding: 24px; }\n" +
|
|
140
|
+
" (dark card: .wrap { background:#0e1116; color:#e8e8e8 } .card { background:#161616; border-color:#2a2a2a })\n\n" +
|
|
124
141
|
"═══ CODE / TERMINAL BLOCKS — USE THE BUILT-IN PRIMITIVE, DON'T HAND-ROLL ═══\n" +
|
|
125
|
-
"The #1 recurring bug is a hand-rolled dark code container: you set `background:#0f172a` on a custom div but leave base text inheriting `.wrap`'s
|
|
142
|
+
"The #1 recurring bug is a hand-rolled dark code container: you set `background:#0f172a` on a custom div but leave base text inheriting `.wrap`'s ink, which on a light card is near-black and VANISHES against the dark panel (only the explicitly-coloured syntax spans survive). Don't hand-roll it. The wrapper ships a baked-in, always-safe primitive:\n" +
|
|
126
143
|
" <div class=\"code\"> … </div> (alias: class=\"terminal\")\n" +
|
|
127
144
|
"It locks BOTH background (#0f172a) and ink (#e6edf3), re-scopes `color:inherit` to every child, and provides verified github-dark syntax token classes you can drop onto spans — NO per-token tuning needed:\n" +
|
|
128
145
|
" .kw (keywords #ff7b72) · .string (#a5d6ff) · .fn (function #d2a8ff) · .prop (identifiers #79c0ff) · .num (#ffa657) · .comment (#8b949e) · .muted (#94a3b8) · .accent (#6ee7b7)\n" +
|
|
@@ -130,10 +147,10 @@ export async function main() {
|
|
|
130
147
|
"Plain <pre>/<code> are also already safe (bg+ink token pair). Only reach for a fully custom container when .code/.terminal genuinely don't fit — and then obey the locked-mode rule below.\n" +
|
|
131
148
|
"SAFETY NET: every push self-checks for low-contrast text (WCAG <3:1) after render. If your push trips it, the card gets an amber `⚠ contrast` chip on the meta row with the offender list in the tooltip, and the iframe console.warns with sample rgb pairs. Treat the chip as a build-failure-equivalent — the fix is almost always swapping the hand-rolled container for `.code` / `.terminal` (or, for a non-code locked-bg container, applying the locked-mode rule below).\n\n" +
|
|
132
149
|
"═══ COPY-PASTE STARTER (any OTHER LOCKED-MODE container — brand hero, custom panel) ═══\n" +
|
|
133
|
-
"If a container has a
|
|
150
|
+
"If a container has a background that differs from the card surface, you MUST set its own text color AND re-scope `color: inherit` to its children. Otherwise the children inherit `.wrap`'s committed ink, which may not suit the container's bg (e.g. dark text on a dark hero on a light card → invisible).\n" +
|
|
134
151
|
" .hero { background: #0f172a; color: #e6edf3; border-radius: 12px; padding: 20px 24px; }\n" +
|
|
135
152
|
" .hero * { color: inherit; }\n" +
|
|
136
|
-
"• Same pairing applies in the OPPOSITE direction —
|
|
153
|
+
"• Same pairing applies in the OPPOSITE direction — a light panel sitting on a DARK card. A `.card { background: #fff }` with no `color:` inherits `.wrap`'s dark-card ink (a light cream/gray) → invisible titles on white. Commit text too AND re-scope inherit on children. This bites just as often as the dark case.\n" +
|
|
137
154
|
" .card { background: #ffffff; color: #111111; border: 1px solid #e5e5e5; border-radius: 12px; padding: 24px 32px; }\n" +
|
|
138
155
|
" .card * { color: inherit; }\n" +
|
|
139
156
|
"• 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" +
|
|
@@ -232,6 +249,16 @@ export async function main() {
|
|
|
232
249
|
],
|
|
233
250
|
}));
|
|
234
251
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
252
|
+
if (SUPPRESS_SESSION) {
|
|
253
|
+
return {
|
|
254
|
+
content: [
|
|
255
|
+
{
|
|
256
|
+
type: "text",
|
|
257
|
+
text: "easel: session suppressed (EASEL_SUPPRESS_SESSION=1) — no switcher entry created; easel tools are no-ops in this session.",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
235
262
|
const sessionId = resolveClaudeSessionId();
|
|
236
263
|
const { port } = await ensureHttpServer();
|
|
237
264
|
if (req.params.name === TOOL_OPEN) {
|
|
@@ -304,6 +331,7 @@ export async function main() {
|
|
|
304
331
|
html: args.html,
|
|
305
332
|
title: args.title,
|
|
306
333
|
kind: args.kind,
|
|
334
|
+
theme: args.theme,
|
|
307
335
|
port,
|
|
308
336
|
});
|
|
309
337
|
const url = `http://localhost:${port}/s/${sessionId}`;
|
package/dist/session-store.js
CHANGED
|
@@ -63,11 +63,13 @@ export function getSessionView(id) {
|
|
|
63
63
|
}
|
|
64
64
|
export function appendPush(sessionId, input) {
|
|
65
65
|
const meta = ensureSession(sessionId);
|
|
66
|
+
const theme = input.theme === "light" || input.theme === "dark" ? input.theme : null;
|
|
66
67
|
const push = {
|
|
67
68
|
id: randomUUID(),
|
|
68
69
|
index: meta.nextIndex,
|
|
69
70
|
title: input.title?.trim() || null,
|
|
70
71
|
kind: input.kind?.trim() || null,
|
|
72
|
+
theme,
|
|
71
73
|
html: input.html,
|
|
72
74
|
createdAt: Date.now(),
|
|
73
75
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ammduncan/easel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|
|
@@ -58,6 +58,14 @@ if (sessionId) {
|
|
|
58
58
|
writeFileSync(join(hookDir, `cc-session-${process.ppid}.txt`), `${sessionId}\n`);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Suppressed sessions (e.g. the ammiels-bot dispatcher tick set
|
|
62
|
+
// EASEL_SUPPRESS_SESSION=1) get NO convention reminder: the MCP no-ops every
|
|
63
|
+
// tool, so nagging the agent to label/push would only waste a tool call. Exit
|
|
64
|
+
// before emitting additionalContext.
|
|
65
|
+
if (process.env.EASEL_SUPPRESS_SESSION === "1") {
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
// --- staleness check (cached, every 24h) ------------------------------------
|
|
62
70
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
63
71
|
const INSTALL_DIR = resolve(__dirname, "..");
|
|
@@ -28,9 +28,9 @@ Triggers are **content-shape**, not user-phrasing. If the answer would be a wall
|
|
|
28
28
|
|
|
29
29
|
## Tools
|
|
30
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.).
|
|
31
|
+
- **`push({ html, title?, kind?, theme? })`** — 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.). `theme` (`light`|`dark`) seals the card's canvas mode — frozen at push time, never flips with the global toggle; omit to snapshot the current global theme.
|
|
32
32
|
- **`open()`** — force-open a fresh browser tab for this session.
|
|
33
|
-
- **`config({ preset?, theme?, density? })`** —
|
|
33
|
+
- **`config({ preset?, theme?, density? })`** — set Easel's own chrome + the default for FUTURE pushes. Presets: `paper` | `aurora` | `slate`. Themes: `light` | `dark`. Density: `carded` | `flat`. Note: this does NOT restyle existing cards — each is a frozen snapshot. To change a card's mode, re-push with `theme`.
|
|
34
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
35
|
|
|
36
36
|
Reply in chat with **one line**: `pushed to easel ↗ — #N`. Do not restate the content.
|
|
@@ -84,77 +84,74 @@ Don't pack content. Space carries the rhythm of a presentation.
|
|
|
84
84
|
|
|
85
85
|
### 3. Color
|
|
86
86
|
|
|
87
|
-
- **
|
|
87
|
+
- **You own the canvas — commit to one mode.** A pushed card is an immutable snapshot, sealed at push time; it never flips when the user toggles Easel's global light/dark theme. So pick ONE mode and paint it: set your own `background` AND `color` on `.wrap` (e.g. light: `background: #faf7f0; color: #1a1a1a` — dark: `background: #0b0f17; color: #e5e7eb`). Declare the mode with the push tool's `theme: 'light'|'dark'` param when you've been told which to present in; omit it to snapshot the current global theme. (You *may* leave the surface to the host — the card body behind `.wrap` is frozen too — but painting it yourself is the safe default and what "own the canvas" means.)
|
|
88
88
|
- **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**.
|
|
89
89
|
- **Status colors** (red / amber / green) only when content actually maps to status — not as decoration.
|
|
90
90
|
- **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)`.
|
|
91
91
|
|
|
92
|
-
### 4. Light / dark —
|
|
92
|
+
### 4. Light / dark — commit to ONE mode, no responsiveness
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
The card is frozen to a single mode, so **don't make it adapt**. Pick light or dark, paint concrete colours for that one mode, and stop. Writing `light-dark()` / `prefers-color-scheme` for the page surface is dead weight — the card won't flip, so the adaptive branch never earns its keep and just risks the wrong half winning. The thing that bit us before — hardcoding `color: #111` onto a *host-controlled* canvas that could be dark — can't happen anymore, because you now control the background too: set them as a pair.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
Paint the one mode you chose:
|
|
97
97
|
|
|
98
98
|
```html
|
|
99
99
|
<style>
|
|
100
|
-
:root { color-scheme: light dark; }
|
|
101
100
|
.wrap {
|
|
102
|
-
|
|
101
|
+
background: #faf7f0; /* light card — pick your surface */
|
|
102
|
+
color: #1a1a1a; /* ink to match it */
|
|
103
103
|
padding: 56px 40px 96px;
|
|
104
104
|
font-family: -apple-system, 'Inter', system-ui, sans-serif;
|
|
105
105
|
}
|
|
106
|
-
/*
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
/* Push the wrapper's ink into every child so a frame default never leaks.
|
|
107
|
+
ZERO-specificity (:where) so it neutralises UA link/button colours but is
|
|
108
|
+
beaten by ANY authored container colour — a hand-rolled dark callout keeps
|
|
109
|
+
its own ink instead of inheriting .wrap's and vanishing on its dark bg. */
|
|
110
|
+
:where(.wrap) :where(*) { color: inherit; }
|
|
109
111
|
|
|
110
|
-
/* Cards float above whatever canvas the tool gives us — also adapt */
|
|
111
112
|
.card {
|
|
112
|
-
background:
|
|
113
|
-
border: 1px solid
|
|
113
|
+
background: #ffffff;
|
|
114
|
+
border: 1px solid #e0d9c3;
|
|
114
115
|
border-radius: 12px;
|
|
115
116
|
padding: 24px;
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
-
/* Same treatment for any badge, chip, accent shade you'd previously hardcode */
|
|
119
|
-
.badge {
|
|
120
|
-
background: light-dark(#f0fdf4, #052e16);
|
|
121
|
-
color: light-dark(#028043, #6ee7b7);
|
|
122
|
-
}
|
|
118
|
+
.badge { background: #f0fdf4; color: #028043; }
|
|
123
119
|
</style>
|
|
124
120
|
<div class="wrap">…</div>
|
|
125
121
|
```
|
|
126
122
|
|
|
127
|
-
|
|
123
|
+
(Dark card: `.wrap { background:#0b0f17; color:#e5e7eb }`, `.card { background:#111827; border-color:#1f2937 }`, `.badge { background:#052e16; color:#6ee7b7 }`.) The `--ds-*` tokens still work if you prefer them — they resolve once to the card's frozen mode and stay put — but for a self-owned canvas concrete colours are clearer.
|
|
124
|
+
|
|
125
|
+
**Background and ink are always a pair.** Any container that paints its own background — a terminal/code block locked to dark, a callout, a hero filled with a brand color, a white panel on a dark card — MUST also set its own text color (and re-scope `color: inherit` to its children for nested elements). Otherwise it inherits `.wrap`'s committed ink, which may not suit that container's background (dark ink on a dark hero → invisible). (As long as you use the zero-specificity `:where(.wrap) :where(*)` adoption rule above, a single class on the container — `.tip { background:#111; color:#eee }` — is enough; its colour now wins. The old element-qualified form `.wrap div { color: inherit }` was `(0,1,1)` and silently OUTRANKED such a container, flipping its text to the canvas ink — black-on-black — which was the recurring "dark block, text invisible" bug. No `!important` needed anymore.)
|
|
128
126
|
|
|
129
127
|
#### App / UI recreations are *always* locked-mode
|
|
130
128
|
|
|
131
129
|
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.**
|
|
132
130
|
|
|
133
|
-
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
|
|
131
|
+
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 the card's own mode. If the real app is a warm cream marketing page, the mockup stays warm cream. This was always the rule for recreations; now the *whole* card works this way (sealed at push time), so a mock just commits to the app's real colours like everything else.
|
|
134
132
|
|
|
135
133
|
Two reasons:
|
|
136
134
|
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.
|
|
137
135
|
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.
|
|
138
136
|
|
|
139
|
-
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
|
|
137
|
+
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 card's own ink doesn't leak in:
|
|
140
138
|
|
|
141
139
|
```html
|
|
142
140
|
<style>
|
|
143
141
|
.wrap {
|
|
144
|
-
/*
|
|
145
|
-
|
|
142
|
+
/* the card's committed prose mode (one mode, not adaptive) */
|
|
143
|
+
background: #faf7f0; color: #1a1a1a;
|
|
146
144
|
}
|
|
147
|
-
.wrap
|
|
148
|
-
.wrap div, .wrap b, .wrap em { color: inherit; }
|
|
145
|
+
:where(.wrap) :where(*) { color: inherit; } /* zero-specificity: locked islands below keep their own ink */
|
|
149
146
|
|
|
150
|
-
/* The app mock — LOCKED to the real app's colors,
|
|
147
|
+
/* The app mock — LOCKED to the real app's colors, independent of the card mode */
|
|
151
148
|
.app-mock {
|
|
152
149
|
background: #0a0e1a; /* the app's actual canvas */
|
|
153
150
|
color: #e5edff; /* the app's actual ink */
|
|
154
151
|
border-radius: 12px;
|
|
155
152
|
padding: 32px;
|
|
156
153
|
}
|
|
157
|
-
.app-mock * { color: inherit; } /* re-scope so .wrap's
|
|
154
|
+
.app-mock * { color: inherit; } /* re-scope so .wrap's committed ink can't leak in */
|
|
158
155
|
.app-mock .btn-primary {
|
|
159
156
|
background: #3b82f6;
|
|
160
157
|
color: #fff;
|
|
@@ -174,7 +171,7 @@ How to do it: paint the mockup's outer container with the app's actual `backgrou
|
|
|
174
171
|
</div>
|
|
175
172
|
```
|
|
176
173
|
|
|
177
|
-
Same principle for light-themed apps: lock to the app's cream/white surface, lock the ink, lock every accent.
|
|
174
|
+
Same principle for light-themed apps: lock to the app's cream/white surface, lock the ink, lock every accent. Both the prose card and the mock inside it are frozen — neither moves when the user toggles Easel's global theme.
|
|
178
175
|
|
|
179
176
|
#### Use the actual values, not approximations
|
|
180
177
|
|
|
@@ -388,6 +385,8 @@ Reach for the built-in **`.code`** (alias **`.terminal`**) class instead of hand
|
|
|
388
385
|
|
|
389
386
|
Token classes: `.kw` (keywords) · `.string` · `.fn` (functions) · `.prop` (identifiers/properties) · `.num` · `.comment` · `.muted` · `.accent`. Plain `<pre>`/`<code>` are already safe too (bg + ink token pair). Only hand-roll a custom dark container when neither fits — and then obey the locked-mode pairing rule above.
|
|
390
387
|
|
|
388
|
+
**Reserved names — `code` / `terminal` / `window` are easel primitives.** They paint a locked dark background, so reusing them as your *own* class (`<td class="code">`, `<span class="window">`) inherits that dark fill unintentionally. Two protections: (1) the primitive ink is now locked with `!important`, so a collision renders *readable* dark-on-light instead of invisible dark-on-dark; (2) the viewer logs a console warning when a reserved name lands on an inline/table element. Still — for your own inline code or boxes use a different name (e.g. `mono`, or a plain `<code>` with your styling). The canonical primitive forms are the namespaced **`.easel-code` / `.easel-terminal` / `.easel-window`** (or `[data-easel="code|terminal|window"]`); bare names remain as deprecated aliases.
|
|
389
|
+
|
|
391
390
|
### Semantic chips
|
|
392
391
|
|
|
393
392
|
```html
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Easel codegen guide
|
|
2
|
+
|
|
3
|
+
A short starter so an easel push lands at presentation scale on the first try,
|
|
4
|
+
instead of being iterated up from a dense, small-font, emoji-laden draft.
|
|
5
|
+
|
|
6
|
+
**This guide is additive.** The `using-easel` SKILL.md already owns the long
|
|
7
|
+
rulebook — the fidelity bar, the type/whitespace/color scales, light/dark +
|
|
8
|
+
locked-mode pairing, the injected `--ds-*` tokens, and the `.window` / `.code` /
|
|
9
|
+
`.terminal` / `.chip` / `.full-bleed` primitives. Read it for any of that. This
|
|
10
|
+
guide adds the **composition** rules the SKILL doesn't make first-class, plus a
|
|
11
|
+
copy-paste scaffold (`easel-base.css`) and an icon sprite (`easel-icons.svg`).
|
|
12
|
+
|
|
13
|
+
> Lineage: the type/space/color floors trace to Lookbook `fundamentals.md`
|
|
14
|
+
> (`F`-rules) and the generic-AI tells to `anti-patterns.md` (`AP`). The
|
|
15
|
+
> `--ds-*` tokens are easel's `light-dark()` adaptation of Lookbook tokens.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Use the kit
|
|
20
|
+
|
|
21
|
+
1. Inline the contents of **`easel-base.css`** inside a `<style>` at the top of
|
|
22
|
+
the push.
|
|
23
|
+
2. Inline **`easel-icons.svg`** once (a hidden `<symbol>` sprite), then reference
|
|
24
|
+
icons with `<svg class="ic"><use href="#i-git-branch"/></svg>`.
|
|
25
|
+
3. Wrap all content in `<div class="wrap">`.
|
|
26
|
+
|
|
27
|
+
Everything keys off the wrapper's `--ds-*` tokens with literal fallbacks, so the
|
|
28
|
+
same file renders correctly **standalone** (for a local render-and-look) and
|
|
29
|
+
**inside easel** (where it inherits the user's preset accent + light/dark).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Reserved class names — don't reuse these for your own elements
|
|
34
|
+
|
|
35
|
+
The wrapper injects **structural primitives** under these class names, and they
|
|
36
|
+
paint a **locked background + ink**. If you put one on your own element (a
|
|
37
|
+
`<td class="code">`, a `<span class="window">`), it inherits the primitive's
|
|
38
|
+
dark fill you didn't ask for:
|
|
39
|
+
|
|
40
|
+
| Reserved | What it is | If you want it, prefer |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `code`, `terminal` | locked-dark code block | `easel-code` / `easel-terminal` / `[data-easel="code"]` |
|
|
43
|
+
| `window` (+ `.dark`, `.desktop`) | macOS window chrome | `easel-window` / `[data-easel="window"]` |
|
|
44
|
+
| `chip` (+ `.bug/.ux/.ok/…`) | semantic glow chip | — (use as documented, or your own name) |
|
|
45
|
+
| `full-bleed` | escapes the prose measure | — (use as documented) |
|
|
46
|
+
|
|
47
|
+
**Workaround / rule:** for *your own* inline code, monospace cells, or boxes,
|
|
48
|
+
use a different name — the kit ships **`.mono`** for inline code, and you can use
|
|
49
|
+
a plain `<code>` element with your own styling. Reach for `code`/`terminal`/
|
|
50
|
+
`window` **only** when you actually want that primitive, and prefer the
|
|
51
|
+
namespaced `.easel-*` form. (Bare names still work as deprecated aliases, and
|
|
52
|
+
the wrapper now locks their ink with `!important` so a collision renders
|
|
53
|
+
*readable* dark-on-light instead of invisible — but it's still an unwanted block.
|
|
54
|
+
The viewer logs a console warning when a reserved name lands on an inline/table
|
|
55
|
+
element.)
|
|
56
|
+
|
|
57
|
+
The kit's own generic class names (`card`, `thread`, `callout`, `lanes`,
|
|
58
|
+
`spine`, `node`) are **not** wrapper primitives, so they don't collide with the
|
|
59
|
+
injected CSS — but they *are* generic, so don't also use them as bare names for
|
|
60
|
+
unrelated author elements in the same push.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## The 5 composition rules
|
|
65
|
+
|
|
66
|
+
**1. Presentation scale — body ≥ 18px, nothing below 14px.** [F3 / SKILL §1]
|
|
67
|
+
Lede 46 · title 32 · h2 22 · body 18–21 · eyebrow/mono 14. The scaffold's type
|
|
68
|
+
classes already sit here; don't shrink them to fit more in.
|
|
69
|
+
|
|
70
|
+
**2. Don't fight the prose measure — go vertical.** [the max-width lesson]
|
|
71
|
+
A default (non-`mockup`) push clamps content to a readable column. That clamp
|
|
72
|
+
only hurts when you push *horizontal density* into it (a 4-up row, side-by-side
|
|
73
|
+
text) — then font size is the only variable left and it collapses. A vertical
|
|
74
|
+
flow *wants* a narrow column, so the measure stops being an enemy and the body
|
|
75
|
+
type stays large. If you genuinely need full width (a wide table, a real app
|
|
76
|
+
screen, a matrix), **escape** the measure with `kind:"mockup"` instead of
|
|
77
|
+
shrinking type — see *Picking `kind`*.
|
|
78
|
+
|
|
79
|
+
**3. Column count is a function of content, not a default.** [AP20 / SKILL §6–7]
|
|
80
|
+
- A **sequence** of steps → stack vertically (`.spine` + `.connector`).
|
|
81
|
+
- **Parallel siblings** → a grid (`.lanes`, auto-fit), sized so each tile fits
|
|
82
|
+
its content at ≥16px. Two-up is usually the goldilocks for tiles carrying a
|
|
83
|
+
name + one line.
|
|
84
|
+
- Never a **4-up row of paragraphs** (crushes type) and never an **all-vertical
|
|
85
|
+
stack of siblings** (monotonous, endless scroll). Ask: *is the relationship
|
|
86
|
+
sequential or parallel, and how much does each item carry?*
|
|
87
|
+
|
|
88
|
+
**4. A locked-background container sets its OWN text colour — on every node.**
|
|
89
|
+
[SKILL §4]
|
|
90
|
+
Any container that paints a *fixed* background (`.thread`, a brand hero, a dark
|
|
91
|
+
callout) must commit its own `color` and re-scope `color: inherit` to its
|
|
92
|
+
children — background and text are a pair: commit one, commit the other. The
|
|
93
|
+
scaffold's adoption rule is `:where(.wrap) :where(*)` (zero specificity), so a
|
|
94
|
+
single class on your container — `.callout { background:#111; color:#eee }` —
|
|
95
|
+
now wins on its own; no `!important` needed. (Earlier the scaffold used the
|
|
96
|
+
element-qualified `.wrap div` form at `(0,1,1)`, which outranked a single-class
|
|
97
|
+
container colour and silently flipped its text to the canvas ink → invisible
|
|
98
|
+
dark-on-dark. That's fixed.) Use the built-in `.thread` / the wrapper's
|
|
99
|
+
`.window` / `.code` where they fit — they already lock both.
|
|
100
|
+
|
|
101
|
+
**5. SVG icons, never emoji; colour must mean something — and never rank equal
|
|
102
|
+
siblings.** [AP23 / AP24 / P-AS-01 / F17]
|
|
103
|
+
Pull icons from `easel-icons.svg` (`currentColor`, inherits the chip's colour).
|
|
104
|
+
Two traps, both AI-slop:
|
|
105
|
+
- **Status hues on non-status content.** The `.t-success/-warning/-danger` tones
|
|
106
|
+
are for genuine *status*. Don't paint *categories* (Ignore / Answer / Do /
|
|
107
|
+
Ship) in green/amber/blue — "Do-work = green" means nothing; it's decoration
|
|
108
|
+
in a semantics costume. If a real split exists (e.g. *no-work* vs *work*),
|
|
109
|
+
colour THAT — neutral for one group, the single accent for the other — so
|
|
110
|
+
colour carries information. Otherwise leave them all neutral and let the icon
|
|
111
|
+
differentiate.
|
|
112
|
+
- **Emphasising one of N equal siblings.** A coloured left-border / accent fill
|
|
113
|
+
on one tile in a row of peers (the "accent-bordered hero tile" reflex) reads
|
|
114
|
+
as a stuck selection, not hierarchy. Four exits of one decision are equal —
|
|
115
|
+
don't rank them. Only emphasise when something genuinely *is* primary.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Picking `kind`
|
|
120
|
+
|
|
121
|
+
| Content shape | `kind` | Why |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| Explanation · flow · status · comparison-in-prose | *(omit)* | Keeps the presentation frame: prose measure, `--ds-*` tokens, Inter, light/dark canvas. Compose vertically (rule 2). |
|
|
124
|
+
| A whole push that is **nothing but** one app/UI screen, edge-to-edge | `"mockup"` / `"app"` | Strips the prose-width cap + body padding + tokens so you own every pixel — then you hand-set font, bg, colours, and supply your own page padding. |
|
|
125
|
+
|
|
126
|
+
Mixed (prose **and** an embedded wide specimen)? Omit `kind` and wrap just the
|
|
127
|
+
specimen in `<div class="full-bleed">` — see SKILL "Full-bleed mockups".
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Compose a card — the recipe
|
|
132
|
+
|
|
133
|
+
1. **Hero** — `.kicker` eyebrow → `.lede` → `.deck` (one tight sentence).
|
|
134
|
+
2. **Identify the shape**: is the body a *sequence* (→ `.spine`) or a set of
|
|
135
|
+
*parallel options* (→ `.lanes`)? Most explainers are a short sequence that
|
|
136
|
+
ends in a fork of parallel outcomes (spine → lanes).
|
|
137
|
+
3. **Build tangible, not abstract** [SKILL §5]: an incoming message is a
|
|
138
|
+
`.thread`, a guard rail is a `.callout`, a decision is a `.node` with a
|
|
139
|
+
`git-branch` badge — not labeled rectangles with arrows.
|
|
140
|
+
4. **Glance test**: would a bullet list say this just as well? If yes, the visual
|
|
141
|
+
is decoration — rebuild it or drop it.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Primitive index (in `easel-base.css`)
|
|
146
|
+
|
|
147
|
+
| Class | What | Notes |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `.wrap` | canvas | sets text not background; guards inheritance |
|
|
150
|
+
| `.kicker` `.lede` `.deck` `.title` `.h2` `.body` `.muted` | type scale | presentation sizes |
|
|
151
|
+
| `.mono` | inline code/identifier chip | preset accent on soft surface |
|
|
152
|
+
| `.card` / `.node` | surface / surface + icon badge | locked-surface pairing baked in |
|
|
153
|
+
| `.spine` + `.connector` | vertical sequence | `.connector.short` for tighter gaps |
|
|
154
|
+
| `.lanes` + `.lane` | parallel-sibling grid | auto-fit ≥260px; `.t-*` for state tone |
|
|
155
|
+
| `.icchip` | tinted icon chip | drives off `--c` / `--tint` |
|
|
156
|
+
| `.callout` | tinted note/guard | `.info` `.success` `.danger`; default amber |
|
|
157
|
+
| `.thread` | locked-dark message card | sets its own ink on every node |
|
|
158
|
+
|
|
159
|
+
For app/UI mockups, code, and chips that the SKILL already ships
|
|
160
|
+
(`.window` / `.window.dark`, `.code` / `.terminal`, `.chip`), use those — don't
|
|
161
|
+
re-implement them here.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* ===========================================================================
|
|
2
|
+
easel-base.css — presentation-scale starter for easel pushes.
|
|
3
|
+
|
|
4
|
+
HOW TO USE: inline this whole block inside a <style> at the top of a push,
|
|
5
|
+
then compose with the classes below. Wrap everything in <div class="wrap">.
|
|
6
|
+
|
|
7
|
+
Built ON the easel wrapper's injected --ds-* tokens. A pushed card is an
|
|
8
|
+
IMMUTABLE SNAPSHOT: its mode is sealed at push time and never flips when the
|
|
9
|
+
user toggles Easel's global theme, so these tokens (and their light-dark()
|
|
10
|
+
fallbacks) resolve ONCE to the card's frozen mode and stay there. Every token
|
|
11
|
+
has a literal light-dark() fallback, so this file ALSO renders correctly as a
|
|
12
|
+
standalone .html (e.g. when you render-and-look locally before pushing).
|
|
13
|
+
Inside easel the wrapper's tokens win and the card inherits the user's chosen
|
|
14
|
+
accent for free. You OWN this canvas — commit to one mode; don't write
|
|
15
|
+
responsive (light-dark/prefers-color-scheme) CSS expecting the card to adapt.
|
|
16
|
+
|
|
17
|
+
This is the SCAFFOLD. The full rulebook lives in SKILL.md (fidelity bar,
|
|
18
|
+
type/whitespace/color, locked-mode, .window/.code/.chip primitives,
|
|
19
|
+
full-bleed, kind:"mockup"). Read EASEL-GUIDE.md for the 5 composition rules
|
|
20
|
+
this scaffold encodes.
|
|
21
|
+
=========================================================================== */
|
|
22
|
+
|
|
23
|
+
:root { color-scheme: light dark; }
|
|
24
|
+
|
|
25
|
+
/* ---- canvas: frozen to one mode. The host card surface behind .wrap is itself
|
|
26
|
+
sealed at push time, so inheriting it is safe; set your own `background` here
|
|
27
|
+
when you want full control of the surface (a specific paper tint, a dark
|
|
28
|
+
card). Either way the card never flips. ----------------------------------- */
|
|
29
|
+
.wrap {
|
|
30
|
+
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
|
31
|
+
color: var(--ds-ink, light-dark(#1a1a1a, #e9e9ea));
|
|
32
|
+
line-height: 1.55;
|
|
33
|
+
padding: 8px 4px 40px;
|
|
34
|
+
}
|
|
35
|
+
/* Push the wrapper's text colour into every child so the frame default never
|
|
36
|
+
leaks. ZERO-specificity (:where) on purpose: it still neutralises UA element
|
|
37
|
+
colours (links/buttons) and adopts .wrap's ink for plain text, but is beaten
|
|
38
|
+
by ANY authored container colour — so a hand-rolled dark callout/tooltip
|
|
39
|
+
(`.tip{background:#111;color:#eee}`) keeps its own ink instead of inheriting
|
|
40
|
+
.wrap's light-dark() and going invisible on its own dark background. The old
|
|
41
|
+
element-qualified form (`.wrap div`, (0,1,1)) outranked single-class
|
|
42
|
+
container colours and was the recurring dark-on-dark bug. */
|
|
43
|
+
:where(.wrap) :where(*) { color: inherit; }
|
|
44
|
+
.wrap svg { display: block; }
|
|
45
|
+
|
|
46
|
+
/* ---- type scale — presentation, NEVER below 14px (SKILL §1) --------------- */
|
|
47
|
+
.kicker, .eyebrow {
|
|
48
|
+
font-size: 14px; font-weight: 600; text-transform: uppercase;
|
|
49
|
+
letter-spacing: .14em; color: var(--ds-accent, light-dark(#15803d, #4ade80));
|
|
50
|
+
}
|
|
51
|
+
.lede { font-size: 46px; font-weight: 500; letter-spacing: -.025em; line-height: 1.08; margin: 18px 0 0; }
|
|
52
|
+
.title { font-size: 32px; font-weight: 500; letter-spacing: -.02em; line-height: 1.15; margin: 0; }
|
|
53
|
+
.deck { font-size: 21px; line-height: 1.5; margin: 18px 0 0; max-width: 36ch;
|
|
54
|
+
color: var(--ds-ink-soft, light-dark(#5f5f63, #a0a0a6)); }
|
|
55
|
+
.h2 { font-size: 22px; font-weight: 600; margin: 0; }
|
|
56
|
+
.body { font-size: 19px; }
|
|
57
|
+
.muted { color: var(--ds-muted, light-dark(#6b6b6b, #9a9a9a)); }
|
|
58
|
+
|
|
59
|
+
/* ---- inline mono chip — codes, identifiers, statuses, tokens -------------- */
|
|
60
|
+
.mono {
|
|
61
|
+
font-family: ui-monospace, 'Roboto Mono Variable', monospace; font-size: 14px;
|
|
62
|
+
padding: 2px 8px; border-radius: 6px;
|
|
63
|
+
background: var(--ds-surface-soft, light-dark(#f3f4f6, #1f2329));
|
|
64
|
+
color: var(--ds-accent, light-dark(#15803d, #6ee7a8)) !important;
|
|
65
|
+
border: 1px solid var(--ds-line-soft, light-dark(#e3e5e8, #2c3038));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ---- card / node (card + leading icon badge + heading) -------------------- */
|
|
69
|
+
.card {
|
|
70
|
+
background: var(--ds-surface, light-dark(#ffffff, #15171c));
|
|
71
|
+
border: 1px solid var(--ds-line, light-dark(#e7e7ea, #2a2d34));
|
|
72
|
+
border-radius: 14px; padding: 24px 26px;
|
|
73
|
+
box-shadow: var(--ds-shadow-md, 0 1px 2px light-dark(rgba(0,0,0,.05), rgba(0,0,0,.3)));
|
|
74
|
+
}
|
|
75
|
+
.card * { color: inherit; } /* locked-surface pairing: surface set → keep text from leaking */
|
|
76
|
+
|
|
77
|
+
.node { display: flex; gap: 18px; align-items: flex-start; }
|
|
78
|
+
.node .badge {
|
|
79
|
+
flex: none; width: 46px; height: 46px; border-radius: 12px;
|
|
80
|
+
display: grid; place-items: center;
|
|
81
|
+
background: var(--ds-accent-soft, light-dark(#f0f7f2, #16241c));
|
|
82
|
+
color: var(--ds-accent, light-dark(#15803d, #4ade80));
|
|
83
|
+
}
|
|
84
|
+
.node .badge svg { width: 24px; height: 24px; }
|
|
85
|
+
.node .h2 + p, .node .h2 ~ p { font-size: 18px; margin: 6px 0 0;
|
|
86
|
+
color: var(--ds-ink-soft, light-dark(#56565a, #9b9ba1)); }
|
|
87
|
+
|
|
88
|
+
/* ===========================================================================
|
|
89
|
+
PRIMITIVE 1 — spine + lanes (sequence vs. parallel siblings)
|
|
90
|
+
RULE: a sequence of steps stacks VERTICALLY (.spine + .connector).
|
|
91
|
+
parallel siblings go in a GRID (.lanes) sized so each fits — never a
|
|
92
|
+
4-up row of paragraphs, never an all-vertical stack of siblings.
|
|
93
|
+
=========================================================================== */
|
|
94
|
+
.spine { display: flex; flex-direction: column; gap: 14px; }
|
|
95
|
+
.connector {
|
|
96
|
+
width: 2px; height: 26px; margin-left: 31px;
|
|
97
|
+
background: var(--ds-line, light-dark(#d9d9dd, #34373d));
|
|
98
|
+
}
|
|
99
|
+
.connector.short { height: 16px; }
|
|
100
|
+
|
|
101
|
+
/* auto-fit: tiles flow to as many columns as fit at >=260px, then reflow down */
|
|
102
|
+
.lanes { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
|
103
|
+
.lane {
|
|
104
|
+
background: var(--ds-surface, light-dark(#ffffff, #15171c));
|
|
105
|
+
border: 1px solid var(--ds-line, light-dark(#e7e7ea, #2a2d34));
|
|
106
|
+
border-radius: 12px; padding: 18px 20px;
|
|
107
|
+
}
|
|
108
|
+
.lane * { color: inherit; }
|
|
109
|
+
.lane .top { display: flex; align-items: center; gap: 12px; }
|
|
110
|
+
.lane .nm { font-size: 19px; font-weight: 600; }
|
|
111
|
+
.lane .ds { font-size: 16px; line-height: 1.5; margin: 12px 0 0;
|
|
112
|
+
color: var(--ds-ink-soft, light-dark(#56565a, #9b9ba1)); }
|
|
113
|
+
|
|
114
|
+
/* icon chip — drives off --c (icon colour) + --tint (chip bg) when a tone is set */
|
|
115
|
+
.icchip {
|
|
116
|
+
flex: none; width: 38px; height: 38px; border-radius: 10px;
|
|
117
|
+
display: grid; place-items: center;
|
|
118
|
+
background: var(--tint, var(--ds-surface-soft, light-dark(#f1f5f9, #1c2129)));
|
|
119
|
+
color: var(--c, var(--ds-accent, light-dark(#15803d, #4ade80)));
|
|
120
|
+
}
|
|
121
|
+
.icchip svg { width: 20px; height: 20px; }
|
|
122
|
+
|
|
123
|
+
/* semantic tones — apply --c / --tint ONLY when a tile genuinely maps to a
|
|
124
|
+
status/state (RULE: status colour where state maps to status, else one accent) */
|
|
125
|
+
.t-neutral { --c: light-dark(#64748b, #94a3b8); --tint: light-dark(#f1f5f9, #1c2129); }
|
|
126
|
+
.t-info { --c: light-dark(#2563eb, #60a5fa); --tint: light-dark(#eef3ff, #12203a); }
|
|
127
|
+
.t-success { --c: light-dark(#15803d, #4ade80); --tint: light-dark(#ecf9f0, #102a1c); }
|
|
128
|
+
.t-warning { --c: light-dark(#b45309, #f59e0b); --tint: light-dark(#fdf4e8, #291d0a); }
|
|
129
|
+
.t-danger { --c: light-dark(#b91c1c, #f87171); --tint: light-dark(#fdecec, #2a1414); }
|
|
130
|
+
/* NO accent-border / emphasis on one sibling: four parallel exits are equal
|
|
131
|
+
weight — singling one out with a coloured bar is AP23 slop (status double-
|
|
132
|
+
encoded, the "accent-bordered tile" AI reflex). Differentiate by icon, not
|
|
133
|
+
by ranking siblings that aren't ranked. */
|
|
134
|
+
|
|
135
|
+
/* ===========================================================================
|
|
136
|
+
PRIMITIVE 2 — callout (tinted, leading icon): guard rails, notes, warnings
|
|
137
|
+
Default = warning amber. Add .info / .success / .danger to retone.
|
|
138
|
+
=========================================================================== */
|
|
139
|
+
.callout {
|
|
140
|
+
display: flex; gap: 16px; align-items: flex-start;
|
|
141
|
+
border-radius: 14px; padding: 20px 24px;
|
|
142
|
+
background: var(--co-bg, light-dark(#fdf8ed, #1d180c));
|
|
143
|
+
border: 1px solid var(--co-bd, light-dark(#f0d9a8, #4a3c1c));
|
|
144
|
+
}
|
|
145
|
+
.callout * { color: inherit; }
|
|
146
|
+
.callout .cico {
|
|
147
|
+
flex: none; width: 40px; height: 40px; border-radius: 10px;
|
|
148
|
+
display: grid; place-items: center;
|
|
149
|
+
background: var(--co-ic-bg, light-dark(#f7ecd0, #2a2110));
|
|
150
|
+
color: var(--co-ic, light-dark(#b45309, #e0a64a));
|
|
151
|
+
}
|
|
152
|
+
.callout .cico svg { width: 22px; height: 22px; }
|
|
153
|
+
.callout .q { font-size: 19px; font-weight: 600;
|
|
154
|
+
color: var(--ds-ink, light-dark(#1a1a1a, #e9e9ea)); }
|
|
155
|
+
.callout .a { font-size: 17px; margin-top: 6px;
|
|
156
|
+
color: var(--co-ic, light-dark(#7a6a3a, #cbb87e)); }
|
|
157
|
+
.callout.info { --co-bg: light-dark(#eef3ff,#0e1a30); --co-bd: light-dark(#cdddfb,#1d3257); --co-ic-bg: light-dark(#dde9ff,#16284a); --co-ic: light-dark(#2563eb,#7fb0ff); }
|
|
158
|
+
.callout.success { --co-bg: light-dark(#ecf9f0,#0c2417); --co-bd: light-dark(#c6e9d3,#1c402c); --co-ic-bg: light-dark(#d6f0e0,#143020); --co-ic: light-dark(#15803d,#74d99a); }
|
|
159
|
+
.callout.danger { --co-bg: light-dark(#fdecec,#240f0f); --co-bd: light-dark(#f5cccc,#4a1f1f); --co-ic-bg: light-dark(#fadada,#341515); --co-ic: light-dark(#b91c1c,#f59a9a); }
|
|
160
|
+
|
|
161
|
+
/* ===========================================================================
|
|
162
|
+
PRIMITIVE 3 — thread / message card (LOCKED DARK)
|
|
163
|
+
It sets its OWN text colour on every node (the .wrap inherit-guard outranks
|
|
164
|
+
a descendant universal, so locked-bg containers must commit their own ink).
|
|
165
|
+
For app/UI mockups use the wrapper's .window / .window.dark instead.
|
|
166
|
+
=========================================================================== */
|
|
167
|
+
.thread {
|
|
168
|
+
background: #14161b; border: 1px solid #262a31; border-radius: 14px;
|
|
169
|
+
overflow: hidden; box-shadow: 0 6px 24px rgba(0,0,0,.22);
|
|
170
|
+
}
|
|
171
|
+
.thread .bar { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-bottom: 1px solid #262a31; }
|
|
172
|
+
.thread .dot { width: 11px; height: 11px; border-radius: 50%; }
|
|
173
|
+
.thread .tt { margin-left: 8px; font-size: 13px; font-family: ui-monospace, monospace; color: #7d828c !important; }
|
|
174
|
+
.thread .body { padding: 16px 20px 20px; }
|
|
175
|
+
.thread .who { font-size: 14px; font-weight: 600; color: #8ab4ff !important; }
|
|
176
|
+
.thread .msg { font-size: 18px; line-height: 1.5; margin: 6px 0 0; color: #e6e9ef !important; }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!-- easel-icons.svg — Lucide (ISC license) · one set · 1.75 stroke · currentColor.
|
|
2
|
+
Inline this whole block once near the top of a push, then reference an icon with:
|
|
3
|
+
<svg class="ic"><use href="#i-git-branch"/></svg>
|
|
4
|
+
and size it with CSS (.icchip svg / .badge svg already do). currentColor means
|
|
5
|
+
the icon inherits its container's `color` — set --c on a .icchip or color on a .badge.
|
|
6
|
+
NEVER use emoji as icons (SKILL fidelity bar / Lookbook P-AS-01). Swap the SET
|
|
7
|
+
wholesale if you change libraries; never mix per-icon. -->
|
|
8
|
+
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
9
|
+
<symbol id="i-git-branch" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></symbol>
|
|
10
|
+
<symbol id="i-message-square" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></symbol>
|
|
11
|
+
<symbol id="i-send" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></symbol>
|
|
12
|
+
<symbol id="i-wrench" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></symbol>
|
|
13
|
+
<symbol id="i-rocket" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09"/><path d="M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05"/></symbol>
|
|
14
|
+
<symbol id="i-bell-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M13.73 21a2 2 0 0 1-3.46 0"/><path d="M18.63 13A17.9 17.9 0 0 1 18 8"/><path d="M6.26 6.26A5.86 5.86 0 0 0 6 8c0 7-3 9-3 9h14"/><path d="M18 8a6 6 0 0 0-9.33-5"/><line x1="2" y1="2" x2="22" y2="22"/></symbol>
|
|
15
|
+
<symbol id="i-ban" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></symbol>
|
|
16
|
+
<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></symbol>
|
|
17
|
+
<symbol id="i-check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/></symbol>
|
|
18
|
+
<symbol id="i-triangle-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></symbol>
|
|
19
|
+
<symbol id="i-clock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></symbol>
|
|
20
|
+
<symbol id="i-zap" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></symbol>
|
|
21
|
+
<symbol id="i-arrow-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></symbol>
|
|
22
|
+
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></symbol>
|
|
23
|
+
<symbol id="i-file-text" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></symbol>
|
|
24
|
+
<symbol id="i-user" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></symbol>
|
|
25
|
+
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></symbol>
|
|
26
|
+
<symbol id="i-trending-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M16 7h6v6"/><path d="m22 7-8.5 8.5-5-5L2 17"/></symbol>
|
|
27
|
+
<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></symbol>
|
|
28
|
+
</svg>
|