From 65620a21c3743235a1e8addebc6fe3e478992679 Mon Sep 17 00:00:00 2001 From: Ben Vinegar <2153+benvinegar@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:47:05 -0400 Subject: [PATCH] feat(server): bake a snippet kit into every snippet doc 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 --- CHANGELOG.md | 6 ++++ bin/demoData.js | 50 ++++++++++---------------- guide/DESIGN_GUIDE.md | 53 ++++++++++++++++++++++----- server/snippetPage.ts | 77 +++++++++++++++++++++++++++++++++++++++- skills/sideshow/SKILL.md | 4 ++- test/api.test.ts | 7 +++- 6 files changed, 153 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7b6c41..fccf0da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ All notable user-visible changes to this project are documented in this file. body and both MCP `publish_snippet` tools, `--session-title` on `sideshow publish`. Applied only when the publish creates the session — it never overwrites a title, including renames made in the viewer. +- A snippet kit baked into every snippet doc, so agents publish compact + markup instead of hand-written inline CSS: bare `button`/`input`/`select`/ + `textarea` pre-styled to match the viewer, SVG utility classes (`t`/`ts`/ + `th` text presets, `box`, `arr`, `leader`, `node`, `c-*` color ramps with + dark-mode-aware text), and a shared `#arrow` marker injected into every + doc. The design guide documents it as a compact reference table. ### Changed diff --git a/bin/demoData.js b/bin/demoData.js index 53fdc28..b824bb2 100644 --- a/bin/demoData.js +++ b/bin/demoData.js @@ -2,44 +2,30 @@ // agents draw on the surface. Keep this file dependency-free like the CLI. const JWT_DIAGRAM = ` - - - - - - - - - - - - + + + + - - - + + Client + /api (guarded) + /auth/refresh - - Client - - /api (guarded) - - /auth/refresh + request + expired JWT + - request + expired JWT - + 401 token_expired + - 401 token_expired - + refresh token (httpOnly cookie) + - refresh token (httpOnly cookie) - + new JWT + rotated refresh token + - new JWT + rotated refresh token - - - retry with new JWT - + retry with new JWT + `; const JWT_EXPLAINER = ` diff --git a/guide/DESIGN_GUIDE.md b/guide/DESIGN_GUIDE.md index 2e0c3e0..00dda32 100644 --- a/guide/DESIGN_GUIDE.md +++ b/guide/DESIGN_GUIDE.md @@ -55,19 +55,53 @@ replies short; do substantial revisions as snippet updates instead. - **Never use `position: fixed`** — the iframe sizes to content height and fixed elements break that. Use normal-flow layout. +## Built-in kit — reach for it before writing CSS + +Bare `button`, `input`, `select`, and `textarea` are pre-styled to match the +viewer, hover/focus included — write the plain element, don't restyle it. +Checkboxes, radios, ranges, and progress bars are themed via `accent-color`. + +SVG utility classes, available in every snippet: + +| class | effect | +| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `t` / `ts` / `th` | text presets: 14px / 12px muted / 14px medium heading | +| `box` | neutral rect — secondary fill, faint stroke, rx 8 | +| `arr` | 1.2px connector line | +| `leader` | dashed guide line | +| `node` | pointer cursor + hover dim, for clickable shapes | +| `c-blue` `c-teal` `c-amber` `c-coral` `c-green` `c-red` `c-gray` | color ramp: fill+stroke on shapes (or a whole ``); child `` auto-switches to readable ink in light and dark | + +A `` is injected into every snippet doc — end any line with +`marker-end="url(#arrow)"` and the arrowhead inherits the line's stroke color. + +```html + + + + API + + 202 + job id + + +``` + +Icons: the Tabler webfont is on the CSP allowlist — +`` +then ``. + ## Theming — dark mode is mandatory -CSS variables are pre-defined and adapt to light/dark automatically. Use them -instead of hardcoded colors; never write `color: #333` (invisible in dark mode). +For anything the kit doesn't cover, use the pre-defined CSS variables — they +adapt to light/dark automatically. Never hardcode colors; `color: #333` is +invisible in dark mode. -- Backgrounds: `--color-background-primary` (surface), `-secondary`, `-tertiary`, - and semantic `-info`, `-danger`, `-success`, `-warning` -- Text: `--color-text-primary`, `-secondary` (muted), `-tertiary` (hints), - plus semantic variants as above +- Backgrounds: `--color-background-primary|secondary|tertiary` and semantic + `-info|-danger|-success|-warning` +- Text: `--color-text-primary|secondary|tertiary`, plus the same semantic variants - Borders: `--color-border-tertiary` (default, faint), `-secondary`, `-primary`, plus semantic variants -- Fonts: `--font-sans` (default), `--font-serif`, `--font-mono` -- Radius: `--border-radius-md` (8px), `-lg` (12px), `-xl` (16px) +- Fonts: `--font-sans|serif|mono`; radius: `--border-radius-md|lg|xl` (8/12/16px) Mental test: if the background were near-black, would every element still read? @@ -91,6 +125,7 @@ Two globals are injected into every snippet: - Flat and clean: no gradients, drop shadows, or decorative effects. - Sentence case for headings and labels. No emoji. - Two font weights only: 400 and 500. -- SVG works great — for diagrams use ``. +- SVG works great — for diagrams use `` + with the kit classes above. - Keep it focused: one concept per snippet. Publish a series of small snippets with distinct titles rather than one giant page. diff --git a/server/snippetPage.ts b/server/snippetPage.ts index 1d8436d..7dc73ae 100644 --- a/server/snippetPage.ts +++ b/server/snippetPage.ts @@ -88,6 +88,80 @@ body { } `; +// Snippet kit: element defaults and SVG utility classes baked into every +// snippet doc so agents publish compact markup instead of hand-writing inline +// CSS. Documented as a reference table in guide/DESIGN_GUIDE.md — keep the +// two in sync. Note: CSS rules override SVG presentation attributes, so bare +// element selectors here must never set properties snippets commonly set via +// attributes (fill/font-size on text, etc.) — that's why text styling is +// opt-in via classes. +const KIT_CSS = ` +:root { + color-scheme: light dark; + --c-teal-bg: #e1f4f1; --c-teal-line: #1fa996; --c-teal-text: #0c6e62; + --c-coral-bg: #fdece5; --c-coral-line: #e8835e; --c-coral-text: #a44f28; +} +@media (prefers-color-scheme: dark) { + :root { + --c-teal-bg: rgba(31, 169, 150, 0.18); --c-teal-text: #6fd0c2; + --c-coral-bg: rgba(232, 131, 94, 0.18); --c-coral-text: #f0a987; + } +} +button { + font: 500 14px/1.4 var(--font-sans); + color: var(--color-text-primary); + background: none; + border: 0.5px solid var(--color-border-secondary); + border-radius: var(--border-radius-md); + padding: 6px 14px; + cursor: pointer; +} +button:hover { background: var(--color-background-secondary); } +input:not([type=checkbox]):not([type=radio]):not([type=range]), select, textarea { + font: 14px/1.4 var(--font-sans); + color: var(--color-text-primary); + background: var(--color-background-primary); + border: 0.5px solid var(--color-border-secondary); + border-radius: var(--border-radius-md); + padding: 6px 10px; + outline: none; +} +input:focus, select:focus, textarea:focus { border-color: var(--color-border-info); } +input::placeholder, textarea::placeholder { color: var(--color-text-tertiary); } +textarea { resize: vertical; } +input[type=checkbox], input[type=radio], input[type=range], progress { + accent-color: var(--color-border-info); +} +svg { font-family: var(--font-sans); fill: var(--color-text-primary); } +.t { font-size: 14px; } +.ts { font-size: 12px; fill: var(--color-text-secondary); } +.th { font-size: 14px; font-weight: 500; } +.box { fill: var(--color-background-secondary); stroke: var(--color-border-tertiary); rx: 8px; } +.arr { stroke: var(--color-text-secondary); stroke-width: 1.2; fill: none; } +.leader { stroke: var(--color-border-secondary); stroke-width: 1; stroke-dasharray: 3 4; fill: none; } +.node { cursor: pointer; } +.node:hover { opacity: 0.75; } +.c-blue, .c-blue .box { fill: var(--color-background-info); stroke: var(--color-border-info); } +.c-blue text, text.c-blue { fill: var(--color-text-info); stroke: none; } +.c-teal, .c-teal .box { fill: var(--c-teal-bg); stroke: var(--c-teal-line); } +.c-teal text, text.c-teal { fill: var(--c-teal-text); stroke: none; } +.c-amber, .c-amber .box { fill: var(--color-background-warning); stroke: var(--color-border-warning); } +.c-amber text, text.c-amber { fill: var(--color-text-warning); stroke: none; } +.c-coral, .c-coral .box { fill: var(--c-coral-bg); stroke: var(--c-coral-line); } +.c-coral text, text.c-coral { fill: var(--c-coral-text); stroke: none; } +.c-green, .c-green .box { fill: var(--color-background-success); stroke: var(--color-border-success); } +.c-green text, text.c-green { fill: var(--color-text-success); stroke: none; } +.c-red, .c-red .box { fill: var(--color-background-danger); stroke: var(--color-border-danger); } +.c-red text, text.c-red { fill: var(--color-text-danger); stroke: none; } +.c-gray, .c-gray .box { fill: var(--color-background-secondary); stroke: var(--color-border-secondary); } +.c-gray text, text.c-gray { fill: var(--color-text-secondary); stroke: none; } +`; + +// Shared SVG defs injected into every snippet doc. Inline SVGs anywhere in +// the document can reference these by id; the arrowhead inherits the +// referencing line's stroke color via context-stroke. +const SVG_DEFS = ``; + // Bridge to the host viewer: sendPrompt/openLink mirror Claude's widget // globals, and a ResizeObserver reports content height so the parent can // size the sandboxed (opaque-origin) iframe. @@ -135,9 +209,10 @@ export function renderSnippetPage(snippet: Snippet): string { ${escapeHtml(snippet.title)} - + +${SVG_DEFS} ${snippet.html} diff --git a/skills/sideshow/SKILL.md b/skills/sideshow/SKILL.md index 2106ad8..475c6a1 100644 --- a/skills/sideshow/SKILL.md +++ b/skills/sideshow/SKILL.md @@ -43,7 +43,9 @@ Rules of thumb: beats one giant page. - **Iterate with `sideshow update `** (same card, new version) instead of publishing near-duplicates. Versions are kept; the user can flip between them. -- Use the theme CSS variables from the guide so snippets work in dark mode. +- Use the built-in kit from the guide (pre-styled form elements, SVG utility + classes) before writing CSS; for anything else use the theme CSS variables + so snippets work in dark mode. ## The feedback loop diff --git a/test/api.test.ts b/test/api.test.ts index 92aebd8..c351c7f 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -118,13 +118,18 @@ test("update bumps version and keeps history; old version renderable", async () assert.ok(old.includes("

v1

")); }); -test("snippet page is wrapped with CSP and bridge", async () => { +test("snippet page is wrapped with CSP, bridge, and kit", async () => { const app = makeApp(); const s = (await (await app.request("/api/snippets", json({ html: "

x

" }))).json()) as any; const page = await (await app.request(`/s/${s.id}`)).text(); assert.ok(page.includes("Content-Security-Policy")); assert.ok(page.includes("window.sendPrompt")); assert.ok(page.includes("__sideshow")); + // Snippet kit: SVG utilities in the stylesheet and the shared arrow marker + // injected before the snippet body so url(#arrow) resolves. + assert.ok(page.includes(".c-blue")); + assert.ok(page.indexOf('x

")); + assert.ok(page.includes(' {