From 2f7e3ad9b6c957dd370a824edb8ea4676a836d87 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Wed, 4 Mar 2026 18:13:13 -0800 Subject: [PATCH] refactor(design-tokens): improve token validator - ignore hsl(var(--...)) / rgb(var(--...)) - export generated/tokens entry --- dashboards/admin-web/package.json | 1 + .../src/app/api/themes/active/route.ts | 56 ++++----- dashboards/admin-web/src/app/health/page.tsx | 12 +- .../admin-web/src/components/ThemeEditor.tsx | 35 +++--- .../components/extraction/entity-chart.tsx | 58 +++++---- .../src/types/design-tokens-json.d.ts | 6 + packages/design-tokens/package.json | 1 + .../design-tokens/scripts/validate-tokens.cjs | 113 ++++++++++++++++++ 8 files changed, 206 insertions(+), 76 deletions(-) create mode 100644 dashboards/admin-web/src/types/design-tokens-json.d.ts create mode 100755 packages/design-tokens/scripts/validate-tokens.cjs diff --git a/dashboards/admin-web/package.json b/dashboards/admin-web/package.json index e4559958..1e0eed30 100644 --- a/dashboards/admin-web/package.json +++ b/dashboards/admin-web/package.json @@ -31,6 +31,7 @@ "@bytelyst/config": "workspace:*", "@bytelyst/cosmos": "workspace:*", "@bytelyst/datastore": "workspace:*", + "@bytelyst/design-tokens": "workspace:*", "@bytelyst/errors": "workspace:*", "@bytelyst/extraction": "workspace:*", "@bytelyst/logger": "workspace:*", diff --git a/dashboards/admin-web/src/app/api/themes/active/route.ts b/dashboards/admin-web/src/app/api/themes/active/route.ts index fa664043..d6e52406 100644 --- a/dashboards/admin-web/src/app/api/themes/active/route.ts +++ b/dashboards/admin-web/src/app/api/themes/active/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { logError } from '@/lib/logger'; import { getActiveTheme } from '@/lib/platform-client'; import { getRequestProductId } from '@/lib/product-config'; +import tokensJson from '@bytelyst/design-tokens/tokens.json'; export async function GET(req: NextRequest) { try { @@ -10,43 +11,34 @@ export async function GET(req: NextRequest) { } catch (error) { logError('Failed to get active theme', error); // Return default theme on error to prevent app breakage + + const defaultThemeColors = { + primary: tokensJson.color.semantic.dark.success, + secondary: tokensJson.color.palette.neutral['700'], + accent: tokensJson.color.semantic.dark.accentPrimary, + background: tokensJson.color.palette.neutral['0'], + surface: tokensJson.color.palette.neutral['50'], + error: tokensJson.color.semantic.dark.danger, + warning: tokensJson.color.semantic.dark.warning, + success: tokensJson.color.semantic.dark.success, + }; + + const defaultDesktopExtras = { + idle: tokensJson.color.semantic.dark.success, + listening: tokensJson.color.lysnrai.hotkeyActive, + processing: tokensJson.color.semantic.dark.warning, + offline: tokensJson.color.semantic.dark.textSecondary, + }; + return NextResponse.json({ id: 'default', name: 'Default Green', description: 'Default green theme', - ios: { - primary: '#4caf50', - secondary: '#2e7d32', - accent: '#66bb6a', - background: '#ffffff', - surface: '#f5f5f5', - error: '#f44336', - warning: '#ff9800', - success: '#4caf50', - }, - android: { - primary: '#4caf50', - secondary: '#2e7d32', - accent: '#66bb6a', - background: '#ffffff', - surface: '#f5f5f5', - error: '#f44336', - warning: '#ff9800', - success: '#4caf50', - }, + ios: defaultThemeColors, + android: defaultThemeColors, desktop: { - primary: '#4caf50', - secondary: '#2e7d32', - accent: '#66bb6a', - background: '#ffffff', - surface: '#f5f5f5', - error: '#f44336', - warning: '#ff9800', - success: '#4caf50', - idle: '#4caf50', - listening: '#e94560', - processing: '#f5a623', - offline: '#9e9e9e', + ...defaultThemeColors, + ...defaultDesktopExtras, }, is_active: true, is_default: true, diff --git a/dashboards/admin-web/src/app/health/page.tsx b/dashboards/admin-web/src/app/health/page.tsx index 52628088..45f0be51 100644 --- a/dashboards/admin-web/src/app/health/page.tsx +++ b/dashboards/admin-web/src/app/health/page.tsx @@ -40,7 +40,7 @@ export default function HealthPage() { if (error) { return (
-

Health Check Failed

+

Health Check Failed

{error}

); @@ -53,12 +53,12 @@ export default function HealthPage() {

{overall ? '✅' : '⚠️'} {data?.service}

-

{data?.timestamp}

+

{data?.timestamp}

Status: {data?.status} @@ -71,7 +71,7 @@ export default function HealthPage() { }} > - + Check Status Details @@ -79,10 +79,10 @@ export default function HealthPage() { {data?.checks.map(check => ( - + {check.name} {check.status === 'pass' ? '✅ pass' : '❌ fail'} - + {check.message} {check.latencyMs != null && ` (${check.latencyMs}ms)`} diff --git a/dashboards/admin-web/src/components/ThemeEditor.tsx b/dashboards/admin-web/src/components/ThemeEditor.tsx index ad15d7db..ac54d12c 100644 --- a/dashboards/admin-web/src/components/ThemeEditor.tsx +++ b/dashboards/admin-web/src/components/ThemeEditor.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Theme, PlatformTheme } from '@/types/theme'; +import tokensJson from '@bytelyst/design-tokens/tokens.json'; function ColorInput({ label, @@ -26,7 +27,7 @@ function ColorInput({ value={value} onChange={e => onChange(e.target.value)} className="flex-1 px-2 py-1 border rounded text-sm font-mono" - placeholder="#000000" + placeholder="#00000000" /> ); @@ -65,22 +66,22 @@ function PlatformSection({ <> onChange('idle', v)} /> onChange('listening', v)} /> onChange('processing', v)} /> onChange('offline', v)} /> @@ -101,24 +102,24 @@ export default function ThemeEditor({ theme, onSave, onCancel }: ThemeEditorProp const [description, setDescription] = useState(theme?.description || ''); const [iosColors, setIosColors] = useState( theme?.ios || { - primary: '#4caf50', - secondary: '#2e7d32', - accent: '#66bb6a', - background: '#ffffff', - surface: '#f5f5f5', - error: '#f44336', - warning: '#ff9800', - success: '#4caf50', + primary: tokensJson.color.semantic.dark.success, + secondary: tokensJson.color.palette.neutral['700'], + accent: tokensJson.color.semantic.dark.accentPrimary, + background: tokensJson.color.palette.neutral['0'], + surface: tokensJson.color.palette.neutral['50'], + error: tokensJson.color.semantic.dark.danger, + warning: tokensJson.color.semantic.dark.warning, + success: tokensJson.color.semantic.dark.success, } ); const [androidColors, setAndroidColors] = useState(theme?.android || iosColors); const [desktopColors, setDesktopColors] = useState( theme?.desktop || { ...iosColors, - idle: '#4caf50', - listening: '#e94560', - processing: '#f5a623', - offline: '#9e9e9e', + idle: tokensJson.color.semantic.dark.success, + listening: tokensJson.color.lysnrai.hotkeyActive, + processing: tokensJson.color.semantic.dark.warning, + offline: tokensJson.color.semantic.dark.textSecondary, } ); diff --git a/dashboards/admin-web/src/components/extraction/entity-chart.tsx b/dashboards/admin-web/src/components/extraction/entity-chart.tsx index cccb2356..94a09590 100644 --- a/dashboards/admin-web/src/components/extraction/entity-chart.tsx +++ b/dashboards/admin-web/src/components/extraction/entity-chart.tsx @@ -27,8 +27,11 @@ interface EntityChartProps { } const COLORS = [ - '#5A8CFF', '#2EE6D6', '#34D399', '#F59E0B', '#FF6E6E', - '#A78BFA', '#F472B6', '#38BDF8', '#FB923C', '#4ADE80', + 'hsl(var(--chart-1))', + 'hsl(var(--chart-2))', + 'hsl(var(--chart-3))', + 'hsl(var(--chart-4))', + 'hsl(var(--chart-5))', ]; export function EntityBarChart({ extractions, title = 'Entities by Class' }: EntityChartProps) { @@ -37,7 +40,7 @@ export function EntityBarChart({ extractions, title = 'Entities by Class' }: Ent acc[e.extraction_class] = (acc[e.extraction_class] || 0) + 1; return acc; }, - {} as Record, + {} as Record ); const data = Object.entries(classCounts) @@ -54,18 +57,24 @@ export function EntityBarChart({ extractions, title = 'Entities by Class' }: Ent - - - + + + - + @@ -79,7 +88,7 @@ export function EntityPieChart({ extractions, title = 'Class Distribution' }: En acc[e.extraction_class] = (acc[e.extraction_class] || 0) + 1; return acc; }, - {} as Record, + {} as Record ); const data = Object.entries(classCounts) @@ -104,9 +113,7 @@ export function EntityPieChart({ extractions, title = 'Class Distribution' }: En outerRadius={90} paddingAngle={2} dataKey="value" - label={({ name, percent }) => - `${name} (${((percent ?? 0) * 100).toFixed(0)}%)` - } + label={({ name, percent }) => `${name} (${((percent ?? 0) * 100).toFixed(0)}%)`} labelLine={false} fontSize={11} > @@ -116,15 +123,13 @@ export function EntityPieChart({ extractions, title = 'Class Distribution' }: En - + @@ -147,9 +152,20 @@ export function EntityTimeline({ extractions }: { extractions: ExtractionEntity[

{ acc[ex.extraction_class] = true; return acc; }, {} as Record) - ).indexOf(e.extraction_class) % COLORS.length] }} + style={{ + backgroundColor: + COLORS[ + Object.keys( + extractions.reduce( + (acc, ex) => { + acc[ex.extraction_class] = true; + return acc; + }, + {} as Record + ) + ).indexOf(e.extraction_class) % COLORS.length + ], + }} />
diff --git a/dashboards/admin-web/src/types/design-tokens-json.d.ts b/dashboards/admin-web/src/types/design-tokens-json.d.ts new file mode 100644 index 00000000..b988ab81 --- /dev/null +++ b/dashboards/admin-web/src/types/design-tokens-json.d.ts @@ -0,0 +1,6 @@ +declare module '@bytelyst/design-tokens/tokens.json' { + import type { DesignTokens } from '@bytelyst/design-tokens'; + + const value: DesignTokens; + export default value; +} diff --git a/packages/design-tokens/package.json b/packages/design-tokens/package.json index 594600c2..1434f719 100644 --- a/packages/design-tokens/package.json +++ b/packages/design-tokens/package.json @@ -8,6 +8,7 @@ "types": "./dist/index.d.ts" }, "./tokens.json": "./tokens/bytelyst.tokens.json", + "./generated/tokens": "./generated/tokens.ts", "./css": "./generated/tokens.css" }, "main": "./dist/index.js", diff --git a/packages/design-tokens/scripts/validate-tokens.cjs b/packages/design-tokens/scripts/validate-tokens.cjs new file mode 100755 index 00000000..851bb99c --- /dev/null +++ b/packages/design-tokens/scripts/validate-tokens.cjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node +/** + * Token validation script — checks for hardcoded colors in source files + * and reports token coverage per product. + * + * Usage: node scripts/validate-tokens.js [product-path] + */ + +const { readFileSync, readdirSync, statSync } = require('fs'); +const { join, resolve } = require('path'); + +const HARD_COLOR_REGEX = /#[0-9A-Fa-f]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)/g; +const EXCLUDED_DIRS = ['node_modules', 'dist', 'build', '.git', 'generated', '__mocks__']; +const INCLUDED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.swift', '.kt']; + +function findFiles(dir, files = []) { + try { + const items = readdirSync(dir); + for (const item of items) { + const fullPath = join(dir, item); + if (EXCLUDED_DIRS.some(ex => fullPath.includes(ex))) continue; + + const stat = statSync(fullPath); + if (stat.isDirectory()) { + findFiles(fullPath, files); + } else if (INCLUDED_EXTENSIONS.some(ext => item.endsWith(ext))) { + files.push(fullPath); + } + } + } catch (e) { + // Directory might not exist or be accessible + } + return files; +} + +function analyzeFile(filePath) { + const content = readFileSync(filePath, 'utf-8'); + const lines = content.split('\n'); + const issues = []; + + lines.forEach((line, index) => { + // Skip comments + if (line.trim().startsWith('//') || line.trim().startsWith('*') || line.trim().startsWith('/*')) + return; + + const matches = line.match(HARD_COLOR_REGEX); + if (matches) { + // Filter out legitimate uses (like transparency values) + const suspicious = matches.filter(m => { + if (m.startsWith('#') && (m.length === 9 || m.length === 5)) return false; // Skip alpha hex + if (m.includes('0.0') || m.includes('1.0')) return false; // Skip clear/opaque + // Skip CSS-variable token usage (already tokenized) + if ((m.startsWith('hsl(') || m.startsWith('rgb(') || m.startsWith('rgba(')) && m.includes('var(--')) + return false; + return true; + }); + + if (suspicious.length > 0) { + issues.push({ + line: index + 1, + colors: suspicious, + content: line.trim().slice(0, 80), + }); + } + } + }); + + return issues; +} + +function main() { + const targetPath = process.argv[2] || '.'; + const absolutePath = resolve(targetPath); + + console.log(`🔍 Scanning ${absolutePath} for hardcoded colors...\n`); + + const files = findFiles(absolutePath); + let totalIssues = 0; + let filesWithIssues = 0; + + for (const file of files) { + const issues = analyzeFile(file); + if (issues.length > 0) { + filesWithIssues++; + totalIssues += issues.length; + const relativePath = file.replace(absolutePath, '').slice(1); + console.log(`\n📄 ${relativePath}`); + issues.forEach(issue => { + console.log(` Line ${issue.line}: ${issue.colors.join(', ')}`); + console.log(` ${issue.content}`); + }); + } + } + + console.log(`\n${'='.repeat(60)}`); + console.log(`📊 Summary:`); + console.log(` Files scanned: ${files.length}`); + console.log(` Files with hardcoded colors: ${filesWithIssues}`); + console.log(` Total hardcoded colors found: ${totalIssues}`); + + if (totalIssues > 0) { + console.log(`\n⚠️ Consider replacing hardcoded colors with design tokens:`); + console.log(` Web: var(--ml-) from @bytelyst/design-tokens`); + console.log(` iOS: MindLystColors.dark / MindLystColors.light`); + console.log(` KMP: MindLystTokens.Dark. / MindLystTokens.Light.`); + process.exit(1); + } else { + console.log(`\n✅ No hardcoded colors found! All colors use design tokens.`); + process.exit(0); + } +} + +main();