@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/CLAUDE.md +98 -0
- package/README.md +411 -0
- package/bin/sv-style.js +74 -0
- package/docs/BAD_EXAMPLES.md +145 -0
- package/docs/COMPONENTS/BUTTON.md +108 -0
- package/docs/COMPONENTS/CARD.md +68 -0
- package/docs/DESIGN_SYSTEM.md +614 -0
- package/package.json +22 -0
- package/src/assets.js +67 -0
- package/src/bundle.js +67 -0
- package/src/config.js +67 -0
- package/src/doctor.js +113 -0
- package/src/fs-utils.js +15 -0
- package/src/init.js +182 -0
- package/src/login.js +65 -0
- package/src/mcp.js +215 -0
- package/src/path-rewrite.js +48 -0
- package/src/shutdown.js +26 -0
- package/src/update.js +101 -0
- package/templates/AGENTS_SNIPPET.md +40 -0
- package/templates/CLAUDE_SNIPPET.md +14 -0
- package/tokens/tokens-core.css +51 -0
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
|
+
}
|
package/src/shutdown.js
ADDED
|
@@ -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
|
+
}
|