@systemverification/styling-kit 2.0.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/src/mcp.js ADDED
@@ -0,0 +1,215 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema,
9
+ } from '@modelcontextprotocol/sdk/types.js';
10
+ import { createShutdownHandler } from './shutdown.js';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ const KIT_ROOT = path.resolve(__dirname, '..');
14
+
15
+ // RGB tuples of the documented palette — any rgba() using other values is a violation
16
+ const PALETTE_TUPLES = new Set([
17
+ '11,27,40', // dark-blue
18
+ '67,83,100', // light-blue
19
+ '242,242,234', // sand
20
+ '247,249,101', // yellow
21
+ '255,255,255', // white
22
+ '239,68,68', // error
23
+ '16,185,129', // success
24
+ '251,146,60', // warning/orange
25
+ '248,113,113', // error-light (#f87171)
26
+ '248,250,170', // hover yellow (#f8faaa)
27
+ '245,249,80', // active yellow (#f5f950)
28
+ '0,0,0', // pure black (shadows)
29
+ ]);
30
+
31
+ function lineNumber(source, index) {
32
+ return source.slice(0, index).split('\n').length;
33
+ }
34
+
35
+ function validateContent(source, filePath) {
36
+ const issues = [];
37
+ const ext = path.extname(filePath).toLowerCase();
38
+
39
+ if (!['.css', '.html', '.htm', '.svelte', '.vue', '.jsx', '.tsx'].includes(ext)) {
40
+ return issues;
41
+ }
42
+
43
+ // Rule 1: Hardcoded hex colors in property values (after a colon)
44
+ const hexRegex = /(?<=:\s*)#[0-9a-fA-F]{6}\b|(?<=:\s*)#[0-9a-fA-F]{3}\b/g;
45
+ let match;
46
+ while ((match = hexRegex.exec(source)) !== null) {
47
+ issues.push({
48
+ type: 'error',
49
+ rule: 'no-hardcoded-hex',
50
+ message: `Hardcoded hex color: ${match[0]} — use a var(--color-*) token instead`,
51
+ line: lineNumber(source, match.index),
52
+ });
53
+ }
54
+
55
+ // Rule 2: Hardcoded px font sizes
56
+ const pxFontRegex = /font-size\s*:\s*([\d.]+)px/g;
57
+ while ((match = pxFontRegex.exec(source)) !== null) {
58
+ issues.push({
59
+ type: 'error',
60
+ rule: 'no-px-font-size',
61
+ message: `Hardcoded px font size: font-size: ${match[1]}px — use rem from the typography scale`,
62
+ line: lineNumber(source, match.index),
63
+ });
64
+ }
65
+
66
+ // Rule 3: rgba() values whose RGB channels are not from the documented palette
67
+ const rgbaRegex = /rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g;
68
+ while ((match = rgbaRegex.exec(source)) !== null) {
69
+ const key = `${match[1]},${match[2]},${match[3]}`;
70
+ if (!PALETTE_TUPLES.has(key)) {
71
+ issues.push({
72
+ type: 'error',
73
+ rule: 'no-off-palette-rgba',
74
+ message: `rgba(${match[1]}, ${match[2]}, ${match[3]}, ...) uses RGB values not in the documented palette`,
75
+ line: lineNumber(source, match.index),
76
+ });
77
+ }
78
+ }
79
+
80
+ return issues;
81
+ }
82
+
83
+ // ─── Server setup ─────────────────────────────────────────────────────────────
84
+
85
+ const server = new Server(
86
+ { name: 'sv-style', version: '1.0.0' },
87
+ { capabilities: { tools: {} } }
88
+ );
89
+
90
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
91
+ tools: [
92
+ {
93
+ name: 'get_tokens',
94
+ description: 'Returns the full SV Style System CSS token definitions (tokens.css content).',
95
+ inputSchema: { type: 'object', properties: {} },
96
+ },
97
+ {
98
+ name: 'get_component_spec',
99
+ description: 'Returns the component specification markdown for a named component.',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: {
103
+ name: {
104
+ type: 'string',
105
+ description: 'Component name (e.g. BUTTON, CARD). Case-insensitive.',
106
+ },
107
+ },
108
+ required: ['name'],
109
+ },
110
+ },
111
+ {
112
+ name: 'validate_file',
113
+ description:
114
+ 'Validates a CSS/HTML file for SV Style violations: hardcoded hex colors, px font sizes, and rgba values not from the documented palette. Returns an array of violation objects.',
115
+ inputSchema: {
116
+ type: 'object',
117
+ properties: {
118
+ path: {
119
+ type: 'string',
120
+ description: 'Path to the file to validate, relative to the current working directory.',
121
+ },
122
+ },
123
+ required: ['path'],
124
+ },
125
+ },
126
+ ],
127
+ }));
128
+
129
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
130
+ const { name, arguments: args } = request.params;
131
+
132
+ if (name === 'get_tokens') {
133
+ process.stderr.write(`[sv-style mcp] get_tokens\n`);
134
+ const tokensPath = path.join(KIT_ROOT, 'tokens', 'tokens-core.css');
135
+ const content = fs.readFileSync(tokensPath, 'utf8');
136
+ return { content: [{ type: 'text', text: content }] };
137
+ }
138
+
139
+ if (name === 'get_component_spec') {
140
+ const componentName = (args.name || '').toUpperCase();
141
+ process.stderr.write(`[sv-style mcp] get_component_spec ${componentName}\n`);
142
+ const specPath = path.join(KIT_ROOT, 'docs', 'COMPONENTS', `${componentName}.md`);
143
+ if (!fs.existsSync(specPath)) {
144
+ const available = fs.readdirSync(path.join(KIT_ROOT, 'docs', 'COMPONENTS'))
145
+ .filter(f => f.endsWith('.md'))
146
+ .map(f => f.replace('.md', ''));
147
+ return {
148
+ content: [{
149
+ type: 'text',
150
+ text: `Component '${componentName}' not found. Available: ${available.join(', ')}`,
151
+ }],
152
+ isError: true,
153
+ };
154
+ }
155
+ const content = fs.readFileSync(specPath, 'utf8');
156
+ return { content: [{ type: 'text', text: content }] };
157
+ }
158
+
159
+ if (name === 'validate_file') {
160
+ const filePath = path.resolve(process.cwd(), args.path);
161
+
162
+ if (!fs.existsSync(filePath)) {
163
+ process.stderr.write(`[sv-style mcp] validate_file ${args.path} → file not found\n`);
164
+ return {
165
+ content: [{ type: 'text', text: `File not found: ${args.path}` }],
166
+ isError: true,
167
+ };
168
+ }
169
+
170
+ // Skip validation if the file is the managed tokens.css
171
+ const configPath = path.join(process.cwd(), 'sv-style.json');
172
+ if (fs.existsSync(configPath)) {
173
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
174
+ const managedTokens = path.resolve(process.cwd(), config.svStyle.paths.tokens);
175
+ if (filePath === managedTokens) {
176
+ process.stderr.write(`[sv-style mcp] validate_file ${args.path} → skipped (managed tokens)\n`);
177
+ return { content: [{ type: 'text', text: '[]' }] };
178
+ }
179
+ }
180
+
181
+ const source = fs.readFileSync(filePath, 'utf8');
182
+ const issues = validateContent(source, filePath);
183
+ process.stderr.write(`[sv-style mcp] validate_file ${args.path} → ${issues.length} violation(s)\n`);
184
+ return { content: [{ type: 'text', text: JSON.stringify(issues, null, 2) }] };
185
+ }
186
+
187
+ return {
188
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
189
+ isError: true,
190
+ };
191
+ });
192
+
193
+ // ─── Start ────────────────────────────────────────────────────────────────────
194
+
195
+ const transport = new StdioServerTransport();
196
+
197
+ server.onerror = (err) => {
198
+ process.stderr.write(`[sv-style mcp] ${err}\n`);
199
+ // Malformed stdin input (JSON.parse throws SyntaxError; invalid JSON-RPC throws ZodError).
200
+ // Return a JSON-RPC parse-error so the client is not left waiting silently.
201
+ const isParseError = err instanceof SyntaxError || err?.name === 'ZodError';
202
+ if (isParseError) {
203
+ transport.send({
204
+ jsonrpc: '2.0',
205
+ id: null,
206
+ error: { code: -32700, message: 'Parse error' },
207
+ }).catch(() => {});
208
+ }
209
+ };
210
+
211
+ await server.connect(transport);
212
+
213
+ const shutdownGracefully = createShutdownHandler(server);
214
+ process.on('SIGTERM', shutdownGracefully);
215
+ process.on('SIGINT', shutdownGracefully);
@@ -0,0 +1,48 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Compute the web-accessible URL prefix for assets.
6
+ * e.g. assetsDir="public/sv-assets", publicDir="public" → "/sv-assets"
7
+ * assetsDir="static/assets", publicDir="public" → "/static/assets"
8
+ */
9
+ export function computeAssetUrlPrefix(assetsDir, publicDir) {
10
+ const normalizedAssets = assetsDir.replace(/\\/g, '/');
11
+ const normalizedPublic = publicDir.replace(/\\/g, '/').replace(/\/$/, '');
12
+ if (normalizedAssets.startsWith(normalizedPublic + '/')) {
13
+ return '/' + normalizedAssets.slice(normalizedPublic.length + 1);
14
+ }
15
+ return '/' + normalizedAssets;
16
+ }
17
+
18
+ /**
19
+ * Rewrite @font-face src URLs in an installed fonts.css so they point to the
20
+ * actual installed asset location instead of the placeholder "/fonts/".
21
+ */
22
+ export function rewriteFontPaths(fontsFilePath, assetUrlPrefix) {
23
+ let content = fs.readFileSync(fontsFilePath, 'utf8');
24
+ content = content.replace(/url\("\/fonts\//g, `url("${assetUrlPrefix}/fonts/`);
25
+ fs.writeFileSync(fontsFilePath, content);
26
+ }
27
+
28
+ /**
29
+ * Rewrite asset references in installed docs (.md files) so paths match the
30
+ * actual installed asset location rather than the kit source placeholders.
31
+ */
32
+ export function rewriteDocAssetPaths(docsDirPath, assetUrlPrefix) {
33
+ function walk(dir) {
34
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
35
+ const fullPath = path.join(dir, entry.name);
36
+ if (entry.isDirectory()) {
37
+ walk(fullPath);
38
+ } else if (entry.name.endsWith('.md')) {
39
+ let content = fs.readFileSync(fullPath, 'utf8');
40
+ content = content.replace(/url\("\/fonts\//g, `url("${assetUrlPrefix}/fonts/`);
41
+ content = content.replace(/src="\/logo\.svg"/g, `src="${assetUrlPrefix}/logo/logo.svg"`);
42
+ content = content.replace(/href="\/favicon\.png"/g, `href="${assetUrlPrefix}/favicon.png"`);
43
+ fs.writeFileSync(fullPath, content);
44
+ }
45
+ }
46
+ }
47
+ walk(docsDirPath);
48
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Factory for the graceful-shutdown handler used by the MCP server.
3
+ *
4
+ * Exported as a separate module so it can be unit-tested directly without
5
+ * spawning a child process.
6
+ *
7
+ * @param {object} server - Object with a `close()` method returning a Promise.
8
+ * @param {object} [opts]
9
+ * @param {(code: number) => void} [opts.exit] - Defaults to process.exit.
10
+ * @param {{ write: (s: string) => void }} [opts.stderr] - Defaults to process.stderr.
11
+ * @returns {() => void} Idempotent shutdown function safe to use as a signal handler.
12
+ */
13
+ export function createShutdownHandler(server, { exit = process.exit, stderr = process.stderr } = {}) {
14
+ let shuttingDown = false;
15
+ return function shutdownGracefully() {
16
+ if (shuttingDown) return;
17
+ shuttingDown = true;
18
+ server.close()
19
+ .catch((err) => {
20
+ stderr.write(`[sv-style mcp] shutdown error: ${err}\n`);
21
+ })
22
+ .finally(() => {
23
+ exit(0);
24
+ });
25
+ };
26
+ }
package/src/update.js ADDED
@@ -0,0 +1,101 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { copyDir } from './fs-utils.js';
5
+ import { computeAssetUrlPrefix, rewriteFontPaths, rewriteDocAssetPaths } from './path-rewrite.js';
6
+ import { CONFIG_FILE, readConfig, writeConfig, migrateConfig, TOOL_VERSION } from './config.js';
7
+ import { downloadBundle } from './assets.js';
8
+ import { unpackBundle } from './bundle.js';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const KIT_ROOT = path.resolve(__dirname, '..');
12
+
13
+ export default async function update(_deps = {}) {
14
+ const _downloadBundle = _deps.downloadBundle ?? downloadBundle;
15
+ const cwd = _deps.cwd ?? process.cwd();
16
+ const _exit = _deps.exit ?? ((code) => process.exit(code));
17
+ const configPath = path.join(cwd, CONFIG_FILE);
18
+
19
+ if (!fs.existsSync(configPath)) {
20
+ console.error(`sv-style.json not found. Run 'init' first.`);
21
+ return _exit(1);
22
+ }
23
+
24
+ let config = readConfig(cwd);
25
+
26
+ // Migrate v1 config to v2 schema if needed
27
+ const migrated = migrateConfig(config);
28
+ if (migrated !== config) {
29
+ console.log(`sv-style.json migrated to v2 schema.`);
30
+ config = migrated;
31
+ writeConfig(cwd, config);
32
+ }
33
+
34
+ const { paths, blob } = config.svStyle;
35
+ const publicDir = paths.publicDir || 'public';
36
+ const assetUrlPrefix = computeAssetUrlPrefix(paths.assets, publicDir);
37
+ const stylesDir = paths.tokens.replace(/\/[^/]+$/, '');
38
+
39
+ // Update tokens-core.css
40
+ fs.copyFileSync(
41
+ path.join(KIT_ROOT, 'tokens', 'tokens-core.css'),
42
+ path.join(cwd, paths.tokens)
43
+ );
44
+
45
+ // Update docs (rewrite asset paths to match installed location)
46
+ copyDir(
47
+ path.join(KIT_ROOT, 'docs'),
48
+ path.join(cwd, paths.docs)
49
+ );
50
+ rewriteDocAssetPaths(path.join(cwd, paths.docs), assetUrlPrefix);
51
+
52
+ // Attempt to refresh the private asset bundle
53
+ const previousMode = config.svStyle.installMode;
54
+ console.log(`\nDownloading private assets from Azure Blob...`);
55
+ try {
56
+ const zipBuffer = await _downloadBundle(blob);
57
+ const { version: assetsVersion } = unpackBundle(zipBuffer, {
58
+ assetsDir: path.join(cwd, paths.assets),
59
+ stylesDir: path.join(cwd, stylesDir),
60
+ });
61
+ rewriteFontPaths(path.join(cwd, paths.fonts), assetUrlPrefix);
62
+
63
+ config.svStyle.installMode = 'full';
64
+ config.svStyle.assetsVersion = assetsVersion;
65
+ config.svStyle.toolVersion = TOOL_VERSION;
66
+ writeConfig(cwd, config);
67
+
68
+ const upgraded = previousMode !== 'full' ? ' (upgraded from degraded)' : '';
69
+ console.log(`
70
+ SV Style System updated${upgraded} (tool v${TOOL_VERSION}, assets v${assetsVersion}):
71
+ Tokens → ${paths.tokens}
72
+ Fonts → ${paths.fonts}
73
+ Assets → ${paths.assets}/
74
+ Docs → ${paths.docs}/
75
+ `);
76
+ } catch (err) {
77
+ config.svStyle.toolVersion = TOOL_VERSION;
78
+ writeConfig(cwd, config);
79
+
80
+ const staying = previousMode === 'full'
81
+ ? 'Existing assets kept — they may be outdated.'
82
+ : 'Remaining in degraded mode.';
83
+
84
+ console.warn(`
85
+ ⚠ Asset download failed — ${staying}
86
+ Reason: ${err.message}
87
+
88
+ To complete refresh:
89
+ 1. Authenticate: az login
90
+ 2. Retry: sv-style update
91
+
92
+ Run 'sv-style doctor' for full status.
93
+ `);
94
+
95
+ console.log(`
96
+ SV Style System updated (tokens/docs only, tool v${TOOL_VERSION}):
97
+ Tokens → ${paths.tokens}
98
+ Docs → ${paths.docs}/
99
+ `);
100
+ }
101
+ }
@@ -0,0 +1,40 @@
1
+ ## SV Style System
2
+
3
+ ### Agent TL;DR
4
+
5
+ - Never hardcode colors — always use `var(--color-*)` tokens
6
+ - `--color-yellow` is the only accent color — interactive/CTA elements only
7
+ - Dark mode only — no light theme is defined
8
+ - All interactive components need 5 states: default, hover, active, focus-visible, disabled
9
+ - When unsure if CSS is compliant, call `validate_file` — don't guess
10
+ - Target clean validation (empty array) before marking work done
11
+
12
+ ### Sources of truth
13
+
14
+ - `{{DOCS_DIR}}/DESIGN_SYSTEM.md` — all visual decisions
15
+ - `{{DOCS_DIR}}/COMPONENTS/` — per-component specs
16
+ - `{{DOCS_DIR}}/BAD_EXAMPLES.md` — anti-patterns to avoid
17
+ - `{{TOKENS_PATH}}` — CSS custom properties
18
+
19
+ ### Use cases
20
+
21
+ #### 1. Before writing any styled code
22
+
23
+ 1. Call `get_tokens` and read the full output
24
+ 2. Read `{{DOCS_DIR}}/DESIGN_SYSTEM.md`
25
+ 3. For each interactive component (button, input, card), call `get_component_spec` to check
26
+ whether a spec exists — style to spec if found; otherwise follow design system conventions directly
27
+
28
+ #### 2. After writing or modifying CSS
29
+
30
+ 1. Call `validate_file` on every CSS file you created or modified
31
+ 2. Fix all violations reported
32
+ 3. Repeat until every result is `[]`
33
+ 4. Do not mark work done while violations remain
34
+
35
+ #### 3. Constraint override rule
36
+
37
+ If asked to hardcode a color, use `px` for font sizes, or bypass any design system rule:
38
+ - Warn that this will cause token drift and validation failures
39
+ - Suggest the correct `var(--color-*)` token or `rem` value instead
40
+ - If overridden explicitly, recommend a follow-up cleanup task
@@ -0,0 +1,14 @@
1
+ ## SV Style — Claude MCP tools
2
+
3
+ See `AGENTS.md` for design system context, workflow, and constraints.
4
+
5
+ Keyword search does not find SV Style MCP tools — load with `select:` before calling:
6
+
7
+ | Environment | Prefix |
8
+ |---|---|
9
+ | Claude Code (`.claude/settings.json`) | `mcp__sv-style__` |
10
+ | VS Code (`.vscode/mcp.json`) | `mcp__vscode-mcp-gateway__` |
11
+
12
+ Available tools: `get_tokens`, `get_component_spec`, `validate_file`
13
+
14
+ Example: `select:mcp__vscode-mcp-gateway__validate_file`
@@ -0,0 +1,51 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ SV Style System — Design Tokens (Core)
3
+ System Verification · https://systemverification.com
4
+ ───────────────────────────────────────────────────────────── */
5
+
6
+ :root {
7
+ /* ── Core palette ── */
8
+ --color-dark-blue: #0B1B28; /* Primary background, dark surfaces, input backgrounds */
9
+ --color-light-blue: #435364; /* Secondary backgrounds, dividers, card borders */
10
+ --color-sand: #F2F2EA; /* Primary text color on dark backgrounds */
11
+ --color-yellow: #F7F965; /* Accent / CTA — buttons, links, active states, focus rings */
12
+ --color-white: #ffffff; /* Headings text, high-emphasis text */
13
+
14
+ /* ── Semantic colors ── */
15
+ --color-error: #ef4444; /* Error states, destructive actions, unhealthy badges */
16
+ --color-success: #10b981; /* Running/healthy states, positive badges */
17
+ --color-yellow-hover: #f8faaa; /* Primary button hover — lightened yellow */
18
+ --color-yellow-active: #f5f950;/* Primary button active — saturated yellow */
19
+ --color-error-light: #f87171; /* Danger button text, error badge text — lightened red */
20
+
21
+ /* ── Border radius scale ── */
22
+ --radius-sm: 4px; /* Inputs, code blocks, small elements */
23
+ --radius-md: 8px; /* Cards, form sections, panels */
24
+ --radius-pill: 100px; /* Buttons, badges, tags, pills */
25
+
26
+ /* ── Motion ── */
27
+ --transition: 0.3s ease; /* Default transition for all interactive elements */
28
+
29
+ /* ── Semantic text tokens ── */
30
+ --text-primary: var(--color-white);
31
+ --text-secondary: rgba(242, 242, 234, 0.7);
32
+ --text-muted: rgba(242, 242, 234, 0.5);
33
+
34
+ /* ── Surface tokens ── */
35
+ --surface-card: rgba(67, 83, 100, 0.35);
36
+ --surface-hover: rgba(67, 83, 100, 0.5);
37
+
38
+ /* ── Border tokens ── */
39
+ --border-subtle: rgba(67, 83, 100, 0.4);
40
+
41
+ /* ── Focus ── */
42
+ --focus-ring: var(--color-yellow);
43
+
44
+ /* ── Typography scale ── */
45
+ --text-display: 48px;
46
+ --text-h1: 32px;
47
+ --text-h2: 24px;
48
+ --text-h3: 18px;
49
+ --text-body: 14px;
50
+ --text-label: 12px;
51
+ }