// ============================================================================ // Core data types matching .beads/issues.jsonl schema // ============================================================================ export interface BeadDependency { issue_id: string; depends_on_id: string; type: "blocks" | "parent-child" | "relates_to"; created_at: string; created_by?: string; metadata?: string; } export interface BeadIssue { id: string; title: string; description?: string; status: "open" | "in_progress" | "blocked" | "deferred" | "closed"; priority: number; // 0 (critical) to 4 (backlog) issue_type: "task" | "bug" | "feature" | "chore" | "epic"; owner?: string; assignee?: string; labels?: string[]; created_at: string; created_by?: string; updated_at: string; closed_at?: string; close_reason?: string; acceptance_criteria?: string; estimated_minutes?: number; dependencies?: BeadDependency[]; } // ============================================================================ // Graph data types for visualization // ============================================================================ export interface GraphNode { [key: string]: unknown; // index signature for react-force-graph compatibility id: string; title: string; description?: string; status: BeadIssue["status"]; priority: number; issueType: BeadIssue["issue_type"]; owner?: string; assignee?: string; createdBy?: string; createdAt: string; updatedAt: string; closedAt?: string; closeReason?: string; acceptanceCriteria?: string; estimatedMinutes?: number; prefix: string; // e.g. "my-app", "backend", "frontend" labels: string[]; // free-form tags from issue, e.g. ["mobile", "ux", "build"] // Computed blockerCount: number; // issues this blocks dependentCount: number; // issues that depend on this blockerIds: string[]; // IDs of issues this blocks dependentIds: string[]; // IDs of issues blocking this // Force-graph position (set by simulation) x?: number; y?: number; fx?: number; fy?: number; // Animation metadata (set by live-update merge logic, consumed by paintNode) _spawnTime?: number; // Date.now() when this node first appeared (for pop-in animation) _removeTime?: number; // Date.now() when this node was marked for removal (for shrink-out) _changedAt?: number; // Date.now() when status/priority changed (for ripple animation) _prevStatus?: string; // Previous status value before the change (for color transition) } // ============================================================================ // Color mode for legend/node fill switching // ============================================================================ export type ColorMode = "status" | "priority" | "type" | "owner" | "assignee" | "prefix"; export const COLOR_MODE_LABELS: Record = { status: "Status", priority: "Priority", type: "Type", owner: "Owner", assignee: "Assignee", prefix: "Prefix", }; export interface GraphLink { source: string; target: string; type: "blocks" | "parent-child" | "relates_to"; createdAt?: string; // ISO 8601 from BeadDependency.created_at (for timeline replay) // Animation metadata (set by live-update merge logic, consumed by paintLink) _spawnTime?: number; // Date.now() when this link first appeared (for fade-in animation) _removeTime?: number; // Date.now() when this link was marked for removal (for fade-out) } export interface GraphData { nodes: GraphNode[]; links: GraphLink[]; } export interface BeadsApiResponse { issues: BeadIssue[]; dependencies: BeadDependency[]; graphData: GraphData; stats: { total: number; open: number; inProgress: number; blocked: number; closed: number; actionable: number; edges: number; prefixes: string[]; }; } // ============================================================================ // Status and priority display helpers // ============================================================================ export const STATUS_COLORS: Record = { open: "#10b981", // emerald-500 in_progress: "#f59e0b", // amber-500 blocked: "#ef4444", // red-500 deferred: "#8b5cf6", // violet-500 closed: "#a1a1aa", // zinc-400 }; export const STATUS_LABELS: Record = { open: "Open", in_progress: "In Progress", blocked: "Blocked", deferred: "Deferred", closed: "Closed", }; export const PRIORITY_LABELS: Record = { 0: "P0 - Critical", 1: "P1 - High", 2: "P2 - Medium", 3: "P3 - Low", 4: "P4 - Backlog", }; export const PRIORITY_COLORS: Record = { 0: "#ef4444", // red 1: "#f97316", // orange 2: "#3b82f6", // blue 3: "#a1a1aa", // zinc 4: "#d4d4d8", // zinc-300 }; // ============================================================================ // Catppuccin Latte accent palette // ============================================================================ /** Catppuccin Latte accent colors — 85 colors for collision-free prefix mapping */ export const CATPPUCCIN_ACCENTS: string[] = [ // Original 14 Catppuccin Latte accents "#d20f39", // Red "#179299", // Teal "#fe640b", // Peach "#1e66f5", // Blue "#40a02b", // Green "#8839ef", // Mauve "#df8e1d", // Yellow "#209fb5", // Sapphire "#ea76cb", // Pink "#04a5e5", // Sky "#e64553", // Maroon "#7287fd", // Lavender "#dd7878", // Flamingo "#dc8a78", // Rosewater // Extended palette (71 additional colors) "#862d2d", "#bf4040", "#d27979", "#931f1f", "#d22d2d", "#e06c6c", "#a11212", "#e61919", "#ed5e5e", "#86592d", "#bf8040", "#d2a679", "#93591f", "#d2802d", "#e0a66c", "#a15912", "#e68019", "#eda65e", "#86862d", "#bfbf40", "#d2d279", "#93931f", "#d2d22d", "#e0e06c", "#a1a112", "#e6e619", "#eded5e", "#59862d", "#80bf40", "#a6d279", "#59931f", "#80d22d", "#a6e06c", "#59a112", "#80e619", "#a6ed5e", "#2d862d", "#40bf40", "#79d279", "#1f931f", "#2dd22d", "#6ce06c", "#12a112", "#19e619", "#5eed5e", "#2d8659", "#40bf80", "#79d2a6", "#1f9359", "#2dd280", "#6ce0a6", "#12a159", "#19e680", "#5eeda6", "#2d8686", "#40bfbf", "#79d2d2", "#1f9393", "#2dd2d2", "#6ce0e0", "#12a1a1", "#19e6e6", "#5eeded", "#2d5986", "#4080bf", "#79a6d2", "#1f5993", "#2d80d2", "#6ca6e0", "#1259a1", "#1980e6", ]; export const CATPPUCCIN_ACCENT_NAMES: string[] = [ "Red", "Teal", "Peach", "Blue", "Green", "Mauve", "Yellow", "Sapphire", "Pink", "Sky", "Maroon", "Lavender", "Flamingo", "Rosewater", "Color1", "Color2", "Color3", "Color4", "Color5", "Color6", "Color7", "Color8", "Color9", "Color10", "Color11", "Color12", "Color13", "Color14", "Color15", "Color16", "Color17", "Color18", "Color19", "Color20", "Color21", "Color22", "Color23", "Color24", "Color25", "Color26", "Color27", "Color28", "Color29", "Color30", "Color31", "Color32", "Color33", "Color34", "Color35", "Color36", "Color37", "Color38", "Color39", "Color40", "Color41", "Color42", "Color43", "Color44", "Color45", "Color46", "Color47", "Color48", "Color49", "Color50", "Color51", "Color52", "Color53", "Color54", "Color55", "Color56", "Color57", "Color58", "Color59", "Color60", "Color61", "Color62", "Color63", "Color64", "Color65", "Color66", "Color67", "Color68", "Color69", "Color70", "Color71", ]; /** Catppuccin Latte Surface2 — used for "unassigned" / unknown values */ export const CATPPUCCIN_UNASSIGNED = "#acb0be"; export const TYPE_ICONS: Record = { bug: "\uD83D\uDC1B", feature: "\u2728", task: "\uD83D\uDCDD", epic: "\uD83C\uDFAF", chore: "\uD83D\uDD27", }; export const TYPE_COLORS: Record = { epic: "#8839ef", // Catppuccin Mauve task: "#1e66f5", // Catppuccin Blue bug: "#d20f39", // Catppuccin Red feature: "#40a02b", // Catppuccin Green chore: "#acb0be", // Catppuccin Surface2 (gray) }; export const TYPE_LABELS: Record = { epic: "Epic", task: "Task", bug: "Bug", feature: "Feature", chore: "Chore", }; // ============================================================================ // Dynamic prefix label and color generation // ============================================================================ /** * Generate a deterministic color from a string using a hash-based hue. * Uses FNV-1a for better distribution across the hue wheel. */ function hashColor(str: string): string { let hash = 2166136261; // FNV offset basis for (let i = 0; i < str.length; i++) { hash ^= str.charCodeAt(i); hash = (hash * 16777619) >>> 0; // FNV prime, keep as uint32 } const hue = hash % 360; return `hsl(${hue}, 65%, 55%)`; } /** * Humanize a prefix string into a display label. * "my-lib" -> "My Lib", "backend" -> "Backend" */ function humanizePrefix(prefix: string): string { return prefix .split(/[-_]/) .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); } /** * Get a display label for a prefix. Returns a human-readable name * auto-generated from the prefix string. */ export function getPrefixLabel(prefix: string): string { return humanizePrefix(prefix); } /** * Get a color for a prefix. Generates a deterministic color * from the prefix string using hash-based hue. */ export function getPrefixColor(prefix: string): string { return hashColor(prefix); } /** * Deterministically map a person handle/name to a Catppuccin Latte accent color. * Uses FNV-1a hash modulo 14 to index into CATPPUCCIN_ACCENTS. * Returns CATPPUCCIN_UNASSIGNED for undefined/null/empty strings. */ export function getPersonColor(person: string | undefined): string { if (!person) return CATPPUCCIN_UNASSIGNED; let hash = 2166136261; // FNV offset basis for (let i = 0; i < person.length; i++) { hash ^= person.charCodeAt(i); hash = (hash * 16777619) >>> 0; } return CATPPUCCIN_ACCENTS[hash % CATPPUCCIN_ACCENTS.length]; } /** * Deterministically map a prefix string to a Catppuccin Latte accent color. * Same approach as getPersonColor but for project prefixes. */ export function getCatppuccinPrefixColor(prefix: string): string { return getPersonColor(prefix); } // Backward-compatible exports — delegates to dynamic functions // so existing code using PREFIX_COLORS[prefix] || fallback still works. // Prefer using getPrefixColor() / getPrefixLabel() directly. export const PREFIX_LABELS = new Proxy({} as Record, { get: (_target, prop: string) => getPrefixLabel(prop), has: () => true, }); export const PREFIX_COLORS = new Proxy({} as Record, { get: (_target, prop: string) => getPrefixColor(prop), has: () => true, }); // ============================================================================ // Assignee avatar info for ATProto handle resolution // ============================================================================ export interface AssigneeAvatarInfo { handle: string; // the ATProto handle from node.assignee avatar?: string; // resolved avatar URL (initially undefined, filled by resolution) updatedAt: string; // ISO 8601 timestamp from node.updatedAt (for "last worked on" tooltip) }