- Web3 authentication with MetaMask, WalletConnect, Coinbase wallet options - Demo mode for testing without wallet - Overview dashboard with KPI cards, asset allocation, positions, accounts, alerts - Transaction Builder module (full IDE-style drag-and-drop canvas with 28 gap fixes) - Accounts module with multi-account/subaccount hierarchical structures - Treasury Management module with positions table and 14-day cash forecast - Financial Reporting module with IPSAS, US GAAP, IFRS compliance - Compliance & Risk module with KYC/AML/Sanctions monitoring - Settlement & Clearing module with DVP/FOP/PVP operations - Settings with role-based permissions and enterprise controls - Dark theme professional UI with Solace Bank branding - HashRouter for static hosting compatibility Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
335 lines
15 KiB
TypeScript
335 lines
15 KiB
TypeScript
import { useState, type DragEvent } from 'react';
|
|
import { Search, Star, Clock, ChevronRight, ChevronDown, GripVertical } from 'lucide-react';
|
|
import { componentCategories, componentItems } from '../data/components';
|
|
import type { ComponentItem, ActivityTab } from '../types';
|
|
|
|
interface LeftPanelProps {
|
|
width: number;
|
|
activityTab: ActivityTab;
|
|
recentComponents: string[];
|
|
}
|
|
|
|
const activityTabLabels: Record<ActivityTab, string> = {
|
|
builder: 'Components',
|
|
assets: 'Assets',
|
|
templates: 'Templates',
|
|
compliance: 'Compliance',
|
|
routes: 'Routes',
|
|
protocols: 'Protocols',
|
|
agents: 'Agents',
|
|
terminal: 'Terminal',
|
|
audit: 'Audit',
|
|
settings: 'Settings',
|
|
};
|
|
|
|
const activityTabCategories: Partial<Record<ActivityTab, string[]>> = {
|
|
assets: ['assets'],
|
|
templates: ['templates'],
|
|
compliance: ['compliance'],
|
|
routes: ['routing'],
|
|
protocols: ['messaging'],
|
|
};
|
|
|
|
export default function LeftPanel({ width, activityTab, recentComponents }: LeftPanelProps) {
|
|
const [search, setSearch] = useState('');
|
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
|
new Set(componentCategories.map(c => c.id))
|
|
);
|
|
const [favorites, setFavorites] = useState<Set<string>>(new Set(['transfer', 'swap', 'kyc']));
|
|
const [activeFilter, setActiveFilter] = useState<'all' | 'favorites' | 'recent'>('all');
|
|
const [tooltipItem, setTooltipItem] = useState<ComponentItem | null>(null);
|
|
const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 });
|
|
|
|
const toggleCategory = (id: string) => {
|
|
const next = new Set(expandedCategories);
|
|
if (next.has(id)) next.delete(id); else next.add(id);
|
|
setExpandedCategories(next);
|
|
};
|
|
|
|
const toggleFavorite = (id: string, e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
const next = new Set(favorites);
|
|
if (next.has(id)) next.delete(id); else next.add(id);
|
|
setFavorites(next);
|
|
};
|
|
|
|
const onDragStart = (e: DragEvent, item: ComponentItem) => {
|
|
e.dataTransfer.setData('application/transactflow-component', JSON.stringify(item));
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
|
|
// Create drag preview
|
|
const preview = document.createElement('div');
|
|
preview.className = 'drag-preview';
|
|
preview.innerHTML = `<span>${item.icon}</span> <span>${item.label}</span>`;
|
|
preview.style.cssText = 'position:fixed;top:-100px;left:-100px;background:#1a1a20;border:1px solid #3b82f6;border-radius:6px;padding:6px 12px;color:#e4e4e8;font-size:12px;display:flex;align-items:center;gap:6px;z-index:10000;pointer-events:none;';
|
|
document.body.appendChild(preview);
|
|
e.dataTransfer.setDragImage(preview, 0, 0);
|
|
setTimeout(() => document.body.removeChild(preview), 0);
|
|
};
|
|
|
|
const showTooltip = (item: ComponentItem, e: React.MouseEvent) => {
|
|
setTooltipItem(item);
|
|
setTooltipPos({ x: e.clientX + 12, y: e.clientY - 10 });
|
|
};
|
|
|
|
const hideTooltip = () => setTooltipItem(null);
|
|
|
|
// For non-builder tabs, show filtered content
|
|
if (activityTab !== 'builder') {
|
|
const categoryFilter = activityTabCategories[activityTab];
|
|
|
|
if (activityTab === 'settings') {
|
|
return (
|
|
<div className="left-panel" style={{ width }}>
|
|
<div className="panel-header"><span className="panel-title">{activityTabLabels[activityTab]}</span></div>
|
|
<div className="left-panel-content">
|
|
<div className="settings-panel">
|
|
<div className="settings-group">
|
|
<div className="settings-group-header">Workspace</div>
|
|
<div className="settings-item"><span>Theme</span><span className="settings-value">Dark</span></div>
|
|
<div className="settings-item"><span>Font Size</span><span className="settings-value">13px</span></div>
|
|
<div className="settings-item"><span>Snap to Grid</span><span className="settings-value">Enabled</span></div>
|
|
<div className="settings-item"><span>Grid Size</span><span className="settings-value">16px</span></div>
|
|
</div>
|
|
<div className="settings-group">
|
|
<div className="settings-group-header">Canvas</div>
|
|
<div className="settings-item"><span>Auto-save</span><span className="settings-value">On</span></div>
|
|
<div className="settings-item"><span>Minimap</span><span className="settings-value">Visible</span></div>
|
|
<div className="settings-item"><span>Animations</span><span className="settings-value">Enabled</span></div>
|
|
</div>
|
|
<div className="settings-group">
|
|
<div className="settings-group-header">Compliance</div>
|
|
<div className="settings-item"><span>Auto-validate</span><span className="settings-value">Off</span></div>
|
|
<div className="settings-item"><span>Jurisdiction</span><span className="settings-value">Multi</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (activityTab === 'terminal' || activityTab === 'audit') {
|
|
return (
|
|
<div className="left-panel" style={{ width }}>
|
|
<div className="panel-header"><span className="panel-title">{activityTabLabels[activityTab]}</span></div>
|
|
<div className="left-panel-content">
|
|
<div className="empty-state">
|
|
<p>{activityTab === 'terminal' ? 'Terminal output is shown in the bottom panel.' : 'Audit trail is shown in the bottom panel.'}</p>
|
|
<p style={{ marginTop: 8, fontSize: 11 }}>Use <kbd style={{ background: '#1a1a20', border: '1px solid #2a2a32', borderRadius: 3, padding: '1px 4px', fontSize: 10 }}>Ctrl+`</kbd> to toggle the bottom panel.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (activityTab === 'agents') {
|
|
const agentList = [
|
|
{ name: 'Builder Agent', desc: 'Helps construct transaction flows', color: '#3b82f6' },
|
|
{ name: 'Compliance Agent', desc: 'Monitors policy violations', color: '#22c55e' },
|
|
{ name: 'Routing Agent', desc: 'Optimizes execution paths', color: '#f97316' },
|
|
{ name: 'ISO-20022 Agent', desc: 'Generates messaging payloads', color: '#a855f7' },
|
|
{ name: 'Settlement Agent', desc: 'Manages settlement instructions', color: '#eab308' },
|
|
{ name: 'Risk Agent', desc: 'Evaluates transaction risk', color: '#ef4444' },
|
|
{ name: 'Documentation Agent', desc: 'Generates deal memos', color: '#6b7280' },
|
|
];
|
|
return (
|
|
<div className="left-panel" style={{ width }}>
|
|
<div className="panel-header"><span className="panel-title">Agents</span></div>
|
|
<div className="left-panel-content">
|
|
{agentList.map(a => (
|
|
<div key={a.name} className="agent-list-item">
|
|
<div className="agent-list-dot" style={{ background: a.color }} />
|
|
<div>
|
|
<div className="agent-list-name">{a.name}</div>
|
|
<div className="agent-list-desc">{a.desc}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// For assets, templates, compliance, routes, protocols: show filtered components
|
|
const filteredItems = categoryFilter
|
|
? componentItems.filter(i => categoryFilter.includes(i.category))
|
|
: componentItems;
|
|
|
|
const searchFiltered = filteredItems.filter(item =>
|
|
item.label.toLowerCase().includes(search.toLowerCase()) ||
|
|
item.description.toLowerCase().includes(search.toLowerCase())
|
|
);
|
|
|
|
return (
|
|
<div className="left-panel" style={{ width }}>
|
|
<div className="panel-header"><span className="panel-title">{activityTabLabels[activityTab]}</span></div>
|
|
<div className="left-panel-search">
|
|
<Search size={14} className="search-icon" />
|
|
<input
|
|
type="text"
|
|
placeholder={`Search ${activityTabLabels[activityTab].toLowerCase()}...`}
|
|
value={search}
|
|
onChange={e => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="left-panel-content">
|
|
<div className="category-items flat">
|
|
{searchFiltered.map(item => (
|
|
<div
|
|
key={item.id}
|
|
className="component-item"
|
|
draggable
|
|
onDragStart={e => onDragStart(e, item)}
|
|
onMouseEnter={e => showTooltip(item, e)}
|
|
onMouseLeave={hideTooltip}
|
|
>
|
|
<GripVertical size={12} className="drag-handle" />
|
|
<span className="component-icon">{item.icon}</span>
|
|
<span className="component-label">{item.label}</span>
|
|
<button className={`fav-btn ${favorites.has(item.id) ? 'active' : ''}`} onClick={e => toggleFavorite(item.id, e)}>
|
|
<Star size={11} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
{searchFiltered.length === 0 && <div className="empty-state">No items found</div>}
|
|
</div>
|
|
</div>
|
|
{tooltipItem && (
|
|
<div className="component-tooltip" style={{ top: tooltipPos.y, left: tooltipPos.x }}>
|
|
<div className="tooltip-header">{tooltipItem.icon} {tooltipItem.label}</div>
|
|
<div className="tooltip-desc">{tooltipItem.description}</div>
|
|
<div className="tooltip-meta">
|
|
<span className="tooltip-cat" style={{ color: tooltipItem.color }}>
|
|
{componentCategories.find(c => c.id === tooltipItem.category)?.label}
|
|
</span>
|
|
</div>
|
|
{tooltipItem.inputs && <div className="tooltip-fields"><strong>Inputs:</strong> {tooltipItem.inputs.join(', ')}</div>}
|
|
{tooltipItem.outputs && <div className="tooltip-fields"><strong>Outputs:</strong> {tooltipItem.outputs.join(', ')}</div>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Builder tab: full component library
|
|
const filtered = componentItems.filter(item =>
|
|
item.label.toLowerCase().includes(search.toLowerCase()) ||
|
|
item.description.toLowerCase().includes(search.toLowerCase())
|
|
);
|
|
|
|
const displayItems = activeFilter === 'favorites'
|
|
? filtered.filter(i => favorites.has(i.id))
|
|
: activeFilter === 'recent'
|
|
? filtered.filter(i => recentComponents.includes(i.id)).sort((a, b) => recentComponents.indexOf(a.id) - recentComponents.indexOf(b.id))
|
|
: filtered;
|
|
|
|
return (
|
|
<div className="left-panel" style={{ width }}>
|
|
<div className="panel-header">
|
|
<span className="panel-title">Components</span>
|
|
</div>
|
|
|
|
<div className="left-panel-search">
|
|
<Search size={14} className="search-icon" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search components..."
|
|
value={search}
|
|
onChange={e => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="left-panel-filters">
|
|
<button className={`filter-btn ${activeFilter === 'all' ? 'active' : ''}`} onClick={() => setActiveFilter('all')}>All</button>
|
|
<button className={`filter-btn ${activeFilter === 'favorites' ? 'active' : ''}`} onClick={() => setActiveFilter('favorites')}>
|
|
<Star size={11} /> Favorites
|
|
</button>
|
|
<button className={`filter-btn ${activeFilter === 'recent' ? 'active' : ''}`} onClick={() => setActiveFilter('recent')}>
|
|
<Clock size={11} /> Recent
|
|
</button>
|
|
</div>
|
|
|
|
<div className="left-panel-content">
|
|
{activeFilter === 'all' && !search ? (
|
|
componentCategories.map(cat => {
|
|
const catItems = displayItems.filter(i => i.category === cat.id);
|
|
if (catItems.length === 0) return null;
|
|
const isExpanded = expandedCategories.has(cat.id);
|
|
return (
|
|
<div key={cat.id} className="component-category">
|
|
<div className="category-header" onClick={() => toggleCategory(cat.id)}>
|
|
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
<span className="category-icon">{cat.icon}</span>
|
|
<span className="category-label">{cat.label}</span>
|
|
<span className="category-count">{catItems.length}</span>
|
|
</div>
|
|
{isExpanded && (
|
|
<div className="category-items">
|
|
{catItems.map(item => (
|
|
<div
|
|
key={item.id}
|
|
className="component-item"
|
|
draggable
|
|
onDragStart={e => onDragStart(e, item)}
|
|
onMouseEnter={e => showTooltip(item, e)}
|
|
onMouseLeave={hideTooltip}
|
|
>
|
|
<GripVertical size={12} className="drag-handle" />
|
|
<span className="component-icon">{item.icon}</span>
|
|
<span className="component-label">{item.label}</span>
|
|
<button className={`fav-btn ${favorites.has(item.id) ? 'active' : ''}`} onClick={e => toggleFavorite(item.id, e)}>
|
|
<Star size={11} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
) : (
|
|
<div className="category-items flat">
|
|
{displayItems.map(item => (
|
|
<div
|
|
key={item.id}
|
|
className="component-item"
|
|
draggable
|
|
onDragStart={e => onDragStart(e, item)}
|
|
onMouseEnter={e => showTooltip(item, e)}
|
|
onMouseLeave={hideTooltip}
|
|
>
|
|
<GripVertical size={12} className="drag-handle" />
|
|
<span className="component-icon">{item.icon}</span>
|
|
<span className="component-label">{item.label}</span>
|
|
<span className="component-category-badge">
|
|
{componentCategories.find(c => c.id === item.category)?.label}
|
|
</span>
|
|
<button className={`fav-btn ${favorites.has(item.id) ? 'active' : ''}`} onClick={e => toggleFavorite(item.id, e)}>
|
|
<Star size={11} />
|
|
</button>
|
|
</div>
|
|
))}
|
|
{displayItems.length === 0 && (
|
|
<div className="empty-state">
|
|
{activeFilter === 'recent' ? 'No recently used components. Drag a component to the canvas to see it here.' : 'No matching components found.'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{tooltipItem && (
|
|
<div className="component-tooltip" style={{ top: tooltipPos.y, left: tooltipPos.x }}>
|
|
<div className="tooltip-header">{tooltipItem.icon} {tooltipItem.label}</div>
|
|
<div className="tooltip-desc">{tooltipItem.description}</div>
|
|
<div className="tooltip-meta">
|
|
<span className="tooltip-cat" style={{ color: tooltipItem.color }}>
|
|
{componentCategories.find(c => c.id === tooltipItem.category)?.label}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|