Skip to content

Commit 22984f4

Browse files
benvinegarclaude
andauthored
feat(server): bake a snippet kit into every snippet doc (#6)
Element defaults (button/input/select/textarea matching the viewer, accent-color on toggles), SVG utility classes (t/ts/th presets, box, arr, leader, node, c-* color ramps with dark-aware text), and a shared context-stroke arrow marker injected into every doc. The design guide documents it as a compact reference table; the demo JWT diagram rewritten with the kit is 46% smaller. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent fbc8f9d commit 22984f4

6 files changed

Lines changed: 153 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ All notable user-visible changes to this project are documented in this file.
1818
body and both MCP `publish_snippet` tools, `--session-title` on
1919
`sideshow publish`. Applied only when the publish creates the session —
2020
it never overwrites a title, including renames made in the viewer.
21+
- A snippet kit baked into every snippet doc, so agents publish compact
22+
markup instead of hand-written inline CSS: bare `button`/`input`/`select`/
23+
`textarea` pre-styled to match the viewer, SVG utility classes (`t`/`ts`/
24+
`th` text presets, `box`, `arr`, `leader`, `node`, `c-*` color ramps with
25+
dark-mode-aware text), and a shared `#arrow` marker injected into every
26+
doc. The design guide documents it as a compact reference table.
2127

2228
### Changed
2329

bin/demoData.js

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,30 @@
22
// agents draw on the surface. Keep this file dependency-free like the CLI.
33

44
const JWT_DIAGRAM = `
5-
<svg width="100%" viewBox="0 0 680 320" font-family="var(--font-sans)" font-size="13">
6-
<defs>
7-
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
8-
<path d="M0 0L10 5L0 10z" fill="var(--color-text-secondary)"/>
9-
</marker>
10-
<marker id="arr-danger" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
11-
<path d="M0 0L10 5L0 10z" fill="var(--color-text-danger)"/>
12-
</marker>
13-
<marker id="arr-success" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse">
14-
<path d="M0 0L10 5L0 10z" fill="var(--color-text-success)"/>
15-
</marker>
16-
</defs>
5+
<svg width="100%" viewBox="0 0 680 320">
6+
<line class="leader" x1="110" y1="52" x2="110" y2="300"/>
7+
<line class="leader" x1="340" y1="52" x2="340" y2="300"/>
8+
<line class="leader" x1="570" y1="52" x2="570" y2="300"/>
179
18-
<line x1="110" y1="52" x2="110" y2="300" stroke="var(--color-border-tertiary)" stroke-dasharray="3 4"/>
19-
<line x1="340" y1="52" x2="340" y2="300" stroke="var(--color-border-tertiary)" stroke-dasharray="3 4"/>
20-
<line x1="570" y1="52" x2="570" y2="300" stroke="var(--color-border-tertiary)" stroke-dasharray="3 4"/>
10+
<rect class="box" x="35" y="10" width="150" height="40"/>
11+
<text class="th" x="110" y="35" text-anchor="middle">Client</text>
12+
<g class="c-blue"><rect class="box" x="265" y="10" width="150" height="40"/><text class="th" x="340" y="35" text-anchor="middle">/api (guarded)</text></g>
13+
<g class="c-amber"><rect class="box" x="495" y="10" width="150" height="40"/><text class="th" x="570" y="35" text-anchor="middle">/auth/refresh</text></g>
2114
22-
<rect x="35" y="10" width="150" height="40" rx="8" fill="var(--color-background-secondary)" stroke="var(--color-border-tertiary)"/>
23-
<text x="110" y="35" text-anchor="middle" font-weight="500" fill="var(--color-text-primary)">Client</text>
24-
<rect x="265" y="10" width="150" height="40" rx="8" fill="var(--color-background-info)" stroke="var(--color-border-info)"/>
25-
<text x="340" y="35" text-anchor="middle" font-weight="500" fill="var(--color-text-info)">/api (guarded)</text>
26-
<rect x="495" y="10" width="150" height="40" rx="8" fill="var(--color-background-warning)" stroke="var(--color-border-warning)"/>
27-
<text x="570" y="35" text-anchor="middle" font-weight="500" fill="var(--color-text-warning)">/auth/refresh</text>
15+
<text class="ts" x="225" y="84" text-anchor="middle">request + expired JWT</text>
16+
<line class="arr" x1="110" y1="92" x2="334" y2="92" marker-end="url(#arrow)"/>
2817
29-
<text x="225" y="84" text-anchor="middle" fill="var(--color-text-secondary)">request + expired JWT</text>
30-
<line x1="110" y1="92" x2="334" y2="92" stroke="var(--color-text-secondary)" marker-end="url(#arr)"/>
18+
<text class="ts c-red" x="225" y="120" text-anchor="middle">401 token_expired</text>
19+
<line class="arr c-red" x1="340" y1="128" x2="116" y2="128" marker-end="url(#arrow)"/>
3120
32-
<text x="225" y="120" text-anchor="middle" fill="var(--color-text-danger)">401 token_expired</text>
33-
<line x1="340" y1="128" x2="116" y2="128" stroke="var(--color-text-danger)" marker-end="url(#arr-danger)"/>
21+
<text class="ts" x="340" y="172" text-anchor="middle">refresh token (httpOnly cookie)</text>
22+
<line class="arr" x1="110" y1="180" x2="564" y2="180" marker-end="url(#arrow)"/>
3423
35-
<text x="340" y="172" text-anchor="middle" fill="var(--color-text-secondary)">refresh token (httpOnly cookie)</text>
36-
<line x1="110" y1="180" x2="564" y2="180" stroke="var(--color-text-secondary)" marker-end="url(#arr)"/>
24+
<text class="ts c-green" x="340" y="208" text-anchor="middle">new JWT + rotated refresh token</text>
25+
<line class="arr c-green" x1="570" y1="216" x2="116" y2="216" marker-end="url(#arrow)"/>
3726
38-
<text x="340" y="208" text-anchor="middle" fill="var(--color-text-success)">new JWT + rotated refresh token</text>
39-
<line x1="570" y1="216" x2="116" y2="216" stroke="var(--color-text-success)" marker-end="url(#arr-success)"/>
40-
41-
<text x="225" y="260" text-anchor="middle" fill="var(--color-text-secondary)">retry with new JWT</text>
42-
<line x1="110" y1="268" x2="334" y2="268" stroke="var(--color-text-secondary)" marker-end="url(#arr)"/>
27+
<text class="ts" x="225" y="260" text-anchor="middle">retry with new JWT</text>
28+
<line class="arr" x1="110" y1="268" x2="334" y2="268" marker-end="url(#arrow)"/>
4329
</svg>`;
4430

4531
const JWT_EXPLAINER = `

guide/DESIGN_GUIDE.md

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,53 @@ replies short; do substantial revisions as snippet updates instead.
5555
- **Never use `position: fixed`** — the iframe sizes to content height and
5656
fixed elements break that. Use normal-flow layout.
5757

58+
## Built-in kit — reach for it before writing CSS
59+
60+
Bare `button`, `input`, `select`, and `textarea` are pre-styled to match the
61+
viewer, hover/focus included — write the plain element, don't restyle it.
62+
Checkboxes, radios, ranges, and progress bars are themed via `accent-color`.
63+
64+
SVG utility classes, available in every snippet:
65+
66+
| class | effect |
67+
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
68+
| `t` / `ts` / `th` | text presets: 14px / 12px muted / 14px medium heading |
69+
| `box` | neutral rect — secondary fill, faint stroke, rx 8 |
70+
| `arr` | 1.2px connector line |
71+
| `leader` | dashed guide line |
72+
| `node` | pointer cursor + hover dim, for clickable shapes |
73+
| `c-blue` `c-teal` `c-amber` `c-coral` `c-green` `c-red` `c-gray` | color ramp: fill+stroke on shapes (or a whole `<g>`); child `<text>` auto-switches to readable ink in light and dark |
74+
75+
A `<marker id="arrow">` is injected into every snippet doc — end any line with
76+
`marker-end="url(#arrow)"` and the arrowhead inherits the line's stroke color.
77+
78+
```html
79+
<svg width="100%" viewBox="0 0 680 70">
80+
<g class="c-blue">
81+
<rect class="box" x="10" y="10" width="130" height="40" />
82+
<text class="th" x="75" y="35" text-anchor="middle">API</text>
83+
</g>
84+
<text class="ts" x="250" y="24" text-anchor="middle">202 + job id</text>
85+
<line class="arr" x1="140" y1="30" x2="360" y2="30" marker-end="url(#arrow)" />
86+
</svg>
87+
```
88+
89+
Icons: the Tabler webfont is on the CSP allowlist —
90+
`<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">`
91+
then `<i class="ti ti-check"></i>`.
92+
5893
## Theming — dark mode is mandatory
5994

60-
CSS variables are pre-defined and adapt to light/dark automatically. Use them
61-
instead of hardcoded colors; never write `color: #333` (invisible in dark mode).
95+
For anything the kit doesn't cover, use the pre-defined CSS variables — they
96+
adapt to light/dark automatically. Never hardcode colors; `color: #333` is
97+
invisible in dark mode.
6298

63-
- Backgrounds: `--color-background-primary` (surface), `-secondary`, `-tertiary`,
64-
and semantic `-info`, `-danger`, `-success`, `-warning`
65-
- Text: `--color-text-primary`, `-secondary` (muted), `-tertiary` (hints),
66-
plus semantic variants as above
99+
- Backgrounds: `--color-background-primary|secondary|tertiary` and semantic
100+
`-info|-danger|-success|-warning`
101+
- Text: `--color-text-primary|secondary|tertiary`, plus the same semantic variants
67102
- Borders: `--color-border-tertiary` (default, faint), `-secondary`, `-primary`,
68103
plus semantic variants
69-
- Fonts: `--font-sans` (default), `--font-serif`, `--font-mono`
70-
- Radius: `--border-radius-md` (8px), `-lg` (12px), `-xl` (16px)
104+
- Fonts: `--font-sans|serif|mono`; radius: `--border-radius-md|lg|xl` (8/12/16px)
71105

72106
Mental test: if the background were near-black, would every element still read?
73107

@@ -91,6 +125,7 @@ Two globals are injected into every snippet:
91125
- Flat and clean: no gradients, drop shadows, or decorative effects.
92126
- Sentence case for headings and labels. No emoji.
93127
- Two font weights only: 400 and 500.
94-
- SVG works great — for diagrams use `<svg width="100%" viewBox="0 0 680 H">`.
128+
- SVG works great — for diagrams use `<svg width="100%" viewBox="0 0 680 H">`
129+
with the kit classes above.
95130
- Keep it focused: one concept per snippet. Publish a series of small snippets
96131
with distinct titles rather than one giant page.

server/snippetPage.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,80 @@ body {
8888
}
8989
`;
9090

91+
// Snippet kit: element defaults and SVG utility classes baked into every
92+
// snippet doc so agents publish compact markup instead of hand-writing inline
93+
// CSS. Documented as a reference table in guide/DESIGN_GUIDE.md — keep the
94+
// two in sync. Note: CSS rules override SVG presentation attributes, so bare
95+
// element selectors here must never set properties snippets commonly set via
96+
// attributes (fill/font-size on text, etc.) — that's why text styling is
97+
// opt-in via classes.
98+
const KIT_CSS = `
99+
:root {
100+
color-scheme: light dark;
101+
--c-teal-bg: #e1f4f1; --c-teal-line: #1fa996; --c-teal-text: #0c6e62;
102+
--c-coral-bg: #fdece5; --c-coral-line: #e8835e; --c-coral-text: #a44f28;
103+
}
104+
@media (prefers-color-scheme: dark) {
105+
:root {
106+
--c-teal-bg: rgba(31, 169, 150, 0.18); --c-teal-text: #6fd0c2;
107+
--c-coral-bg: rgba(232, 131, 94, 0.18); --c-coral-text: #f0a987;
108+
}
109+
}
110+
button {
111+
font: 500 14px/1.4 var(--font-sans);
112+
color: var(--color-text-primary);
113+
background: none;
114+
border: 0.5px solid var(--color-border-secondary);
115+
border-radius: var(--border-radius-md);
116+
padding: 6px 14px;
117+
cursor: pointer;
118+
}
119+
button:hover { background: var(--color-background-secondary); }
120+
input:not([type=checkbox]):not([type=radio]):not([type=range]), select, textarea {
121+
font: 14px/1.4 var(--font-sans);
122+
color: var(--color-text-primary);
123+
background: var(--color-background-primary);
124+
border: 0.5px solid var(--color-border-secondary);
125+
border-radius: var(--border-radius-md);
126+
padding: 6px 10px;
127+
outline: none;
128+
}
129+
input:focus, select:focus, textarea:focus { border-color: var(--color-border-info); }
130+
input::placeholder, textarea::placeholder { color: var(--color-text-tertiary); }
131+
textarea { resize: vertical; }
132+
input[type=checkbox], input[type=radio], input[type=range], progress {
133+
accent-color: var(--color-border-info);
134+
}
135+
svg { font-family: var(--font-sans); fill: var(--color-text-primary); }
136+
.t { font-size: 14px; }
137+
.ts { font-size: 12px; fill: var(--color-text-secondary); }
138+
.th { font-size: 14px; font-weight: 500; }
139+
.box { fill: var(--color-background-secondary); stroke: var(--color-border-tertiary); rx: 8px; }
140+
.arr { stroke: var(--color-text-secondary); stroke-width: 1.2; fill: none; }
141+
.leader { stroke: var(--color-border-secondary); stroke-width: 1; stroke-dasharray: 3 4; fill: none; }
142+
.node { cursor: pointer; }
143+
.node:hover { opacity: 0.75; }
144+
.c-blue, .c-blue .box { fill: var(--color-background-info); stroke: var(--color-border-info); }
145+
.c-blue text, text.c-blue { fill: var(--color-text-info); stroke: none; }
146+
.c-teal, .c-teal .box { fill: var(--c-teal-bg); stroke: var(--c-teal-line); }
147+
.c-teal text, text.c-teal { fill: var(--c-teal-text); stroke: none; }
148+
.c-amber, .c-amber .box { fill: var(--color-background-warning); stroke: var(--color-border-warning); }
149+
.c-amber text, text.c-amber { fill: var(--color-text-warning); stroke: none; }
150+
.c-coral, .c-coral .box { fill: var(--c-coral-bg); stroke: var(--c-coral-line); }
151+
.c-coral text, text.c-coral { fill: var(--c-coral-text); stroke: none; }
152+
.c-green, .c-green .box { fill: var(--color-background-success); stroke: var(--color-border-success); }
153+
.c-green text, text.c-green { fill: var(--color-text-success); stroke: none; }
154+
.c-red, .c-red .box { fill: var(--color-background-danger); stroke: var(--color-border-danger); }
155+
.c-red text, text.c-red { fill: var(--color-text-danger); stroke: none; }
156+
.c-gray, .c-gray .box { fill: var(--color-background-secondary); stroke: var(--color-border-secondary); }
157+
.c-gray text, text.c-gray { fill: var(--color-text-secondary); stroke: none; }
158+
`;
159+
160+
// Shared SVG defs injected into every snippet doc. Inline SVGs anywhere in
161+
// the document can reference these by id; the arrowhead inherits the
162+
// referencing line's stroke color via context-stroke.
163+
const SVG_DEFS = `<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="context-stroke"/></marker></defs></svg>`;
164+
91165
// Bridge to the host viewer: sendPrompt/openLink mirror Claude's widget
92166
// globals, and a ResizeObserver reports content height so the parent can
93167
// size the sandboxed (opaque-origin) iframe.
@@ -135,9 +209,10 @@ export function renderSnippetPage(snippet: Snippet): string {
135209
<meta name="viewport" content="width=device-width, initial-scale=1">
136210
<meta http-equiv="Content-Security-Policy" content="${CSP}">
137211
<title>${escapeHtml(snippet.title)}</title>
138-
<style>${TOKENS_CSS}</style>
212+
<style>${TOKENS_CSS}${KIT_CSS}</style>
139213
</head>
140214
<body>
215+
${SVG_DEFS}
141216
${snippet.html}
142217
<script>${BRIDGE_JS}</script>
143218
</body>

skills/sideshow/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ Rules of thumb:
4343
beats one giant page.
4444
- **Iterate with `sideshow update <id>`** (same card, new version) instead of
4545
publishing near-duplicates. Versions are kept; the user can flip between them.
46-
- Use the theme CSS variables from the guide so snippets work in dark mode.
46+
- Use the built-in kit from the guide (pre-styled form elements, SVG utility
47+
classes) before writing CSS; for anything else use the theme CSS variables
48+
so snippets work in dark mode.
4749

4850
## The feedback loop
4951

test/api.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,18 @@ test("update bumps version and keeps history; old version renderable", async ()
118118
assert.ok(old.includes("<p>v1</p>"));
119119
});
120120

121-
test("snippet page is wrapped with CSP and bridge", async () => {
121+
test("snippet page is wrapped with CSP, bridge, and kit", async () => {
122122
const app = makeApp();
123123
const s = (await (await app.request("/api/snippets", json({ html: "<p>x</p>" }))).json()) as any;
124124
const page = await (await app.request(`/s/${s.id}`)).text();
125125
assert.ok(page.includes("Content-Security-Policy"));
126126
assert.ok(page.includes("window.sendPrompt"));
127127
assert.ok(page.includes("__sideshow"));
128+
// Snippet kit: SVG utilities in the stylesheet and the shared arrow marker
129+
// injected before the snippet body so url(#arrow) resolves.
130+
assert.ok(page.includes(".c-blue"));
131+
assert.ok(page.indexOf('<marker id="arrow"') < page.indexOf("<p>x</p>"));
132+
assert.ok(page.includes('<marker id="arrow"'));
128133
});
129134

130135
test("comments attach to snippets and filter by author/after", async () => {

0 commit comments

Comments
 (0)