From 712dcd9b379584570b0c395a75b2e4020587a162 Mon Sep 17 00:00:00 2001 From: Jonathan Jewell <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 3 May 2026 18:46:48 +0100 Subject: [PATCH] chore: remove TS sources ahead of AffineScript migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes the 16 .ts/.tsx source files across the 6 TS-bearing tool packages. Per the hyperpolymath language policy, AffineScript is the migration target; the AffineScript port replaces these files (using the modularisation tooling from the affinescript repo) and is being prepared on a separate branch. No package.json / tsconfig.json changes here — those will be revised by the AffineScript port to match the new build pipeline. Files removed: - tools/cli/src/cli.ts - tools/github-action/src/index.ts - tools/monitoring-api/src/server.ts - tools/monitoring-api/src/routes/{badge,dashboard,leaderboard,scan,stats,violations}.ts - tools/stale/packages/core/src/{index.ts, database/arangodb.ts} - tools/stale/packages/scanner/src/{index.ts, scanner.ts} - tools/stale/packages/stale/components/react/src/{index.ts, Button/Button.tsx, Modal/Modal.tsx} Originals remain accessible via git history (main@HEAD~1 prior to this commit). Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/cli/src/cli.ts | 246 ----------- tools/github-action/src/index.ts | 151 ------- tools/monitoring-api/src/routes/badge.ts | 91 ---- tools/monitoring-api/src/routes/dashboard.ts | 66 --- .../monitoring-api/src/routes/leaderboard.ts | 58 --- tools/monitoring-api/src/routes/scan.ts | 152 ------- tools/monitoring-api/src/routes/stats.ts | 50 --- tools/monitoring-api/src/routes/violations.ts | 115 ----- tools/monitoring-api/src/server.ts | 127 ------ .../packages/core/src/database/arangodb.ts | 409 ------------------ tools/stale/packages/core/src/index.ts | 11 - tools/stale/packages/scanner/src/index.ts | 11 - tools/stale/packages/scanner/src/scanner.ts | 72 --- .../components/react/src/Button/Button.tsx | 82 ---- .../components/react/src/Modal/Modal.tsx | 212 --------- .../stale/components/react/src/index.ts | 10 - 16 files changed, 1863 deletions(-) delete mode 100644 tools/cli/src/cli.ts delete mode 100644 tools/github-action/src/index.ts delete mode 100644 tools/monitoring-api/src/routes/badge.ts delete mode 100644 tools/monitoring-api/src/routes/dashboard.ts delete mode 100644 tools/monitoring-api/src/routes/leaderboard.ts delete mode 100644 tools/monitoring-api/src/routes/scan.ts delete mode 100644 tools/monitoring-api/src/routes/stats.ts delete mode 100644 tools/monitoring-api/src/routes/violations.ts delete mode 100644 tools/monitoring-api/src/server.ts delete mode 100644 tools/stale/packages/core/src/database/arangodb.ts delete mode 100644 tools/stale/packages/core/src/index.ts delete mode 100644 tools/stale/packages/scanner/src/index.ts delete mode 100644 tools/stale/packages/scanner/src/scanner.ts delete mode 100644 tools/stale/packages/stale/components/react/src/Button/Button.tsx delete mode 100644 tools/stale/packages/stale/components/react/src/Modal/Modal.tsx delete mode 100644 tools/stale/packages/stale/components/react/src/index.ts diff --git a/tools/cli/src/cli.ts b/tools/cli/src/cli.ts deleted file mode 100644 index 049c7b3..0000000 --- a/tools/cli/src/cli.ts +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env node - -import { Command } from 'commander'; -import chalk from 'chalk'; -import ora from 'ora'; -import Table from 'cli-table3'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { createScanner } from '@accessibility-everywhere/scanner'; - -const program = new Command(); - -program - .name('accessibility-scan') - .description('Command-line tool for accessibility scanning') - .version('1.0.0'); - -// Scan command -program - .command('scan') - .description('Scan a URL for accessibility issues') - .argument('', 'URL to scan') - .option('-l, --level ', 'WCAG level (A, AA, AAA)', 'AA') - .option('-o, --output ', 'Output file for results (JSON)') - .option('-f, --format ', 'Output format (json, table, markdown)', 'table') - .option('--screenshot', 'Take screenshot') - .action(async (url: string, options: any) => { - const spinner = ora('Scanning for accessibility issues...').start(); - - try { - const scanner = createScanner(); - const result = await scanner.scan({ - url, - wcagLevel: options.level, - screenshot: options.screenshot, - }); - - spinner.succeed('Scan complete!'); - - // Display results - console.log('\n' + '='.repeat(70)); - console.log(chalk.bold.blue('Accessibility Report')); - console.log('='.repeat(70)); - console.log(`URL: ${url}`); - console.log(`WCAG Level: ${options.level}`); - console.log(`Score: ${getScoreColor(result.score)}${result.score}/100${chalk.reset()} (Grade ${getGrade(result.score)})`); - console.log('='.repeat(70) + '\n'); - - // Summary table - const summaryTable = new Table({ - head: ['Metric', 'Count'], - colWidths: [30, 10], - }); - - summaryTable.push( - ['✅ Passes', result.passes.length], - ['❌ Violations', result.violations.length], - ['⚠️ Needs Review', result.incomplete.length] - ); - - console.log(summaryTable.toString() + '\n'); - - // Violations - if (result.violations.length > 0) { - console.log(chalk.bold.red(`Found ${result.violations.length} violations:\n`)); - - if (options.format === 'table') { - const violationsTable = new Table({ - head: ['Impact', 'Description', 'Instances', 'WCAG'], - colWidths: [12, 50, 10, 15], - }); - - result.violations.forEach((v: any) => { - violationsTable.push([ - getImpactColor(v.impact) + v.impact + chalk.reset(), - v.description, - v.nodes.length, - v.wcag.join(', '), - ]); - }); - - console.log(violationsTable.toString()); - } else if (options.format === 'markdown') { - console.log(generateMarkdown(result, url)); - } else { - console.log(JSON.stringify(result, null, 2)); - } - } else { - console.log(chalk.green.bold('🎉 No violations found! Great job!')); - } - - // Save to file if requested - if (options.output) { - await fs.writeJson(options.output, result, { spaces: 2 }); - console.log(chalk.gray(`\n✓ Results saved to ${options.output}`)); - } - - // Exit with error code if violations found - process.exit(result.violations.length > 0 ? 1 : 0); - } catch (error: any) { - spinner.fail('Scan failed'); - console.error(chalk.red(error.message)); - process.exit(1); - } - }); - -// CI command (for continuous integration) -program - .command('ci') - .description('Run accessibility scan for CI/CD') - .argument('', 'URL to scan') - .option('-l, --level ', 'WCAG level (A, AA, AAA)', 'AA') - .option('--min-score ', 'Minimum required score', '70') - .option('--fail-on-violations', 'Fail if any violations found') - .action(async (url: string, options: any) => { - const spinner = ora('Running CI scan...').start(); - - try { - const scanner = createScanner(); - const result = await scanner.scan({ - url, - wcagLevel: options.level, - }); - - spinner.succeed('CI scan complete'); - - const minScore = parseInt(options.minScore); - const failOnViolations = options.failOnViolations; - - console.log(`Score: ${result.score}/100`); - console.log(`Violations: ${result.violations.length}`); - - if (failOnViolations && result.violations.length > 0) { - console.error(chalk.red(`✗ Failed: Found ${result.violations.length} violations`)); - process.exit(1); - } - - if (result.score < minScore) { - console.error(chalk.red(`✗ Failed: Score ${result.score} below minimum ${minScore}`)); - process.exit(1); - } - - console.log(chalk.green('✓ Passed all checks')); - process.exit(0); - } catch (error: any) { - spinner.fail('CI scan failed'); - console.error(chalk.red(error.message)); - process.exit(1); - } - }); - -// Multi-scan command -program - .command('batch') - .description('Scan multiple URLs from a file') - .argument('', 'File containing URLs (one per line)') - .option('-l, --level ', 'WCAG level (A, AA, AAA)', 'AA') - .option('-o, --output ', 'Output directory for results', './scan-results') - .action(async (file: string, options: any) => { - try { - const urls = (await fs.readFile(file, 'utf-8')) - .split('\n') - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')); - - console.log(chalk.blue(`Scanning ${urls.length} URLs...`)); - - await fs.ensureDir(options.output); - - const scanner = createScanner(); - let completed = 0; - let failed = 0; - - for (const url of urls) { - const spinner = ora(`[${completed + 1}/${urls.length}] ${url}`).start(); - - try { - const result = await scanner.scan({ - url, - wcagLevel: options.level, - }); - - const filename = url.replace(/[^a-z0-9]/gi, '_') + '.json'; - const filepath = path.join(options.output, filename); - await fs.writeJson(filepath, result, { spaces: 2 }); - - spinner.succeed(`${url} - Score: ${result.score}`); - completed++; - } catch (error: any) { - spinner.fail(`${url} - ${error.message}`); - failed++; - } - } - - console.log(chalk.green(`\n✓ Completed: ${completed}`)); - if (failed > 0) { - console.log(chalk.red(`✗ Failed: ${failed}`)); - } - } catch (error: any) { - console.error(chalk.red(error.message)); - process.exit(1); - } - }); - -// Helper functions -function getGrade(score: number): string { - if (score >= 90) return 'A'; - if (score >= 80) return 'B'; - if (score >= 70) return 'C'; - if (score >= 60) return 'D'; - return 'F'; -} - -function getScoreColor(score: number): string { - if (score >= 90) return chalk.green.bold(''); - if (score >= 70) return chalk.yellow.bold(''); - return chalk.red.bold(''); -} - -function getImpactColor(impact: string): string { - const colors: Record = { - critical: chalk.red.bold(''), - serious: chalk.red(''), - moderate: chalk.yellow(''), - minor: chalk.blue(''), - }; - return colors[impact] || ''; -} - -function generateMarkdown(result: any, url: string): string { - let md = `# Accessibility Report\n\n`; - md += `**URL:** ${url}\n`; - md += `**Score:** ${result.score}/100\n\n`; - md += `## Violations\n\n`; - - result.violations.forEach((v: any, i: number) => { - md += `### ${i + 1}. ${v.help}\n\n`; - md += `- **Impact:** ${v.impact}\n`; - md += `- **Instances:** ${v.nodes.length}\n`; - md += `- **Help:** ${v.helpUrl}\n\n`; - }); - - return md; -} - -program.parse(); diff --git a/tools/github-action/src/index.ts b/tools/github-action/src/index.ts deleted file mode 100644 index c7a83e4..0000000 --- a/tools/github-action/src/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -import * as core from '@actions/core'; -import * as github from '@actions/github'; -import { createScanner } from '@accessibility-everywhere/scanner'; - -async function run() { - try { - // Get inputs - const url = core.getInput('url', { required: true }); - const wcagLevel = core.getInput('wcag-level') as 'A' | 'AA' | 'AAA'; - const failOnViolations = core.getInput('fail-on-violations') === 'true'; - const minScore = parseInt(core.getInput('min-score') || '0'); - const commentPR = core.getInput('comment-pr') === 'true'; - const githubToken = core.getInput('github-token'); - - core.info(`Scanning ${url} for WCAG ${wcagLevel} compliance...`); - - // Run scan - const scanner = createScanner(); - const result = await scanner.scan({ - url, - wcagLevel, - screenshot: false, - }); - - // Set outputs - core.setOutput('score', result.score); - core.setOutput('violations', result.violations.length); - core.setOutput('passes', result.passes.length); - core.setOutput('report-url', `https://accessibility-everywhere.org/report?url=${encodeURIComponent(url)}`); - - // Generate summary - const summary = generateSummary(result, url, wcagLevel); - core.summary.addRaw(summary).write(); - - // Post PR comment if requested - if (commentPR && githubToken && github.context.payload.pull_request) { - await postPRComment(githubToken, result, url, wcagLevel); - } - - // Log results - core.info(`\n${'='.repeat(60)}`); - core.info(`Accessibility Score: ${result.score}/100`); - core.info(`Violations: ${result.violations.length}`); - core.info(`Passes: ${result.passes.length}`); - core.info(`Incomplete: ${result.incomplete.length}`); - core.info(`${'='.repeat(60)}\n`); - - // Log violations - if (result.violations.length > 0) { - core.warning(`Found ${result.violations.length} accessibility violations:`); - result.violations.forEach((v, i) => { - core.warning(`${i + 1}. [${v.impact.toUpperCase()}] ${v.description}`); - core.warning(` Help: ${v.helpUrl}`); - core.warning(` Instances: ${v.nodes.length}`); - }); - } - - // Check failure conditions - if (failOnViolations && result.violations.length > 0) { - core.setFailed(`Found ${result.violations.length} accessibility violations`); - } - - if (minScore > 0 && result.score < minScore) { - core.setFailed(`Accessibility score ${result.score} is below minimum required score ${minScore}`); - } - - if (result.violations.length === 0 && result.score >= minScore) { - core.info('✓ Accessibility check passed!'); - } - } catch (error: any) { - core.setFailed(`Action failed: ${error.message}`); - } -} - -function generateSummary(result: any, url: string, wcagLevel: string): string { - const grade = getGrade(result.score); - const gradeEmoji = { - A: '🟢', - B: '🟡', - C: '🟠', - D: '🔴', - F: '🔴', - }[grade]; - - let markdown = `# Accessibility Report ${gradeEmoji}\n\n`; - markdown += `**URL:** ${url}\n`; - markdown += `**WCAG Level:** ${wcagLevel}\n`; - markdown += `**Score:** ${result.score}/100 (Grade ${grade})\n\n`; - - markdown += `## Summary\n\n`; - markdown += `| Metric | Count |\n`; - markdown += `|--------|-------|\n`; - markdown += `| ✅ Passes | ${result.passes.length} |\n`; - markdown += `| ❌ Violations | ${result.violations.length} |\n`; - markdown += `| ⚠️ Needs Review | ${result.incomplete.length} |\n\n`; - - if (result.violations.length > 0) { - markdown += `## Violations\n\n`; - result.violations.slice(0, 10).forEach((v: any, i: number) => { - const impact = v.impact as 'critical' | 'serious' | 'moderate' | 'minor'; - const impactEmoji = { - critical: '🔴', - serious: '🟠', - moderate: '🟡', - minor: '🔵', - }[impact] || '⚪'; - - markdown += `### ${i + 1}. ${impactEmoji} ${v.help}\n\n`; - markdown += `**Impact:** ${v.impact}\n\n`; - markdown += `**Description:** ${v.description}\n\n`; - markdown += `**Instances:** ${v.nodes.length}\n\n`; - markdown += `**Learn more:** ${v.helpUrl}\n\n`; - }); - - if (result.violations.length > 10) { - markdown += `\n*... and ${result.violations.length - 10} more violations*\n\n`; - } - } - - markdown += `\n---\n\n`; - markdown += `[View full report](https://accessibility-everywhere.org/report?url=${encodeURIComponent(url)})\n`; - - return markdown; -} - -async function postPRComment(token: string, result: any, url: string, wcagLevel: string) { - const octokit = github.getOctokit(token); - const { context } = github; - - if (!context.payload.pull_request) { - return; - } - - const summary = generateSummary(result, url, wcagLevel); - - await octokit.rest.issues.createComment({ - ...context.repo, - issue_number: context.payload.pull_request.number, - body: summary, - }); -} - -function getGrade(score: number): string { - if (score >= 90) return 'A'; - if (score >= 80) return 'B'; - if (score >= 70) return 'C'; - if (score >= 60) return 'D'; - return 'F'; -} - -run(); diff --git a/tools/monitoring-api/src/routes/badge.ts b/tools/monitoring-api/src/routes/badge.ts deleted file mode 100644 index 8d53078..0000000 --- a/tools/monitoring-api/src/routes/badge.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const badgeRouter = Router(); - -// GET /v1/badge/:domain - Get badge for a domain -badgeRouter.get('/:domain', async (req, res, next) => { - try { - const { domain } = req.params; - const format = req.query.format || 'json'; - - // Find site by domain - const sites = await db.sites.byExample({ domain }).then(c => c.all()); - - if (sites.length === 0) { - return res.status(404).json({ - error: { - message: 'Site not found', - status: 404, - }, - }); - } - - const site = sites[0]; - - if (format === 'svg') { - // Generate SVG badge - const svg = generateBadgeSVG(site.currentScore); - res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'public, max-age=3600'); - res.send(svg); - } else { - // Return JSON - res.json({ - success: true, - data: { - domain, - score: site.currentScore, - grade: getGrade(site.currentScore), - lastScanned: site.lastScanned, - badgeUrl: `${req.protocol}://${req.get('host')}/v1/badge/${domain}?format=svg`, - }, - }); - } - } catch (error) { - next(error); - } -}); - -function getGrade(score: number): string { - if (score >= 90) return 'A'; - if (score >= 80) return 'B'; - if (score >= 70) return 'C'; - if (score >= 60) return 'D'; - return 'F'; -} - -function generateBadgeSVG(score: number): string { - const grade = getGrade(score); - const color = { - A: '#28a745', - B: '#8bc34a', - C: '#ffc107', - D: '#ff9800', - F: '#dc3545', - }[grade]; - - return ` - - Accessibility Score: ${score} (Grade ${grade}) - - - - - - - - - - - - - - - accessibility - - ${grade} (${score}) - - - `.trim(); -} diff --git a/tools/monitoring-api/src/routes/dashboard.ts b/tools/monitoring-api/src/routes/dashboard.ts deleted file mode 100644 index 4ecf9d5..0000000 --- a/tools/monitoring-api/src/routes/dashboard.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const dashboardRouter = Router(); - -// GET /v1/dashboard/:orgId - Get organization dashboard -dashboardRouter.get('/:orgId', async (req, res, next) => { - try { - const { orgId } = req.params; - - // Get organization - const org = await db.organizations.document(orgId); - if (!org) { - return res.status(404).json({ - error: { - message: 'Organization not found', - status: 404, - }, - }); - } - - // Get organization sites - const sites = await db.getOrganizationSites(orgId); - - // Calculate aggregate stats - const totalSites = sites.length; - const averageScore = - totalSites > 0 - ? sites.reduce((sum, site) => sum + site.currentScore, 0) / totalSites - : 0; - - // Count total violations across all sites - let totalViolations = 0; - for (const site of sites) { - const cursor = await db.violations.byExample({ - siteKey: site._key, - fixed: false, - }); - const count = cursor.count ?? 0; - totalViolations += count; - } - - res.json({ - success: true, - data: { - organization: { - name: org.name, - tier: org.tier, - }, - stats: { - totalSites, - averageScore: Math.round(averageScore), - totalViolations, - }, - sites: sites.map(site => ({ - domain: site.domain, - url: site.url, - score: site.currentScore, - lastScanned: site.lastScanned, - })), - }, - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/routes/leaderboard.ts b/tools/monitoring-api/src/routes/leaderboard.ts deleted file mode 100644 index 92f41a5..0000000 --- a/tools/monitoring-api/src/routes/leaderboard.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const leaderboardRouter = Router(); - -// GET /v1/leaderboard - Get top sites by accessibility score -leaderboardRouter.get('/', async (req, res, next) => { - try { - const limit = parseInt(req.query.limit as string) || 100; - const sites = await db.getTopSites(limit); - - const leaderboard = sites.map((site, index) => ({ - rank: index + 1, - domain: site.domain, - url: site.url, - score: site.currentScore, - violations: site.scanCount || 0, - lastScanned: site.lastScanned, - trend: site.previousScore - ? site.currentScore - site.previousScore - : 0, - })); - - res.json({ - success: true, - data: { - sites: leaderboard, - total: leaderboard.length, - lastUpdated: new Date().toISOString(), - }, - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/leaderboard/category/:category - Get leaderboard by category -leaderboardRouter.get('/category/:category', async (req, res, next) => { - try { - const { category } = req.params; - const limit = parseInt(req.query.limit as string) || 100; - - // This would filter by category in production - // For now, return top sites - const sites = await db.getTopSites(limit); - - res.json({ - success: true, - data: { - category, - sites, - total: sites.length, - }, - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/routes/scan.ts b/tools/monitoring-api/src/routes/scan.ts deleted file mode 100644 index 1d29cce..0000000 --- a/tools/monitoring-api/src/routes/scan.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Router } from 'express'; -import Joi from 'joi'; -import { db, scanner } from '../server'; - -export const scanRouter = Router(); - -// Validation schema -const scanSchema = Joi.object({ - url: Joi.string().uri().required(), - wcagLevel: Joi.string().valid('A', 'AA', 'AAA').default('AA'), - screenshot: Joi.boolean().default(false), -}); - -// POST /v1/scan - Scan a URL for accessibility issues -scanRouter.post('/', async (req, res, next) => { - try { - // Validate request - const { error, value } = scanSchema.validate(req.body); - if (error) { - return res.status(400).json({ - error: { - message: error.details[0].message, - status: 400, - }, - }); - } - - const { url, wcagLevel, screenshot } = value; - - // Run scan - const result = await scanner.scan({ - url, - wcagLevel, - screenshot, - }); - - // Extract domain - const urlObj = new URL(url); - const domain = urlObj.hostname; - - // Store in database - let site = await db.getSiteByUrl(url); - let siteKey: string; - - if (!site) { - // Create new site - const siteDoc = await db.sites.save({ - url, - domain, - firstScanned: new Date(), - lastScanned: new Date(), - scanCount: 1, - currentScore: result.score, - status: 'active', - } as any); - siteKey = siteDoc._key; - } else { - // Update existing site - siteKey = site._key; - await db.sites.update(site._key, { - lastScanned: new Date(), - scanCount: (site.scanCount || 0) + 1, - previousScore: site.currentScore, - currentScore: result.score, - }); - } - - // Store scan - const scanDoc = await db.scans.save({ - siteKey, - timestamp: result.timestamp, - score: result.score, - violations: result.violations.length, - passes: result.passes.length, - incomplete: result.incomplete.length, - url, - wcagLevel, - duration: result.duration, - userAgent: result.metadata.userAgent, - } as any); - - // Store violations - for (const violation of result.violations) { - for (const node of violation.nodes) { - await db.violations.save({ - scanKey: scanDoc._key, - siteKey, - wcagCriterion: violation.wcag[0] || 'unknown', - wcagLevel: wcagLevel, - impact: violation.impact, - description: violation.description, - helpUrl: violation.helpUrl, - selector: node.target.join(' > '), - html: node.html, - timestamp: new Date(), - fixed: false, - } as any); - } - } - - // Return results - res.json({ - success: true, - data: { - url, - scanId: scanDoc._key, - score: result.score, - violations: result.violations.length, - passes: result.passes.length, - incomplete: result.incomplete.length, - wcagLevel, - timestamp: result.timestamp, - details: { - violations: result.violations, - passes: result.passes, - incomplete: result.incomplete, - }, - }, - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/scan/:scanId - Get scan results by ID -scanRouter.get('/:scanId', async (req, res, next) => { - try { - const { scanId } = req.params; - - const scan = await db.scans.document(scanId); - if (!scan) { - return res.status(404).json({ - error: { - message: 'Scan not found', - status: 404, - }, - }); - } - - const violations = await db.getViolationsForScan(scanId); - - res.json({ - success: true, - data: { - scan, - violations, - }, - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/routes/stats.ts b/tools/monitoring-api/src/routes/stats.ts deleted file mode 100644 index 39a8d2f..0000000 --- a/tools/monitoring-api/src/routes/stats.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const statsRouter = Router(); - -// GET /v1/stats - Get global statistics -statsRouter.get('/', async (req, res, next) => { - try { - const sitesCount = await db.sites.count(); - const scansCount = await db.scans.count(); - const violationsCount = await db.violations.count(); - - const commonViolations = await db.getCommonViolations(5); - - res.json({ - success: true, - data: { - totalSites: sitesCount.count, - totalScans: scansCount.count, - totalViolations: violationsCount.count, - commonViolations, - timestamp: new Date().toISOString(), - }, - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/stats/site/:siteKey - Get stats for a specific site -statsRouter.get('/site/:siteKey', async (req, res, next) => { - try { - const { siteKey } = req.params; - - const site = await db.sites.document(siteKey); - const scans = await db.getRecentScansForSite(siteKey, 30); - const trend = await db.getSiteViolationTrend(siteKey, 30); - - res.json({ - success: true, - data: { - site, - recentScans: scans, - trend, - }, - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/routes/violations.ts b/tools/monitoring-api/src/routes/violations.ts deleted file mode 100644 index 0736039..0000000 --- a/tools/monitoring-api/src/routes/violations.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Router } from 'express'; -import { db } from '../server'; - -export const violationsRouter = Router(); - -// POST /v1/violations - Report a violation (from browser extension) -violationsRouter.post('/', async (req, res, next) => { - try { - const { url, violation, timestamp, userAgent } = req.body; - - if (!url || !violation) { - return res.status(400).json({ - error: { - message: 'URL and violation are required', - status: 400, - }, - }); - } - - // Get or create site - let site = await db.getSiteByUrl(url); - let siteKey: string; - - if (!site) { - const urlObj = new URL(url); - const siteDoc = await db.sites.save({ - url, - domain: urlObj.hostname, - firstScanned: new Date(), - lastScanned: new Date(), - scanCount: 0, - currentScore: 0, - status: 'active', - } as any); - siteKey = siteDoc._key; - } else { - siteKey = site._key; - } - - // Store violation - await db.violations.save({ - siteKey, - scanKey: '', // No scan key for direct reports - wcagCriterion: violation.wcagCriterion || 'unknown', - wcagLevel: violation.wcagLevel || 'AA', - impact: violation.impact || 'moderate', - description: violation.description || '', - helpUrl: violation.helpUrl || '', - selector: violation.selector || '', - html: violation.html || '', - timestamp: new Date(timestamp || Date.now()), - fixed: false, - } as any); - - res.json({ - success: true, - message: 'Violation reported successfully', - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/violations/common - Get most common violations -violationsRouter.get('/common', async (req, res, next) => { - try { - const limit = parseInt(req.query.limit as string) || 10; - const violations = await db.getCommonViolations(limit); - - res.json({ - success: true, - data: violations, - }); - } catch (error) { - next(error); - } -}); - -// GET /v1/violations/site/:siteKey - Get violations for a site -violationsRouter.get('/site/:siteKey', async (req, res, next) => { - try { - const { siteKey } = req.params; - const fixed = req.query.fixed === 'true'; - - const violations = await db.violations.byExample({ - siteKey, - fixed, - }).then(cursor => cursor.all()); - - res.json({ - success: true, - data: violations, - }); - } catch (error) { - next(error); - } -}); - -// PATCH /v1/violations/:violationId/fixed - Mark violation as fixed -violationsRouter.patch('/:violationId/fixed', async (req, res, next) => { - try { - const { violationId } = req.params; - - await db.violations.update(violationId, { - fixed: true, - }); - - res.json({ - success: true, - message: 'Violation marked as fixed', - }); - } catch (error) { - next(error); - } -}); diff --git a/tools/monitoring-api/src/server.ts b/tools/monitoring-api/src/server.ts deleted file mode 100644 index 3f3482d..0000000 --- a/tools/monitoring-api/src/server.ts +++ /dev/null @@ -1,127 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import helmet from 'helmet'; -import compression from 'compression'; -import rateLimit from 'express-rate-limit'; -import { createArangoDBService } from '@accessibility-everywhere/core'; -import { createScanner } from '@accessibility-everywhere/scanner'; -import dotenv from 'dotenv'; - -// Load environment variables -dotenv.config(); - -// Import routes -import { scanRouter } from './routes/scan'; -import { violationsRouter } from './routes/violations'; -import { leaderboardRouter } from './routes/leaderboard'; -import { badgeRouter } from './routes/badge'; -import { statsRouter } from './routes/stats'; -import { dashboardRouter } from './routes/dashboard'; - -const app = express(); -const PORT = process.env.PORT || 3000; - -// Middleware -app.use(helmet()); -app.use(cors()); -app.use(compression()); -app.use(express.json({ limit: '10mb' })); - -// Rate limiting -const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per windowMs - message: 'Too many requests from this IP, please try again later.', -}); - -app.use('/v1/', limiter); - -// Initialize database -export const db = createArangoDBService(); -export const scanner = createScanner(); - -// Initialize database connection -async function initializeDatabase() { - try { - await db.initialize(); - console.log('✓ Database initialized successfully'); - } catch (error) { - console.error('✗ Database initialization failed:', error); - process.exit(1); - } -} - -// Health check -app.get('/health', (req, res) => { - res.json({ - status: 'healthy', - timestamp: new Date().toISOString(), - version: '1.0.0', - }); -}); - -// API version info -app.get('/v1', (req, res) => { - res.json({ - name: 'Accessibility Everywhere Monitoring API', - version: '1.0.0', - description: 'Accessibility violation reporting and analytics API', - endpoints: { - scan: '/v1/scan', - violations: '/v1/violations', - leaderboard: '/v1/leaderboard', - badge: '/v1/badge/:domain', - stats: '/v1/stats', - dashboard: '/v1/dashboard/:orgId', - }, - documentation: 'https://docs.accessibility-everywhere.org/api', - }); -}); - -// Routes -app.use('/v1/scan', scanRouter); -app.use('/v1/violations', violationsRouter); -app.use('/v1/leaderboard', leaderboardRouter); -app.use('/v1/badge', badgeRouter); -app.use('/v1/stats', statsRouter); -app.use('/v1/dashboard', dashboardRouter); - -// Error handling -app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { - console.error('Error:', err); - res.status(err.status || 500).json({ - error: { - message: err.message || 'Internal server error', - status: err.status || 500, - }, - }); -}); - -// 404 handler -app.use((req, res) => { - res.status(404).json({ - error: { - message: 'Endpoint not found', - status: 404, - }, - }); -}); - -// Start server -async function startServer() { - await initializeDatabase(); - - app.listen(PORT, () => { - console.log(`✓ Accessibility Everywhere API listening on port ${PORT}`); - console.log(`✓ Environment: ${process.env.NODE_ENV || 'development'}`); - console.log(`✓ Health check: http://localhost:${PORT}/health`); - console.log(`✓ API docs: http://localhost:${PORT}/v1`); - }); -} - -startServer().catch(error => { - console.error('Failed to start server:', error); - process.exit(1); -}); - -export default app; diff --git a/tools/stale/packages/core/src/database/arangodb.ts b/tools/stale/packages/core/src/database/arangodb.ts deleted file mode 100644 index 383a9ed..0000000 --- a/tools/stale/packages/core/src/database/arangodb.ts +++ /dev/null @@ -1,409 +0,0 @@ -import { Database, aql } from 'arangojs'; -import { DocumentCollection, EdgeCollection } from 'arangojs/collection'; - -export interface ArangoConfig { - url: string; - database: string; - username: string; - password: string; -} - -export interface Site { - _key: string; - url: string; - domain: string; - firstScanned: Date; - lastScanned: Date; - scanCount: number; - currentScore: number; - previousScore?: number; - status: 'active' | 'inactive' | 'failed'; - metadata?: Record; -} - -export interface Scan { - _key: string; - siteKey: string; - timestamp: Date; - score: number; - violations: number; - passes: number; - incomplete: number; - url: string; - wcagLevel: 'A' | 'AA' | 'AAA'; - duration: number; - userAgent?: string; -} - -export interface Violation { - _key: string; - scanKey: string; - siteKey: string; - wcagCriterion: string; - wcagLevel: 'A' | 'AA' | 'AAA'; - impact: 'critical' | 'serious' | 'moderate' | 'minor'; - description: string; - helpUrl: string; - selector: string; - html: string; - timestamp: Date; - fixed: boolean; -} - -export interface WCAGCriterion { - _key: string; - criterion: string; - level: 'A' | 'AA' | 'AAA'; - principle: 'perceivable' | 'operable' | 'understandable' | 'robust'; - guideline: string; - title: string; - description: string; - successCriteria: string; - techniques: string[]; - failures: string[]; -} - -export interface Organization { - _key: string; - name: string; - domain: string; - contactEmail?: string; - tier: 'free' | 'pro' | 'enterprise'; - createdAt: Date; - apiKey?: string; -} - -export class ArangoDBService { - private db: Database; - public sites!: DocumentCollection; - public scans!: DocumentCollection; - public violations!: DocumentCollection; - public wcagCriteria!: DocumentCollection; - public organizations!: DocumentCollection; - public siteScans!: EdgeCollection; - public scanViolations!: EdgeCollection; - public violationCriteria!: EdgeCollection; - public orgSites!: EdgeCollection; - - constructor(config: ArangoConfig) { - this.db = new Database({ - url: config.url, - databaseName: config.database, - auth: { - username: config.username, - password: config.password, - }, - }); - } - - async initialize(): Promise { - // Create database if it doesn't exist - const databases = await this.db.listDatabases(); - if (!databases.includes(this.db.name)) { - await this.db.createDatabase(this.db.name); - } - - // Create collections - await this.createCollectionIfNotExists('sites'); - await this.createCollectionIfNotExists('scans'); - await this.createCollectionIfNotExists('violations'); - await this.createCollectionIfNotExists('wcag_criteria'); - await this.createCollectionIfNotExists('organizations'); - - // Create edge collections for graph relationships - await this.createEdgeCollectionIfNotExists('site_scans'); - await this.createEdgeCollectionIfNotExists('scan_violations'); - await this.createEdgeCollectionIfNotExists('violation_criteria'); - await this.createEdgeCollectionIfNotExists('org_sites'); - - // Assign collections - this.sites = this.db.collection('sites'); - this.scans = this.db.collection('scans'); - this.violations = this.db.collection('violations'); - this.wcagCriteria = this.db.collection('wcag_criteria'); - this.organizations = this.db.collection('organizations'); - this.siteScans = this.db.collection('site_scans'); - this.scanViolations = this.db.collection('scan_violations'); - this.violationCriteria = this.db.collection('violation_criteria'); - this.orgSites = this.db.collection('org_sites'); - - // Create indexes - await this.createIndexes(); - - // Initialize WCAG criteria - await this.initializeWCAGCriteria(); - } - - private async createCollectionIfNotExists(name: string): Promise { - const collections = await this.db.listCollections(); - if (!collections.some(c => c.name === name)) { - await this.db.createCollection(name); - } - } - - private async createEdgeCollectionIfNotExists(name: string): Promise { - const collections = await this.db.listCollections(); - if (!collections.some(c => c.name === name)) { - await this.db.createEdgeCollection(name); - } - } - - private async createIndexes(): Promise { - // Sites indexes - await this.sites.ensureIndex({ type: 'persistent', fields: ['url'], unique: true }); - await this.sites.ensureIndex({ type: 'persistent', fields: ['domain'] }); - await this.sites.ensureIndex({ type: 'persistent', fields: ['currentScore'] }); - - // Scans indexes - await this.scans.ensureIndex({ type: 'persistent', fields: ['siteKey'] }); - await this.scans.ensureIndex({ type: 'persistent', fields: ['timestamp'] }); - await this.scans.ensureIndex({ type: 'persistent', fields: ['score'] }); - - // Violations indexes - await this.violations.ensureIndex({ type: 'persistent', fields: ['scanKey'] }); - await this.violations.ensureIndex({ type: 'persistent', fields: ['siteKey'] }); - await this.violations.ensureIndex({ type: 'persistent', fields: ['wcagCriterion'] }); - await this.violations.ensureIndex({ type: 'persistent', fields: ['impact'] }); - await this.violations.ensureIndex({ type: 'persistent', fields: ['fixed'] }); - - // WCAG Criteria indexes - await this.wcagCriteria.ensureIndex({ type: 'persistent', fields: ['criterion'], unique: true }); - await this.wcagCriteria.ensureIndex({ type: 'persistent', fields: ['level'] }); - - // Organizations indexes - await this.organizations.ensureIndex({ type: 'persistent', fields: ['domain'] }); - await this.organizations.ensureIndex({ type: 'persistent', fields: ['apiKey'], unique: true, sparse: true }); - } - - private async initializeWCAGCriteria(): Promise { - const count = await this.wcagCriteria.count(); - if (count.count === 0) { - // Insert WCAG 2.1 Level AA criteria - const criteria = this.getWCAGCriteriaData(); - for (const criterion of criteria) { - await this.wcagCriteria.save(criterion); - } - } - } - - private getWCAGCriteriaData(): WCAGCriterion[] { - return [ - { - _key: '1_1_1', - criterion: '1.1.1', - level: 'A', - principle: 'perceivable', - guideline: '1.1', - title: 'Non-text Content', - description: 'All non-text content has a text alternative', - successCriteria: 'Provide text alternatives for any non-text content', - techniques: ['H37', 'H36', 'G94', 'G95'], - failures: ['F3', 'F13', 'F20', 'F30', 'F38', 'F39', 'F65', 'F67', 'F71', 'F72'], - }, - { - _key: '1_3_1', - criterion: '1.3.1', - level: 'A', - principle: 'perceivable', - guideline: '1.3', - title: 'Info and Relationships', - description: 'Information, structure, and relationships can be programmatically determined', - successCriteria: 'Information, structure, and relationships conveyed through presentation can be programmatically determined', - techniques: ['H42', 'H43', 'H44', 'H48', 'H51', 'H63', 'H71', 'H73', 'H85'], - failures: ['F2', 'F33', 'F34', 'F42', 'F43', 'F46', 'F48', 'F68', 'F87', 'F90', 'F91', 'F92'], - }, - { - _key: '1_4_3', - criterion: '1.4.3', - level: 'AA', - principle: 'perceivable', - guideline: '1.4', - title: 'Contrast (Minimum)', - description: 'Text has a contrast ratio of at least 4.5:1', - successCriteria: 'The visual presentation of text and images of text has a contrast ratio of at least 4.5:1', - techniques: ['G17', 'G18', 'G145', 'G148', 'G174'], - failures: ['F24', 'F83'], - }, - { - _key: '2_1_1', - criterion: '2.1.1', - level: 'A', - principle: 'operable', - guideline: '2.1', - title: 'Keyboard', - description: 'All functionality is available from a keyboard', - successCriteria: 'All functionality of the content is operable through a keyboard interface', - techniques: ['G202', 'H91'], - failures: ['F54', 'F55', 'F42'], - }, - { - _key: '2_4_1', - criterion: '2.4.1', - level: 'A', - principle: 'operable', - guideline: '2.4', - title: 'Bypass Blocks', - description: 'A mechanism is available to bypass blocks of content', - successCriteria: 'A mechanism is available to bypass blocks of content that are repeated on multiple Web pages', - techniques: ['G1', 'G123', 'G124', 'H69', 'H70'], - failures: ['F'], - }, - { - _key: '2_4_2', - criterion: '2.4.2', - level: 'A', - principle: 'operable', - guideline: '2.4', - title: 'Page Titled', - description: 'Web pages have titles that describe topic or purpose', - successCriteria: 'Web pages have titles that describe topic or purpose', - techniques: ['G88', 'H25'], - failures: ['F25'], - }, - { - _key: '3_1_1', - criterion: '3.1.1', - level: 'A', - principle: 'understandable', - guideline: '3.1', - title: 'Language of Page', - description: 'The default human language can be programmatically determined', - successCriteria: 'The default human language of each Web page can be programmatically determined', - techniques: ['H57'], - failures: [], - }, - { - _key: '3_2_3', - criterion: '3.2.3', - level: 'AA', - principle: 'understandable', - guideline: '3.2', - title: 'Consistent Navigation', - description: 'Navigation mechanisms are consistent', - successCriteria: 'Navigational mechanisms that are repeated on multiple Web pages occur in the same relative order', - techniques: ['G61'], - failures: ['F66'], - }, - { - _key: '4_1_1', - criterion: '4.1.1', - level: 'A', - principle: 'robust', - guideline: '4.1', - title: 'Parsing', - description: 'Content can be parsed by user agents', - successCriteria: 'Elements have complete start and end tags, are nested according to specifications', - techniques: ['G134', 'G192', 'H88', 'H74', 'H93', 'H94'], - failures: ['F70', 'F77'], - }, - { - _key: '4_1_2', - criterion: '4.1.2', - level: 'A', - principle: 'robust', - guideline: '4.1', - title: 'Name, Role, Value', - description: 'User interface components have programmatically determined name, role, and value', - successCriteria: 'For all user interface components, the name and role can be programmatically determined', - techniques: ['G108', 'H91', 'H44', 'H64', 'H65', 'H88'], - failures: ['F15', 'F20', 'F59', 'F68', 'F79', 'F86', 'F89'], - }, - ]; - } - - // Query methods - async getSiteByUrl(url: string): Promise { - const cursor = await this.db.query(aql` - FOR site IN sites - FILTER site.url == ${url} - LIMIT 1 - RETURN site - `); - const results = await cursor.all(); - return results.length > 0 ? results[0] : null; - } - - async getRecentScansForSite(siteKey: string, limit: number = 10): Promise { - const cursor = await this.db.query(aql` - FOR scan IN scans - FILTER scan.siteKey == ${siteKey} - SORT scan.timestamp DESC - LIMIT ${limit} - RETURN scan - `); - return cursor.all(); - } - - async getViolationsForScan(scanKey: string): Promise { - const cursor = await this.db.query(aql` - FOR violation IN violations - FILTER violation.scanKey == ${scanKey} - SORT violation.impact DESC - RETURN violation - `); - return cursor.all(); - } - - async getTopSites(limit: number = 100): Promise { - const cursor = await this.db.query(aql` - FOR site IN sites - FILTER site.currentScore > 0 - SORT site.currentScore DESC - LIMIT ${limit} - RETURN site - `); - return cursor.all(); - } - - async getCommonViolations(limit: number = 10): Promise { - const cursor = await this.db.query(aql` - FOR violation IN violations - COLLECT wcagCriterion = violation.wcagCriterion WITH COUNT INTO count - SORT count DESC - LIMIT ${limit} - RETURN { - criterion: wcagCriterion, - count: count - } - `); - return cursor.all(); - } - - async getSiteViolationTrend(siteKey: string, days: number = 30): Promise { - const startDate = new Date(); - startDate.setDate(startDate.getDate() - days); - - const cursor = await this.db.query(aql` - FOR scan IN scans - FILTER scan.siteKey == ${siteKey} AND scan.timestamp >= ${startDate} - SORT scan.timestamp ASC - RETURN { - timestamp: scan.timestamp, - violations: scan.violations, - score: scan.score - } - `); - return cursor.all(); - } - - async getOrganizationSites(orgKey: string): Promise { - const cursor = await this.db.query(aql` - FOR org IN organizations - FILTER org._key == ${orgKey} - FOR v, e IN 1..1 OUTBOUND org org_sites - RETURN v - `); - return cursor.all(); - } -} - -export function createArangoDBService(config?: Partial): ArangoDBService { - const defaultConfig: ArangoConfig = { - url: process.env.ARANGO_URL || 'http://localhost:8529', - database: process.env.ARANGO_DATABASE || 'accessibility', - username: process.env.ARANGO_USERNAME || 'root', - password: process.env.ARANGO_PASSWORD || 'development', - }; - - return new ArangoDBService({ ...defaultConfig, ...config }); -} diff --git a/tools/stale/packages/core/src/index.ts b/tools/stale/packages/core/src/index.ts deleted file mode 100644 index bbf377a..0000000 --- a/tools/stale/packages/core/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './database/arangodb'; -export { - ArangoDBService, - createArangoDBService, - type ArangoConfig, - type Site, - type Scan, - type Violation, - type WCAGCriterion, - type Organization, -} from './database/arangodb'; diff --git a/tools/stale/packages/scanner/src/index.ts b/tools/stale/packages/scanner/src/index.ts deleted file mode 100644 index dea06a3..0000000 --- a/tools/stale/packages/scanner/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './scanner'; -export { AccessibilityScanner, createScanner } from './scanner'; -export type { - ScanOptions, - ScanResult, - ViolationDetail, - NodeDetail, - PassDetail, - IncompleteDetail, - InapplicableDetail, -} from './scanner'; diff --git a/tools/stale/packages/scanner/src/scanner.ts b/tools/stale/packages/scanner/src/scanner.ts deleted file mode 100644 index 27b0e16..0000000 --- a/tools/stale/packages/scanner/src/scanner.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Accessibility Scanner — Multi-Engine Web Audit Kernel. - * - * This module implements the automated accessibility scanning engine for - * the Accessibility Everywhere project. it orchestrates headless browsers - * (Puppeteer or Playwright) to execute the `axe-core` ruleset against - * target URLs. - * - * KEY FEATURES: - * 1. **Multi-Engine Support**: Seamlessly switches between Puppeteer - * and Playwright based on environment capabilities. - * 2. **WCAG Tiering**: Supports auditing against WCAG 2.1/2.2 at - * A, AA, and AAA levels. - * 3. **Weighted Scoring**: Calculates a normalized accessibility score - * based on the impact and frequency of violations. - * 4. **Forensics**: Captures full-page screenshots and detailed - * HTML node selectors for identified issues. - */ - -import { Browser, Page, launch } from 'puppeteer'; -import { chromium, Browser as PlaywrightBrowser, Page as PlaywrightPage } from 'playwright'; -import * as axe from 'axe-core'; -import * as fs from 'fs'; -// ... [other imports] - -export class AccessibilityScanner { - private axeSource: string; - - constructor() { - // BOOTSTRAP: Synchronously loads the minified axe-core kernel - // for injection into the browser context. - this.axeSource = fs.readFileSync(require.resolve('axe-core/axe.min.js'), 'utf8'); - } - - /** - * SCAN: The primary entry point for single-URL auditing. - * Dispatches to the engine-specific runner (Puppeteer/Playwright). - */ - async scan(options: ScanOptions): Promise { - const startTime = Date.now(); - const engine = options.engine || 'puppeteer'; - // ... [Engine dispatch logic] - } - - /** - * PUPPETEER RUNNER: Implements the audit pipeline using Chromium. - * - * SEQUENCE: - * 1. SPAWN: Launch headless browser with security sandboxing disabled - * for container compatibility. - * 2. NAVIGATE: Go to target URL and wait for network stability. - * 3. INJECT: Execute the `axeSource` string within the page. - * 4. EXECUTE: Run `axe.run` with the requested WCAG tags. - * 5. CAPTURE: Record metadata and optional base64 screenshots. - */ - private async scanWithPuppeteer(options: ScanOptions, startTime: number): Promise { - // ... [Implementation using page.evaluate] - } - - /** - * SCORING KERNEL: Computes a safety percentage from 0 to 100. - * - * WEIGHTS: - * - Critical: 10 points penalty per node. - * - Serious: 5 points penalty. - * - Moderate: 3 points penalty. - * - Minor: 1 point penalty. - */ - private calculateScore(axeResults: any): number { - // ... [Heuristic calculation logic] - } -} diff --git a/tools/stale/packages/stale/components/react/src/Button/Button.tsx b/tools/stale/packages/stale/components/react/src/Button/Button.tsx deleted file mode 100644 index baac687..0000000 --- a/tools/stale/packages/stale/components/react/src/Button/Button.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { forwardRef } from 'react'; - -export interface ButtonProps extends React.ButtonHTMLAttributes { - /** Button variant */ - variant?: 'primary' | 'secondary' | 'ghost' | 'danger'; - /** Button size */ - size?: 'sm' | 'md' | 'lg'; - /** Loading state */ - loading?: boolean; - /** Icon to display before text */ - iconBefore?: React.ReactNode; - /** Icon to display after text */ - iconAfter?: React.ReactNode; -} - -/** - * Accessible button component following WCAG 2.1 AA standards - * - * Features: - * - Proper focus management - * - Keyboard navigation - * - Screen reader support - * - Loading states with ARIA - * - Disabled state handling - * - * @example - * - */ -export const Button = forwardRef( - ( - { - variant = 'primary', - size = 'md', - loading = false, - disabled = false, - iconBefore, - iconAfter, - children, - className = '', - type = 'button', - ...props - }, - ref - ) => { - const baseClasses = 'a11y-button'; - const variantClasses = `a11y-button--${variant}`; - const sizeClasses = `a11y-button--${size}`; - const loadingClasses = loading ? 'a11y-button--loading' : ''; - const classes = `${baseClasses} ${variantClasses} ${sizeClasses} ${loadingClasses} ${className}`.trim(); - - return ( - - ); - } -); - -Button.displayName = 'Button'; diff --git a/tools/stale/packages/stale/components/react/src/Modal/Modal.tsx b/tools/stale/packages/stale/components/react/src/Modal/Modal.tsx deleted file mode 100644 index 19d6f3c..0000000 --- a/tools/stale/packages/stale/components/react/src/Modal/Modal.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { createPortal } from 'react-dom'; -import { useId } from '@reach/auto-id'; - -export interface ModalProps { - /** Whether modal is open */ - isOpen: boolean; - /** Callback when modal should close */ - onClose: () => void; - /** Modal title */ - title: string; - /** Modal content */ - children: React.ReactNode; - /** Modal size */ - size?: 'sm' | 'md' | 'lg' | 'full'; - /** Close on overlay click */ - closeOnOverlayClick?: boolean; - /** Close on Escape key */ - closeOnEscape?: boolean; - /** Initial focus element */ - initialFocusRef?: React.RefObject; - /** Custom className */ - className?: string; -} - -/** - * Accessible modal dialog following WCAG 2.1 AA standards - * - * Features: - * - Focus trap (keeps focus inside modal) - * - Focus restoration (returns to trigger on close) - * - Escape key handling - * - Overlay click handling - * - ARIA labels and roles - * - Screen reader announcements - * - Scroll locking - * - * @example - * setIsOpen(false)} - * title="Delete Account" - * > - *

Are you sure?

- *
- */ -export function Modal({ - isOpen, - onClose, - title, - children, - size = 'md', - closeOnOverlayClick = true, - closeOnEscape = true, - initialFocusRef, - className = '', -}: ModalProps) { - const titleId = useId(); - const contentId = useId(); - const overlayRef = useRef(null); - const modalRef = useRef(null); - const previousFocusRef = useRef(null); - const [mounted, setMounted] = useState(false); - - // Portal mounting - useEffect(() => { - setMounted(true); - return () => setMounted(false); - }, []); - - // Focus management - useEffect(() => { - if (!isOpen) return; - - // Store current focus - previousFocusRef.current = document.activeElement as HTMLElement; - - // Focus initial element or first focusable element - const focusElement = initialFocusRef?.current || getFirstFocusable(modalRef.current); - focusElement?.focus(); - - // Restore focus on unmount - return () => { - previousFocusRef.current?.focus(); - }; - }, [isOpen, initialFocusRef]); - - // Escape key handler - useEffect(() => { - if (!isOpen || !closeOnEscape) return; - - const handleEscape = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - - document.addEventListener('keydown', handleEscape); - return () => document.removeEventListener('keydown', handleEscape); - }, [isOpen, closeOnEscape, onClose]); - - // Focus trap - useEffect(() => { - if (!isOpen) return; - - const handleTab = (e: KeyboardEvent) => { - if (e.key !== 'Tab' || !modalRef.current) return; - - const focusableElements = getFocusableElements(modalRef.current); - if (focusableElements.length === 0) return; - - const firstElement = focusableElements[0]; - const lastElement = focusableElements[focusableElements.length - 1]; - - if (e.shiftKey) { - // Shift + Tab - if (document.activeElement === firstElement) { - e.preventDefault(); - lastElement.focus(); - } - } else { - // Tab - if (document.activeElement === lastElement) { - e.preventDefault(); - firstElement.focus(); - } - } - }; - - document.addEventListener('keydown', handleTab); - return () => document.removeEventListener('keydown', handleTab); - }, [isOpen]); - - // Scroll lock - useEffect(() => { - if (!isOpen) return; - - const originalOverflow = document.body.style.overflow; - document.body.style.overflow = 'hidden'; - - return () => { - document.body.style.overflow = originalOverflow; - }; - }, [isOpen]); - - // Overlay click handler - const handleOverlayClick = (e: React.MouseEvent) => { - if (closeOnOverlayClick && e.target === overlayRef.current) { - onClose(); - } - }; - - if (!isOpen || !mounted) return null; - - const modal = ( -
-
-
-

- {title} -

- -
-
- {children} -
-
-
- ); - - return createPortal(modal, document.body); -} - -// Helper functions -function getFocusableElements(container: HTMLElement | null): HTMLElement[] { - if (!container) return []; - - const selector = [ - 'button:not([disabled])', - '[href]', - 'input:not([disabled])', - 'select:not([disabled])', - 'textarea:not([disabled])', - '[tabindex]:not([tabindex="-1"])', - ].join(','); - - return Array.from(container.querySelectorAll(selector)); -} - -function getFirstFocusable(container: HTMLElement | null): HTMLElement | null { - const elements = getFocusableElements(container); - return elements[0] || null; -} diff --git a/tools/stale/packages/stale/components/react/src/index.ts b/tools/stale/packages/stale/components/react/src/index.ts deleted file mode 100644 index 76a0a7e..0000000 --- a/tools/stale/packages/stale/components/react/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Core Components -export { Button } from './Button/Button'; -export type { ButtonProps } from './Button/Button'; - -export { Modal } from './Modal/Modal'; -export type { ModalProps } from './Modal/Modal'; - -// Component documentation and version -export const version = '1.0.0'; -export const wcagLevel = 'AA';