feat(#5): add icon presets, dark mode theme store, and icon-aware graph rendering

This commit is contained in:
Josh Creek
2026-02-25 19:32:11 +00:00
parent b9c7d9eee0
commit 2f4a784646
12 changed files with 195 additions and 23 deletions
+28
View File
@@ -1,2 +1,30 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
:root {
--app-bg: #e2e8f0;
--panel-bg: #f8fafc;
--panel-border: #cbd5e1;
--panel-contrast: #0f172a;
--muted-text: #334155;
--chip-bg: #e2e8f0;
--chip-text: #0f172a;
--modal-overlay: rgba(15, 23, 42, 0.58);
}
:root[data-theme='dark'] {
--app-bg: #0b1220;
--panel-bg: #111827;
--panel-border: #334155;
--panel-contrast: #e2e8f0;
--muted-text: #94a3b8;
--chip-bg: #1e293b;
--chip-text: #e2e8f0;
--modal-overlay: rgba(2, 6, 23, 0.78);
}
html,
body {
background: var(--app-bg);
color: var(--panel-contrast);
}
+27
View File
@@ -0,0 +1,27 @@
export interface IconDefinition {
key: string;
label: string;
path: string;
}
const ICONS: IconDefinition[] = [
{ key: 'server', label: 'Server', path: '/icons/server.svg' },
{ key: 'router', label: 'Router', path: '/icons/router.svg' },
{ key: 'switch', label: 'Switch', path: '/icons/switch.svg' },
{ key: 'storage', label: 'Storage', path: '/icons/storage.svg' },
{ key: 'desktop', label: 'Desktop', path: '/icons/desktop.svg' },
{ key: 'cloud', label: 'Cloud', path: '/icons/cloud.svg' }
];
const ICON_BY_KEY = new Map(ICONS.map((icon) => [icon.key, icon]));
export function listIconDefinitions(): IconDefinition[] {
return ICONS;
}
export function resolveIconPath(iconKey?: string): string | undefined {
if (!iconKey) {
return undefined;
}
return ICON_BY_KEY.get(iconKey)?.path;
}
+39 -23
View File
@@ -1,4 +1,5 @@
import type { ConnectedPortRef, NetworkData, Port } from '../types';
import { resolveIconPath } from '../config/iconRegistry';
import type { GraphEdgeElement, GraphNodeElement, GraphTransformResult } from './types';
interface EndpointOwner {
@@ -8,8 +9,12 @@ interface EndpointOwner {
ports: Map<string, Port>;
}
function toOwnerKey(kind: EndpointOwner['kind'], name: string): string {
return `${kind}:${name}`;
function normalizeOwnerName(name: string): string {
return name.trim().toLowerCase();
}
function toOwnerLookupKey(kind: EndpointOwner['kind'], name: string): string {
return `${kind}:${normalizeOwnerName(name)}`;
}
function toMachineNodeId(name: string): string {
@@ -75,7 +80,7 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
vmCount: number;
}
> = {};
const ownersByKey = new Map<string, EndpointOwner>();
const ownersByLookup = new Map<string, EndpointOwner>();
const seenWarnings = new Set<string>();
const warn = (message: string) => {
@@ -86,14 +91,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
};
const addOwner = (owner: EndpointOwner) => {
const ownerKey = toOwnerKey(owner.kind, owner.name);
if (ownersByKey.has(ownerKey)) {
const ownerLookupKey = toOwnerLookupKey(owner.kind, owner.name);
if (ownersByLookup.has(ownerLookupKey)) {
warn(
`Duplicate ${owner.kind} name "${owner.name}" found. First occurrence will be used for link resolution.`
);
return;
}
ownersByKey.set(ownerKey, owner);
ownersByLookup.set(ownerLookupKey, owner);
};
let hostingEdgeCounter = 0;
@@ -110,24 +115,27 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
nodes.push({
data: {
id: machineNodeId,
label: machine.machineName,
rawName: machine.machineName,
kind: 'machine',
id: machineNodeId,
label: machine.machineName,
rawName: machine.machineName,
kind: 'machine',
iconKey: machine.iconKey,
iconUrl: resolveIconPath(machine.iconKey),
nodeWidth: 200,
nodeHeight: 110,
details: {
type: 'machine',
name: machine.machineName,
ip: machine.ipAddress,
role: machine.role,
os: machine.operatingSystem,
cpu: machine.hardware?.cpu ?? 'Unknown',
ram: machine.hardware?.ram ?? 'Unknown',
gpu: machine.hardware?.gpu ?? undefined,
ports: machinePortsList,
vmCount: machineVms.length,
vms: machineVms
name: machine.machineName,
iconKey: machine.iconKey,
ip: machine.ipAddress,
role: machine.role,
os: machine.operatingSystem,
cpu: machine.hardware?.cpu ?? 'Unknown',
ram: machine.hardware?.ram ?? 'Unknown',
gpu: machine.hardware?.gpu ?? undefined,
ports: machinePortsList,
vmCount: machineVms.length,
vms: machineVms
}
}
});
@@ -148,11 +156,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
rawName: vm.name,
kind: 'vm',
hostMachineId: machineNodeId,
iconKey: vm.iconKey,
iconUrl: resolveIconPath(vm.iconKey),
nodeWidth: 140,
nodeHeight: 66,
details: {
type: 'vm',
name: vm.name,
iconKey: vm.iconKey,
ip: vm.ipAddress,
role: vm.role,
hostName: machine.machineName
@@ -199,11 +210,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
label: device.name,
rawName: device.name,
kind: 'device',
iconKey: device.iconKey,
iconUrl: resolveIconPath(device.iconKey),
nodeWidth: 176,
nodeHeight: 116,
details: {
type: 'device',
name: device.name,
iconKey: device.iconKey,
ip: device.ipAddress,
deviceType: device.type,
notes: device.notes,
@@ -228,15 +242,17 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
const seenPhysicalConnections = new Set<string>();
let physicalEdgeCounter = 0;
for (const owner of ownersByKey.values()) {
for (const owner of ownersByLookup.values()) {
for (const port of owner.ports.values()) {
const connectedTo = parseConnectedTo(port.connectedTo);
if (!connectedTo) {
continue;
}
const machineTargetOwner = ownersByKey.get(toOwnerKey('machine', connectedTo.device));
const deviceTargetOwner = ownersByKey.get(toOwnerKey('device', connectedTo.device));
const machineTargetOwner = ownersByLookup.get(
toOwnerLookupKey('machine', connectedTo.device)
);
const deviceTargetOwner = ownersByLookup.get(toOwnerLookupKey('device', connectedTo.device));
if (machineTargetOwner && deviceTargetOwner) {
warn(
`Link from "${owner.name}:${port.portName}" references ambiguous device "${connectedTo.device}" (matches both machine and device names).`
+5
View File
@@ -13,6 +13,7 @@ export interface PortDetails {
export interface MachineDetails {
type: 'machine';
name: string;
iconKey?: string;
ip: string;
role: string;
os: string;
@@ -31,6 +32,7 @@ export interface MachineDetails {
export interface DeviceDetails {
type: 'device';
name: string;
iconKey?: string;
ip: string;
deviceType: string;
notes?: string;
@@ -40,6 +42,7 @@ export interface DeviceDetails {
export interface VmDetails {
type: 'vm';
name: string;
iconKey?: string;
ip: string;
role: string;
hostName: string;
@@ -55,6 +58,8 @@ export interface GraphNodeData {
hostMachineId?: string;
nodeWidth?: number;
nodeHeight?: number;
iconUrl?: string;
iconKey?: string;
details?: GraphNodeDetails;
}
+55
View File
@@ -0,0 +1,55 @@
import { writable } from 'svelte/store';
export type ThemeMode = 'light' | 'dark';
const STORAGE_KEY = 'ond-theme';
function getInitialTheme(): ThemeMode {
if (typeof window === 'undefined') {
return 'light';
}
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark') {
return stored;
}
return 'light';
}
function applyTheme(theme: ThemeMode) {
if (typeof document === 'undefined') {
return;
}
document.documentElement.setAttribute('data-theme', theme);
}
function createThemeStore() {
const store = writable<ThemeMode>('light');
return {
subscribe: store.subscribe,
initialize() {
const theme = getInitialTheme();
store.set(theme);
applyTheme(theme);
},
toggle() {
store.update((current) => {
const next = current === 'dark' ? 'light' : 'dark';
if (typeof window !== 'undefined') {
window.localStorage.setItem(STORAGE_KEY, next);
}
applyTheme(next);
return next;
});
},
set(theme: ThemeMode) {
if (typeof window !== 'undefined') {
window.localStorage.setItem(STORAGE_KEY, theme);
}
applyTheme(theme);
store.set(theme);
}
};
}
export const themeMode = createThemeStore();
+3
View File
@@ -21,6 +21,7 @@ export interface VM {
name: string;
role: string;
ipAddress: string;
iconKey?: string;
}
export interface Software {
@@ -32,6 +33,7 @@ export interface Machine {
ipAddress: string;
role: string;
operatingSystem: string;
iconKey?: string;
software: Software;
hardware: Hardware;
ports?: Port[];
@@ -41,6 +43,7 @@ export interface NetworkDevice {
name: string;
ipAddress: string;
type: string;
iconKey?: string;
notes?: string;
ports?: Port[];
}
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M24 46h24a10 10 0 0 0 2-19 14 14 0 0 0-27-2 9 9 0 0 0 1 21z" fill="#0ea5e9"/>
</svg>

After

Width:  |  Height:  |  Size: 169 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect x="10" y="10" width="44" height="30" rx="4" fill="#7c3aed"/>
<rect x="24" y="42" width="16" height="6" rx="2" fill="#c4b5fd"/>
<rect x="18" y="50" width="28" height="4" rx="2" fill="#a78bfa"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect x="9" y="24" width="46" height="20" rx="5" fill="#2563eb"/>
<path d="M18 20c4-4 8-6 14-6s10 2 14 6" stroke="#93c5fd" stroke-width="3" stroke-linecap="round"/>
<path d="M22 24c3-3 6-4 10-4s7 1 10 4" stroke="#bfdbfe" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="20" cy="34" r="2" fill="#dbeafe"/>
<circle cx="28" cy="34" r="2" fill="#dbeafe"/>
</svg>

After

Width:  |  Height:  |  Size: 449 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect x="10" y="10" width="44" height="18" rx="4" fill="#0ea5e9"/>
<rect x="10" y="36" width="44" height="18" rx="4" fill="#0284c7"/>
<circle cx="18" cy="19" r="2" fill="#e0f2fe"/>
<circle cx="18" cy="45" r="2" fill="#e0f2fe"/>
</svg>

After

Width:  |  Height:  |  Size: 316 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect x="12" y="10" width="40" height="44" rx="6" fill="#16a34a"/>
<rect x="18" y="18" width="28" height="8" rx="2" fill="#dcfce7"/>
<rect x="18" y="30" width="28" height="8" rx="2" fill="#dcfce7"/>
<rect x="18" y="42" width="28" height="8" rx="2" fill="#dcfce7"/>
</svg>

After

Width:  |  Height:  |  Size: 353 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect x="8" y="18" width="48" height="28" rx="6" fill="#f59e0b"/>
<rect x="14" y="26" width="8" height="4" rx="1" fill="#fff7ed"/>
<rect x="24" y="26" width="8" height="4" rx="1" fill="#fff7ed"/>
<rect x="34" y="26" width="8" height="4" rx="1" fill="#fff7ed"/>
<rect x="44" y="26" width="6" height="4" rx="1" fill="#fff7ed"/>
<rect x="14" y="34" width="8" height="4" rx="1" fill="#fff7ed"/>
<rect x="24" y="34" width="8" height="4" rx="1" fill="#fff7ed"/>
<rect x="34" y="34" width="8" height="4" rx="1" fill="#fff7ed"/>
<rect x="44" y="34" width="6" height="4" rx="1" fill="#fff7ed"/>
</svg>

After

Width:  |  Height:  |  Size: 684 B