Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | 3x 1x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 6x 6x 6x 6x 6x 6x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 1x 3x 2x 2x 3x 1x 1x 1x 1x | import type { GraphModel, GraphNode } from "./graph-model.js" /** Canonical language key from a repo-relative file path. */ export function languageFromPath(filePath: string): string { const base = filePath.split("/").pop() ?? filePath const dot = base.lastIndexOf(".") if (dot < 0) { if (base === "Dockerfile" || base.startsWith("Dockerfile.")) return "docker" if (base === "Makefile") return "make" return "other" } const ext = base.slice(dot + 1).toLowerCase() const map: Record<string, string> = { ts: "typescript", tsx: "typescript", mts: "typescript", cts: "typescript", js: "javascript", jsx: "javascript", mjs: "javascript", cjs: "javascript", py: "python", pyi: "python", md: "markdown", mdx: "markdown", json: "json", yaml: "yaml", yml: "yaml", css: "css", scss: "scss", html: "html", sql: "sql", sh: "shell", bash: "shell", zsh: "shell", rs: "rust", go: "go", java: "java", kt: "kotlin", swift: "swift", rb: "ruby", php: "php", vue: "vue", svelte: "svelte", } return map[ext] ?? ext } const LANGUAGE_COLORS: Record<string, string> = { typescript: "#3b82f6", javascript: "#eab308", python: "#22c55e", markdown: "#94a3b8", json: "#f97316", yaml: "#a855f7", css: "#ec4899", scss: "#f472b6", html: "#f87171", sql: "#06b6d4", shell: "#84cc16", rust: "#f97316", go: "#38bdf8", java: "#ef4444", kotlin: "#a78bfa", swift: "#fb7185", ruby: "#dc2626", php: "#8b5cf6", vue: "#10b981", svelte: "#f43f5e", docker: "#2563eb", make: "#64748b", other: "#64748b", } /** Stable accent per language — known palette first, else deterministic HSL. */ export function colorForLanguage(language: string): string { const known = LANGUAGE_COLORS[language] if (known) { return known } let hash = 0 for (let i = 0; i < language.length; i++) { hash = (hash * 33 + language.charCodeAt(i)) | 0 } const hue = Math.abs(hash) % 360 return `hsl(${hue}, 58%, 56%)` } export function enrichImportGraphNode(node: { id: string; label: string }): GraphNode { const fileType = languageFromPath(node.id) return { id: node.id, label: node.label, fileType, color: colorForLanguage(fileType), } } export type FileTypeLegendEntry = { type: string color: string count: number } export function buildFileTypeLegend(model: GraphModel): FileTypeLegendEntry[] { const counts = new Map<string, { color: string; count: number }>() for (const node of model.nodes) { const type = node.fileType ?? languageFromPath(node.id) const color = node.color ?? colorForLanguage(type) const prev = counts.get(type) if (prev) { prev.count++ } else { counts.set(type, { color, count: 1 }) } } return [...counts.entries()] .map(([type, { color, count }]) => ({ type, color, count })) .sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)) } |