saglitzdesign-mcp 0.3.2 → 0.5.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/README.md +27 -1
- package/dist/a11y.js +98 -0
- package/dist/index.js +59 -1
- package/dist/prompts.js +183 -0
- package/dist/tokens.js +148 -0
- package/knowledge/marketing/paywall-benchmarks.md +58 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ expert‑level guidance on **web, iOS, Android and macOS design** — plus the
|
|
|
9
9
|
**UX, copywriting, SEO, GEO and marketing** knowledge that makes a product
|
|
10
10
|
actually convert.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
57 curated knowledge documents · 12 tools · 7 build/review workflows · real token & a11y generators · phased roadmaps · real‑world visual examples
|
|
13
13
|
|
|
14
14
|
[](https://www.npmjs.com/package/saglitzdesign-mcp)
|
|
15
15
|
[](LICENSE)
|
|
@@ -58,6 +58,30 @@ instead of guessing.
|
|
|
58
58
|
| 📣 **Marketing** | Branding & identity (strategy, logo systems, brand voice) · email marketing (design, lifecycle, deliverability) · ad creative (hooks, platform specs, testing) |
|
|
59
59
|
| 🖼️ **Patterns & examples** | Real‑world patterns studied from top apps & sites, plus a curated screenshot library served as images |
|
|
60
60
|
|
|
61
|
+
## Workflows (`/` prompts) — "build me a…"
|
|
62
|
+
|
|
63
|
+
Beyond answering questions, SaglitzDesign ships **prompts** that orchestrate an
|
|
64
|
+
entire build end‑to‑end. In Claude Code they appear in the `/` menu; invoke one
|
|
65
|
+
and the agent runs the full method — roadmap → positioning & copy → real
|
|
66
|
+
examples → **writes the actual code** → opens it in a browser, screenshots,
|
|
67
|
+
scores it against the critique rubric, and iterates until it passes.
|
|
68
|
+
|
|
69
|
+
| Prompt | What it does |
|
|
70
|
+
|---|---|
|
|
71
|
+
| **`build_landing_page`** | Designs & builds a conversion‑focused landing page, copy‑first, with a visual critique loop. |
|
|
72
|
+
| **`build_website`** | Builds a multi‑page marketing site — positioning, IA, SEO/GEO, shared design system. |
|
|
73
|
+
| **`build_mobile_app_ui`** | Builds iOS or Android screens on the correct platform baseline (HIG/Liquid Glass or Material 3). |
|
|
74
|
+
| **`critique_screenshot`** | Grounded, reproducible critique of an attached UI screenshot against the fixed 0–40 rubric — cites specific elements, no padding. |
|
|
75
|
+
| **`review_paywall`** | Scores a paywall / subscription onboarding against real RevenueCat 2026 conversion benchmarks. |
|
|
76
|
+
| **`design_review`** | Audits an existing site/app against the checklists and the 0–40 critique rubric, ranked by severity. |
|
|
77
|
+
| **`redesign`** | Improves an existing UI (bolder / quieter / higher‑converting) using the craft standards, with before→after scoring. |
|
|
78
|
+
|
|
79
|
+
> Just say, e.g., *"/build_landing_page for a SaaS invoicing tool for freelancers"* — the workflow asks for anything missing, then builds it.
|
|
80
|
+
>
|
|
81
|
+
> The visual critique loop uses whatever browser tool is connected (Claude in
|
|
82
|
+
> Chrome, Playwright, or chrome‑devtools MCP) to see and refine its own output.
|
|
83
|
+
> Without one, it reviews the code directly.
|
|
84
|
+
|
|
61
85
|
## Tools
|
|
62
86
|
|
|
63
87
|
| Tool | What it does |
|
|
@@ -70,6 +94,8 @@ instead of guessing.
|
|
|
70
94
|
| **`get_design_examples`** | **Real screenshots** of a pattern from top apps/sites, returned as images with notes on what each does well. |
|
|
71
95
|
| **`design_review_checklist`** | An assembled audit checklist per project type and focus (UI, UX, accessibility, SEO, GEO, conversion, copywriting). |
|
|
72
96
|
| **`seo_geo_guide`** | SEO and GEO guides, optionally narrowed to a topic. |
|
|
97
|
+
| **`generate_design_tokens`** | **Real artifacts, not advice** — turns a color/spacing/type spec into CSS variables, Tailwind v4, SwiftUI, Jetpack Compose, and W3C DTCG JSON. |
|
|
98
|
+
| **`audit_accessibility`** | Deterministic WCAG 2.2 checks — exact contrast ratios for color pairs + tap‑target sizes per platform, with fixes. |
|
|
73
99
|
| **`list_design_knowledge`** | Browse the full knowledge index by category / platform. |
|
|
74
100
|
| **`knowledge_freshness`** | Reports each doc's age vs a per‑category staleness threshold, so the base can be kept current. |
|
|
75
101
|
|
package/dist/a11y.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Deterministic, design-time accessibility checks: WCAG 2.2 contrast + tap targets.
|
|
2
|
+
// Mirrors the axe-style rules designers wish ran at design time, not just in CI.
|
|
3
|
+
import { normalizeHex } from "./tokens.js";
|
|
4
|
+
function channelLuminance(c) {
|
|
5
|
+
const s = c / 255;
|
|
6
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
7
|
+
}
|
|
8
|
+
function relativeLuminance(hex) {
|
|
9
|
+
const h = normalizeHex(hex).slice(1);
|
|
10
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
11
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
12
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
13
|
+
return 0.2126 * channelLuminance(r) + 0.7152 * channelLuminance(g) + 0.0722 * channelLuminance(b);
|
|
14
|
+
}
|
|
15
|
+
/** WCAG contrast ratio between two opaque colors (1..21). */
|
|
16
|
+
export function contrastRatio(fg, bg) {
|
|
17
|
+
const l1 = relativeLuminance(fg);
|
|
18
|
+
const l2 = relativeLuminance(bg);
|
|
19
|
+
const [hi, lo] = l1 > l2 ? [l1, l2] : [l2, l1];
|
|
20
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
21
|
+
}
|
|
22
|
+
export function checkContrast(pairs) {
|
|
23
|
+
return pairs.map((p, i) => {
|
|
24
|
+
const label = p.label ?? `pair ${i + 1}`;
|
|
25
|
+
const fg = normalizeHex(p.foreground);
|
|
26
|
+
const bg = normalizeHex(p.background);
|
|
27
|
+
if (!fg || !bg) {
|
|
28
|
+
return { label, detail: `${p.foreground} on ${p.background}`, required: "valid hex", actual: "invalid hex", pass: false, fix: "Use #RGB / #RRGGBB / #RRGGBBAA." };
|
|
29
|
+
}
|
|
30
|
+
const ratio = contrastRatio(fg, bg);
|
|
31
|
+
const required = p.ui_component ? 3 : p.large_text ? 3 : 4.5;
|
|
32
|
+
const kind = p.ui_component ? "non-text UI (WCAG 1.4.11)" : p.large_text ? "large text (WCAG 1.4.3 AA)" : "normal text (WCAG 1.4.3 AA)";
|
|
33
|
+
const pass = ratio >= required;
|
|
34
|
+
return {
|
|
35
|
+
label,
|
|
36
|
+
detail: `${fg} on ${bg} — ${kind}`,
|
|
37
|
+
required: `≥ ${required}:1`,
|
|
38
|
+
actual: `${ratio.toFixed(2)}:1`,
|
|
39
|
+
pass,
|
|
40
|
+
fix: pass ? undefined : `Increase contrast to ≥${required}:1 (darken/lighten one color). AAA target is ${p.large_text || p.ui_component ? "4.5" : "7"}:1.`,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function checkTargets(targets) {
|
|
45
|
+
const MIN = { ios: 44, android: 48, web: 24 }; // pt / dp / css px (WCAG 2.2 min for web)
|
|
46
|
+
const REC = 44; // recommended comfortable minimum across platforms
|
|
47
|
+
return targets.map((t, i) => {
|
|
48
|
+
const platform = t.platform ?? "web";
|
|
49
|
+
const min = MIN[platform];
|
|
50
|
+
const smaller = Math.min(t.width, t.height);
|
|
51
|
+
const pass = smaller >= min;
|
|
52
|
+
const unit = platform === "ios" ? "pt" : platform === "android" ? "dp" : "px";
|
|
53
|
+
return {
|
|
54
|
+
label: t.label ?? `target ${i + 1}`,
|
|
55
|
+
detail: `${t.width}×${t.height}${unit} (${platform})`,
|
|
56
|
+
required: `≥ ${min}×${min}${unit}${platform === "web" ? ` (rec. ${REC})` : ""}`,
|
|
57
|
+
actual: `${smaller}${unit} smallest side`,
|
|
58
|
+
pass,
|
|
59
|
+
fix: pass ? undefined : `Enlarge to ≥${min}${unit}, or expand the hit area (padding) beyond the visible bounds.`,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export function contrastReport(pairs, targets) {
|
|
64
|
+
const out = ["# Accessibility audit (WCAG 2.2)"];
|
|
65
|
+
const cr = checkContrast(pairs);
|
|
66
|
+
const tr = checkTargets(targets);
|
|
67
|
+
const all = [...cr, ...tr];
|
|
68
|
+
const fails = all.filter((r) => !r.pass).length;
|
|
69
|
+
out.push(`\n**${all.length} checks · ${all.length - fails} pass · ${fails} fail**\n`);
|
|
70
|
+
if (cr.length) {
|
|
71
|
+
out.push("## Color contrast");
|
|
72
|
+
out.push("| element | measured | required | result |");
|
|
73
|
+
out.push("|---|---|---|---|");
|
|
74
|
+
for (const r of cr)
|
|
75
|
+
out.push(`| ${r.label} — ${r.detail} | ${r.actual} | ${r.required} | ${r.pass ? "✅ pass" : "❌ FAIL"} |`);
|
|
76
|
+
const failFixes = cr.filter((r) => !r.pass);
|
|
77
|
+
if (failFixes.length) {
|
|
78
|
+
out.push("\n**Fixes:**");
|
|
79
|
+
for (const r of failFixes)
|
|
80
|
+
out.push(`- **${r.label}:** ${r.fix}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (tr.length) {
|
|
84
|
+
out.push("\n## Tap / target sizes");
|
|
85
|
+
out.push("| target | size | required | result |");
|
|
86
|
+
out.push("|---|---|---|---|");
|
|
87
|
+
for (const r of tr)
|
|
88
|
+
out.push(`| ${r.label} | ${r.detail} | ${r.required} | ${r.pass ? "✅ pass" : "❌ FAIL"} |`);
|
|
89
|
+
const failFixes = tr.filter((r) => !r.pass);
|
|
90
|
+
if (failFixes.length) {
|
|
91
|
+
out.push("\n**Fixes:**");
|
|
92
|
+
for (const r of failFixes)
|
|
93
|
+
out.push(`- **${r.label}:** ${r.fix}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
out.push("\n---\n_Deterministic checks only (contrast + target size). These are ~the machine-verifiable slice of accessibility; axe-core-style automation catches ~57% of issues by volume. Still verify manually: keyboard/focus order, screen-reader labels & announcements, Dynamic Type at 200%, motion, and cognitive load. See get_design_doc(\"accessibility\")._");
|
|
97
|
+
return out.join("\n");
|
|
98
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ import { dirname, join } from "node:path";
|
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
import { loadKnowledge, searchKnowledge, sections } from "./knowledge.js";
|
|
9
9
|
import { loadExamples, searchExamples, imageMime } from "./examples.js";
|
|
10
|
+
import { registerPrompts } from "./prompts.js";
|
|
11
|
+
import { generateTokens, validateColors, DEFAULT_SPACING, DEFAULT_RADII, DEFAULT_FONT_SIZES, DEFAULT_FONT_FAMILIES, } from "./tokens.js";
|
|
12
|
+
import { contrastReport } from "./a11y.js";
|
|
10
13
|
// knowledge/ sits next to dist/ (repo root) both in dev (tsx) and after build
|
|
11
14
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
15
|
const knowledgeDir = [join(here, "..", "knowledge"), join(here, "..", "..", "knowledge")].find(existsSync);
|
|
@@ -19,7 +22,7 @@ const examplesDir = join(knowledgeDir, "examples");
|
|
|
19
22
|
const examples = loadExamples(examplesDir);
|
|
20
23
|
const server = new McpServer({
|
|
21
24
|
name: "saglitzdesign",
|
|
22
|
-
version: "0.
|
|
25
|
+
version: "0.5.0",
|
|
23
26
|
});
|
|
24
27
|
function docHeader(d) {
|
|
25
28
|
return `# ${d.title}\n_id: ${d.id} · category: ${d.category} · platform: ${d.platform} · tags: ${d.tags.join(", ")}_\n`;
|
|
@@ -368,6 +371,61 @@ server.tool("knowledge_freshness", "Report how fresh each knowledge document is
|
|
|
368
371
|
];
|
|
369
372
|
return text(lines.join("\n"));
|
|
370
373
|
});
|
|
374
|
+
// ── Tool 11: generate design tokens ──────────────────────────────────────────
|
|
375
|
+
server.tool("generate_design_tokens", "Turn a design-token spec (semantic colors + optional spacing/radius/type scales) into REAL, ready-to-use artifact files: CSS custom properties, Tailwind v4 @theme, SwiftUI, Jetpack Compose, and W3C DTCG JSON. Deterministic — outputs code, not advice. Use it to give a project one source of truth across web, iOS and Android. Pair with audit_accessibility to verify the palette's contrast.", {
|
|
376
|
+
name: z.string().optional().describe("Token set / brand name (default 'Brand')"),
|
|
377
|
+
colors: z.record(z.string()).describe("Semantic color roles → hex. e.g. {\"primary\":\"#4F46E5\",\"onPrimary\":\"#FFFFFF\",\"surface\":\"#0A0A0B\",\"textPrimary\":\"#F5F5F5\",\"danger\":\"#EF4444\"}"),
|
|
378
|
+
format: z.enum(["css", "tailwind", "swiftui", "compose", "dtcg", "all"]).optional().describe("Output format (default 'all')"),
|
|
379
|
+
spacing: z.array(z.number()).optional().describe("px spacing scale (default 8pt scale 2..96)"),
|
|
380
|
+
radii: z.record(z.number()).optional().describe("radius name→px (default sm/md/lg/xl/full; use 9999 for pill)"),
|
|
381
|
+
fontSizes: z.record(z.number()).optional().describe("type scale name→px (default xs..4xl)"),
|
|
382
|
+
fontFamilies: z.record(z.string()).optional().describe("font role→stack (default sans/mono)"),
|
|
383
|
+
}, async ({ name, colors, format, spacing, radii, fontSizes, fontFamilies }) => {
|
|
384
|
+
const bad = validateColors(colors);
|
|
385
|
+
if (bad.length)
|
|
386
|
+
return text(`Invalid hex value(s): ${bad.join(", ")}. Use #RGB, #RRGGBB, or #RRGGBBAA.`);
|
|
387
|
+
if (Object.keys(colors).length === 0)
|
|
388
|
+
return text("Provide at least one color role in `colors`.");
|
|
389
|
+
const spec = {
|
|
390
|
+
name: name || "Brand",
|
|
391
|
+
colors,
|
|
392
|
+
spacing: spacing && spacing.length ? spacing : DEFAULT_SPACING,
|
|
393
|
+
radii: radii && Object.keys(radii).length ? radii : DEFAULT_RADII,
|
|
394
|
+
fontSizes: fontSizes && Object.keys(fontSizes).length ? fontSizes : DEFAULT_FONT_SIZES,
|
|
395
|
+
fontFamilies: fontFamilies && Object.keys(fontFamilies).length ? fontFamilies : DEFAULT_FONT_FAMILIES,
|
|
396
|
+
};
|
|
397
|
+
return text(generateTokens(spec, format ?? "all"));
|
|
398
|
+
});
|
|
399
|
+
// ── Tool 12: accessibility audit ─────────────────────────────────────────────
|
|
400
|
+
server.tool("audit_accessibility", "Deterministic design-time accessibility checks: WCAG 2.2 color-contrast ratios for text/UI color pairs, and minimum tap/target sizes per platform (iOS 44pt, Android 48dp, web 24px min / 44 recommended). Returns exact ratios, pass/fail, and fixes — the machine-verifiable slice of a11y you can run before code. For keyboard/screen-reader/Dynamic Type checks, see get_design_doc('accessibility').", {
|
|
401
|
+
contrast_pairs: z.array(z.object({
|
|
402
|
+
foreground: z.string().describe("text/element hex"),
|
|
403
|
+
background: z.string().describe("background hex"),
|
|
404
|
+
label: z.string().optional().describe("what this is, e.g. 'body text on surface'"),
|
|
405
|
+
large_text: z.boolean().optional().describe("true if ≥24px or ≥18.66px bold (threshold drops to 3:1)"),
|
|
406
|
+
ui_component: z.boolean().optional().describe("true for non-text UI: borders, icons, focus rings (3:1)"),
|
|
407
|
+
})).optional().describe("Color pairs to check for contrast"),
|
|
408
|
+
tap_targets: z.array(z.object({
|
|
409
|
+
label: z.string().optional(),
|
|
410
|
+
width: z.number().describe("width in pt/dp/px"),
|
|
411
|
+
height: z.number().describe("height in pt/dp/px"),
|
|
412
|
+
platform: z.enum(["ios", "android", "web"]).optional().describe("default web"),
|
|
413
|
+
})).optional().describe("Interactive targets to check for minimum size"),
|
|
414
|
+
}, async ({ contrast_pairs, tap_targets }) => {
|
|
415
|
+
const pairs = (contrast_pairs ?? []);
|
|
416
|
+
const targets = (tap_targets ?? []);
|
|
417
|
+
if (pairs.length === 0 && targets.length === 0) {
|
|
418
|
+
return text("Provide `contrast_pairs` and/or `tap_targets` to audit. Example: {\"contrast_pairs\":[{\"foreground\":\"#6B7280\",\"background\":\"#FFFFFF\",\"label\":\"muted text\"}]}");
|
|
419
|
+
}
|
|
420
|
+
return text(contrastReport(pairs, targets));
|
|
421
|
+
});
|
|
422
|
+
// ── prompts (user-invocable build/review/redesign workflows) ─────────────────
|
|
423
|
+
registerPrompts(server, {
|
|
424
|
+
brief: z
|
|
425
|
+
.string()
|
|
426
|
+
.optional()
|
|
427
|
+
.describe("What to build/review, in your words (audience, offer, stack, URL…). Optional — the workflow will ask for anything missing."),
|
|
428
|
+
});
|
|
371
429
|
// ── start ────────────────────────────────────────────────────────────────────
|
|
372
430
|
const transport = new StdioServerTransport();
|
|
373
431
|
await server.connect(transport);
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// SaglitzDesign MCP prompts — user-invocable workflows that orchestrate the
|
|
2
|
+
// server's knowledge tools into an end-to-end "build / review / redesign"
|
|
3
|
+
// experience. In Claude Code these appear in the "/" (slash) prompt menu.
|
|
4
|
+
const TOOLS_NOTE = `You have the SaglitzDesign knowledge tools available. Use them — do not design from memory:
|
|
5
|
+
- get_design_roadmap(project_type) — the phased plan; call FIRST.
|
|
6
|
+
- search_design_knowledge(query) / get_design_doc(id) — rules & specs.
|
|
7
|
+
- get_component_guidance(component, platform) — per-component specs + patterns.
|
|
8
|
+
- get_design_examples(query, platform) — real annotated screenshots to reference.
|
|
9
|
+
- get_design_language(language) — Material 3 / Liquid Glass / iOS / Android / macOS / Fluent / web-trends / tokens.
|
|
10
|
+
- seo_geo_guide(scope, topic) — SEO & GEO for web.
|
|
11
|
+
- design_review_checklist(project_type, focus) — the audit gate.`;
|
|
12
|
+
const CRITIQUE_LOOP = `## Visual critique loop (do this, don't skip it)
|
|
13
|
+
After you build something runnable:
|
|
14
|
+
1. Run it and open it in a browser. If a browser-automation tool is available
|
|
15
|
+
(Claude in Chrome, Playwright, or chrome-devtools MCP), navigate to the page
|
|
16
|
+
and take a screenshot at both mobile (390px) and desktop (1440px) widths.
|
|
17
|
+
If no browser tool is available, say so and review the code directly instead.
|
|
18
|
+
2. Look at the screenshot as a critical senior designer. Score it against the
|
|
19
|
+
rubric in get_design_doc("design-critique-scoring") (0–40) and run the
|
|
20
|
+
matching design_review_checklist.
|
|
21
|
+
3. Fix the highest-severity issues first (hierarchy, one primary CTA, spacing
|
|
22
|
+
from the scale, contrast ≥ the required ratios, real content stress).
|
|
23
|
+
4. Re-screenshot and repeat until it passes the checklist and the squint test
|
|
24
|
+
(the primary action is the first thing you see). Report the before/after.`;
|
|
25
|
+
const QUALITY_BAR = `## Non-negotiables (from the knowledge base)
|
|
26
|
+
- Content & copy BEFORE chrome: write the real headline/CTA/empty/error copy first (never lorem ipsum).
|
|
27
|
+
- Exactly one primary CTA per view; verb-first labels ("Start free trial", never "Submit").
|
|
28
|
+
- Everything on the 8pt spacing scale; tokens for color/type/spacing, not ad-hoc values.
|
|
29
|
+
- Text contrast ≥4.5:1, non-text ≥3:1; visible focus states; keyboard reachable.
|
|
30
|
+
- Design every state: default, empty, loading, error, long-content, zero-results.
|
|
31
|
+
- Respect prefers-reduced-motion; motion durations/easings per the motion doc.
|
|
32
|
+
- For web: semantic HTML, LCP ≤2.5s discipline (image/font rules), no layout shift.`;
|
|
33
|
+
const BUILD_PROMPTS = [
|
|
34
|
+
{
|
|
35
|
+
name: "build_landing_page",
|
|
36
|
+
title: "Build a landing page (SaglitzDesign)",
|
|
37
|
+
description: "Design & build a conversion-focused landing page end-to-end using SaglitzDesign expertise, with a visual critique loop.",
|
|
38
|
+
build: (brief) => `Build a high-converting **landing page**${brief ? ` for: ${brief}` : ""}, using the SaglitzDesign method.
|
|
39
|
+
|
|
40
|
+
${TOOLS_NOTE}
|
|
41
|
+
|
|
42
|
+
## Sequence
|
|
43
|
+
1. Call get_design_roadmap("landing-page") and follow its phases.
|
|
44
|
+
2. **Positioning & message first.** If key facts are missing (who it's for, the offer, the one conversion goal, proof points), ask me up to 4 concise questions before building. Then draft the hero headline, subhead, primary CTA + risk-reducers, and the section narrative (hero → proof → benefits → objections/FAQ → final CTA). Pull rules from get_design_doc("storybrand-copywriting"), get_design_doc("conversion-ux"), get_design_doc("influence-persuasion").
|
|
45
|
+
3. **Reference real examples:** get_design_examples("hero", "web"), get_design_examples("pricing", "web"), get_design_examples("social proof", "web").
|
|
46
|
+
4. **Build it.** Write the actual code (default to a single responsive HTML file with inline CSS unless I specify a stack like Next.js/React/Tailwind). Apply the foundations: typography, color-systems, spacing-layout, buttons, visual-craft-standards.
|
|
47
|
+
5. **SEO/GEO:** apply seo_geo_guide("both") essentials — semantic HTML, meta/title, one H1, JSON-LD, fast images/fonts.
|
|
48
|
+
|
|
49
|
+
${QUALITY_BAR}
|
|
50
|
+
|
|
51
|
+
${CRITIQUE_LOOP}
|
|
52
|
+
|
|
53
|
+
Finish with: the files created, how to preview it, the critique score, and what you'd test/improve next.`,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "build_website",
|
|
57
|
+
title: "Build a website (SaglitzDesign)",
|
|
58
|
+
description: "Design & build a multi-page marketing website end-to-end using SaglitzDesign expertise, with a visual critique loop.",
|
|
59
|
+
build: (brief) => `Build a marketing **website**${brief ? ` for: ${brief}` : ""}, using the SaglitzDesign method.
|
|
60
|
+
|
|
61
|
+
${TOOLS_NOTE}
|
|
62
|
+
|
|
63
|
+
## Sequence
|
|
64
|
+
1. Call get_design_roadmap("website") and follow its phases (positioning → copy → IA/SEO → design → build → CRO).
|
|
65
|
+
2. **Positioning + IA first.** Ask me up to 4 questions if the audience, offer, conversion goal, or page set is unclear. Then propose a sitemap (home, product/features, pricing, about, contact, etc.), each page mapped to one search intent.
|
|
66
|
+
3. **Copy before layout** for each page (get_design_doc("storybrand-copywriting"), get_design_doc("marketing-website-roadmap")).
|
|
67
|
+
4. **SEO/GEO foundations up front:** seo_geo_guide("both") — rendering, meta, schema plan, llms.txt, internal linking.
|
|
68
|
+
5. **Reference real examples** via get_design_examples for each section type.
|
|
69
|
+
6. **Build it** as a coherent multi-page site (default: static HTML/CSS with shared styles, or a Next.js app if I ask). Shared design tokens; consistent nav/footer; every page in all states.
|
|
70
|
+
|
|
71
|
+
${QUALITY_BAR}
|
|
72
|
+
|
|
73
|
+
${CRITIQUE_LOOP}
|
|
74
|
+
|
|
75
|
+
Finish with: sitemap built, files, preview instructions, per-page critique scores, SEO/GEO checklist status, and next steps.`,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "build_mobile_app_ui",
|
|
79
|
+
title: "Build a mobile app UI (SaglitzDesign)",
|
|
80
|
+
description: "Design & build iOS or Android app screens end-to-end using SaglitzDesign expertise, with a visual critique loop.",
|
|
81
|
+
build: (brief) => `Design and build **mobile app UI**${brief ? ` for: ${brief}` : ""}, using the SaglitzDesign method.
|
|
82
|
+
|
|
83
|
+
${TOOLS_NOTE}
|
|
84
|
+
|
|
85
|
+
## Sequence
|
|
86
|
+
1. Ask which platform (iOS or Android) and stack (SwiftUI / Jetpack Compose / React Native / Flutter) if not stated, plus the 2–3 core screens to build. Ask up to 4 questions max.
|
|
87
|
+
2. Call get_design_roadmap("ios-app") or ("android-app") and follow it.
|
|
88
|
+
3. Load the platform baseline: get_design_language("ios-app-design") + ("apple-hig-liquid-glass"), or ("android-app-design") + ("material-3"). Respect native navigation, controls, safe areas, Dynamic Type / sp.
|
|
89
|
+
4. **Reference real examples** with get_design_examples (platform "mobile"): onboarding, paywall, navigation, empty-state, etc.
|
|
90
|
+
5. **Build the screens** with get_component_guidance for each element; design every state; thumb-zone the primary actions.
|
|
91
|
+
|
|
92
|
+
${QUALITY_BAR}
|
|
93
|
+
|
|
94
|
+
${CRITIQUE_LOOP}
|
|
95
|
+
|
|
96
|
+
Finish with: screens built, how to run/preview, critique scores, platform-fit notes, and next steps.`,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
const ACTION_PROMPTS = [
|
|
100
|
+
{
|
|
101
|
+
name: "critique_screenshot",
|
|
102
|
+
title: "Critique a screenshot (SaglitzDesign)",
|
|
103
|
+
description: "Grounded, reproducible visual critique of an attached UI screenshot against the fixed 0–40 rubric — cites specific elements, no padding.",
|
|
104
|
+
build: (brief) => `Critique the **attached UI screenshot**${brief ? ` (context: ${brief})` : ""} as a rigorous senior designer, using the SaglitzDesign method.
|
|
105
|
+
|
|
106
|
+
${TOOLS_NOTE}
|
|
107
|
+
|
|
108
|
+
## Method — avoid the failure modes of typical AI critique
|
|
109
|
+
Research shows most AI critiques (a) hallucinate issues inconsistently, (b) pad the list to look thorough, and (c) critique a text description instead of the actual pixels. Do NOT do these. Instead:
|
|
110
|
+
|
|
111
|
+
1. **Look at the image first.** Describe what you actually see (layout, hierarchy, the primary action, states shown) before judging. If you're unsure what an element is, say so — don't invent.
|
|
112
|
+
2. **Apply the fixed rubric.** Call get_design_doc("design-critique-scoring") and score each of the 10 heuristics 0–4 for a total /40. Use the SAME rubric every time so scores are reproducible.
|
|
113
|
+
3. **Cite specific elements.** Every finding must point to a concrete element ("the secondary 'Learn more' button competes with the primary CTA — two filled buttons"), not generic advice.
|
|
114
|
+
4. **No padding.** Report only real issues. If the screen is genuinely good, a short list is the correct answer — do not manufacture findings to seem thorough.
|
|
115
|
+
5. **Rank by severity P0→P3** and give one concrete fix per finding, citing the SaglitzDesign rule/doc it comes from.
|
|
116
|
+
6. If it's a known screen type, also run the matching design_review_checklist and get_design_examples to compare against how top apps handle it.
|
|
117
|
+
|
|
118
|
+
Output: the /40 score with per-heuristic line, then findings ranked by severity (element → problem → why (cite rule) → fix), then the 3 highest-impact changes. Keep it tight and specific.`,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "review_paywall",
|
|
122
|
+
title: "Review a paywall / onboarding (SaglitzDesign)",
|
|
123
|
+
description: "Score a paywall or subscription onboarding against real RevenueCat 2026 conversion benchmarks and paywall-anatomy rules.",
|
|
124
|
+
build: (brief) => `Review this **paywall / subscription onboarding**${brief ? `: ${brief}` : ""} using the SaglitzDesign method and real 2026 benchmarks.
|
|
125
|
+
|
|
126
|
+
${TOOLS_NOTE}
|
|
127
|
+
|
|
128
|
+
## Method
|
|
129
|
+
1. Load the data: get_design_doc("paywall-benchmarks") (RevenueCat 2026: hard paywall ~10.7% vs freemium ~2.1%; 17–32 day trials convert 42.5% vs 25.5% for <4 days; 55% of 3-day-trial cancels happen Day 0; Android involuntary churn ~2.2× iOS) and get_design_doc("onboarding-paywall") for anatomy.
|
|
130
|
+
2. If given a screenshot, look at it and describe the actual paywall (model, plans, trial, price placement, CTA, trust copy). If given a description, work from that; ask up to 3 questions only if a benchmark-critical fact is missing (model, trial length, platform).
|
|
131
|
+
3. Score it against the review rubric in paywall-benchmarks.md — each item pass/fail with the benchmark it maps to.
|
|
132
|
+
4. Estimate where it likely leaves conversion on the table (e.g. "≤4-day trial → ~40% relative conversion lost vs a 14–30 day trial").
|
|
133
|
+
5. Give prioritized, concrete fixes (model choice, trial length, price placement, trial reminder, Android dunning, trust microcopy), each tied to a benchmark number.
|
|
134
|
+
|
|
135
|
+
Output: a pass/fail scorecard, the top 3 conversion risks with their benchmark impact, and the exact changes to make. Be specific and numeric — no generic "add social proof" advice.`,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "design_review",
|
|
139
|
+
title: "Design review (SaglitzDesign)",
|
|
140
|
+
description: "Audit an existing website / app / landing page against SaglitzDesign checklists and the 0–40 critique rubric.",
|
|
141
|
+
build: (brief) => `Do an expert **design review**${brief ? ` of: ${brief}` : ""}, using the SaglitzDesign method.
|
|
142
|
+
|
|
143
|
+
${TOOLS_NOTE}
|
|
144
|
+
|
|
145
|
+
## Sequence
|
|
146
|
+
1. Identify the project type (website / landing-page / mobile-app / macos-app / dashboard). If it's a URL or running app and a browser tool is available, open it and screenshot mobile + desktop; otherwise review the code/design provided.
|
|
147
|
+
2. Run design_review_checklist for that type (and a focused pass: accessibility, conversion, seo, etc.).
|
|
148
|
+
3. Score against get_design_doc("design-critique-scoring") (0–40) with per-heuristic notes.
|
|
149
|
+
4. Report findings ranked by severity (P0→P3): what's wrong, why (cite the rule/doc), and the concrete fix. Separate "must fix" from "polish".
|
|
150
|
+
5. If asked, apply the top fixes and re-review.
|
|
151
|
+
|
|
152
|
+
Be specific and prescriptive — every finding cites a SaglitzDesign rule and gives an actionable fix, not vague advice.`,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "redesign",
|
|
156
|
+
title: "Redesign / improve a UI (SaglitzDesign)",
|
|
157
|
+
description: "Improve an existing UI (bolder, quieter, cleaner, higher-converting) using SaglitzDesign craft standards, with a visual critique loop.",
|
|
158
|
+
build: (brief) => `**Redesign / improve** the UI${brief ? `: ${brief}` : ""}, using the SaglitzDesign method.
|
|
159
|
+
|
|
160
|
+
${TOOLS_NOTE}
|
|
161
|
+
|
|
162
|
+
## Sequence
|
|
163
|
+
1. First review the current state: run design_review_checklist and score it (get_design_doc("design-critique-scoring")). State the top problems and the design direction you'll take (and why).
|
|
164
|
+
2. Pull the relevant craft docs: visual-craft-standards, typography-craft, and the foundations (typography, color-systems, spacing-layout). For conversion goals, add conversion-ux + storybrand-copywriting.
|
|
165
|
+
3. Reference real examples with get_design_examples for the pattern in question.
|
|
166
|
+
4. **Apply the changes in code**, preserving working behavior. Improve hierarchy by de-emphasizing secondary content rather than only enlarging primary; fix spacing to the scale; one primary CTA; contrast; states.
|
|
167
|
+
|
|
168
|
+
${QUALITY_BAR}
|
|
169
|
+
|
|
170
|
+
${CRITIQUE_LOOP}
|
|
171
|
+
|
|
172
|
+
Finish with a concrete before→after: the score change, what you changed and why, and what to test next.`,
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
const ALL_PROMPTS = [...BUILD_PROMPTS, ...ACTION_PROMPTS];
|
|
176
|
+
// Registered against an McpServer-like object exposing registerPrompt.
|
|
177
|
+
export function registerPrompts(server, briefArg) {
|
|
178
|
+
for (const p of ALL_PROMPTS) {
|
|
179
|
+
server.registerPrompt(p.name, { title: p.title, description: p.description, argsSchema: briefArg }, ({ brief }) => ({
|
|
180
|
+
messages: [{ role: "user", content: { type: "text", text: p.build((brief ?? "").trim()) } }],
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
}
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Deterministic design-token artifact generator.
|
|
2
|
+
// One source spec → CSS variables, Tailwind v4, SwiftUI, Jetpack Compose, DTCG JSON.
|
|
3
|
+
// Fills the gap where Style Dictionary v4 doesn't yet emit the stable DTCG 2025.10 format.
|
|
4
|
+
export const DEFAULT_SPACING = [2, 4, 8, 12, 16, 24, 32, 48, 64, 96];
|
|
5
|
+
export const DEFAULT_RADII = { sm: 6, md: 10, lg: 16, xl: 24, full: 9999 };
|
|
6
|
+
export const DEFAULT_FONT_SIZES = { xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24, "3xl": 30, "4xl": 40 };
|
|
7
|
+
export const DEFAULT_FONT_FAMILIES = {
|
|
8
|
+
sans: "Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif",
|
|
9
|
+
mono: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
10
|
+
};
|
|
11
|
+
const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
12
|
+
export function normalizeHex(hex) {
|
|
13
|
+
if (!HEX_RE.test(hex))
|
|
14
|
+
return null;
|
|
15
|
+
let h = hex.slice(1);
|
|
16
|
+
if (h.length === 3)
|
|
17
|
+
h = h.split("").map((c) => c + c).join("");
|
|
18
|
+
if (h.length === 4)
|
|
19
|
+
h = h.split("").map((c) => c + c).join("");
|
|
20
|
+
return "#" + h.toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
function hexParts(hex) {
|
|
23
|
+
const h = normalizeHex(hex).slice(1);
|
|
24
|
+
return {
|
|
25
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
26
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
27
|
+
b: parseInt(h.slice(4, 6), 16),
|
|
28
|
+
a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function kebab(s) {
|
|
32
|
+
return s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
function pascal(s) {
|
|
35
|
+
return s.replace(/(^|[-_\s]+)([a-zA-Z0-9])/g, (_, __, c) => c.toUpperCase());
|
|
36
|
+
}
|
|
37
|
+
function camel(s) {
|
|
38
|
+
const p = pascal(s);
|
|
39
|
+
return p.charAt(0).toLowerCase() + p.slice(1);
|
|
40
|
+
}
|
|
41
|
+
// ── CSS custom properties ────────────────────────────────────────────────────
|
|
42
|
+
function toCss(s) {
|
|
43
|
+
const lines = [":root {"];
|
|
44
|
+
lines.push(" /* color */");
|
|
45
|
+
for (const [k, v] of Object.entries(s.colors))
|
|
46
|
+
lines.push(` --color-${kebab(k)}: ${normalizeHex(v)};`);
|
|
47
|
+
lines.push(" /* spacing */");
|
|
48
|
+
s.spacing.forEach((v) => lines.push(` --space-${v}: ${v}px;`));
|
|
49
|
+
lines.push(" /* radius */");
|
|
50
|
+
for (const [k, v] of Object.entries(s.radii))
|
|
51
|
+
lines.push(` --radius-${kebab(k)}: ${v >= 9999 ? "9999px" : v + "px"};`);
|
|
52
|
+
lines.push(" /* font size */");
|
|
53
|
+
for (const [k, v] of Object.entries(s.fontSizes))
|
|
54
|
+
lines.push(` --text-${kebab(k)}: ${(v / 16).toFixed(4).replace(/\.?0+$/, "")}rem;`);
|
|
55
|
+
lines.push(" /* font family */");
|
|
56
|
+
for (const [k, v] of Object.entries(s.fontFamilies))
|
|
57
|
+
lines.push(` --font-${kebab(k)}: ${v};`);
|
|
58
|
+
lines.push("}");
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
// ── Tailwind v4 (@theme) ─────────────────────────────────────────────────────
|
|
62
|
+
function toTailwind(s) {
|
|
63
|
+
const lines = ["@theme {"];
|
|
64
|
+
for (const [k, v] of Object.entries(s.colors))
|
|
65
|
+
lines.push(` --color-${kebab(k)}: ${normalizeHex(v)};`);
|
|
66
|
+
s.spacing.forEach((v) => lines.push(` --spacing-${v}: ${v}px;`));
|
|
67
|
+
for (const [k, v] of Object.entries(s.radii))
|
|
68
|
+
lines.push(` --radius-${kebab(k)}: ${v >= 9999 ? "9999px" : v + "px"};`);
|
|
69
|
+
for (const [k, v] of Object.entries(s.fontSizes))
|
|
70
|
+
lines.push(` --text-${kebab(k)}: ${(v / 16).toFixed(4).replace(/\.?0+$/, "")}rem;`);
|
|
71
|
+
for (const [k, v] of Object.entries(s.fontFamilies))
|
|
72
|
+
lines.push(` --font-${kebab(k)}: ${v};`);
|
|
73
|
+
lines.push("}");
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
// ── SwiftUI ──────────────────────────────────────────────────────────────────
|
|
77
|
+
function toSwiftUI(s) {
|
|
78
|
+
const lines = ["import SwiftUI", "", `enum ${pascal(s.name)}Tokens {`];
|
|
79
|
+
lines.push(" // Colors");
|
|
80
|
+
for (const [k, v] of Object.entries(s.colors)) {
|
|
81
|
+
const { r, g, b, a } = hexParts(v);
|
|
82
|
+
lines.push(` static let ${camel(k)} = Color(.sRGB, red: ${(r / 255).toFixed(3)}, green: ${(g / 255).toFixed(3)}, blue: ${(b / 255).toFixed(3)}, opacity: ${a.toFixed(3)})`);
|
|
83
|
+
}
|
|
84
|
+
lines.push(" // Spacing (pt)");
|
|
85
|
+
s.spacing.forEach((v) => lines.push(` static let space${v}: CGFloat = ${v}`));
|
|
86
|
+
lines.push(" // Corner radius (pt)");
|
|
87
|
+
for (const [k, v] of Object.entries(s.radii))
|
|
88
|
+
lines.push(` static let radius${pascal(k)}: CGFloat = ${v >= 9999 ? 9999 : v}`);
|
|
89
|
+
lines.push(" // Font size (pt)");
|
|
90
|
+
for (const [k, v] of Object.entries(s.fontSizes))
|
|
91
|
+
lines.push(` static let text${pascal(k)}: CGFloat = ${v}`);
|
|
92
|
+
lines.push("}");
|
|
93
|
+
return lines.join("\n");
|
|
94
|
+
}
|
|
95
|
+
// ── Jetpack Compose ──────────────────────────────────────────────────────────
|
|
96
|
+
function toCompose(s) {
|
|
97
|
+
const lines = ["import androidx.compose.ui.graphics.Color", "import androidx.compose.ui.unit.dp", "import androidx.compose.ui.unit.sp", "", `object ${pascal(s.name)}Tokens {`];
|
|
98
|
+
lines.push(" // Colors (0xAARRGGBB)");
|
|
99
|
+
for (const [k, v] of Object.entries(s.colors)) {
|
|
100
|
+
const { r, g, b, a } = hexParts(v);
|
|
101
|
+
const argb = ((Math.round(a * 255) << 24) | (r << 16) | (g << 8) | b) >>> 0;
|
|
102
|
+
lines.push(` val ${camel(k)} = Color(0x${argb.toString(16).padStart(8, "0").toUpperCase()})`);
|
|
103
|
+
}
|
|
104
|
+
lines.push(" // Spacing");
|
|
105
|
+
s.spacing.forEach((v) => lines.push(` val space${v} = ${v}.dp`));
|
|
106
|
+
lines.push(" // Corner radius");
|
|
107
|
+
for (const [k, v] of Object.entries(s.radii))
|
|
108
|
+
lines.push(` val radius${pascal(k)} = ${v >= 9999 ? 9999 : v}.dp`);
|
|
109
|
+
lines.push(" // Font size");
|
|
110
|
+
for (const [k, v] of Object.entries(s.fontSizes))
|
|
111
|
+
lines.push(` val text${pascal(k)} = ${v}.sp`);
|
|
112
|
+
lines.push("}");
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
// ── DTCG JSON (W3C Design Tokens Community Group, stable 2025.10) ─────────────
|
|
116
|
+
function toDtcg(s) {
|
|
117
|
+
const obj = {
|
|
118
|
+
$description: `${s.name} design tokens (DTCG format)`,
|
|
119
|
+
color: Object.fromEntries(Object.entries(s.colors).map(([k, v]) => [kebab(k), { $type: "color", $value: normalizeHex(v) }])),
|
|
120
|
+
spacing: Object.fromEntries(s.spacing.map((v) => [String(v), { $type: "dimension", $value: { value: v, unit: "px" } }])),
|
|
121
|
+
radius: Object.fromEntries(Object.entries(s.radii).map(([k, v]) => [kebab(k), { $type: "dimension", $value: { value: v, unit: "px" } }])),
|
|
122
|
+
fontSize: Object.fromEntries(Object.entries(s.fontSizes).map(([k, v]) => [kebab(k), { $type: "dimension", $value: { value: v, unit: "px" } }])),
|
|
123
|
+
fontFamily: Object.fromEntries(Object.entries(s.fontFamilies).map(([k, v]) => [kebab(k), { $type: "fontFamily", $value: v.split(",").map((f) => f.trim()) }])),
|
|
124
|
+
};
|
|
125
|
+
return JSON.stringify(obj, null, 2);
|
|
126
|
+
}
|
|
127
|
+
const GENERATORS = {
|
|
128
|
+
css: { label: "CSS custom properties (tokens.css)", lang: "css", fn: toCss },
|
|
129
|
+
tailwind: { label: "Tailwind v4 theme (app.css)", lang: "css", fn: toTailwind },
|
|
130
|
+
swiftui: { label: "SwiftUI (Tokens.swift)", lang: "swift", fn: toSwiftUI },
|
|
131
|
+
compose: { label: "Jetpack Compose (Tokens.kt)", lang: "kotlin", fn: toCompose },
|
|
132
|
+
dtcg: { label: "DTCG JSON (tokens.json)", lang: "json", fn: toDtcg },
|
|
133
|
+
};
|
|
134
|
+
export function generateTokens(spec, format) {
|
|
135
|
+
const formats = format === "all" ? Object.keys(GENERATORS) : [format];
|
|
136
|
+
const blocks = formats.map((f) => {
|
|
137
|
+
const g = GENERATORS[f];
|
|
138
|
+
return `## ${g.label}\n\`\`\`${g.lang}\n${g.fn(spec)}\n\`\`\``;
|
|
139
|
+
});
|
|
140
|
+
return `# ${spec.name} — design tokens\n\n${blocks.join("\n\n")}`;
|
|
141
|
+
}
|
|
142
|
+
export function validateColors(colors) {
|
|
143
|
+
const bad = [];
|
|
144
|
+
for (const [k, v] of Object.entries(colors))
|
|
145
|
+
if (!normalizeHex(v))
|
|
146
|
+
bad.push(`${k}: "${v}"`);
|
|
147
|
+
return bad;
|
|
148
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: paywall-benchmarks
|
|
3
|
+
title: "Paywall & Onboarding Conversion Benchmarks (2026)"
|
|
4
|
+
category: marketing
|
|
5
|
+
platform: mobile
|
|
6
|
+
tags: [paywall, subscription, onboarding, conversion, revenuecat, trial, retention]
|
|
7
|
+
sources: ["https://www.revenuecat.com/state-of-subscription-apps/", "https://www.revenuecat.com/blog/growth/subscription-app-trends-benchmarks-2026/", "https://www.revenuecat.com/blog/engineering/android-paywall-gap/"]
|
|
8
|
+
updated: 2026-07-08
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Paywall & Onboarding Conversion Benchmarks (2026)
|
|
12
|
+
|
|
13
|
+
Real benchmarks from RevenueCat's *State of Subscription Apps 2026* (115k+ apps, $16B+ tracked revenue). Use these to judge whether a paywall/onboarding is competitive — not vibes.
|
|
14
|
+
|
|
15
|
+
## Headline numbers
|
|
16
|
+
|
|
17
|
+
| Metric | Benchmark | Implication |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| Hard paywall (signup gate) conversion | **~10.7%** install→trial/purchase | ~5× freemium |
|
|
20
|
+
| Freemium (optional paywall) conversion | **~2.1%** | Higher reach, far lower conversion |
|
|
21
|
+
| Day-60 revenue per install (hard vs freemium) | **$3.09 vs $0.38** | ~8× |
|
|
22
|
+
| Trial length 17–32 days → trial-start→paid | **42.5%** | Longer trials convert better |
|
|
23
|
+
| Trial length <4 days → trial-start→paid | **25.5%** | …yet **46.5% of apps use ≤4-day trials** (mistake) |
|
|
24
|
+
| Share of 3-day-trial cancellations on Day 0 | **55%** (84% by Day 1) | The first session decides everything |
|
|
25
|
+
| iOS Day-35 subscriber retention (median) | **~2.6%** of installs | |
|
|
26
|
+
| Android Day-35 (median) | **~0.9%** | Android lags ~3× |
|
|
27
|
+
| Android involuntary (billing-failure) churn | **~2.2× iOS** | Dunning/grace-period handling matters |
|
|
28
|
+
|
|
29
|
+
## Design rules derived
|
|
30
|
+
|
|
31
|
+
1. **Choose the model deliberately.** Hard paywall ≈5× conversion and ≈8× RPI — default to it unless the product's value genuinely requires free exploration first (network/UGC/virality). Freemium is a reach play, not a revenue play.
|
|
32
|
+
2. **Longer trials win.** Move to 14–30 day trials; ≤4-day trials leave ~40% relative conversion on the table. Pair with a **trial reminder** before charge (trust + fewer chargebacks).
|
|
33
|
+
3. **Win Day 0.** 55% of cancels happen the first day → the onboarding must deliver the "aha" and the paywall must state value + price clearly in session one. Instrument Day-0 retention as the leading indicator.
|
|
34
|
+
4. **Show value before the wall when possible**, but don't bury the paywall — hard-paywall apps that clearly present the offer up front convert best.
|
|
35
|
+
5. **Paywall anatomy that converts** (from `onboarding-paywall` pattern): benefit checklist (≤6 concrete benefits), 1–2 plan cards with the recommended one badged, trial timeline ("today → day 12 reminder → day 14 charge"), price + terms disclosed right next to the CTA, restore-purchases link, single primary CTA.
|
|
36
|
+
6. **Android: fix involuntary churn.** Grace periods, account hold, billing-retry, and clear payment-update prompts — 2.2× iOS involuntary churn is mostly a dunning-design problem, not intent.
|
|
37
|
+
7. **Personalized onboarding lifts paywall conversion** only when answers visibly change the experience or the paywall framing; ≤5 steps with a progress indicator.
|
|
38
|
+
|
|
39
|
+
## Review rubric (score a paywall against this)
|
|
40
|
+
|
|
41
|
+
- [ ] Model fits the product (hard vs freemium) — justified, not accidental
|
|
42
|
+
- [ ] Trial length ≥14 days (or a deliberate reason it's shorter)
|
|
43
|
+
- [ ] Pre-charge trial reminder scheduled
|
|
44
|
+
- [ ] Single primary CTA; price + billing terms adjacent to it
|
|
45
|
+
- [ ] ≤6 concrete benefits, recommended plan badged
|
|
46
|
+
- [ ] Restore purchases + terms/privacy links present
|
|
47
|
+
- [ ] Day-0 activation designed (aha before or at the wall)
|
|
48
|
+
- [ ] Android: grace period + billing-retry + payment-update UX
|
|
49
|
+
- [ ] Trust microcopy ("cancel anytime", "you won't be charged until…")
|
|
50
|
+
|
|
51
|
+
## Anti-patterns
|
|
52
|
+
|
|
53
|
+
- ≤4-day trials by default (industry's most common, costly mistake).
|
|
54
|
+
- Freemium chosen "to be nice" when the product could hard-gate — leaves ~5× on the table.
|
|
55
|
+
- Paywall with hidden/unclear price, or price far from the CTA.
|
|
56
|
+
- No trial reminder → surprise charges → chargebacks + 1-star reviews.
|
|
57
|
+
- Ignoring Android billing failures (treating churn as intent when it's involuntary).
|
|
58
|
+
- Multi-step personalization that never changes anything downstream.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saglitzdesign-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "SaglitzDesign — expert MCP server for mobile app & website design: UI, UX, buttons, design languages, SEO and GEO guidance backed by a curated knowledge base and real-world pattern research.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|