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();