UNPKG

signalforge

Version:

Fine-grained reactive state management with automatic dependency tracking - Ultra-optimized, zero dependencies

471 lines (470 loc) 25.8 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { enableDevTools, isDevToolsEnabled, onDevToolsEvent, listSignals, getSignal, getDependencies, getSubscribers, getPerformanceMetrics, clearPerformanceMetrics, } from '../runtime'; const styles = { panel: (state) => ({ position: 'fixed', ...getPositionStyles(state.position), width: state.isMinimized ? '320px' : `${state.width}px`, height: state.isMinimized ? '48px' : `${state.height}px`, backgroundColor: '#1e1e1e', color: '#d4d4d4', fontFamily: 'Monaco, Menlo, "Courier New", monospace', fontSize: '12px', border: '1px solid #3e3e3e', borderRadius: '8px', boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)', display: 'flex', flexDirection: 'column', zIndex: 999999, overflow: 'hidden', transition: 'all 0.2s ease', }), header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', backgroundColor: '#252526', borderBottom: '1px solid #3e3e3e', cursor: 'move', userSelect: 'none', }, headerTitle: { fontSize: '13px', fontWeight: 600, color: '#cccccc', display: 'flex', alignItems: 'center', gap: '8px', }, headerActions: { display: 'flex', gap: '6px', }, button: (variant = 'secondary') => ({ padding: '4px 8px', backgroundColor: variant === 'primary' ? '#0e639c' : variant === 'danger' ? '#c72e2e' : '#3e3e3e', color: '#ffffff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '11px', fontWeight: 500, transition: 'background-color 0.15s', }), iconButton: { padding: '4px 8px', backgroundColor: 'transparent', color: '#cccccc', border: 'none', cursor: 'pointer', fontSize: '14px', transition: 'color 0.15s', }, tabs: { display: 'flex', gap: '0', backgroundColor: '#2d2d30', borderBottom: '1px solid #3e3e3e', }, tab: (isActive) => ({ padding: '8px 16px', backgroundColor: isActive ? '#1e1e1e' : 'transparent', color: isActive ? '#ffffff' : '#969696', border: 'none', borderBottom: isActive ? '2px solid #0e639c' : '2px solid transparent', cursor: 'pointer', fontSize: '12px', fontWeight: 500, transition: 'all 0.15s', }), content: { flex: 1, overflow: 'auto', padding: '12px', backgroundColor: '#1e1e1e', }, signalList: { display: 'flex', flexDirection: 'column', gap: '8px', }, signalCard: { padding: '12px', backgroundColor: '#252526', border: '1px solid #3e3e3e', borderRadius: '6px', cursor: 'pointer', transition: 'border-color 0.15s, box-shadow 0.15s', }, signalCardHover: { borderColor: '#0e639c', boxShadow: '0 0 0 1px #0e639c', }, signalHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px', }, signalName: { fontSize: '13px', fontWeight: 600, color: '#4fc3f7', }, signalType: (type) => ({ padding: '2px 6px', backgroundColor: type === 'signal' ? '#2d5a2d' : type === 'computed' ? '#5a4d2d' : '#4d2d5a', color: type === 'signal' ? '#81c784' : type === 'computed' ? '#ffb74d' : '#ba68c8', borderRadius: '3px', fontSize: '10px', fontWeight: 600, textTransform: 'uppercase', }), signalValue: { fontSize: '12px', color: '#ce9178', marginBottom: '6px', wordBreak: 'break-all', }, signalMeta: { display: 'flex', gap: '12px', fontSize: '11px', color: '#858585', }, metricsGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '12px', marginBottom: '16px', }, metricCard: { padding: '12px', backgroundColor: '#252526', border: '1px solid #3e3e3e', borderRadius: '6px', }, metricLabel: { fontSize: '11px', color: '#858585', marginBottom: '4px', }, metricValue: (color) => ({ fontSize: '20px', fontWeight: 600, color: color || '#4fc3f7', }), searchInput: { width: '100%', padding: '8px 12px', backgroundColor: '#3c3c3c', color: '#cccccc', border: '1px solid #3e3e3e', borderRadius: '4px', fontSize: '12px', marginBottom: '12px', }, graphContainer: { width: '100%', height: '400px', backgroundColor: '#252526', border: '1px solid #3e3e3e', borderRadius: '6px', overflow: 'auto', position: 'relative', }, graphNode: (type) => ({ padding: '8px 12px', backgroundColor: type === 'signal' ? '#2d5a2d' : type === 'computed' ? '#5a4d2d' : '#4d2d5a', color: '#ffffff', borderRadius: '4px', fontSize: '11px', fontWeight: 500, border: '2px solid transparent', transition: 'all 0.15s', cursor: 'pointer', }), emptyState: { textAlign: 'center', padding: '48px 24px', color: '#858585', }, badge: (color) => ({ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', minWidth: '18px', height: '18px', padding: '0 4px', backgroundColor: color, color: '#ffffff', borderRadius: '9px', fontSize: '10px', fontWeight: 600, }), }; function getPositionStyles(position) { switch (position) { case 'bottom-right': return { bottom: '16px', right: '16px' }; case 'bottom-left': return { bottom: '16px', left: '16px' }; case 'top-right': return { top: '16px', right: '16px' }; case 'top-left': return { top: '16px', left: '16px' }; } } const SignalsTab = () => { const [signals, setSignals] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [expandedSignals, setExpandedSignals] = useState(new Set()); useEffect(() => { if (isDevToolsEnabled()) { setSignals(listSignals()); } }, []); useEffect(() => { if (!isDevToolsEnabled()) return; const cleanup = onDevToolsEvent('*', () => { setSignals(listSignals()); }); return cleanup; }, []); const filteredSignals = useMemo(() => { if (!searchQuery) return signals; const query = searchQuery.toLowerCase(); return signals.filter(signal => (signal.name?.toLowerCase().includes(query)) || signal.id.toLowerCase().includes(query) || signal.type.toLowerCase().includes(query)); }, [signals, searchQuery]); const toggleExpanded = useCallback((id) => { setExpandedSignals(prev => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }, []); if (!isDevToolsEnabled()) { return (_jsxs("div", { style: styles.emptyState, children: [_jsx("div", { style: { fontSize: '32px', marginBottom: '12px' }, children: "\uD83D\uDD12" }), _jsx("div", { style: { fontSize: '14px', fontWeight: 600, marginBottom: '8px' }, children: "DevTools Not Enabled" }), _jsx("div", { children: "Enable DevTools to monitor signals" })] })); } return (_jsxs(_Fragment, { children: [_jsx("input", { type: "text", placeholder: "\uD83D\uDD0D Search signals by name, ID, or type...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: styles.searchInput }), filteredSignals.length === 0 ? (_jsxs("div", { style: styles.emptyState, children: [_jsx("div", { style: { fontSize: '32px', marginBottom: '12px' }, children: searchQuery ? '🔍' : '📡' }), _jsx("div", { style: { fontSize: '14px', fontWeight: 600, marginBottom: '8px' }, children: searchQuery ? 'No Signals Found' : 'No Active Signals' }), _jsx("div", { children: searchQuery ? 'Try a different search query' : 'Create signals to see them here' })] })) : (_jsx("div", { style: styles.signalList, children: filteredSignals.map((signal) => { const isExpanded = expandedSignals.has(signal.id); return (_jsx(SignalCard, { signal: signal, isExpanded: isExpanded, onToggle: () => toggleExpanded(signal.id) }, signal.id)); }) })), _jsxs("div", { style: { marginTop: '16px', fontSize: '11px', color: '#858585', textAlign: 'center' }, children: [filteredSignals.length, " signal", filteredSignals.length !== 1 ? 's' : '', " active", searchQuery && signals.length !== filteredSignals.length && (_jsxs(_Fragment, { children: [" \u00B7 ", signals.length - filteredSignals.length, " hidden"] }))] })] })); }; const SignalCard = ({ signal, isExpanded, onToggle }) => { const [isHovered, setIsHovered] = useState(false); const dependencies = getDependencies(signal.id); const subscribers = getSubscribers(signal.id); const cardStyle = { ...styles.signalCard, ...(isHovered ? styles.signalCardHover : {}), }; return (_jsxs("div", { style: cardStyle, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), onClick: onToggle, children: [_jsxs("div", { style: styles.signalHeader, children: [_jsx("div", { style: styles.signalName, children: signal.name || signal.id }), _jsx("span", { style: styles.signalType(signal.type), children: signal.type })] }), _jsx("div", { style: styles.signalValue, children: formatValue(signal.value) }), _jsxs("div", { style: styles.signalMeta, children: [_jsxs("span", { children: ["\uD83D\uDD04 ", signal.updateCount, " update", signal.updateCount !== 1 ? 's' : ''] }), _jsxs("span", { children: ["\uD83D\uDC65 ", signal.subscriberCount, " subscriber", signal.subscriberCount !== 1 ? 's' : ''] }), dependencies.length > 0 && (_jsxs("span", { children: ["\uD83D\uDD17 ", dependencies.length, " dep", dependencies.length !== 1 ? 's' : ''] }))] }), isExpanded && (_jsxs("div", { style: { marginTop: '12px', paddingTop: '12px', borderTop: '1px solid #3e3e3e' }, children: [_jsxs("div", { style: { fontSize: '11px', color: '#858585', marginBottom: '8px' }, children: [_jsx("strong", { children: "ID:" }), " ", signal.id] }), dependencies.length > 0 && (_jsxs("div", { style: { fontSize: '11px', color: '#858585', marginBottom: '8px' }, children: [_jsx("strong", { children: "Dependencies:" }), _jsx("div", { style: { marginTop: '4px', display: 'flex', flexWrap: 'wrap', gap: '4px' }, children: dependencies.map(depId => { const dep = getSignal(depId); return (_jsx("span", { style: { padding: '2px 6px', backgroundColor: '#3e3e3e', borderRadius: '3px', fontSize: '10px', }, children: dep?.name || depId }, depId)); }) })] })), subscribers.length > 0 && (_jsxs("div", { style: { fontSize: '11px', color: '#858585', marginBottom: '8px' }, children: [_jsx("strong", { children: "Subscribers:" }), _jsx("div", { style: { marginTop: '4px', display: 'flex', flexWrap: 'wrap', gap: '4px' }, children: subscribers.map(subId => { const sub = getSignal(subId); return (_jsx("span", { style: { padding: '2px 6px', backgroundColor: '#3e3e3e', borderRadius: '3px', fontSize: '10px', }, children: sub?.name || subId }, subId)); }) })] })), _jsxs("div", { style: { fontSize: '11px', color: '#858585' }, children: [_jsx("strong", { children: "Created:" }), " ", new Date(signal.createdAt).toLocaleTimeString()] }), _jsxs("div", { style: { fontSize: '11px', color: '#858585' }, children: [_jsx("strong", { children: "Updated:" }), " ", new Date(signal.updatedAt).toLocaleTimeString()] })] }))] })); }; const PerformanceTab = () => { const [metrics, setMetrics] = useState([]); const [autoRefresh, setAutoRefresh] = useState(true); useEffect(() => { if (isDevToolsEnabled()) { setMetrics(getPerformanceMetrics()); } }, []); useEffect(() => { if (!isDevToolsEnabled() || !autoRefresh) return; const cleanup = onDevToolsEvent('signal-updated', () => { setMetrics(getPerformanceMetrics()); }); return cleanup; }, [autoRefresh]); const handleClearMetrics = useCallback(() => { clearPerformanceMetrics(); setMetrics([]); }, []); if (!isDevToolsEnabled()) { return (_jsxs("div", { style: styles.emptyState, children: [_jsx("div", { style: { fontSize: '32px', marginBottom: '12px' }, children: "\uD83D\uDD12" }), _jsx("div", { style: { fontSize: '14px', fontWeight: 600, marginBottom: '8px' }, children: "DevTools Not Enabled" }), _jsx("div", { children: "Enable DevTools to track performance" })] })); } const stats = useMemo(() => { if (metrics.length === 0) { return { total: 0, avg: 0, min: 0, max: 0, slow: 0 }; } const durations = metrics.map(m => m.duration); const avg = durations.reduce((a, b) => a + b, 0) / durations.length; const min = Math.min(...durations); const max = Math.max(...durations); const slow = metrics.filter(m => m.duration > 16).length; return { total: metrics.length, avg, min, max, slow }; }, [metrics]); const recentMetrics = metrics.slice(-20); return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }, children: [_jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', color: '#cccccc' }, children: [_jsx("input", { type: "checkbox", checked: autoRefresh, onChange: (e) => setAutoRefresh(e.target.checked) }), "Auto-refresh"] }), _jsx("button", { onClick: handleClearMetrics, style: styles.button('danger'), disabled: metrics.length === 0, children: "Clear Metrics" })] }), _jsxs("div", { style: styles.metricsGrid, children: [_jsxs("div", { style: styles.metricCard, children: [_jsx("div", { style: styles.metricLabel, children: "Total Updates" }), _jsx("div", { style: styles.metricValue('#4fc3f7'), children: stats.total })] }), _jsxs("div", { style: styles.metricCard, children: [_jsx("div", { style: styles.metricLabel, children: "Avg Duration" }), _jsxs("div", { style: styles.metricValue('#81c784'), children: [stats.avg.toFixed(3), "ms"] })] }), _jsxs("div", { style: styles.metricCard, children: [_jsx("div", { style: styles.metricLabel, children: "Min Duration" }), _jsxs("div", { style: styles.metricValue('#ffb74d'), children: [stats.min.toFixed(3), "ms"] })] }), _jsxs("div", { style: styles.metricCard, children: [_jsx("div", { style: styles.metricLabel, children: "Max Duration" }), _jsxs("div", { style: styles.metricValue('#ba68c8'), children: [stats.max.toFixed(3), "ms"] })] }), stats.slow > 0 && (_jsxs("div", { style: styles.metricCard, children: [_jsx("div", { style: styles.metricLabel, children: "\u26A0\uFE0F Slow Updates" }), _jsx("div", { style: styles.metricValue('#f44336'), children: stats.slow })] }))] }), recentMetrics.length === 0 ? (_jsxs("div", { style: styles.emptyState, children: [_jsx("div", { style: { fontSize: '32px', marginBottom: '12px' }, children: "\uD83D\uDCCA" }), _jsx("div", { style: { fontSize: '14px', fontWeight: 600, marginBottom: '8px' }, children: "No Performance Data" }), _jsx("div", { children: "Update signals to see performance metrics" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: { fontSize: '13px', fontWeight: 600, marginBottom: '12px', color: '#cccccc' }, children: ["Recent Updates (Last ", recentMetrics.length, ")"] }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: '6px' }, children: recentMetrics.reverse().map((metric, index) => (_jsx(PerformanceMetricCard, { metric: metric }, index))) })] }))] })); }; const PerformanceMetricCard = ({ metric }) => { const signal = getSignal(metric.signalId); const isSlow = metric.duration > 16; return (_jsxs("div", { style: { padding: '8px 12px', backgroundColor: isSlow ? '#3d2121' : '#252526', border: `1px solid ${isSlow ? '#c72e2e' : '#3e3e3e'}`, borderRadius: '4px', fontSize: '11px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', }, children: [_jsxs("div", { children: [_jsx("span", { style: { color: '#4fc3f7', fontWeight: 600 }, children: signal?.name || metric.signalId }), _jsxs("span", { style: { color: '#858585', marginLeft: '8px' }, children: ["(", metric.type, ")"] }), metric.skipped && (_jsx("span", { style: { color: '#ffb74d', marginLeft: '8px' }, children: "\u26A1 skipped" })), isSlow && (_jsx("span", { style: { color: '#f44336', marginLeft: '8px' }, children: "\u26A0\uFE0F slow" }))] }), _jsxs("div", { style: { color: isSlow ? '#f44336' : '#81c784', fontWeight: 600 }, children: [metric.duration.toFixed(3), "ms"] })] })); }; const PluginsTab = () => { return (_jsxs("div", { style: styles.emptyState, children: [_jsx("div", { style: { fontSize: '32px', marginBottom: '12px' }, children: "\uD83E\uDDE9" }), _jsx("div", { style: { fontSize: '14px', fontWeight: 600, marginBottom: '8px' }, children: "Plugins Coming Soon" }), _jsx("div", { children: "Plugin inspection and management will be available in a future release" })] })); }; const DependencyGraph = () => { const signals = listSignals(); if (signals.length === 0) { return (_jsxs("div", { style: styles.emptyState, children: [_jsx("div", { style: { fontSize: '32px', marginBottom: '12px' }, children: "\uD83D\uDD78\uFE0F" }), _jsx("div", { style: { fontSize: '14px', fontWeight: 600, marginBottom: '8px' }, children: "No Signals to Visualize" }), _jsx("div", { children: "Create signals to see dependency graph" })] })); } return (_jsx("div", { style: styles.graphContainer, children: _jsxs("div", { style: { padding: '16px' }, children: [_jsx("div", { style: { fontSize: '13px', fontWeight: 600, marginBottom: '16px', color: '#cccccc' }, children: "Dependency Graph" }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: '12px' }, children: signals.map(signal => (_jsx(DependencyNode, { signal: signal }, signal.id))) })] }) })); }; const DependencyNode = ({ signal }) => { const dependencies = getDependencies(signal.id); return (_jsxs("div", { style: { marginLeft: dependencies.length > 0 ? '0' : '0' }, children: [_jsxs("div", { style: styles.graphNode(signal.type), children: [_jsx("div", { style: { fontWeight: 600 }, children: signal.name || signal.id }), _jsxs("div", { style: { fontSize: '10px', opacity: 0.7, marginTop: '4px' }, children: [signal.type, " \u00B7 ", signal.updateCount, " updates"] })] }), dependencies.length > 0 && (_jsx("div", { style: { marginLeft: '20px', marginTop: '8px', borderLeft: '2px solid #3e3e3e', paddingLeft: '12px' }, children: dependencies.map(depId => { const dep = getSignal(depId); if (!dep) return null; return (_jsx("div", { style: { marginBottom: '8px' }, children: _jsxs("div", { style: styles.graphNode(dep.type), children: [_jsx("div", { style: { fontWeight: 600 }, children: dep.name || dep.id }), _jsx("div", { style: { fontSize: '10px', opacity: 0.7, marginTop: '4px' }, children: dep.type })] }) }, depId)); }) }))] })); }; export const DevToolsPanel = () => { const [panelState, setPanelState] = useState({ isOpen: true, isMinimized: false, position: 'bottom-right', width: 600, height: 500, }); const [activeTab, setActiveTab] = useState('signals'); const [isDragging, setIsDragging] = useState(false); const dragStartPos = useRef({ x: 0, y: 0 }); useEffect(() => { if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') { if (!isDevToolsEnabled()) { enableDevTools({ enabled: true, trackPerformance: true, logToConsole: false, slowUpdateThreshold: 16, emitPerformanceWarnings: true, }); } } }, []); const handleMinimize = useCallback(() => { setPanelState(prev => ({ ...prev, isMinimized: !prev.isMinimized })); }, []); const handleClose = useCallback(() => { setPanelState(prev => ({ ...prev, isOpen: false })); }, []); const handleMouseDown = useCallback((e) => { if (e.target.closest('button, input')) return; setIsDragging(true); dragStartPos.current = { x: e.clientX, y: e.clientY }; }, []); const handleMouseMove = useCallback((e) => { if (!isDragging) return; }, [isDragging]); const handleMouseUp = useCallback(() => { setIsDragging(false); }, []); useEffect(() => { if (isDragging) { document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; } }, [isDragging, handleMouseMove, handleMouseUp]); if (!panelState.isOpen) { return null; } return (_jsxs("div", { style: styles.panel(panelState), children: [_jsxs("div", { style: styles.header, onMouseDown: handleMouseDown, children: [_jsxs("div", { style: styles.headerTitle, children: [_jsx("span", { children: "\uD83D\uDD25" }), _jsx("span", { children: "SignalForge DevTools" }), isDevToolsEnabled() && (_jsx("span", { style: styles.badge('#4caf50'), children: "ON" }))] }), _jsxs("div", { style: styles.headerActions, children: [_jsx("button", { onClick: handleMinimize, style: styles.iconButton, title: panelState.isMinimized ? 'Maximize' : 'Minimize', children: panelState.isMinimized ? '◻' : '_' }), _jsx("button", { onClick: handleClose, style: styles.iconButton, title: "Close", children: "\u2715" })] })] }), !panelState.isMinimized && (_jsxs(_Fragment, { children: [_jsxs("div", { style: styles.tabs, children: [_jsx("button", { onClick: () => setActiveTab('signals'), style: styles.tab(activeTab === 'signals'), children: "\uD83D\uDCE1 Signals" }), _jsx("button", { onClick: () => setActiveTab('performance'), style: styles.tab(activeTab === 'performance'), children: "\u26A1 Performance" }), _jsx("button", { onClick: () => setActiveTab('plugins'), style: styles.tab(activeTab === 'plugins'), children: "\uD83E\uDDE9 Plugins" })] }), _jsxs("div", { style: styles.content, children: [activeTab === 'signals' && _jsx(SignalsTab, {}), activeTab === 'performance' && _jsx(PerformanceTab, {}), activeTab === 'plugins' && _jsx(PluginsTab, {})] })] }))] })); }; export const DevToolsProvider = ({ children }) => { const [showDevTools, setShowDevTools] = useState(false); useEffect(() => { if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'production') { setShowDevTools(true); } const handleKeyDown = (e) => { if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'D') { e.preventDefault(); setShowDevTools(prev => !prev); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, []); return (_jsxs(_Fragment, { children: [children, showDevTools && _jsx(DevToolsPanel, {})] })); }; function formatValue(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (typeof value === 'string') return `"${value}"`; if (typeof value === 'number') return String(value); if (typeof value === 'boolean') return String(value); if (Array.isArray(value)) { if (value.length === 0) return '[]'; if (value.length <= 3) return JSON.stringify(value); return `[${value.length} items]`; } if (typeof value === 'object') { const keys = Object.keys(value); if (keys.length === 0) return '{}'; if (keys.length <= 3) { try { return JSON.stringify(value); } catch { return '{...}'; } } return `{${keys.length} keys}`; } return String(value); } export default DevToolsPanel;