feat(dss-ui): Button component with design tokens only
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled

- Button.css uses only CSS custom properties, no fallbacks
- Token validator now blocks hardcoded values (strict_mode: true)
- Hook scripts converted from ESM (.js) to CommonJS (.cjs)
- Storybook unified config with HMR disabled for nginx proxy
- Added dss-ui package with Figma-synced components and stories

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DSS
2025-12-11 18:47:57 -03:00
parent 44cea9443b
commit 09b234a07f
82 changed files with 4847 additions and 9 deletions

View File

@@ -0,0 +1,215 @@
#!/usr/bin/env node
/**
* DSS Complexity Monitor Hook
* Tracks code complexity metrics and warns on high-complexity code.
* Written from scratch for DSS.
*/
const fs = require('fs');
const path = require('path');
// Configuration
const DEFAULT_CONFIG = {
complexity_monitor: {
enabled: true,
max_function_lines: 50,
max_component_lines: 200,
max_props: 10,
max_nesting_depth: 4,
warn_only: true
}
};
function loadConfig() {
const configPath = path.join(process.env.HOME || '', '.dss', 'hooks-config.json');
try {
if (fs.existsSync(configPath)) {
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return { ...DEFAULT_CONFIG, ...userConfig };
}
} catch (e) {
// Use defaults
}
return DEFAULT_CONFIG;
}
function countLines(content) {
return content.split('\n').length;
}
function countProps(content) {
// Count props in interface/type definition
const propsMatch = content.match(/(?:interface|type)\s+\w*Props[^{]*\{([^}]+)\}/);
if (propsMatch) {
const propsContent = propsMatch[1];
// Count semicolons or newlines with property definitions
const props = propsContent.split(/[;\n]/).filter(line => {
const trimmed = line.trim();
return trimmed && !trimmed.startsWith('//') && trimmed.includes(':');
});
return props.length;
}
return 0;
}
function countNestingDepth(content) {
let maxDepth = 0;
let currentDepth = 0;
for (const char of content) {
if (char === '{' || char === '(') {
currentDepth++;
maxDepth = Math.max(maxDepth, currentDepth);
} else if (char === '}' || char === ')') {
currentDepth = Math.max(0, currentDepth - 1);
}
}
return maxDepth;
}
function countFunctions(content) {
const patterns = [
/function\s+\w+\s*\([^)]*\)\s*\{/g,
/const\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g,
/const\s+\w+\s*=\s*(?:async\s*)?function/g
];
let count = 0;
for (const pattern of patterns) {
const matches = content.match(pattern);
if (matches) count += matches.length;
}
return count;
}
function analyzeComplexity(content, filePath, config) {
const issues = [];
const monitorConfig = config.complexity_monitor || {};
const ext = path.extname(filePath).toLowerCase();
// Only analyze JS/TS files
if (!['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
return issues;
}
const lines = countLines(content);
const props = countProps(content);
const nesting = countNestingDepth(content);
const functions = countFunctions(content);
// Check component size (for tsx/jsx files)
if (['.tsx', '.jsx'].includes(ext)) {
if (lines > monitorConfig.max_component_lines) {
issues.push({
type: 'component_size',
severity: 'medium',
message: `Component has ${lines} lines (max: ${monitorConfig.max_component_lines})`,
suggestion: 'Consider breaking into smaller components'
});
}
if (props > monitorConfig.max_props) {
issues.push({
type: 'prop_count',
severity: 'medium',
message: `Component has ${props} props (max: ${monitorConfig.max_props})`,
suggestion: 'Consider grouping related props or using composition'
});
}
}
// Check nesting depth
if (nesting > monitorConfig.max_nesting_depth) {
issues.push({
type: 'nesting_depth',
severity: 'high',
message: `Nesting depth of ${nesting} (max: ${monitorConfig.max_nesting_depth})`,
suggestion: 'Extract nested logic into separate functions'
});
}
// Check function count (indicator of file doing too much)
if (functions > 10) {
issues.push({
type: 'function_count',
severity: 'low',
message: `File contains ${functions} functions`,
suggestion: 'Consider splitting into multiple modules'
});
}
return issues;
}
function formatOutput(issues, filePath) {
if (issues.length === 0) return '';
const severityIcons = {
high: '[HIGH]',
medium: '[MED]',
low: '[LOW]'
};
const lines = [`\n=== DSS Complexity Monitor: ${filePath} ===\n`];
for (const issue of issues) {
const icon = severityIcons[issue.severity] || '[?]';
lines.push(`${icon} ${issue.message}`);
lines.push(` Suggestion: ${issue.suggestion}\n`);
}
lines.push('='.repeat(50));
return lines.join('\n');
}
async function main() {
const config = loadConfig();
if (!config.complexity_monitor?.enabled) {
process.exit(0);
}
// Read input from stdin
let inputData;
try {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
inputData = JSON.parse(Buffer.concat(chunks).toString());
} catch (e) {
process.exit(0);
}
const toolName = inputData.tool_name || '';
const toolInput = inputData.tool_input || {};
if (!['Edit', 'Write'].includes(toolName)) {
process.exit(0);
}
const filePath = toolInput.file_path || '';
let content = '';
if (toolName === 'Write') {
content = toolInput.content || '';
} else if (toolName === 'Edit') {
content = toolInput.new_string || '';
}
if (!content || !filePath) {
process.exit(0);
}
const issues = analyzeComplexity(content, filePath, config);
if (issues.length > 0) {
const output = formatOutput(issues, filePath);
console.error(output);
}
process.exit(0);
}
main().catch(() => process.exit(0));

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env node
/**
* DSS Git Backup Hook
* Automatically commits changes when Claude Code session ends.
* Written from scratch for DSS.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const DEFAULT_CONFIG = {
git_backup: {
enabled: true,
require_git_repo: true,
commit_only_if_changes: true,
include_timestamp: true,
commit_prefix: 'auto-backup',
show_logs: false
}
};
// Prevent duplicate execution (Claude Code bug workaround)
const STATE_DIR = path.join(__dirname, '..', '.state');
const LOCK_FILE = path.join(STATE_DIR, '.git-backup.lock');
const LOCK_TIMEOUT_MS = 3000;
function loadConfig() {
const configPath = path.join(process.env.HOME || '', '.dss', 'hooks-config.json');
try {
if (fs.existsSync(configPath)) {
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return { ...DEFAULT_CONFIG, ...userConfig };
}
} catch (e) {
// Use defaults
}
return DEFAULT_CONFIG;
}
function checkLock() {
try {
if (!fs.existsSync(STATE_DIR)) {
fs.mkdirSync(STATE_DIR, { recursive: true });
}
if (fs.existsSync(LOCK_FILE)) {
const lastRun = parseInt(fs.readFileSync(LOCK_FILE, 'utf8'));
if (!isNaN(lastRun) && (Date.now() - lastRun < LOCK_TIMEOUT_MS)) {
return false; // Already ran recently
}
}
fs.writeFileSync(LOCK_FILE, Date.now().toString(), 'utf8');
return true;
} catch (e) {
return true; // Proceed on error
}
}
function isGitRepo() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
return true;
} catch (e) {
return false;
}
}
function hasChanges() {
try {
const status = execSync('git status --porcelain', { encoding: 'utf8' });
return status.trim().length > 0;
} catch (e) {
return false;
}
}
function getChangeSummary() {
try {
const status = execSync('git status --short', { encoding: 'utf8' });
const lines = status.trim().split('\n').filter(Boolean);
let added = 0, modified = 0, deleted = 0;
for (const line of lines) {
const status = line.trim().charAt(0);
if (status === 'A' || status === '?') added++;
else if (status === 'M') modified++;
else if (status === 'D') deleted++;
}
return { added, modified, deleted, total: lines.length };
} catch (e) {
return { added: 0, modified: 0, deleted: 0, total: 0 };
}
}
function createBackup(config) {
const backupConfig = config.git_backup || {};
try {
// Stage all changes
execSync('git add -A', { stdio: 'pipe' });
// Build commit message
const parts = [backupConfig.commit_prefix || 'auto-backup'];
if (backupConfig.include_timestamp) {
const timestamp = new Date().toISOString().replace('T', ' ').replace(/\..+/, '');
parts.push(timestamp);
}
const summary = getChangeSummary();
const summaryText = `(${summary.total} files: +${summary.added} ~${summary.modified} -${summary.deleted})`;
const commitMessage = `${parts.join(': ')} ${summaryText}\n\nGenerated by DSS Git Backup Hook`;
// Create commit
execSync(`git commit -m "${commitMessage}"`, { stdio: 'pipe' });
// Get commit hash
const commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
return { success: true, hash: commitHash, files: summary.total };
} catch (e) {
return { success: false, error: e.message };
}
}
function log(config, message) {
if (config.git_backup?.show_logs) {
console.log(JSON.stringify({
systemMessage: message,
continue: true
}));
}
}
function main() {
// Prevent duplicate execution
if (!checkLock()) {
process.exit(0);
}
// Prevent hook recursion
if (process.env.STOP_HOOK_ACTIVE === 'true') {
process.exit(0);
}
const config = loadConfig();
if (!config.git_backup?.enabled) {
process.exit(0);
}
// Check for git repo
if (config.git_backup.require_git_repo && !isGitRepo()) {
log(config, 'DSS Git Backup: Not a git repository, skipping');
process.exit(0);
}
// Check for changes
if (config.git_backup.commit_only_if_changes && !hasChanges()) {
log(config, 'DSS Git Backup: No changes to commit');
process.exit(0);
}
// Create backup
const result = createBackup(config);
if (result.success) {
log(config, `DSS Git Backup: Committed ${result.files} files (${result.hash})`);
} else {
log(config, `DSS Git Backup: Failed - ${result.error}`);
}
process.exit(0);
}
main();

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env node
/**
* DSS Session Summary Hook
* Generates a summary report at the end of each Claude Code session.
* Written from scratch for DSS.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const DEFAULT_CONFIG = {
session_summary: {
enabled: true,
output_file: '.dss-session-summary.md',
include_git_diff: true,
include_file_list: true,
max_diff_lines: 100
}
};
function loadConfig() {
const configPath = path.join(process.env.HOME || '', '.dss', 'hooks-config.json');
try {
if (fs.existsSync(configPath)) {
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return { ...DEFAULT_CONFIG, ...userConfig };
}
} catch (e) {
// Use defaults
}
return DEFAULT_CONFIG;
}
function getGitInfo() {
const info = {
branch: '',
status: '',
diff: '',
modifiedFiles: []
};
try {
// Check if in git repo
execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
// Get branch
info.branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
// Get status
info.status = execSync('git status --short', { encoding: 'utf8' }).trim();
// Get modified files
const statusLines = info.status.split('\n').filter(Boolean);
info.modifiedFiles = statusLines.map(line => {
const parts = line.trim().split(/\s+/);
return {
status: parts[0],
file: parts.slice(1).join(' ')
};
});
// Get diff summary
try {
info.diff = execSync('git diff --stat', { encoding: 'utf8' }).trim();
} catch (e) {
info.diff = '';
}
} catch (e) {
// Not a git repo or git not available
}
return info;
}
function getSessionStats() {
// Try to read from session state if available
const stats = {
startTime: new Date().toISOString(),
filesModified: 0,
linesAdded: 0,
linesRemoved: 0
};
try {
// Get diff stats from git
const diffStat = execSync('git diff --numstat', { encoding: 'utf8' });
const lines = diffStat.trim().split('\n').filter(Boolean);
for (const line of lines) {
const [added, removed] = line.split('\t');
stats.linesAdded += parseInt(added) || 0;
stats.linesRemoved += parseInt(removed) || 0;
stats.filesModified++;
}
} catch (e) {
// Git not available
}
return stats;
}
function generateReport(config) {
const summaryConfig = config.session_summary || {};
const gitInfo = getGitInfo();
const stats = getSessionStats();
const timestamp = new Date().toLocaleString();
const lines = [];
lines.push('# DSS Session Summary');
lines.push(`\n**Generated:** ${timestamp}`);
if (gitInfo.branch) {
lines.push(`**Branch:** ${gitInfo.branch}`);
}
lines.push('\n## Changes Overview');
lines.push('');
lines.push(`- Files modified: ${stats.filesModified}`);
lines.push(`- Lines added: +${stats.linesAdded}`);
lines.push(`- Lines removed: -${stats.linesRemoved}`);
if (summaryConfig.include_file_list && gitInfo.modifiedFiles.length > 0) {
lines.push('\n## Modified Files');
lines.push('');
lines.push('| Status | File |');
lines.push('|--------|------|');
const statusLabels = {
'M': 'Modified',
'A': 'Added',
'D': 'Deleted',
'R': 'Renamed',
'??': 'Untracked'
};
for (const file of gitInfo.modifiedFiles.slice(0, 20)) {
const label = statusLabels[file.status] || file.status;
lines.push(`| ${label} | ${file.file} |`);
}
if (gitInfo.modifiedFiles.length > 20) {
lines.push(`| ... | +${gitInfo.modifiedFiles.length - 20} more files |`);
}
}
if (summaryConfig.include_git_diff && gitInfo.diff) {
lines.push('\n## Diff Summary');
lines.push('');
lines.push('```');
const diffLines = gitInfo.diff.split('\n');
const maxLines = summaryConfig.max_diff_lines || 100;
lines.push(diffLines.slice(0, maxLines).join('\n'));
if (diffLines.length > maxLines) {
lines.push(`... (${diffLines.length - maxLines} more lines)`);
}
lines.push('```');
}
lines.push('\n---');
lines.push('*Generated by DSS Session Summary Hook*');
return lines.join('\n');
}
function main() {
const config = loadConfig();
if (!config.session_summary?.enabled) {
process.exit(0);
}
try {
const report = generateReport(config);
const outputFile = config.session_summary.output_file || '.dss-session-summary.md';
const outputPath = path.join(process.cwd(), outputFile);
fs.writeFileSync(outputPath, report, 'utf8');
// Output confirmation
console.log(JSON.stringify({
systemMessage: `Session summary saved to ${outputFile}`,
continue: true
}));
} catch (e) {
// Fail silently
}
process.exit(0);
}
main();

View File

@@ -105,12 +105,10 @@ HARDCODED_PATTERNS = [
# Allowlist patterns (common exceptions)
ALLOWLIST = [
r"#000000?", # Pure black
r"#fff(fff)?", # Pure white
r"transparent",
r"inherit",
r"currentColor",
r"var\(--", # Already using CSS variables
r"var\(--[^,)]+\)$", # CSS variables WITHOUT fallbacks only
r"theme\.", # Already using theme
r"colors\.", # Already using colors object
]
@@ -122,9 +120,9 @@ def get_config():
default_config = {
"token_validator": {
"enabled": True,
"strict_mode": False,
"warn_only": True,
"categories": ["color", "spacing", "typography"],
"strict_mode": True, # Block writes with hardcoded values
"warn_only": False, # Actually enforce token usage
"categories": ["color", "spacing", "typography", "border", "effects"],
}
}