mirror of
https://github.com/jcreek/OpenNetworkDiagram.git
synced 2026-07-12 18:43:44 +00:00
feat(#5): add icon presets, dark mode theme store, and icon-aware graph rendering
This commit is contained in:
+28
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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).`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
Reference in New Issue
Block a user