feat: add logo, favicon, and theme system
- Add custom SVG favicon and Logo component (house + network nodes motif) - Update page title to Homelable with meta description - Show Logo in sidebar header and toolbar - Add theme store and ThemeModal for canvas style switching - Refactor node colors and edge styles for theme support
This commit is contained in:
@@ -1,25 +1,23 @@
|
||||
import { createElement } from 'react'
|
||||
import { Handle, Position, type NodeProps, type Node } from '@xyflow/react'
|
||||
import { type LucideIcon } from 'lucide-react'
|
||||
import type { NodeData, NodeStatus } from '@/types'
|
||||
import type { NodeData } from '@/types'
|
||||
import { resolveNodeColors } from '@/utils/nodeColors'
|
||||
import { resolveNodeIcon } from '@/utils/nodeIcons'
|
||||
|
||||
const STATUS_COLORS: Record<NodeStatus, string> = {
|
||||
online: '#39d353',
|
||||
offline: '#f85149',
|
||||
pending: '#e3b341',
|
||||
unknown: '#8b949e',
|
||||
}
|
||||
import { useThemeStore } from '@/stores/themeStore'
|
||||
import { THEMES } from '@/utils/themes'
|
||||
|
||||
interface BaseNodeProps extends NodeProps<Node<NodeData>> {
|
||||
icon: LucideIcon
|
||||
}
|
||||
|
||||
export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
|
||||
const activeTheme = useThemeStore((s) => s.activeTheme)
|
||||
const theme = THEMES[activeTheme]
|
||||
|
||||
const resolvedIcon = resolveNodeIcon(typeIcon, data.custom_icon)
|
||||
const colors = resolveNodeColors(data)
|
||||
const statusColor = STATUS_COLORS[data.status]
|
||||
const colors = resolveNodeColors(data, activeTheme)
|
||||
const statusColor = theme.colors.statusColors[data.status]
|
||||
const isOnline = data.status === 'online'
|
||||
|
||||
return (
|
||||
@@ -38,24 +36,40 @@ export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
|
||||
minWidth: 140,
|
||||
}}
|
||||
>
|
||||
<Handle type="source" position={Position.Top} id="top" className="!bg-[#30363d] !border-[#8b949e]" />
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Top}
|
||||
id="top"
|
||||
style={{ background: theme.colors.handleBackground, borderColor: theme.colors.handleBorder }}
|
||||
/>
|
||||
<Handle type="target" position={Position.Top} id="top-t" style={{ opacity: 0, width: 12, height: 12 }} />
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className="flex items-center justify-center w-7 h-7 rounded-md shrink-0"
|
||||
style={{ color: isOnline ? colors.icon : '#8b949e', background: '#161b22' }}
|
||||
style={{
|
||||
color: isOnline ? colors.icon : theme.colors.nodeSubtextColor,
|
||||
background: theme.colors.nodeIconBackground,
|
||||
}}
|
||||
>
|
||||
{createElement(resolvedIcon, { size: 15 })}
|
||||
</div>
|
||||
|
||||
{/* Details */}
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="text-xs font-medium leading-tight truncate max-w-[110px]" title={data.label}>
|
||||
<div
|
||||
className="text-xs font-medium leading-tight truncate max-w-[110px]"
|
||||
style={{ color: theme.colors.nodeLabelColor }}
|
||||
title={data.label}
|
||||
>
|
||||
{data.label}
|
||||
</div>
|
||||
{data.ip && (
|
||||
<div className="font-mono text-[10px] text-[#8b949e] truncate" title={data.ip}>
|
||||
<div
|
||||
className="font-mono text-[10px] truncate"
|
||||
style={{ color: theme.colors.nodeSubtextColor }}
|
||||
title={data.ip}
|
||||
>
|
||||
{data.ip}
|
||||
</div>
|
||||
)}
|
||||
@@ -68,7 +82,12 @@ export function BaseNode({ data, selected, icon: typeIcon }: BaseNodeProps) {
|
||||
title={data.status}
|
||||
/>
|
||||
|
||||
<Handle type="source" position={Position.Bottom} id="bottom" className="!bg-[#30363d] !border-[#8b949e]" />
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="bottom"
|
||||
style={{ background: theme.colors.handleBackground, borderColor: theme.colors.handleBorder }}
|
||||
/>
|
||||
<Handle type="target" position={Position.Bottom} id="bottom-t" style={{ opacity: 0, width: 12, height: 12 }} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,34 +1,46 @@
|
||||
import { Handle, Position, NodeResizer, type NodeProps, type Node } from '@xyflow/react'
|
||||
import { Layers } from 'lucide-react'
|
||||
import type { NodeData, NodeStatus } from '@/types'
|
||||
import type { NodeData } from '@/types'
|
||||
import { resolveNodeColors } from '@/utils/nodeColors'
|
||||
import { useThemeStore } from '@/stores/themeStore'
|
||||
import { THEMES } from '@/utils/themes'
|
||||
import { BaseNode } from './BaseNode'
|
||||
|
||||
const STATUS_COLORS: Record<NodeStatus, string> = {
|
||||
online: '#39d353',
|
||||
offline: '#f85149',
|
||||
pending: '#e3b341',
|
||||
unknown: '#8b949e',
|
||||
}
|
||||
|
||||
export function ProxmoxGroupNode(props: NodeProps<Node<NodeData>>) {
|
||||
const { data, selected } = props
|
||||
const colors = resolveNodeColors(data)
|
||||
|
||||
const activeTheme = useThemeStore((s) => s.activeTheme)
|
||||
const theme = THEMES[activeTheme]
|
||||
const colors = resolveNodeColors(data, activeTheme)
|
||||
|
||||
// Render as a regular node when container mode is disabled
|
||||
if (data.container_mode === false) {
|
||||
const proxmoxAccent = theme.colors.nodeAccents.proxmox.border
|
||||
return (
|
||||
<>
|
||||
<BaseNode {...props} icon={Layers} />
|
||||
<Handle type="source" position={Position.Left} id="cluster-left" title="Same cluster" style={{ background: '#ff6e00', borderColor: '#ff6e0088', width: 6, height: 6 }} />
|
||||
<Handle type="source" position={Position.Right} id="cluster-right" title="Same cluster" style={{ background: '#ff6e00', borderColor: '#ff6e0088', width: 6, height: 6 }} />
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
id="cluster-left"
|
||||
title="Same cluster"
|
||||
style={{ background: proxmoxAccent, borderColor: `${proxmoxAccent}88`, width: 6, height: 6 }}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="cluster-right"
|
||||
title="Same cluster"
|
||||
style={{ background: proxmoxAccent, borderColor: `${proxmoxAccent}88`, width: 6, height: 6 }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const statusColor = STATUS_COLORS[data.status]
|
||||
const statusColor = theme.colors.statusColors[data.status]
|
||||
const isOnline = data.status === 'online'
|
||||
const glow = colors.border
|
||||
const proxmoxAccent = theme.colors.nodeAccents.proxmox.border
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -37,7 +49,7 @@ export function ProxmoxGroupNode(props: NodeProps<Node<NodeData>>) {
|
||||
minHeight={160}
|
||||
isVisible={selected}
|
||||
lineStyle={{ borderColor: glow, opacity: 0.6 }}
|
||||
handleStyle={{ borderColor: glow, backgroundColor: '#21262d' }}
|
||||
handleStyle={{ borderColor: glow, backgroundColor: theme.colors.nodeCardBackground }}
|
||||
/>
|
||||
|
||||
{/* Group border */}
|
||||
@@ -56,38 +68,78 @@ export function ProxmoxGroupNode(props: NodeProps<Node<NodeData>>) {
|
||||
{/* Header bar */}
|
||||
<div
|
||||
className="flex items-center gap-2 px-2.5 py-1.5 shrink-0"
|
||||
style={{ background: isOnline ? `${glow}18` : '#161b2288', borderBottom: `1px solid ${isOnline ? `${glow}33` : '#30363d'}` }}
|
||||
style={{
|
||||
background: isOnline ? `${glow}18` : `${theme.colors.nodeIconBackground}88`,
|
||||
borderBottom: `1px solid ${isOnline ? `${glow}33` : theme.colors.handleBackground}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center w-5 h-5 rounded-md shrink-0"
|
||||
style={{ color: isOnline ? colors.icon : '#8b949e', background: '#161b22' }}
|
||||
style={{
|
||||
color: isOnline ? colors.icon : theme.colors.nodeSubtextColor,
|
||||
background: theme.colors.nodeIconBackground,
|
||||
}}
|
||||
>
|
||||
<Layers size={12} />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="text-[11px] font-semibold leading-tight truncate" style={{ color: isOnline ? glow : '#c9d1d9' }}>
|
||||
<span
|
||||
className="text-[11px] font-semibold leading-tight truncate"
|
||||
style={{ color: isOnline ? glow : theme.colors.nodeLabelColor }}
|
||||
>
|
||||
{data.label}
|
||||
</span>
|
||||
{data.ip && (
|
||||
<span className="font-mono text-[9px] text-[#8b949e] truncate">{data.ip}</span>
|
||||
<span
|
||||
className="font-mono text-[9px] truncate"
|
||||
style={{ color: theme.colors.nodeSubtextColor }}
|
||||
>
|
||||
{data.ip}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* Status dot */}
|
||||
<div className="w-1.5 h-1.5 rounded-full shrink-0" style={{ backgroundColor: statusColor }} title={data.status} />
|
||||
<div
|
||||
className="w-1.5 h-1.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: statusColor }}
|
||||
title={data.status}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Inner area — React Flow places children here */}
|
||||
<div className="flex-1 relative" />
|
||||
</div>
|
||||
|
||||
<Handle type="source" position={Position.Top} id="top" className="!bg-[#30363d] !border-[#8b949e]" />
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Top}
|
||||
id="top"
|
||||
style={{ background: theme.colors.handleBackground, borderColor: theme.colors.handleBorder }}
|
||||
/>
|
||||
<Handle type="target" position={Position.Top} id="top-t" style={{ opacity: 0, width: 12, height: 12 }} />
|
||||
<Handle type="source" position={Position.Bottom} id="bottom" className="!bg-[#30363d] !border-[#8b949e]" />
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
id="bottom"
|
||||
style={{ background: theme.colors.handleBackground, borderColor: theme.colors.handleBorder }}
|
||||
/>
|
||||
<Handle type="target" position={Position.Bottom} id="bottom-t" style={{ opacity: 0, width: 12, height: 12 }} />
|
||||
|
||||
{/* Cluster handles — left/right for same-cluster links */}
|
||||
<Handle type="source" position={Position.Left} id="cluster-left" title="Same cluster" style={{ background: '#ff6e00', borderColor: '#ff6e0088', width: 6, height: 6 }} />
|
||||
<Handle type="source" position={Position.Right} id="cluster-right" title="Same cluster" style={{ background: '#ff6e00', borderColor: '#ff6e0088', width: 6, height: 6 }} />
|
||||
{/* Cluster handles */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
id="cluster-left"
|
||||
title="Same cluster"
|
||||
style={{ background: proxmoxAccent, borderColor: `${proxmoxAccent}88`, width: 6, height: 6 }}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="cluster-right"
|
||||
title="Same cluster"
|
||||
style={{ background: proxmoxAccent, borderColor: `${proxmoxAccent}88`, width: 6, height: 6 }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user