sync: UI polish - welcome screen, connections modal, search modals
- Redesigned welcome screen with centered Start button and CAPTURE quick add - Improved QuickAddInput with elegant mode tabs and circular submit button - Restored existing connections view in connections modal with edit/delete - Spotlight-style search modals across all search experiences - Three-panel color system and neutral design updates - Fixed textarea resize jump and node ID badge styling
This commit is contained in:
+483
-163
@@ -734,13 +734,14 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: '#0a0a0a',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #1f1f1f'
|
||||
gap: '16px',
|
||||
background: '#141414',
|
||||
padding: '16px 20px',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid #262626',
|
||||
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.02)'
|
||||
}}>
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="#666">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="#525252" style={{ flexShrink: 0 }}>
|
||||
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<input
|
||||
@@ -748,15 +749,16 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
value={nodeSearchQuery}
|
||||
onChange={(e) => setNodeSearchQuery(e.target.value)}
|
||||
onKeyDown={handleNodeSearchKeyDown}
|
||||
placeholder="Search node to connect..."
|
||||
placeholder="Connect to node..."
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: '#f8fafc',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit'
|
||||
color: '#fafafa',
|
||||
fontSize: '16px',
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 400
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -765,26 +767,30 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
{nodeSearchSuggestions.length > 0 && (
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
background: '#0a0a0a',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '8px',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto'
|
||||
background: '#141414',
|
||||
border: '1px solid #262626',
|
||||
borderRadius: '12px',
|
||||
maxHeight: '240px',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.02)'
|
||||
}}>
|
||||
{nodeSearchSuggestions.map((suggestion, index) => (
|
||||
<div
|
||||
key={suggestion.id}
|
||||
onClick={() => handleSelectNodeSuggestion(suggestion)}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
padding: '14px 16px',
|
||||
cursor: 'pointer',
|
||||
borderBottom: index < nodeSearchSuggestions.length - 1 ? '1px solid #1f1f1f' : 'none',
|
||||
background: index === selectedSearchIndex ? '#1f1f1f' : 'transparent',
|
||||
transition: 'background 0.1s'
|
||||
background: index === selectedSearchIndex ? '#1a1a1a' : 'transparent',
|
||||
transition: 'background 100ms ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (index !== selectedSearchIndex) {
|
||||
e.currentTarget.style.background = '#151515';
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
@@ -793,30 +799,35 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
color: '#000',
|
||||
background: '#22c55e',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '20px',
|
||||
textAlign: 'center',
|
||||
letterSpacing: '0.05em'
|
||||
}}>
|
||||
{suggestion.id}
|
||||
</span>
|
||||
<span style={{ fontSize: '13px', color: '#f8fafc' }}>
|
||||
{suggestion.title}
|
||||
</span>
|
||||
</div>
|
||||
{suggestion.dimensions && suggestion.dimensions.length > 0 && (
|
||||
<div style={{ fontSize: '11px', color: '#94a3b8' }}>
|
||||
{suggestion.dimensions.slice(0, 3).join(', ')}
|
||||
{suggestion.dimensions.length > 3 && ` +${suggestion.dimensions.length - 3} more`}
|
||||
</div>
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
color: '#0a0a0a',
|
||||
background: '#22c55e',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '28px',
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
fontFamily: "'SF Mono', 'Fira Code', monospace"
|
||||
}}>
|
||||
#{suggestion.id}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '15px',
|
||||
color: '#e5e5e5',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1
|
||||
}}>
|
||||
{suggestion.title}
|
||||
</span>
|
||||
{index === selectedSearchIndex && (
|
||||
<span style={{ color: '#525252', fontSize: '13px' }}>↵</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -826,7 +837,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
|
||||
{/* Existing Connections */}
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '12px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
<div style={{ fontSize: '12px', color: '#737373', marginBottom: '12px', letterSpacing: '0.02em' }}>
|
||||
Existing Connections
|
||||
</div>
|
||||
{loadingEdges.has(activeTab) ? (
|
||||
@@ -877,7 +888,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
flex: 1,
|
||||
fontSize: '11px',
|
||||
color: '#ddd',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '0',
|
||||
padding: '4px',
|
||||
@@ -1038,12 +1049,12 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#000' }}>
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'transparent' }}>
|
||||
{/* Tab Bar */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
borderBottom: '1px solid #333',
|
||||
background: '#0a0a0a',
|
||||
borderBottom: '1px solid #1a1a1a',
|
||||
background: '#0f0f0f',
|
||||
flexShrink: 0,
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden'
|
||||
@@ -1068,9 +1079,9 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRight: '1px solid #333',
|
||||
background: isActive ? '#000' : '#0a0a0a',
|
||||
borderBottom: isActive ? '2px solid #22c55e' : 'none',
|
||||
borderRight: '1px solid #1a1a1a',
|
||||
background: isActive ? '#121212' : '#0f0f0f',
|
||||
borderBottom: isActive ? '2px solid #666' : 'none',
|
||||
paddingBottom: isActive ? '0' : '2px',
|
||||
minWidth: '120px',
|
||||
maxWidth: '200px'
|
||||
@@ -1315,7 +1326,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
style={{
|
||||
color: '#3b82f6',
|
||||
fontSize: '11px',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '6px',
|
||||
padding: '4px 6px',
|
||||
@@ -1364,7 +1375,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Connections Button */}
|
||||
{/* Connections Button - Green CTA */}
|
||||
<button
|
||||
onClick={() => setShowConnectionsModal(true)}
|
||||
style={{
|
||||
@@ -1374,9 +1385,9 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
padding: '6px 10px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
color: '#94a3b8',
|
||||
color: '#22c55e',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1f1f1f',
|
||||
border: '1px solid #166534',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
textTransform: 'uppercase',
|
||||
@@ -1385,17 +1396,15 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
flexShrink: 0
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#0f0f0f';
|
||||
e.currentTarget.style.borderColor = '#2a2a2a';
|
||||
e.currentTarget.style.color = '#cbd5f5';
|
||||
e.currentTarget.style.background = '#0f2417';
|
||||
e.currentTarget.style.borderColor = '#22c55e';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#1f1f1f';
|
||||
e.currentTarget.style.color = '#94a3b8';
|
||||
e.currentTarget.style.borderColor = '#166534';
|
||||
}}
|
||||
>
|
||||
<Link size={12} />
|
||||
<Link size={14} />
|
||||
{activeTab && edgesData[activeTab] && edgesData[activeTab].length > 0 && (
|
||||
<span style={{
|
||||
fontSize: '10px',
|
||||
@@ -1412,6 +1421,42 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
onClick={() => confirmDeleteNode(activeTab)}
|
||||
disabled={deletingNode === activeTab}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '6px',
|
||||
color: deletingNode === activeTab ? '#994444' : '#525252',
|
||||
background: 'transparent',
|
||||
border: '1px solid #262626',
|
||||
borderRadius: '6px',
|
||||
cursor: deletingNode === activeTab ? 'wait' : 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
flexShrink: 0
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (deletingNode !== activeTab) {
|
||||
e.currentTarget.style.color = '#dc2626';
|
||||
e.currentTarget.style.borderColor = '#dc2626';
|
||||
e.currentTarget.style.background = 'rgba(220, 38, 38, 0.1)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (deletingNode !== activeTab) {
|
||||
e.currentTarget.style.color = '#525252';
|
||||
e.currentTarget.style.borderColor = '#262626';
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
}
|
||||
}}
|
||||
title="Delete node"
|
||||
>
|
||||
{deletingNode === activeTab ? '...' : <Trash2 size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Title Row with ID and Trash */}
|
||||
@@ -1443,7 +1488,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '0',
|
||||
padding: '4px 8px',
|
||||
@@ -1474,7 +1519,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
padding: '4px 8px',
|
||||
margin: '0',
|
||||
borderRadius: '0',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: '1px solid transparent',
|
||||
transition: 'border-color 0.2s',
|
||||
flex: 1,
|
||||
@@ -1500,36 +1545,6 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
</h1>
|
||||
)}
|
||||
|
||||
{/* Trash Icon */}
|
||||
<button
|
||||
onClick={() => confirmDeleteNode(activeTab)}
|
||||
disabled={deletingNode === activeTab}
|
||||
style={{
|
||||
color: deletingNode === activeTab ? '#994444' : '#dc2626',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: deletingNode === activeTab ? 'not-allowed' : 'pointer',
|
||||
padding: '4px',
|
||||
transition: 'color 0.2s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (deletingNode !== activeTab) {
|
||||
e.currentTarget.style.color = '#b91c1c';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (deletingNode !== activeTab) {
|
||||
e.currentTarget.style.color = '#dc2626';
|
||||
}
|
||||
}}
|
||||
title="Delete node"
|
||||
>
|
||||
{deletingNode === activeTab ? '...' : <Trash2 size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1634,7 +1649,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
color: '#e5e5e5',
|
||||
fontSize: '16px', /* Increased from 15px for better readability */
|
||||
lineHeight: '1.7', /* Increased from 1.6 for more comfortable reading */
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: '0',
|
||||
padding: '12px', /* Increased from 8px to 12px */
|
||||
@@ -1680,7 +1695,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
margin: '0',
|
||||
borderRadius: '0',
|
||||
transition: 'all 0.2s',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
whiteSpace: 'pre-wrap',
|
||||
minHeight: '200px',
|
||||
@@ -1708,7 +1723,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
flex: 1
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
@@ -1773,7 +1788,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
color: '#e5e5e5',
|
||||
fontSize: '11px',
|
||||
lineHeight: '1.4',
|
||||
background: '#0a0a0a',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '0',
|
||||
padding: '8px',
|
||||
@@ -1794,7 +1809,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
lineHeight: '1.4',
|
||||
cursor: 'pointer',
|
||||
padding: '8px',
|
||||
background: '#0a0a0a',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '0',
|
||||
transition: 'all 0.2s',
|
||||
@@ -1815,7 +1830,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
fontStyle: 'italic',
|
||||
cursor: 'pointer',
|
||||
padding: '8px',
|
||||
background: '#0a0a0a',
|
||||
background: 'transparent',
|
||||
border: '1px dashed #1a1a1a',
|
||||
borderRadius: '0',
|
||||
transition: 'all 0.2s'
|
||||
@@ -1854,93 +1869,398 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
onConfirm={handleConfirmNodeDelete}
|
||||
onCancel={handleCancelNodeDelete}
|
||||
/>
|
||||
{showConnectionsModal && (
|
||||
{showConnectionsModal && activeTab && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowConnectionsModal(false);
|
||||
setNodeSearchQuery('');
|
||||
setNodeSearchSuggestions([]);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 0, 0, 0.85)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 20,
|
||||
padding: '20px'
|
||||
paddingTop: '10vh',
|
||||
zIndex: 9999,
|
||||
animation: 'backdropIn 200ms ease-out'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '520px',
|
||||
maxHeight: '85vh',
|
||||
background: '#050505',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '12px',
|
||||
width: '100%',
|
||||
maxWidth: '640px',
|
||||
maxHeight: '80vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)'
|
||||
animation: 'containerIn 200ms cubic-bezier(0.16, 1, 0.3, 1)'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '16px 20px',
|
||||
borderBottom: '1px solid #1f1f1f'
|
||||
{/* Search Input */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
background: '#141414',
|
||||
border: '1px solid #262626',
|
||||
borderRadius: '16px',
|
||||
padding: '20px 24px',
|
||||
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.04), 0 24px 48px -12px rgba(0, 0, 0, 0.6)'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.02em',
|
||||
color: '#f8fafc',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<Link size={16} />
|
||||
Connections
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowConnectionsModal(false)}
|
||||
<svg width="22" height="22" viewBox="0 0 20 20" fill="#525252" style={{ flexShrink: 0 }}>
|
||||
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={nodeSearchQuery}
|
||||
onChange={(e) => setNodeSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
setShowConnectionsModal(false);
|
||||
setNodeSearchQuery('');
|
||||
setNodeSearchSuggestions([]);
|
||||
} else {
|
||||
handleNodeSearchKeyDown(e);
|
||||
}
|
||||
}}
|
||||
placeholder="Search to add connection..."
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f',
|
||||
background: 'transparent',
|
||||
color: '#94a3b8',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s'
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: '#fafafa',
|
||||
fontSize: '18px',
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 400
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#0f0f0f';
|
||||
e.currentTarget.style.borderColor = '#2a2a2a';
|
||||
e.currentTarget.style.color = '#cbd5f5';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#1f1f1f';
|
||||
e.currentTarget.style.color = '#94a3b8';
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: '20px', overflowY: 'auto', flex: 1 }}>
|
||||
{renderConnectionsBody()}
|
||||
/>
|
||||
<kbd style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '4px 8px',
|
||||
background: '#262626',
|
||||
borderRadius: '6px',
|
||||
fontSize: '11px',
|
||||
fontFamily: 'inherit',
|
||||
color: '#737373',
|
||||
border: '1px solid #333'
|
||||
}}>
|
||||
esc
|
||||
</kbd>
|
||||
</div>
|
||||
|
||||
{/* Search Results */}
|
||||
{nodeSearchSuggestions.length > 0 && (
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
background: '#141414',
|
||||
border: '1px solid #262626',
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.04), 0 24px 48px -12px rgba(0, 0, 0, 0.6)',
|
||||
animation: 'resultsIn 150ms ease-out',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{nodeSearchSuggestions.map((suggestion, index) => (
|
||||
<div
|
||||
key={suggestion.id}
|
||||
onClick={() => {
|
||||
handleSelectNodeSuggestion(suggestion);
|
||||
setNodeSearchQuery('');
|
||||
setNodeSearchSuggestions([]);
|
||||
}}
|
||||
style={{
|
||||
padding: '14px 20px',
|
||||
cursor: 'pointer',
|
||||
borderBottom: index < nodeSearchSuggestions.length - 1 ? '1px solid #1f1f1f' : 'none',
|
||||
background: index === selectedSearchIndex ? '#1a1a1a' : 'transparent',
|
||||
transition: 'background 100ms ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '14px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (index !== selectedSearchIndex) {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
color: '#0a0a0a',
|
||||
background: '#22c55e',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '28px',
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
fontFamily: "'SF Mono', 'Fira Code', monospace"
|
||||
}}>
|
||||
#{suggestion.id}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '14px',
|
||||
color: '#e5e5e5',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1
|
||||
}}>
|
||||
{suggestion.title}
|
||||
</span>
|
||||
{index === selectedSearchIndex && (
|
||||
<span style={{ color: '#525252', fontSize: '13px' }}>↵</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty search state */}
|
||||
{nodeSearchQuery && nodeSearchSuggestions.length === 0 && (
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
padding: '24px',
|
||||
background: '#141414',
|
||||
border: '1px solid #262626',
|
||||
borderRadius: '16px',
|
||||
color: '#525252',
|
||||
fontSize: '14px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
No results for "{nodeSearchQuery}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Existing Connections */}
|
||||
{!nodeSearchQuery && (
|
||||
<div style={{
|
||||
marginTop: '16px',
|
||||
background: '#141414',
|
||||
border: '1px solid #262626',
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.04), 0 8px 24px -8px rgba(0, 0, 0, 0.4)',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '16px 20px',
|
||||
borderBottom: '1px solid #1f1f1f',
|
||||
fontSize: '12px',
|
||||
color: '#737373',
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600
|
||||
}}>
|
||||
Existing Connections ({(edgesData[activeTab] || []).length})
|
||||
</div>
|
||||
<div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
|
||||
{loadingEdges.has(activeTab) ? (
|
||||
<div style={{ padding: '24px', color: '#666', fontSize: '13px', textAlign: 'center' }}>
|
||||
Loading...
|
||||
</div>
|
||||
) : (edgesData[activeTab] || []).length === 0 ? (
|
||||
<div style={{ padding: '32px 24px', color: '#525252', fontSize: '14px', textAlign: 'center' }}>
|
||||
No connections yet. Search above to add one.
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{(edgesData[activeTab] || []).map((connection) => (
|
||||
<div
|
||||
key={connection.id}
|
||||
style={{
|
||||
padding: '14px 20px',
|
||||
borderBottom: '1px solid #1f1f1f',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
{/* Connection header row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
color: '#0a0a0a',
|
||||
background: '#22c55e',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '28px',
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
fontFamily: "'SF Mono', 'Fira Code', monospace"
|
||||
}}>
|
||||
#{connection.connected_node.id}
|
||||
</span>
|
||||
<span
|
||||
onClick={() => onNodeClick?.(connection.connected_node.id)}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: '14px',
|
||||
color: '#e5e5e5',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#22c55e'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#e5e5e5'; }}
|
||||
>
|
||||
{connection.connected_node.title}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => deleteEdge(connection.edge.id)}
|
||||
disabled={deletingEdge === connection.edge.id}
|
||||
style={{
|
||||
padding: '6px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#525252',
|
||||
cursor: deletingEdge === connection.edge.id ? 'not-allowed' : 'pointer',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.15s ease',
|
||||
opacity: deletingEdge === connection.edge.id ? 0.5 : 1
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#ef4444'; e.currentTarget.style.background = '#1f1f1f'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#525252'; e.currentTarget.style.background = 'transparent'; }}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Description row */}
|
||||
{edgeEditingId === connection.edge.id ? (
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
<input
|
||||
value={edgeEditingValue}
|
||||
onChange={(e) => setEdgeEditingValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
saveEdgeExplanation(connection.edge.id, connection.edge.context);
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
cancelEditEdgeExplanation();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
placeholder="Add explanation..."
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: '12px',
|
||||
color: '#e5e5e5',
|
||||
background: '#0a0a0a',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '6px',
|
||||
padding: '8px 10px',
|
||||
outline: 'none',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => saveEdgeExplanation(connection.edge.id, connection.edge.context)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#22c55e',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#0a0a0a',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEditEdgeExplanation}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#262626',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#999',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => startEditEdgeExplanation(connection.edge.id, connection.edge.context?.explanation as string | undefined)}
|
||||
style={{
|
||||
fontSize: '12px',
|
||||
color: connection.edge.context?.explanation ? '#888' : '#525252',
|
||||
cursor: 'pointer',
|
||||
padding: '4px 0',
|
||||
fontStyle: connection.edge.context?.explanation ? 'normal' : 'italic'
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#aaa'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = connection.edge.context?.explanation ? '#888' : '#525252'; }}
|
||||
>
|
||||
{(connection.edge.context?.explanation as string) || 'Click to add explanation...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<style jsx>{`
|
||||
@keyframes backdropIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes containerIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96) translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes resultsIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -176,134 +176,51 @@ export default function DimensionSearchModal({
|
||||
|
||||
const modalContent = (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
animation: 'fadeIn 150ms ease-out'
|
||||
}}
|
||||
className="search-backdrop"
|
||||
onClick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Search dimensions"
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
style={{
|
||||
background: '#050505',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '12px',
|
||||
width: '90%',
|
||||
maxWidth: '500px',
|
||||
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)',
|
||||
animation: 'slideIn 150ms ease-out'
|
||||
}}
|
||||
>
|
||||
<div ref={modalRef} className="search-container">
|
||||
{/* Search Input */}
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
borderBottom: (suggestions.length > 0 || canCreateNew) ? '1px solid #1f1f1f' : 'none'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: '#0a0a0a',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f'
|
||||
}}>
|
||||
{/* Search Icon */}
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="#666">
|
||||
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
|
||||
{/* Input */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search or create dimension..."
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '20px',
|
||||
padding: '0 4px',
|
||||
lineHeight: 1,
|
||||
transition: 'color 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#666'; }}
|
||||
aria-label="Close search"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="search-input-wrapper">
|
||||
<svg className="search-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search or create dimension..."
|
||||
className="search-input"
|
||||
/>
|
||||
|
||||
<div className="search-shortcut">
|
||||
<kbd>esc</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
{/* Results */}
|
||||
{suggestions.length > 0 && (
|
||||
<div style={{
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<div className="search-results">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={suggestion.dimension}
|
||||
onClick={() => handleSelectDimension(suggestion.dimension)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: '13px',
|
||||
background: index === selectedIndex ? '#252525' : 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: index < suggestions.length - 1 ? '1px solid #1f1f1f' : 'none',
|
||||
color: '#e5e5e5',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
className={`search-result-item ${index === selectedIndex ? 'selected' : ''}`}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{
|
||||
color: suggestion.isPriority ? '#22c55e' : '#e5e5e5'
|
||||
}}>
|
||||
{suggestion.dimension}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ color: '#666', fontSize: '11px' }}>
|
||||
{suggestion.count}
|
||||
<span className={`result-name ${suggestion.isPriority ? 'priority' : ''}`}>
|
||||
{suggestion.dimension}
|
||||
</span>
|
||||
<span className="result-count">{suggestion.count}</span>
|
||||
{index === selectedIndex && (
|
||||
<span className="result-hint">↵</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -311,57 +228,225 @@ export default function DimensionSearchModal({
|
||||
|
||||
{/* Create New Option */}
|
||||
{canCreateNew && (
|
||||
<button
|
||||
onClick={() => handleSelectDimension(searchQuery.trim())}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
fontSize: '13px',
|
||||
background: selectedIndex === suggestions.length ? '#252525' : 'transparent',
|
||||
border: 'none',
|
||||
borderTop: suggestions.length > 0 ? '1px solid #1f1f1f' : 'none',
|
||||
color: '#22c55e',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
onMouseEnter={() => setSelectedIndex(suggestions.length)}
|
||||
>
|
||||
<span style={{ fontSize: '16px', fontWeight: 300 }}>+</span>
|
||||
Create "{searchQuery.trim()}"
|
||||
</button>
|
||||
<div className="search-create">
|
||||
<button
|
||||
onClick={() => handleSelectDimension(searchQuery.trim())}
|
||||
onMouseEnter={() => setSelectedIndex(suggestions.length)}
|
||||
className={`create-button ${selectedIndex === suggestions.length ? 'selected' : ''}`}
|
||||
>
|
||||
<span className="create-icon">+</span>
|
||||
Create "{searchQuery.trim()}"
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Keyboard Hint */}
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderTop: '1px solid #1f1f1f',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '16px',
|
||||
fontSize: '11px',
|
||||
color: '#666'
|
||||
}}>
|
||||
<span>↑↓ Navigate</span>
|
||||
<span>↵ Select</span>
|
||||
<span>Esc Close</span>
|
||||
</div>
|
||||
{/* Empty state */}
|
||||
{!searchQuery && suggestions.length === 0 && (
|
||||
<div className="search-empty">
|
||||
Start typing to search dimensions
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fadeIn {
|
||||
.search-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
z-index: 9999;
|
||||
animation: backdropIn 200ms ease-out;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-height: 70vh;
|
||||
animation: containerIn 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #525252;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fafafa;
|
||||
font-size: 18px;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
.search-shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.search-shortcut kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 8px;
|
||||
background: #262626;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
color: #737373;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-top: 8px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
||||
animation: resultsIn 150ms ease-out;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid #1f1f1f;
|
||||
cursor: pointer;
|
||||
transition: background 100ms ease;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item:hover,
|
||||
.search-result-item.selected {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
flex: 1;
|
||||
color: #e5e5e5;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.result-name.priority {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.result-count {
|
||||
color: #525252;
|
||||
font-size: 12px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
.result-hint {
|
||||
color: #525252;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search-create {
|
||||
margin-top: 8px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.create-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 100ms ease;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
color: #22c55e;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.create-button:hover,
|
||||
.create-button.selected {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.create-icon {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.search-empty {
|
||||
margin-top: 8px;
|
||||
padding: 32px 24px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
color: #525252;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes backdropIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@keyframes containerIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
transform: scale(0.96) translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resultsIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
|
||||
@@ -160,182 +160,232 @@ export default function EdgeSearchModal({
|
||||
|
||||
const modalContent = (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
animation: 'fadeIn 150ms ease-out'
|
||||
}}
|
||||
className="search-backdrop"
|
||||
onClick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Search nodes to connect"
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
style={{
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '8px',
|
||||
width: '90%',
|
||||
maxWidth: '600px',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.6)',
|
||||
animation: 'slideIn 150ms ease-out'
|
||||
}}
|
||||
>
|
||||
<div ref={modalRef} className="search-container">
|
||||
{/* Search Input */}
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
borderBottom: suggestions.length > 0 ? '1px solid #2a2a2a' : 'none'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: '#0a0a0a',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #333'
|
||||
}}>
|
||||
{/* Search Icon */}
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="#666">
|
||||
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
|
||||
{/* Input */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search node to connect..."
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '20px',
|
||||
padding: '0 4px',
|
||||
lineHeight: 1,
|
||||
transition: 'color 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#666'; }}
|
||||
aria-label="Close search"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="search-input-wrapper">
|
||||
<svg className="search-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Connect to node..."
|
||||
className="search-input"
|
||||
/>
|
||||
|
||||
<div className="search-shortcut">
|
||||
<kbd>esc</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
{/* Results */}
|
||||
{suggestions.length > 0 && (
|
||||
<div style={{
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{suggestions.map((suggestion, index) => {
|
||||
const primaryDimension = suggestion.dimensions && suggestion.dimensions.length > 0
|
||||
? suggestion.dimensions[0]
|
||||
: '';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSelectSuggestion(suggestion)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
fontSize: '13px',
|
||||
background: index === selectedIndex ? '#252525' : 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: index < suggestions.length - 1 ? '1px solid #2a2a2a' : 'none',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
>
|
||||
{primaryDimension && (
|
||||
<span style={{
|
||||
color: '#666',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
minWidth: '60px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em'
|
||||
}}>
|
||||
{primaryDimension}
|
||||
</span>
|
||||
)}
|
||||
<span style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: '#e5e5e5'
|
||||
}}>
|
||||
{suggestion.title}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="search-results">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={suggestion.id}
|
||||
onClick={() => handleSelectSuggestion(suggestion)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
className={`search-result-item ${index === selectedIndex ? 'selected' : ''}`}
|
||||
>
|
||||
<span className="result-id">#{suggestion.id}</span>
|
||||
<span className="result-title">{suggestion.title}</span>
|
||||
{index === selectedIndex && (
|
||||
<span className="result-hint">↵</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Keyboard Hint */}
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderTop: '1px solid #2a2a2a',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '16px',
|
||||
fontSize: '11px',
|
||||
color: '#666'
|
||||
}}>
|
||||
<span>↑↓ Navigate</span>
|
||||
<span>↵ Select</span>
|
||||
<span>Esc Close</span>
|
||||
</div>
|
||||
{/* Empty state */}
|
||||
{searchQuery && suggestions.length === 0 && (
|
||||
<div className="search-empty">
|
||||
No results for "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fadeIn {
|
||||
.search-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
z-index: 9999;
|
||||
animation: backdropIn 200ms ease-out;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-height: 70vh;
|
||||
animation: containerIn 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #525252;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fafafa;
|
||||
font-size: 18px;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
.search-shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.search-shortcut kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 8px;
|
||||
background: #262626;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
color: #737373;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-top: 8px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
||||
animation: resultsIn 150ms ease-out;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid #1f1f1f;
|
||||
cursor: pointer;
|
||||
transition: background 100ms ease;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item:hover,
|
||||
.search-result-item.selected {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.result-id {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
color: #0a0a0a;
|
||||
background: #22c55e;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
min-width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
flex: 1;
|
||||
color: #e5e5e5;
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-hint {
|
||||
color: #525252;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search-empty {
|
||||
margin-top: 8px;
|
||||
padding: 32px 24px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
color: #525252;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes backdropIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@keyframes containerIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
transform: scale(0.96) translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes resultsIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
|
||||
Reference in New Issue
Block a user