mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-12 19:55:14 +02:00
258 lines
7.2 KiB
TypeScript
258 lines
7.2 KiB
TypeScript
import type { GraphMetadata, GraphThemePalette, GraphViewKind } from './types';
|
|
|
|
export interface NodeStyle {
|
|
fill: string;
|
|
stroke: string;
|
|
textFill: string;
|
|
secondaryFill: string;
|
|
shape: 'rect' | 'terminal' | 'double';
|
|
strokeWidth: number;
|
|
accentFill: string;
|
|
neighborFill: string;
|
|
}
|
|
|
|
export interface EdgeStyle {
|
|
color: string;
|
|
width: number;
|
|
dash: number[];
|
|
}
|
|
|
|
const FALLBACK_PALETTE: GraphThemePalette = {
|
|
background: '#f9f8f4',
|
|
backgroundSecondary: '#f2f0ea',
|
|
text: '#0d0c0a',
|
|
textSecondary: '#3c3830',
|
|
textTertiary: '#6c6660',
|
|
border: '#e5e1d7',
|
|
borderLight: '#ede9df',
|
|
accent: '#0b3d2a',
|
|
accentSoft: '#ecf3ee',
|
|
success: '#1c5c38',
|
|
warning: '#8c6310',
|
|
danger: '#9d2f25',
|
|
neutral: '#6c6660',
|
|
neutralSoft: '#9c9690',
|
|
};
|
|
|
|
function readVar(name: string, fallback: string): string {
|
|
if (typeof window === 'undefined') return fallback;
|
|
const value = getComputedStyle(document.documentElement)
|
|
.getPropertyValue(name)
|
|
.trim();
|
|
return value || fallback;
|
|
}
|
|
|
|
function hexToRgb(value: string): [number, number, number] | null {
|
|
const normalized = value.replace('#', '').trim();
|
|
if (normalized.length !== 3 && normalized.length !== 6) return null;
|
|
|
|
const expanded =
|
|
normalized.length === 3
|
|
? normalized
|
|
.split('')
|
|
.map((part) => part + part)
|
|
.join('')
|
|
: normalized;
|
|
|
|
const intValue = Number.parseInt(expanded, 16);
|
|
if (Number.isNaN(intValue)) return null;
|
|
|
|
return [(intValue >> 16) & 255, (intValue >> 8) & 255, intValue & 255];
|
|
}
|
|
|
|
export function withAlpha(color: string, alpha: number): string {
|
|
if (color.startsWith('rgba(')) {
|
|
return color.replace(/rgba\(([^)]+),[^)]+\)/, `rgba($1, ${alpha})`);
|
|
}
|
|
if (color.startsWith('rgb(')) {
|
|
const inner = color.slice(4, -1);
|
|
return `rgba(${inner}, ${alpha})`;
|
|
}
|
|
|
|
const rgb = hexToRgb(color);
|
|
if (!rgb) return color;
|
|
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
|
|
}
|
|
|
|
export function readGraphPalette(): GraphThemePalette {
|
|
return {
|
|
background: readVar('--bg', FALLBACK_PALETTE.background),
|
|
backgroundSecondary: readVar(
|
|
'--bg-secondary',
|
|
FALLBACK_PALETTE.backgroundSecondary,
|
|
),
|
|
text: readVar('--text', FALLBACK_PALETTE.text),
|
|
textSecondary: readVar('--text-secondary', FALLBACK_PALETTE.textSecondary),
|
|
textTertiary: readVar('--text-tertiary', FALLBACK_PALETTE.textTertiary),
|
|
border: readVar('--border', FALLBACK_PALETTE.border),
|
|
borderLight: readVar('--border-light', FALLBACK_PALETTE.borderLight),
|
|
accent: readVar('--accent', FALLBACK_PALETTE.accent),
|
|
accentSoft: readVar('--accent-light', FALLBACK_PALETTE.accentSoft),
|
|
success: readVar('--success', FALLBACK_PALETTE.success),
|
|
warning: readVar('--sev-medium', FALLBACK_PALETTE.warning),
|
|
danger: readVar('--sev-high', FALLBACK_PALETTE.danger),
|
|
neutral: FALLBACK_PALETTE.neutral,
|
|
neutralSoft: FALLBACK_PALETTE.neutralSoft,
|
|
};
|
|
}
|
|
|
|
function cfgNodeStyle(
|
|
type: string,
|
|
palette: GraphThemePalette,
|
|
metadata?: GraphMetadata,
|
|
): NodeStyle {
|
|
if (metadata?.isCompound) {
|
|
return {
|
|
fill: withAlpha(palette.borderLight, 0.9),
|
|
stroke: palette.border,
|
|
textFill: palette.text,
|
|
secondaryFill: palette.textSecondary,
|
|
shape: 'rect',
|
|
strokeWidth: 1.25,
|
|
accentFill: palette.accent,
|
|
neighborFill: palette.accentSoft,
|
|
};
|
|
}
|
|
|
|
switch (type) {
|
|
case 'Entry':
|
|
return {
|
|
fill: palette.success,
|
|
stroke: withAlpha(palette.success, 0.85),
|
|
textFill: '#ffffff',
|
|
secondaryFill: withAlpha('#ffffff', 0.78),
|
|
shape: 'double',
|
|
strokeWidth: 1.8,
|
|
accentFill: palette.accent,
|
|
neighborFill: withAlpha(palette.success, 0.75),
|
|
};
|
|
case 'Exit':
|
|
return {
|
|
fill: palette.textSecondary,
|
|
stroke: withAlpha(palette.textSecondary, 0.85),
|
|
textFill: '#ffffff',
|
|
secondaryFill: withAlpha('#ffffff', 0.78),
|
|
shape: 'double',
|
|
strokeWidth: 1.6,
|
|
accentFill: palette.accent,
|
|
neighborFill: withAlpha(palette.textSecondary, 0.76),
|
|
};
|
|
case 'If':
|
|
return {
|
|
fill: palette.accent,
|
|
stroke: withAlpha(palette.accent, 0.82),
|
|
textFill: '#ffffff',
|
|
secondaryFill: withAlpha('#ffffff', 0.8),
|
|
shape: 'rect',
|
|
strokeWidth: 2,
|
|
accentFill: palette.accent,
|
|
neighborFill: palette.accentSoft,
|
|
};
|
|
case 'Loop':
|
|
return {
|
|
fill: '#6c6660',
|
|
stroke: '#3c3830',
|
|
textFill: '#ffffff',
|
|
secondaryFill: withAlpha('#ffffff', 0.8),
|
|
shape: 'rect',
|
|
strokeWidth: 2.1,
|
|
accentFill: palette.accent,
|
|
neighborFill: withAlpha('#6c6660', 0.74),
|
|
};
|
|
case 'Call':
|
|
return {
|
|
fill: palette.warning,
|
|
stroke: withAlpha(palette.warning, 0.85),
|
|
textFill: '#ffffff',
|
|
secondaryFill: withAlpha('#ffffff', 0.8),
|
|
shape: 'rect',
|
|
strokeWidth: 1.5,
|
|
accentFill: palette.accent,
|
|
neighborFill: withAlpha(palette.warning, 0.76),
|
|
};
|
|
case 'Return':
|
|
return {
|
|
fill: palette.danger,
|
|
stroke: withAlpha(palette.danger, 0.86),
|
|
textFill: '#ffffff',
|
|
secondaryFill: withAlpha('#ffffff', 0.8),
|
|
shape: 'terminal',
|
|
strokeWidth: 1.7,
|
|
accentFill: palette.accent,
|
|
neighborFill: withAlpha(palette.danger, 0.75),
|
|
};
|
|
default:
|
|
return {
|
|
fill: withAlpha(palette.neutral, 0.92),
|
|
stroke: withAlpha(palette.neutral, 0.8),
|
|
textFill: '#ffffff',
|
|
secondaryFill: withAlpha('#ffffff', 0.78),
|
|
shape: 'rect',
|
|
strokeWidth: 1.2,
|
|
accentFill: palette.accent,
|
|
neighborFill: withAlpha(palette.neutralSoft, 0.88),
|
|
};
|
|
}
|
|
}
|
|
|
|
function callGraphNodeStyle(
|
|
palette: GraphThemePalette,
|
|
metadata?: GraphMetadata,
|
|
): NodeStyle {
|
|
const isRecursive = metadata?.isRecursive === true;
|
|
const fill = isRecursive ? '#5a5042' : palette.neutral;
|
|
const stroke = isRecursive ? '#3c3830' : withAlpha(palette.neutral, 0.84);
|
|
|
|
return {
|
|
fill,
|
|
stroke,
|
|
textFill: '#ffffff',
|
|
secondaryFill: withAlpha('#ffffff', 0.74),
|
|
shape: 'rect',
|
|
strokeWidth: isRecursive ? 1.8 : 1.3,
|
|
accentFill: palette.accent,
|
|
neighborFill: isRecursive ? withAlpha(fill, 0.76) : palette.accentSoft,
|
|
};
|
|
}
|
|
|
|
export function getNodeStyle(
|
|
type: string,
|
|
graphKind: GraphViewKind = 'cfg',
|
|
metadata?: GraphMetadata,
|
|
palette = FALLBACK_PALETTE,
|
|
): NodeStyle {
|
|
return graphKind === 'callgraph'
|
|
? callGraphNodeStyle(palette, metadata)
|
|
: cfgNodeStyle(type, palette, metadata);
|
|
}
|
|
|
|
export function getEdgeStyle(
|
|
type: string,
|
|
graphKind: GraphViewKind = 'cfg',
|
|
palette = FALLBACK_PALETTE,
|
|
): EdgeStyle {
|
|
if (graphKind === 'callgraph') {
|
|
return {
|
|
color: withAlpha(palette.neutralSoft, 0.72),
|
|
width: 1.2,
|
|
dash: [],
|
|
};
|
|
}
|
|
|
|
switch (type) {
|
|
case 'True':
|
|
return { color: palette.success, width: 1.8, dash: [] };
|
|
case 'False':
|
|
return { color: palette.danger, width: 1.8, dash: [] };
|
|
case 'Back':
|
|
return { color: palette.textTertiary, width: 1.6, dash: [7, 4] };
|
|
case 'Exception':
|
|
return { color: palette.warning, width: 1.6, dash: [3, 3] };
|
|
default:
|
|
return {
|
|
color: withAlpha(palette.textTertiary, 0.78),
|
|
width: 1.3,
|
|
dash: [],
|
|
};
|
|
}
|
|
}
|