import { Skill } from '@/types/skills'; export type CategoryCounts = Record; export function getCategoryCounts(skills: Skill[]): CategoryCounts { const counts: CategoryCounts = { all: skills.length }; for (const skill of skills) { const key = skill.category.toLowerCase(); counts[key] = (counts[key] ?? 0) + 1; } return counts; } export function getCategoryCount(counts: CategoryCounts, category: string): number { if (category === 'all') return counts.all ?? 0; return counts[category.toLowerCase()] ?? 0; } export const CATEGORY_COLORS: Record = { workflow: { bg: 'bg-violet-100 dark:bg-violet-900/30', text: 'text-violet-700 dark:text-violet-300', border: 'border-violet-200 dark:border-violet-800' }, frontend: { bg: 'bg-cyan-100 dark:bg-cyan-900/30', text: 'text-cyan-700 dark:text-cyan-300', border: 'border-cyan-200 dark:border-cyan-800' }, backend: { bg: 'bg-emerald-100 dark:bg-emerald-900/30', text: 'text-emerald-700 dark:text-emerald-300', border: 'border-emerald-200 dark:border-emerald-800' }, mobile: { bg: 'bg-orange-100 dark:bg-orange-900/30', text: 'text-orange-700 dark:text-orange-300', border: 'border-orange-200 dark:border-orange-800' }, platform: { bg: 'bg-indigo-100 dark:bg-indigo-900/30', text: 'text-indigo-700 dark:text-indigo-300', border: 'border-indigo-200 dark:border-indigo-800' }, infrastructure: { bg: 'bg-slate-100 dark:bg-slate-900/30', text: 'text-slate-700 dark:text-slate-300', border: 'border-slate-200 dark:border-slate-800' }, security: { bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-700 dark:text-red-300', border: 'border-red-200 dark:border-red-800' }, 'mcp-agents': { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300', border: 'border-blue-200 dark:border-blue-800' }, documentation: { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-300', border: 'border-amber-200 dark:border-amber-800' }, creative: { bg: 'bg-pink-100 dark:bg-pink-900/30', text: 'text-pink-700 dark:text-pink-300', border: 'border-pink-200 dark:border-pink-800' }, meta: { bg: 'bg-teal-100 dark:bg-teal-900/30', text: 'text-teal-700 dark:text-teal-300', border: 'border-teal-200 dark:border-teal-800' }, openspec: { bg: 'bg-sky-100 dark:bg-sky-900/30', text: 'text-sky-700 dark:text-sky-300', border: 'border-sky-200 dark:border-sky-800' }, mioflow: { bg: 'bg-fuchsia-100 dark:bg-fuchsia-900/30', text: 'text-fuchsia-700 dark:text-fuchsia-300', border: 'border-fuchsia-200 dark:border-fuchsia-800' }, 'mio-awesome': { bg: 'bg-rose-100 dark:bg-rose-900/30', text: 'text-rose-700 dark:text-rose-300', border: 'border-rose-200 dark:border-rose-800' }, uncategorized: { bg: 'bg-gray-100 dark:bg-gray-900/30', text: 'text-gray-700 dark:text-gray-300', border: 'border-gray-200 dark:border-gray-800' }, }; export function getCategoryStyle(category: string) { const key = category.toLowerCase(); return CATEGORY_COLORS[key] || CATEGORY_COLORS['uncategorized']; } function hashCategoryKey(key: string): number { let hash = 0; for (let i = 0; i < key.length; i++) { hash = (hash * 31 + key.charCodeAt(i)) >>> 0; } return hash; } export type CategoryColorTheme = { dot: string; badgeBg: string; badgeText: string; badgeBorder: string; }; type CategoryHsl = { h: number; s: number }; /** Curated “twilight” palette — fixed hues for known categories, fallbacks for the rest */ const CATEGORY_HSL_MAP: Record = { 'ai-ml': { h: 278, s: 72 }, backend: { h: 158, s: 58 }, database: { h: 204, s: 68 }, 'dev-tools': { h: 28, s: 82 }, frameworks: { h: 318, s: 68 }, frontend: { h: 182, s: 70 }, infrastructure: { h: 215, s: 48 }, multimedia: { h: 12, s: 78 }, other: { h: 48, s: 42 }, security: { h: 355, s: 74 }, uncategorized: { h: 220, s: 14 }, utilities: { h: 252, s: 66 }, engineering: { h: 228, s: 70 }, mobile: { h: 38, s: 76 }, devops: { h: 200, s: 52 }, documentation: { h: 44, s: 72 }, creative: { h: 308, s: 70 }, }; const FALLBACK_HSL_PALETTE: CategoryHsl[] = [ { h: 10, s: 78 }, { h: 345, s: 72 }, { h: 292, s: 68 }, { h: 262, s: 74 }, { h: 232, s: 76 }, { h: 198, s: 80 }, { h: 168, s: 62 }, { h: 138, s: 56 }, { h: 108, s: 58 }, { h: 78, s: 64 }, { h: 52, s: 74 }, { h: 22, s: 80 }, ]; const LIGHT_CANVAS = '#ffffff'; const DARK_CANVAS = 'hsl(222 47% 10%)'; const LIGHT_INK = '#0f172a'; const DARK_INK = '#f1f5f9'; function mixDotInto(canvas: string, dot: string, amount: string): string { return `color-mix(in srgb, ${dot} ${amount}, ${canvas})`; } function resolveCategoryHsl(key: string): CategoryHsl { if (CATEGORY_HSL_MAP[key]) return CATEGORY_HSL_MAP[key]; const idx = hashCategoryKey(key) % FALLBACK_HSL_PALETTE.length; return FALLBACK_HSL_PALETTE[idx]; } /** Deterministic theme — dot + badge tinted from the same curated color */ export function getCategoryColorTheme(category: string, isDark = false): CategoryColorTheme { const key = category.toLowerCase(); const canvas = isDark ? DARK_CANVAS : LIGHT_CANVAS; const ink = isDark ? DARK_INK : LIGHT_INK; let dot: string; if (key === 'all') { dot = isDark ? 'hsl(220 14% 58%)' : 'hsl(220 12% 46%)'; } else { const { h, s } = resolveCategoryHsl(key); const sat = isDark ? Math.max(s - 6, 48) : s; const light = isDark ? 70 + (hashCategoryKey(`${key}:d`) % 8) : 46 + (hashCategoryKey(`${key}:l`) % 6); dot = `hsl(${h} ${sat}% ${light}%)`; } const bgMix = isDark ? '40%' : '26%'; const borderMix = isDark ? '58%' : '45%'; const textMix = isDark ? '80%' : '70%'; return { dot, badgeBg: mixDotInto(canvas, dot, bgMix), badgeText: mixDotInto(ink, dot, textMix), badgeBorder: mixDotInto(canvas, dot, borderMix), }; } export function getCategoryDotColor(category: string): string { return getCategoryColorTheme(category).dot; } export function formatCategoryName(category: string): string { return category .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } export function getSkillPath(skill: Skill): string { // Return the path that will be used for fetching skill content return `/api/skills/${skill.relativePath}`; }