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:
“BeeRad”
2025-12-24 16:55:10 +11:00
parent ce5bee826c
commit 57a20899a5
12 changed files with 1726 additions and 1174 deletions
+62
View File
@@ -130,3 +130,65 @@ html, body {
opacity: 1; opacity: 1;
} }
} }
/* Modal/Popup animations */
@keyframes modalFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes modalSlideUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes modalFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
/* Modal utility classes */
.modal-backdrop {
animation: modalFadeIn 0.15s ease-out;
}
.modal-content {
animation: modalSlideUp 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Text truncation utilities */
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.text-truncate-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.text-truncate-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
+93 -109
View File
@@ -111,6 +111,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
const handleCreated = (event: Event) => { const handleCreated = (event: Event) => {
const detail = (event as CustomEvent<{ delegation: AgentDelegation }>).detail; const detail = (event as CustomEvent<{ delegation: AgentDelegation }>).detail;
@@ -187,29 +188,28 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
}; };
return ( return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#0a0a0a', position: 'relative' }}> <div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', position: 'relative' }}>
{/* Mode Header */} {/* Mode Header */}
{mode === 'quickadd' ? ( {mode === 'quickadd' ? (
<div style={{ <div style={{
padding: '20px', padding: '20px',
borderBottom: '1px solid #1a1a1a', background: 'transparent',
background: '#0a0a0a',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: '24px',
justifyContent: 'space-between',
alignItems: 'center',
height: '100%', height: '100%',
position: 'relative' position: 'relative'
}}> }}>
{/* Collapse button - top right */} {/* Top Bar - just collapse button */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
marginBottom: '24px'
}}>
{onCollapse && ( {onCollapse && (
<button <button
onClick={onCollapse} onClick={onCollapse}
style={{ style={{
position: 'absolute',
top: '12px',
left: '12px',
width: '28px', width: '28px',
height: '28px', height: '28px',
borderRadius: '6px', borderRadius: '6px',
@@ -235,103 +235,85 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
<Minimize2 size={14} /> <Minimize2 size={14} />
</button> </button>
)} )}
{/* Top spacer */} </div>
<div style={{ flex: 1 }} />
{/* Session Section */} {/* Center Section - Start button centered */}
<div style={{ <div style={{
background: 'transparent', flex: 1,
border: 'none', display: 'flex',
borderRadius: '8px', justifyContent: 'center',
padding: '20px', alignItems: 'center'
textAlign: 'center',
width: '100%',
maxWidth: '600px'
}}> }}>
<pre style={{
color: '#22c55e',
fontSize: '13px',
fontFamily: "'JetBrains Mono', ui-monospace, Courier, monospace",
lineHeight: '1.1',
margin: '0 0 20px 0',
letterSpacing: '0',
fontWeight: 700,
textShadow: '0 0 10px rgba(139, 212, 80, 0.4)'
}}>{`
██████╗ █████╗ ██╗ ██╗
██╔══██╗██╔══██╗ ██║ ██║
██████╔╝███████║█████╗███████║
██╔══██╗██╔══██║╚════╝██╔══██║
██║ ██║██║ ██║ ██║ ██║
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝
`}</pre>
<button <button
onClick={() => { onClick={() => {
setMode('session'); setMode('session');
setRahMode('easy'); // Always start new session in easy mode setRahMode('easy');
}} }}
style={{ style={{
width: 'auto', display: 'flex',
padding: '14px 32px', alignItems: 'center',
background: '#22c55e', gap: '14px',
padding: '0',
background: 'transparent',
border: 'none', border: 'none',
borderRadius: '6px', cursor: 'pointer'
color: '#0a0a0a',
fontSize: '14px',
fontWeight: 600,
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
cursor: 'pointer',
transition: 'all 0.15s ease',
letterSpacing: '0.02em'
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = '#2dd46d'; const text = e.currentTarget.querySelector('.start-text') as HTMLElement;
e.currentTarget.style.boxShadow = '0 4px 16px rgba(34, 197, 94, 0.4)'; const icon = e.currentTarget.querySelector('.start-icon') as HTMLElement;
e.currentTarget.style.transform = 'translateY(-2px)'; if (text) text.style.color = '#22c55e';
if (icon) {
icon.style.transform = 'translateY(-3px)';
icon.style.boxShadow = '0 8px 20px rgba(34, 197, 94, 0.3), 0 0 0 4px rgba(34, 197, 94, 0.1)';
}
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = '#22c55e'; const text = e.currentTarget.querySelector('.start-text') as HTMLElement;
e.currentTarget.style.boxShadow = 'none'; const icon = e.currentTarget.querySelector('.start-icon') as HTMLElement;
e.currentTarget.style.transform = 'translateY(0)'; if (text) text.style.color = '#737373';
if (icon) {
icon.style.transform = 'translateY(0)';
icon.style.boxShadow = '0 0 0 0 rgba(34, 197, 94, 0)';
}
}} }}
> >
Start Session <span
className="start-text"
style={{
color: '#737373',
fontSize: '11px',
fontWeight: 500,
letterSpacing: '0.15em',
textTransform: 'uppercase',
transition: 'color 0.2s ease'
}}
>
Start
</span>
<div
className="start-icon"
style={{
width: '44px',
height: '44px',
borderRadius: '50%',
background: '#22c55e',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: '0 0 0 0 rgba(34, 197, 94, 0)'
}}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0a0a0a" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 19V5"/>
<path d="M5 12l7-7 7 7"/>
</svg>
</div>
</button> </button>
</div> </div>
{/* Bottom spacer */} {/* Quick Add - at bottom, full width with padding */}
<div style={{ flex: 1 }} /> <div style={{ width: '100%', padding: '0 16px 16px 16px' }}>
{/* Divider */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
width: '100%',
maxWidth: '600px',
marginBottom: '12px'
}}>
<div style={{ flex: 1, height: '1px', background: '#1a1a1a' }} />
<span style={{
color: '#6b6b6b',
fontSize: '12px',
fontWeight: 700,
fontFamily: "'JetBrains Mono', ui-monospace",
textTransform: 'uppercase',
letterSpacing: '0.15em',
padding: '0 12px'
}}>or</span>
<div style={{ flex: 1, height: '1px', background: '#1a1a1a' }} />
</div>
{/* Quick Add Section */}
<div style={{
background: 'transparent',
padding: '0',
width: '100%',
maxWidth: '600px',
marginBottom: '12px'
}}>
<QuickAddInput <QuickAddInput
activeDelegations={orderedDelegations} activeDelegations={orderedDelegations}
onSubmit={handleQuickAddSubmit} onSubmit={handleQuickAddSubmit}
@@ -375,7 +357,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
<Minimize2 size={14} /> <Minimize2 size={14} />
</button> </button>
)} )}
{/* Quick Add button - positioned at far right */} {/* Capture button - positioned at far right */}
<div style={{ <div style={{
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
@@ -387,29 +369,31 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
zIndex: 20 zIndex: 20
}}> }}>
<button <button
onClick={() => { onClick={() => setMode('quickadd')}
window.dispatchEvent(new CustomEvent('rah:switch-quickadd'));
}}
style={{ style={{
background: 'none',
border: 'none',
color: '#fff',
fontSize: '12px',
cursor: 'pointer',
padding: '4px 8px',
transition: 'color 0.2s',
fontFamily: 'inherit',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '6px', gap: '6px',
padding: '6px 12px',
background: 'transparent',
border: '1px solid #1f1f1f',
borderRadius: '6px',
color: '#e5e5e5',
fontSize: '12px',
fontWeight: 500,
cursor: 'pointer',
transition: 'all 0.15s ease',
fontFamily: 'inherit',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '0.05em' letterSpacing: '0.05em'
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.color = '#d0d0d0'; e.currentTarget.style.background = '#1a1a1a';
e.currentTarget.style.borderColor = '#2a2a2a';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.color = '#fff'; e.currentTarget.style.background = 'transparent';
e.currentTarget.style.borderColor = '#1f1f1f';
}} }}
> >
<span style={{ <span style={{
@@ -419,14 +403,14 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
width: '16px', width: '16px',
height: '16px', height: '16px',
borderRadius: '50%', borderRadius: '50%',
background: '#fff', background: '#22c55e',
color: '#0a0a0a', color: '#0a0a0a',
fontSize: '12px', fontSize: '12px',
lineHeight: 1, lineHeight: 1,
fontWeight: 300, fontWeight: 600,
flexShrink: 0 flexShrink: 0
}}>+</span> }}>+</span>
Quick Add Capture
</button> </button>
</div> </div>
@@ -565,7 +549,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
align-items: center; align-items: center;
gap: 6px; gap: 6px;
padding: 8px 12px 0 12px; padding: 8px 12px 0 12px;
background: #0a0a0a; background: #101010;
border-bottom: 1px solid #1a1a1a; border-bottom: 1px solid #1a1a1a;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
@@ -577,7 +561,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
} }
.agent-tabs::-webkit-scrollbar-track { .agent-tabs::-webkit-scrollbar-track {
background: #0d0d0d; background: #131313;
} }
.agent-tabs::-webkit-scrollbar-thumb { .agent-tabs::-webkit-scrollbar-thumb {
@@ -598,7 +582,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
} }
.agent-tab-wrapper.active { .agent-tab-wrapper.active {
background: #141414; background: #181818;
} }
.agent-tab { .agent-tab {
+198 -239
View File
@@ -25,9 +25,9 @@ const MODE_CONFIG: Array<{
{ {
key: 'link', key: 'link',
label: 'Link', label: 'Link',
hint: 'Drop URLs for auto YouTube/PDF/Web extraction', hint: 'Drop URLs for auto extraction',
icon: ( icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M10 13a5 5 0 0 0 7.54.35l2.12-2.12a5 5 0 1 0-7.07-7.07l-1.29 1.29" strokeLinecap="round" strokeLinejoin="round" /> <path d="M10 13a5 5 0 0 0 7.54.35l2.12-2.12a5 5 0 1 0-7.07-7.07l-1.29 1.29" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14 11a5 5 0 0 0-7.54-.35l-2.12 2.12a5 5 0 1 0 7.07 7.07l1.29-1.29" strokeLinecap="round" strokeLinejoin="round" /> <path d="M14 11a5 5 0 0 0-7.54-.35l-2.12 2.12a5 5 0 1 0 7.07 7.07l1.29-1.29" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
@@ -36,26 +36,23 @@ const MODE_CONFIG: Array<{
{ {
key: 'note', key: 'note',
label: 'Note', label: 'Note',
hint: 'Quick jot no extraction, no summary', hint: 'Quick note, no processing',
icon: ( icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M12 20h9" strokeLinecap="round" /> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12 4h9" strokeLinecap="round" /> <path d="M14 2v6h6" strokeLinecap="round" strokeLinejoin="round" />
<path d="M4 4h1v16H4z" /> <path d="M16 13H8" strokeLinecap="round" />
<path d="M7 9h7" strokeLinecap="round" /> <path d="M16 17H8" strokeLinecap="round" />
<path d="M7 13h5" strokeLinecap="round" />
</svg> </svg>
) )
}, },
{ {
key: 'chat', key: 'chat',
label: 'Chat', label: 'Chat',
hint: 'Paste messy transcripts — store raw text + summary', hint: 'Paste conversations',
icon: ( icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 15a2 2 0 0 1-2 2H9l-4 4V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2z" strokeLinecap="round" strokeLinejoin="round" /> <path d="M21 15a2 2 0 0 1-2 2H9l-4 4V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2z" strokeLinecap="round" strokeLinejoin="round" />
<path d="M7 8h10" strokeLinecap="round" />
<path d="M7 12h6" strokeLinecap="round" />
</svg> </svg>
) )
} }
@@ -67,18 +64,13 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [manualMode, setManualMode] = useState<QuickAddIntent | null>(null); const [manualMode, setManualMode] = useState<QuickAddIntent | null>(null);
const [autoMode, setAutoMode] = useState<QuickAddIntent>('link'); const [autoMode, setAutoMode] = useState<QuickAddIntent>('link');
const [autoReason, setAutoReason] = useState<string | null>(null);
const effectiveMode: QuickAddIntent = manualMode ?? autoMode; const effectiveMode: QuickAddIntent = manualMode ?? autoMode;
const currentPlaceholder = useMemo(() => { const currentPlaceholder = useMemo(() => {
if (effectiveMode === 'note') { if (effectiveMode === 'note') return 'Write a quick note...';
return 'Write a quick note — no extraction, just append to your graph'; if (effectiveMode === 'chat') return 'Paste conversation...';
} return 'Paste a link or write something...';
if (effectiveMode === 'chat') {
return 'Paste any conversation (ChatGPT, Claude, etc.) and we will store raw text + summary';
}
return "Drop links, URL's, ideas or notes to add new nodes";
}, [effectiveMode]); }, [effectiveMode]);
const maxConcurrent = 5; const maxConcurrent = 5;
@@ -89,26 +81,15 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
const inferChatIntent = (value: string) => { const inferChatIntent = (value: string) => {
const trimmed = value.trim(); const trimmed = value.trim();
if (!trimmed) { if (!trimmed || manualMode) return;
setAutoMode('link');
setAutoReason(null);
return;
}
if (manualMode) return;
const newlineCount = (trimmed.match(/\n/g)?.length ?? 0); const newlineCount = (trimmed.match(/\n/g)?.length ?? 0);
const looksLikeTranscript = const looksLikeTranscript =
newlineCount >= 2 || newlineCount >= 2 ||
/You said:|ChatGPT said:|Claude said:|Assistant:|User:/i.test(trimmed) || /You said:|ChatGPT said:|Claude said:|Assistant:|User:/i.test(trimmed);
/\b\d{1,2}:\d{2}\b/.test(trimmed);
if (looksLikeTranscript && trimmed.length > 280) { if (looksLikeTranscript && trimmed.length > 280) {
setAutoMode('chat'); setAutoMode('chat');
setAutoReason('Detected chat transcript');
} else {
setAutoMode('link');
setAutoReason(null);
} }
}; };
@@ -120,7 +101,6 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
const handleModeClick = (mode: QuickAddIntent) => { const handleModeClick = (mode: QuickAddIntent) => {
setManualMode(mode); setManualMode(mode);
setAutoMode(mode); setAutoMode(mode);
setAutoReason(null);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
@@ -132,7 +112,7 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
setInput(''); setInput('');
setManualMode(null); setManualMode(null);
setAutoMode('link'); setAutoMode('link');
setAutoReason(null); setIsExpanded(false);
} catch (error) { } catch (error) {
console.error('[QuickAddInput] Submit error:', error); console.error('[QuickAddInput] Submit error:', error);
} finally { } finally {
@@ -140,147 +120,83 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
} }
}; };
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isLinkMode = effectiveMode === 'link'; if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
const shouldSubmit = isLinkMode
? (e.key === 'Enter' && !e.shiftKey)
: (e.key === 'Enter' && (e.metaKey || e.ctrlKey));
if (shouldSubmit) {
e.preventDefault(); e.preventDefault();
handleSubmit(); handleSubmit();
} }
}; if (e.key === 'Escape') {
setIsExpanded(false);
const renderInputField = () => { setInput('');
if (effectiveMode === 'link') {
return (
<input
type="text"
value={input}
onChange={(e) => handleInputChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={currentPlaceholder}
disabled={isPosting || isSoftLimited}
autoFocus
style={{
flex: 1,
padding: '12px 16px',
background: '#0a0a0a',
border: '2px solid #22c55e',
borderRadius: '6px',
color: '#e5e5e5',
fontSize: '13px',
fontFamily: "'JetBrains Mono', ui-monospace",
outline: 'none',
transition: 'all 0.15s ease',
boxShadow: '0 0 0 3px rgba(34, 197, 94, 0.15)'
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = '#22c55e';
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(34, 197, 94, 0.25)';
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = '#22c55e';
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(34, 197, 94, 0.15)';
}}
/>
);
} }
return (
<textarea
value={input}
onChange={(e) => handleInputChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={currentPlaceholder}
disabled={isPosting || isSoftLimited}
autoFocus
rows={effectiveMode === 'chat' ? 6 : 4}
style={{
flex: 1,
padding: '12px 16px',
background: '#0a0a0a',
border: '2px solid #22c55e',
borderRadius: '6px',
color: '#e5e5e5',
fontSize: '13px',
fontFamily: "'JetBrains Mono', ui-monospace",
outline: 'none',
transition: 'all 0.15s ease',
resize: 'vertical',
minHeight: effectiveMode === 'chat' ? '150px' : '110px',
boxShadow: '0 0 0 3px rgba(34, 197, 94, 0.15)'
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = '#22c55e';
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(34, 197, 94, 0.25)';
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = '#22c55e';
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(34, 197, 94, 0.15)';
}}
/>
);
}; };
// Collapsed state - visible trigger
if (!isExpanded) {
return ( return (
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '10px'
}}>
{!isExpanded ? (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '16px'
}}>
<div style={{
color: '#6b6b6b',
fontSize: '13px',
fontFamily: "'JetBrains Mono', ui-monospace",
textAlign: 'center'
}}>
Quickly add stuff
</div>
<button <button
onClick={() => setIsExpanded(true)} onClick={() => setIsExpanded(true)}
style={{ style={{
width: '48px',
height: '48px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
background: '#22c55e', gap: '12px',
border: 'none', width: '100%',
borderRadius: '50%', padding: '16px 24px',
color: '#0a0a0a', background: 'transparent',
border: '1px solid #333',
borderRadius: '12px',
color: '#888',
fontSize: '12px',
fontWeight: 600,
letterSpacing: '0.1em',
textTransform: 'uppercase',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 150ms ease', transition: 'all 0.2s ease'
fontSize: '28px',
fontWeight: 300,
lineHeight: 1
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.transform = 'scale(1.05)'; e.currentTarget.style.borderColor = '#444';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(34, 197, 94, 0.4)'; e.currentTarget.style.color = '#aaa';
e.currentTarget.style.background = '#1a1a1a';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.borderColor = '#333';
e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.color = '#888';
e.currentTarget.style.background = 'transparent';
}} }}
title="Add new content"
> >
+ <span style={{
width: '22px',
height: '22px',
borderRadius: '50%',
background: '#22c55e',
color: '#0a0a0a',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '14px',
fontWeight: 700
}}>+</span>
CAPTURE
</button> </button>
</div> );
) : ( }
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}> // Expanded state
return (
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '12px',
width: '100%',
background: 'transparent',
padding: '0',
animation: 'fadeIn 150ms ease-out'
}}>
{/* Mode tabs */}
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
{MODE_CONFIG.map((mode) => { {MODE_CONFIG.map((mode) => {
const isActive = effectiveMode === mode.key && (manualMode === mode.key || (!manualMode && autoMode === mode.key)); const isActive = effectiveMode === mode.key;
return ( return (
<button <button
key={mode.key} key={mode.key}
@@ -291,142 +207,185 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '6px', gap: '6px',
padding: '6px 12px', padding: '8px 14px',
borderRadius: '999px', borderRadius: '8px',
border: `1px solid ${isActive ? '#22c55e' : '#1f1f1f'}`, border: isActive ? '1px solid #333' : '1px solid transparent',
background: isActive ? 'rgba(34, 197, 94, 0.15)' : 'transparent', background: isActive ? '#1a1a1a' : 'transparent',
color: isActive ? '#22c55e' : '#9c9c9c', color: isActive ? '#fff' : '#666',
fontSize: '11px', fontSize: '12px',
fontFamily: "'JetBrains Mono', ui-monospace", fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.08em',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.15s ease' transition: 'all 0.15s ease',
textTransform: 'uppercase',
letterSpacing: '0.05em'
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.color = '#999';
e.currentTarget.style.background = '#1a1a1a';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.color = '#666';
e.currentTarget.style.background = 'transparent';
}
}} }}
> >
<span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{mode.icon}</span> <span style={{ display: 'flex', opacity: isActive ? 1 : 0.7 }}>{mode.icon}</span>
{mode.label} {mode.label}
</button> </button>
); );
})} })}
</div>
{autoReason && !manualMode && ( {/* Close button */}
<div style={{
textAlign: 'center',
color: '#9c9c9c',
fontSize: '11px',
letterSpacing: '0.05em',
textTransform: 'uppercase'
}}>
Auto-detected chat transcript
</div>
)}
<div style={{ display: 'flex', gap: '8px', alignItems: 'stretch' }}>
{renderInputField()}
<button <button
onClick={handleSubmit} onClick={() => {
disabled={!input.trim() || isPosting || isSoftLimited} setIsExpanded(false);
aria-label={isPosting ? 'Adding' : 'Add'} setInput('');
title={isPosting ? 'Adding…' : 'Add (Enter)'} }}
style={{ style={{
width: '40px', marginLeft: 'auto',
height: '40px', padding: '8px',
background: 'transparent',
border: 'none',
color: '#666',
cursor: 'pointer',
borderRadius: '6px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
background: input.trim() && !isPosting && !isSoftLimited ? '#22c55e' : '#22c55e', transition: 'color 0.15s ease'
border: `2px solid #22c55e`, }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#999'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#666'; }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6L6 18M6 6l12 12" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</div>
{/* Input area - consistent height */}
<div style={{ position: 'relative' }}>
<textarea
value={input}
onChange={(e) => handleInputChange(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={currentPlaceholder}
disabled={isPosting || isSoftLimited}
autoFocus
style={{
width: '100%',
minHeight: '80px',
maxHeight: '200px',
padding: '12px 14px',
background: '#0a0a0a',
border: '1px solid #1f1f1f',
borderRadius: '8px',
color: '#e5e5e5',
fontSize: '14px',
fontFamily: 'inherit',
outline: 'none',
resize: 'none',
transition: 'border-color 0.15s ease'
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = '#333';
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = '#1f1f1f';
}}
/>
</div>
{/* Footer */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: '11px', color: '#525252' }}>
to submit · esc to close
</span>
<button
onClick={handleSubmit}
disabled={!input.trim() || isPosting || isSoftLimited}
style={{
width: '36px',
height: '36px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0',
background: input.trim() && !isPosting ? '#22c55e' : '#262626',
border: 'none',
borderRadius: '50%', borderRadius: '50%',
color: input.trim() && !isPosting && !isSoftLimited ? '#0a0a0a' : '#0a0a0a', cursor: input.trim() && !isPosting ? 'pointer' : 'not-allowed',
cursor: input.trim() && !isPosting && !isSoftLimited ? 'pointer' : 'not-allowed', transition: 'all 0.2s ease',
transition: 'all 150ms ease', boxShadow: input.trim() && !isPosting ? '0 0 0 0 rgba(34, 197, 94, 0)' : 'none'
opacity: input.trim() && !isPosting && !isSoftLimited ? 1 : 0.7,
flexShrink: 0
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (input.trim() && !isPosting && !isSoftLimited) { if (input.trim() && !isPosting) {
e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(34, 197, 94, 0.4)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(34, 197, 94, 0.3)';
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (input.trim() && !isPosting && !isSoftLimited) {
e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none'; e.currentTarget.style.boxShadow = '0 0 0 0 rgba(34, 197, 94, 0)';
}
}} }}
> >
{isPosting ? ( {isPosting ? (
<span style={{ fontSize: '12px' }}></span> <span style={{
width: '14px',
height: '14px',
border: '2px solid #0a0a0a',
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite'
}} />
) : ( ) : (
<svg <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={input.trim() ? '#0a0a0a' : '#525252'} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
width="16" <path d="M12 19V5"/>
height="16" <path d="M5 12l7-7 7 7"/>
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M12 4l-6.5 6.5 1.42 1.42L11 8.84V20h2V8.84l4.08 3.08 1.42-1.42L12 4z"/>
</svg> </svg>
)} )}
</button> </button>
</div> </div>
</div>
)}
{/* Active processing indicator */} {/* Active processing indicator */}
{activeCount > 0 && ( {activeCount > 0 && (
<div style={{ <div style={{
padding: '10px 14px', padding: '10px 12px',
background: '#0a1a0a', background: '#0a1a0a',
border: '1px solid #1a3a1a', border: '1px solid #1a3a1a',
borderRadius: '6px', borderRadius: '8px',
color: '#22c55e', color: '#22c55e',
fontSize: '11px', fontSize: '11px',
fontFamily: "'JetBrains Mono', ui-monospace",
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '8px' gap: '8px'
}}> }}>
<span style={{ <span style={{
width: '8px', width: '6px',
height: '8px', height: '6px',
borderRadius: '50%', borderRadius: '50%',
background: '#22c55e', background: '#22c55e',
animation: 'pulse 2s ease-in-out infinite' animation: 'pulse 2s ease-in-out infinite'
}} /> }} />
<span> Adding {activeCount} node{activeCount > 1 ? 's' : ''}...
{activeCount === 1
? 'Adding 1 node to your database...'
: `Adding ${activeCount} nodes to your database...`}
</span>
</div>
)}
{isSoftLimited && (
<div style={{
padding: '10px 14px',
background: '#1a0a00',
border: '1px solid #3a2a1a',
borderRadius: '6px',
color: '#ff9b5c',
fontSize: '11px',
fontFamily: "'JetBrains Mono', ui-monospace",
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span></span>
<span>Finish one of the 5 active Quick Adds before adding more.</span>
</div> </div>
)} )}
<style jsx>{` <style jsx>{`
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse { @keyframes pulse {
0%, 100% { opacity: 1; } 0%, 100% { opacity: 1; }
50% { opacity: 0.5; } 50% { opacity: 0.5; }
} }
@keyframes spin {
to { transform: rotate(360deg); }
}
`}</style> `}</style>
</div> </div>
); );
+24 -7
View File
@@ -49,15 +49,32 @@ export default function TerminalInput({
load(); load();
}, [helperId]); }, [helperId]);
// Auto-resize textarea // Auto-resize textarea - only resize when needed to avoid jumps
useEffect(() => { useEffect(() => {
if (textareaRef.current) { const textarea = textareaRef.current;
textareaRef.current.style.height = 'auto'; if (!textarea) return;
const scrollHeight = textareaRef.current.scrollHeight;
const lineHeight = 20; // Approximate line height const lineHeight = 24; // line-height * font-size approximately
const newRows = Math.min(Math.max(1, Math.floor(scrollHeight / lineHeight)), 5); const minHeight = 32;
const maxHeight = 120;
// Check if content overflows current height (need to grow)
const needsGrow = textarea.scrollHeight > textarea.clientHeight;
// Check if we cleared content significantly (need to shrink)
const lineCount = (input.match(/\n/g) || []).length + 1;
const estimatedHeight = Math.max(lineCount * lineHeight, minHeight);
const needsShrink = !input.trim() || (textarea.clientHeight > estimatedHeight + lineHeight);
if (needsGrow || needsShrink) {
// Only recalculate when necessary
textarea.style.height = 'auto';
const scrollHeight = textarea.scrollHeight;
const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
textarea.style.height = `${newHeight}px`;
const newRows = Math.min(Math.max(1, Math.floor(newHeight / lineHeight)), 5);
setRows(newRows); setRows(newRows);
textareaRef.current.style.height = `${scrollHeight}px`;
} }
}, [input]); }, [input]);
+14 -9
View File
@@ -23,13 +23,14 @@ export default function ConfirmDialog({
return ( return (
<div <div
className="modal-backdrop"
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
background: 'rgba(0, 0, 0, 0.8)', background: 'rgba(0, 0, 0, 0.85)',
backdropFilter: 'blur(4px)', backdropFilter: 'blur(4px)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -39,30 +40,34 @@ export default function ConfirmDialog({
}} }}
> >
<div <div
className="modal-content"
style={{ style={{
width: '380px', width: '380px',
maxWidth: '100%', maxWidth: '100%',
background: '#050505', background: '#121212',
border: '1px solid #1f1f1f', border: '1px solid #2a2a2a',
borderRadius: '12px', borderRadius: '8px',
padding: '24px', padding: '24px',
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)' boxShadow: '0 8px 32px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05)'
}} }}
> >
<div style={{ <div style={{
fontSize: '15px', fontSize: '15px',
fontWeight: 600, fontWeight: 600,
color: '#f8fafc', color: '#e5e5e5',
marginBottom: '12px', marginBottom: '12px',
letterSpacing: '0.02em' letterSpacing: '0.01em',
fontFamily: "'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
}}> }}>
{title} {title}
</div> </div>
<div style={{ <div style={{
fontSize: '13px', fontSize: '13px',
color: '#94a3b8', color: '#a8a8a8',
marginBottom: '24px', marginBottom: '24px',
lineHeight: 1.6 lineHeight: 1.6,
wordWrap: 'break-word',
overflowWrap: 'break-word'
}}> }}>
{message} {message}
</div> </div>
+19 -13
View File
@@ -60,13 +60,14 @@ export default function InputDialog({
return ( return (
<div <div
className="modal-backdrop"
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
background: 'rgba(0, 0, 0, 0.8)', background: 'rgba(0, 0, 0, 0.85)',
backdropFilter: 'blur(4px)', backdropFilter: 'blur(4px)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -81,30 +82,34 @@ export default function InputDialog({
}} }}
> >
<div <div
className="modal-content"
style={{ style={{
width: '380px', width: '380px',
maxWidth: '100%', maxWidth: '100%',
background: '#050505', background: '#121212',
border: '1px solid #1f1f1f', border: '1px solid #2a2a2a',
borderRadius: '12px', borderRadius: '8px',
padding: '24px', padding: '24px',
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)' boxShadow: '0 8px 32px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05)'
}} }}
> >
<div style={{ <div style={{
fontSize: '15px', fontSize: '15px',
fontWeight: 600, fontWeight: 600,
color: '#f8fafc', color: '#e5e5e5',
marginBottom: '12px', marginBottom: '12px',
letterSpacing: '0.02em' letterSpacing: '0.01em',
fontFamily: "'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
}}> }}>
{title} {title}
</div> </div>
<div style={{ <div style={{
fontSize: '13px', fontSize: '13px',
color: '#94a3b8', color: '#a8a8a8',
marginBottom: '16px', marginBottom: '16px',
lineHeight: 1.6 lineHeight: 1.6,
wordWrap: 'break-word',
overflowWrap: 'break-word'
}}> }}>
{message} {message}
</div> </div>
@@ -118,14 +123,15 @@ export default function InputDialog({
style={{ style={{
width: '100%', width: '100%',
padding: '10px 12px', padding: '10px 12px',
background: '#0f0f0f', background: '#0a0a0a',
border: '1px solid #2a2a2a', border: '1px solid #2a2a2a',
borderRadius: '8px', borderRadius: '6px',
color: '#f8fafc', color: '#e5e5e5',
fontSize: '13px', fontSize: '13px',
marginBottom: '24px', marginBottom: '24px',
outline: 'none', outline: 'none',
transition: 'border-color 0.2s' transition: 'border-color 0.2s',
fontFamily: "'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
}} }}
onFocus={(e) => { onFocus={(e) => {
e.target.style.borderColor = '#3a3a3a'; e.target.style.borderColor = '#3a3a3a';
+463 -143
View File
@@ -734,13 +734,14 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
<div style={{ <div style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '12px', gap: '16px',
background: '#0a0a0a', background: '#141414',
padding: '12px', padding: '16px 20px',
borderRadius: '8px', borderRadius: '12px',
border: '1px solid #1f1f1f' 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" /> <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> </svg>
<input <input
@@ -748,15 +749,16 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
value={nodeSearchQuery} value={nodeSearchQuery}
onChange={(e) => setNodeSearchQuery(e.target.value)} onChange={(e) => setNodeSearchQuery(e.target.value)}
onKeyDown={handleNodeSearchKeyDown} onKeyDown={handleNodeSearchKeyDown}
placeholder="Search node to connect..." placeholder="Connect to node..."
style={{ style={{
flex: 1, flex: 1,
background: 'none', background: 'none',
border: 'none', border: 'none',
outline: 'none', outline: 'none',
color: '#f8fafc', color: '#fafafa',
fontSize: '14px', fontSize: '16px',
fontFamily: 'inherit' fontFamily: 'inherit',
fontWeight: 400
}} }}
/> />
</div> </div>
@@ -765,26 +767,30 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
{nodeSearchSuggestions.length > 0 && ( {nodeSearchSuggestions.length > 0 && (
<div style={{ <div style={{
marginTop: '8px', marginTop: '8px',
background: '#0a0a0a', background: '#141414',
border: '1px solid #1f1f1f', border: '1px solid #262626',
borderRadius: '8px', borderRadius: '12px',
maxHeight: '200px', maxHeight: '240px',
overflowY: 'auto' 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) => ( {nodeSearchSuggestions.map((suggestion, index) => (
<div <div
key={suggestion.id} key={suggestion.id}
onClick={() => handleSelectNodeSuggestion(suggestion)} onClick={() => handleSelectNodeSuggestion(suggestion)}
style={{ style={{
padding: '12px 16px', padding: '14px 16px',
cursor: 'pointer', cursor: 'pointer',
borderBottom: index < nodeSearchSuggestions.length - 1 ? '1px solid #1f1f1f' : 'none', borderBottom: index < nodeSearchSuggestions.length - 1 ? '1px solid #1f1f1f' : 'none',
background: index === selectedSearchIndex ? '#1f1f1f' : 'transparent', background: index === selectedSearchIndex ? '#1a1a1a' : 'transparent',
transition: 'background 0.1s' transition: 'background 100ms ease',
display: 'flex',
alignItems: 'center',
gap: '12px'
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (index !== selectedSearchIndex) { if (index !== selectedSearchIndex) {
e.currentTarget.style.background = '#151515'; e.currentTarget.style.background = '#1a1a1a';
} }
}} }}
onMouseLeave={(e) => { 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={{ <span style={{
display: 'inline-block', display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px', fontSize: '10px',
fontWeight: 700, fontWeight: 600,
color: '#000', color: '#0a0a0a',
background: '#22c55e', background: '#22c55e',
padding: '2px 6px', padding: '4px 8px',
borderRadius: '6px', borderRadius: '6px',
minWidth: '20px', minWidth: '28px',
textAlign: 'center', textAlign: 'center',
letterSpacing: '0.05em' flexShrink: 0,
fontFamily: "'SF Mono', 'Fira Code', monospace"
}}> }}>
{suggestion.id} #{suggestion.id}
</span> </span>
<span style={{ fontSize: '13px', color: '#f8fafc' }}> <span style={{
fontSize: '15px',
color: '#e5e5e5',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1
}}>
{suggestion.title} {suggestion.title}
</span> </span>
</div> {index === selectedSearchIndex && (
{suggestion.dimensions && suggestion.dimensions.length > 0 && ( <span style={{ color: '#525252', fontSize: '13px' }}></span>
<div style={{ fontSize: '11px', color: '#94a3b8' }}>
{suggestion.dimensions.slice(0, 3).join(', ')}
{suggestion.dimensions.length > 3 && ` +${suggestion.dimensions.length - 3} more`}
</div>
)} )}
</div> </div>
))} ))}
@@ -826,7 +837,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
{/* Existing Connections */} {/* Existing Connections */}
<div style={{ flex: 1, overflowY: 'auto' }}> <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 Existing Connections
</div> </div>
{loadingEdges.has(activeTab) ? ( {loadingEdges.has(activeTab) ? (
@@ -877,7 +888,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
flex: 1, flex: 1,
fontSize: '11px', fontSize: '11px',
color: '#ddd', color: '#ddd',
background: '#000', background: 'transparent',
border: '1px solid #1a1a1a', border: '1px solid #1a1a1a',
borderRadius: '0', borderRadius: '0',
padding: '4px', padding: '4px',
@@ -1038,12 +1049,12 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
return ( return (
<> <>
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#000' }}> <div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'transparent' }}>
{/* Tab Bar */} {/* Tab Bar */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
borderBottom: '1px solid #333', borderBottom: '1px solid #1a1a1a',
background: '#0a0a0a', background: '#0f0f0f',
flexShrink: 0, flexShrink: 0,
overflowX: 'auto', overflowX: 'auto',
overflowY: 'hidden' overflowY: 'hidden'
@@ -1068,9 +1079,9 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
borderRight: '1px solid #333', borderRight: '1px solid #1a1a1a',
background: isActive ? '#000' : '#0a0a0a', background: isActive ? '#121212' : '#0f0f0f',
borderBottom: isActive ? '2px solid #22c55e' : 'none', borderBottom: isActive ? '2px solid #666' : 'none',
paddingBottom: isActive ? '0' : '2px', paddingBottom: isActive ? '0' : '2px',
minWidth: '120px', minWidth: '120px',
maxWidth: '200px' maxWidth: '200px'
@@ -1315,7 +1326,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
style={{ style={{
color: '#3b82f6', color: '#3b82f6',
fontSize: '11px', fontSize: '11px',
background: '#000', background: 'transparent',
border: '1px solid #1a1a1a', border: '1px solid #1a1a1a',
borderRadius: '6px', borderRadius: '6px',
padding: '4px 6px', padding: '4px 6px',
@@ -1364,7 +1375,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
)} )}
</div> </div>
{/* Connections Button */} {/* Connections Button - Green CTA */}
<button <button
onClick={() => setShowConnectionsModal(true)} onClick={() => setShowConnectionsModal(true)}
style={{ style={{
@@ -1374,9 +1385,9 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
padding: '6px 10px', padding: '6px 10px',
fontSize: '11px', fontSize: '11px',
fontWeight: 500, fontWeight: 500,
color: '#94a3b8', color: '#22c55e',
background: 'transparent', background: 'transparent',
border: '1px solid #1f1f1f', border: '1px solid #166534',
borderRadius: '6px', borderRadius: '6px',
cursor: 'pointer', cursor: 'pointer',
textTransform: 'uppercase', textTransform: 'uppercase',
@@ -1385,17 +1396,15 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
flexShrink: 0 flexShrink: 0
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = '#0f0f0f'; e.currentTarget.style.background = '#0f2417';
e.currentTarget.style.borderColor = '#2a2a2a'; e.currentTarget.style.borderColor = '#22c55e';
e.currentTarget.style.color = '#cbd5f5';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'; e.currentTarget.style.background = 'transparent';
e.currentTarget.style.borderColor = '#1f1f1f'; e.currentTarget.style.borderColor = '#166534';
e.currentTarget.style.color = '#94a3b8';
}} }}
> >
<Link size={12} /> <Link size={14} />
{activeTab && edgesData[activeTab] && edgesData[activeTab].length > 0 && ( {activeTab && edgesData[activeTab] && edgesData[activeTab].length > 0 && (
<span style={{ <span style={{
fontSize: '10px', fontSize: '10px',
@@ -1412,6 +1421,42 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
</span> </span>
)} )}
</button> </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> </div>
{/* Title Row with ID and Trash */} {/* Title Row with ID and Trash */}
@@ -1443,7 +1488,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
fontSize: '20px', fontSize: '20px',
fontWeight: 'bold', fontWeight: 'bold',
color: '#fff', color: '#fff',
background: '#000', background: 'transparent',
border: '1px solid #1a1a1a', border: '1px solid #1a1a1a',
borderRadius: '0', borderRadius: '0',
padding: '4px 8px', padding: '4px 8px',
@@ -1474,7 +1519,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
padding: '4px 8px', padding: '4px 8px',
margin: '0', margin: '0',
borderRadius: '0', borderRadius: '0',
background: '#000', background: 'transparent',
border: '1px solid transparent', border: '1px solid transparent',
transition: 'border-color 0.2s', transition: 'border-color 0.2s',
flex: 1, flex: 1,
@@ -1500,36 +1545,6 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
</h1> </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> </div>
@@ -1634,7 +1649,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
color: '#e5e5e5', color: '#e5e5e5',
fontSize: '16px', /* Increased from 15px for better readability */ fontSize: '16px', /* Increased from 15px for better readability */
lineHeight: '1.7', /* Increased from 1.6 for more comfortable reading */ lineHeight: '1.7', /* Increased from 1.6 for more comfortable reading */
background: '#000', background: 'transparent',
border: 'none', border: 'none',
borderRadius: '0', borderRadius: '0',
padding: '12px', /* Increased from 8px to 12px */ padding: '12px', /* Increased from 8px to 12px */
@@ -1680,7 +1695,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
margin: '0', margin: '0',
borderRadius: '0', borderRadius: '0',
transition: 'all 0.2s', transition: 'all 0.2s',
background: '#000', background: 'transparent',
border: 'none', border: 'none',
whiteSpace: 'pre-wrap', whiteSpace: 'pre-wrap',
minHeight: '200px', minHeight: '200px',
@@ -1708,7 +1723,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
background: '#000', background: 'transparent',
flex: 1 flex: 1
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
@@ -1773,7 +1788,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
color: '#e5e5e5', color: '#e5e5e5',
fontSize: '11px', fontSize: '11px',
lineHeight: '1.4', lineHeight: '1.4',
background: '#0a0a0a', background: 'transparent',
border: '1px solid #1a1a1a', border: '1px solid #1a1a1a',
borderRadius: '0', borderRadius: '0',
padding: '8px', padding: '8px',
@@ -1794,7 +1809,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
lineHeight: '1.4', lineHeight: '1.4',
cursor: 'pointer', cursor: 'pointer',
padding: '8px', padding: '8px',
background: '#0a0a0a', background: 'transparent',
border: '1px solid #1a1a1a', border: '1px solid #1a1a1a',
borderRadius: '0', borderRadius: '0',
transition: 'all 0.2s', transition: 'all 0.2s',
@@ -1815,7 +1830,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
fontStyle: 'italic', fontStyle: 'italic',
cursor: 'pointer', cursor: 'pointer',
padding: '8px', padding: '8px',
background: '#0a0a0a', background: 'transparent',
border: '1px dashed #1a1a1a', border: '1px dashed #1a1a1a',
borderRadius: '0', borderRadius: '0',
transition: 'all 0.2s' transition: 'all 0.2s'
@@ -1854,93 +1869,398 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
onConfirm={handleConfirmNodeDelete} onConfirm={handleConfirmNodeDelete}
onCancel={handleCancelNodeDelete} onCancel={handleCancelNodeDelete}
/> />
{showConnectionsModal && ( {showConnectionsModal && activeTab && (
<div <div
onClick={(e) => { onClick={(e) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
setShowConnectionsModal(false); setShowConnectionsModal(false);
setNodeSearchQuery('');
setNodeSearchSuggestions([]);
} }
}} }}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, inset: 0,
left: 0, background: 'rgba(0, 0, 0, 0.85)',
right: 0, backdropFilter: 'blur(8px)',
bottom: 0,
background: 'rgba(0, 0, 0, 0.8)',
backdropFilter: 'blur(4px)',
display: 'flex', display: 'flex',
alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
zIndex: 20, paddingTop: '10vh',
padding: '20px' zIndex: 9999,
animation: 'backdropIn 200ms ease-out'
}} }}
> >
<div <div
style={{ style={{
width: '520px', width: '100%',
maxHeight: '85vh', maxWidth: '640px',
background: '#050505', maxHeight: '80vh',
border: '1px solid #1f1f1f',
borderRadius: '12px',
display: 'flex', display: 'flex',
flexDirection: 'column', 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)'
}} }}
> >
{/* Search Input */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
padding: '16px 20px', gap: '16px',
borderBottom: '1px solid #1f1f1f' 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={{ <svg width="22" height="22" viewBox="0 0 20 20" fill="#525252" style={{ flexShrink: 0 }}>
fontSize: '14px', <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" />
fontWeight: 600, </svg>
letterSpacing: '0.02em', <input
color: '#f8fafc', autoFocus
display: 'flex', type="text"
alignItems: 'center', value={nodeSearchQuery}
gap: '8px' onChange={(e) => setNodeSearchQuery(e.target.value)}
}}> onKeyDown={(e) => {
<Link size={16} /> if (e.key === 'Escape') {
Connections setShowConnectionsModal(false);
</div> setNodeSearchQuery('');
<button setNodeSearchSuggestions([]);
onClick={() => setShowConnectionsModal(false)} } else {
handleNodeSearchKeyDown(e);
}
}}
placeholder="Search to add connection..."
style={{ style={{
width: '32px', flex: 1,
height: '32px', background: 'none',
border: 'none',
outline: 'none',
color: '#fafafa',
fontSize: '18px',
fontFamily: 'inherit',
fontWeight: 400
}}
/>
<kbd style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px 8px',
background: '#262626',
borderRadius: '6px', borderRadius: '6px',
border: '1px solid #1f1f1f', fontSize: '11px',
background: 'transparent', fontFamily: 'inherit',
color: '#94a3b8', 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', cursor: 'pointer',
fontSize: '16px', 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', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
transition: 'all 0.2s' transition: 'all 0.15s ease',
}} opacity: deletingEdge === connection.edge.id ? 0.5 : 1
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';
}} }}
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> </button>
</div> </div>
<div style={{ padding: '20px', overflowY: 'auto', flex: 1 }}> {/* Description row */}
{renderConnectionsBody()} {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> </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> </div>
)} )}
@@ -176,56 +176,19 @@ export default function DimensionSearchModal({
const modalContent = ( const modalContent = (
<div <div
style={{ className="search-backdrop"
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'
}}
onClick={handleBackdropClick} onClick={handleBackdropClick}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Search dimensions" aria-label="Search dimensions"
> >
<div <div ref={modalRef} className="search-container">
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'
}}
>
{/* Search Input */} {/* Search Input */}
<div style={{ <div className="search-input-wrapper">
padding: '16px', <svg className="search-icon" viewBox="0 0 20 20" fill="currentColor">
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" /> <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> </svg>
{/* Input */}
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
@@ -233,77 +196,31 @@ export default function DimensionSearchModal({
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Search or create dimension..." placeholder="Search or create dimension..."
style={{ className="search-input"
flex: 1,
background: 'none',
border: 'none',
outline: 'none',
color: '#fff',
fontSize: '14px',
fontFamily: 'inherit'
}}
/> />
{/* Close Button */} <div className="search-shortcut">
<button <kbd>esc</kbd>
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> </div>
</div> </div>
{/* Suggestions */} {/* Results */}
{suggestions.length > 0 && ( {suggestions.length > 0 && (
<div style={{ <div className="search-results">
maxHeight: '300px',
overflowY: 'auto'
}}>
{suggestions.map((suggestion, index) => ( {suggestions.map((suggestion, index) => (
<button <button
key={suggestion.dimension} key={suggestion.dimension}
onClick={() => handleSelectDimension(suggestion.dimension)} onClick={() => handleSelectDimension(suggestion.dimension)}
onMouseEnter={() => setSelectedIndex(index)} onMouseEnter={() => setSelectedIndex(index)}
style={{ className={`search-result-item ${index === selectedIndex ? 'selected' : ''}`}
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'
}}
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}> <span className={`result-name ${suggestion.isPriority ? 'priority' : ''}`}>
<span style={{
color: suggestion.isPriority ? '#22c55e' : '#e5e5e5'
}}>
{suggestion.dimension} {suggestion.dimension}
</span> </span>
</div> <span className="result-count">{suggestion.count}</span>
<span style={{ color: '#666', fontSize: '11px' }}> {index === selectedIndex && (
{suggestion.count} <span className="result-hint"></span>
</span> )}
</button> </button>
))} ))}
</div> </div>
@@ -311,57 +228,225 @@ export default function DimensionSearchModal({
{/* Create New Option */} {/* Create New Option */}
{canCreateNew && ( {canCreateNew && (
<div className="search-create">
<button <button
onClick={() => handleSelectDimension(searchQuery.trim())} 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)} onMouseEnter={() => setSelectedIndex(suggestions.length)}
className={`create-button ${selectedIndex === suggestions.length ? 'selected' : ''}`}
> >
<span style={{ fontSize: '16px', fontWeight: 300 }}>+</span> <span className="create-icon">+</span>
Create "{searchQuery.trim()}" Create "{searchQuery.trim()}"
</button> </button>
</div>
)} )}
{/* Keyboard Hint */} {/* Empty state */}
<div style={{ {!searchQuery && suggestions.length === 0 && (
padding: '12px 16px', <div className="search-empty">
borderTop: '1px solid #1f1f1f', Start typing to search dimensions
display: 'flex',
justifyContent: 'flex-end',
gap: '16px',
fontSize: '11px',
color: '#666'
}}>
<span> Navigate</span>
<span> Select</span>
<span>Esc Close</span>
</div> </div>
)}
</div> </div>
<style jsx>{` <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; } from { opacity: 0; }
to { opacity: 1; } to { opacity: 1; }
} }
@keyframes slideIn { @keyframes containerIn {
from { from {
opacity: 0; 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 { to {
opacity: 1; opacity: 1;
+186 -136
View File
@@ -160,182 +160,232 @@ export default function EdgeSearchModal({
const modalContent = ( const modalContent = (
<div <div
style={{ className="search-backdrop"
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'
}}
onClick={handleBackdropClick} onClick={handleBackdropClick}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Search nodes to connect" aria-label="Search nodes to connect"
> >
<div <div ref={modalRef} className="search-container">
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'
}}
>
{/* Search Input */} {/* Search Input */}
<div style={{ <div className="search-input-wrapper">
padding: '16px', <svg className="search-icon" viewBox="0 0 20 20" fill="currentColor">
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" /> <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> </svg>
{/* Input */}
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder="Search node to connect..." placeholder="Connect to node..."
style={{ className="search-input"
flex: 1,
background: 'none',
border: 'none',
outline: 'none',
color: '#fff',
fontSize: '14px',
fontFamily: 'inherit'
}}
/> />
{/* Close Button */} <div className="search-shortcut">
<button <kbd>esc</kbd>
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> </div>
</div> </div>
{/* Suggestions */} {/* Results */}
{suggestions.length > 0 && ( {suggestions.length > 0 && (
<div style={{ <div className="search-results">
maxHeight: '300px', {suggestions.map((suggestion, index) => (
overflowY: 'auto'
}}>
{suggestions.map((suggestion, index) => {
const primaryDimension = suggestion.dimensions && suggestion.dimensions.length > 0
? suggestion.dimensions[0]
: '';
return (
<button <button
key={index} key={suggestion.id}
onClick={() => handleSelectSuggestion(suggestion)} onClick={() => handleSelectSuggestion(suggestion)}
onMouseEnter={() => setSelectedIndex(index)} onMouseEnter={() => setSelectedIndex(index)}
style={{ className={`search-result-item ${index === selectedIndex ? 'selected' : ''}`}
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 className="result-id">#{suggestion.id}</span>
<span style={{ <span className="result-title">{suggestion.title}</span>
color: '#666', {index === selectedIndex && (
fontSize: '10px', <span className="result-hint"></span>
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> </button>
); ))}
})}
</div> </div>
)} )}
{/* Keyboard Hint */} {/* Empty state */}
<div style={{ {searchQuery && suggestions.length === 0 && (
padding: '12px 16px', <div className="search-empty">
borderTop: '1px solid #2a2a2a', No results for "{searchQuery}"
display: 'flex',
justifyContent: 'flex-end',
gap: '16px',
fontSize: '11px',
color: '#666'
}}>
<span> Navigate</span>
<span> Select</span>
<span>Esc Close</span>
</div> </div>
)}
</div> </div>
<style jsx>{` <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; } from { opacity: 0; }
to { opacity: 1; } to { opacity: 1; }
} }
@keyframes slideIn { @keyframes containerIn {
from { from {
opacity: 0; 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 { to {
opacity: 1; opacity: 1;
+66 -49
View File
@@ -403,7 +403,7 @@ export default function ThreePanelLayout() {
width: '40px', width: '40px',
flexShrink: 0, flexShrink: 0,
borderRight: '1px solid #2a2a2a', borderRight: '1px solid #2a2a2a',
background: '#0f0f0f', background: '#0a0a0a',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
@@ -446,7 +446,7 @@ export default function ThreePanelLayout() {
overflow: 'hidden', overflow: 'hidden',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
background: '#0f0f0f', background: '#0a0a0a',
position: 'relative', position: 'relative',
padding: '4px' padding: '4px'
}} }}
@@ -486,18 +486,63 @@ export default function ThreePanelLayout() {
style={{ style={{
width: chatCollapsed width: chatCollapsed
? (nodesCollapsed ? (nodesCollapsed
? `calc(100% - 48px - 40px)` ? `calc(100% - 40px)`
: `calc(${effectiveMiddleWidth}% - 48px)`) : `calc(100% - ${effectiveLeftWidth}% - 3px)`)
: `${effectiveMiddleWidth}%`, : `${effectiveMiddleWidth}%`,
flexShrink: 0, flexShrink: 0,
borderRight: chatCollapsed ? 'none' : '1px solid #1a1a1a',
overflow: 'hidden', overflow: 'hidden',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
background: '#0f0f0f', background: '#0a0a0a',
padding: '4px' padding: '8px',
paddingRight: chatCollapsed ? '8px' : '4px'
}} }}
> >
<div style={{
flex: 1,
background: '#141414',
borderRadius: '8px',
border: '1px solid #1f1f1f',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
padding: chatCollapsed ? '20px' : '0',
position: 'relative'
}}>
{/* Expand chat button - shown when collapsed */}
{chatCollapsed && (
<button
onClick={() => setChatCollapsed(false)}
style={{
position: 'absolute',
top: '12px',
right: '12px',
width: '28px',
height: '28px',
borderRadius: '6px',
border: '1px solid #1f1f1f',
background: 'transparent',
color: '#666',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transition: 'all 0.15s ease',
zIndex: 10
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#1a1a1a';
e.currentTarget.style.color = '#999';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = '#666';
}}
title="Expand Chat (⌘\)"
>
<Maximize2 size={14} />
</button>
)}
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}> <div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
<div style={{ <div style={{
height: '100%', height: '100%',
@@ -524,6 +569,7 @@ export default function ThreePanelLayout() {
)} )}
</div> </div>
</div> </div>
</div>
{/* Right Resize Handle */} {/* Right Resize Handle */}
{!nodesCollapsed && !chatCollapsed && ( {!nodesCollapsed && !chatCollapsed && (
@@ -541,47 +587,7 @@ export default function ThreePanelLayout() {
)} )}
{/* Right Panel - Agents (collapsible) */} {/* Right Panel - Agents (collapsible) */}
{chatCollapsed ? ( {!chatCollapsed && (
// Collapsed rail
<div style={{
width: '48px',
flexShrink: 0,
borderLeft: '1px solid #2a2a2a',
background: '#0f0f0f',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
paddingTop: '12px'
}}>
<button
onClick={() => setChatCollapsed(false)}
style={{
width: '28px',
height: '28px',
borderRadius: '6px',
border: '1px solid #1f1f1f',
background: 'transparent',
color: '#666',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transition: 'all 0.15s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#1a1a1a';
e.currentTarget.style.color = '#999';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = '#666';
}}
title="Expand Chat (⌘\)"
>
<Maximize2 size={14} />
</button>
</div>
) : (
<div <div
style={{ style={{
width: `${effectiveRightWidth}%`, width: `${effectiveRightWidth}%`,
@@ -591,9 +597,19 @@ export default function ThreePanelLayout() {
flexDirection: 'column', flexDirection: 'column',
background: '#0a0a0a', background: '#0a0a0a',
position: 'relative', position: 'relative',
padding: '4px' padding: '8px',
paddingLeft: '4px'
}} }}
> >
<div style={{
flex: 1,
background: '#141414',
borderRadius: '8px',
border: '1px solid #1f1f1f',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column'
}}>
<AgentsPanel <AgentsPanel
openTabsData={openTabsData} openTabsData={openTabsData}
activeTabId={activeNodeId} activeTabId={activeNodeId}
@@ -601,6 +617,7 @@ export default function ThreePanelLayout() {
onCollapse={() => setChatCollapsed(true)} onCollapse={() => setChatCollapsed(true)}
/> />
</div> </div>
</div>
)} )}
{/* Settings Modal */} {/* Settings Modal */}
+32 -20
View File
@@ -433,12 +433,12 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
}} }}
style={{ style={{
width: '100%', width: '100%',
padding: '12px 16px', /* Increased from 10px to 12px */ padding: '8px 16px',
margin: '2px 0', /* Added vertical margin for better spacing */ margin: '0',
textAlign: 'left', textAlign: 'left',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '10px', gap: '8px',
fontSize: '14px', fontSize: '14px',
fontFamily: 'inherit', fontFamily: 'inherit',
background: selectedNodes.has(node.id) ? '#1a1a1a' : 'transparent', background: selectedNodes.has(node.id) ? '#1a1a1a' : 'transparent',
@@ -460,7 +460,24 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
} }
}} }}
> >
{/* Node ID Badge */}
<span style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
background: '#22c55e',
color: '#0a0a0a',
fontSize: '9px',
fontWeight: 600,
padding: '1px 5px',
borderRadius: '3px',
flexShrink: 0,
fontFamily: 'monospace',
lineHeight: 1,
height: '16px'
}}>
#{node.id}
</span>
{/* Title */} {/* Title */}
<span style={{ <span style={{
@@ -531,7 +548,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
}; };
return ( return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#000' }}> <div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'transparent' }}>
{/* Search Modal */} {/* Search Modal */}
<SearchModal <SearchModal
isOpen={showSearchModal} isOpen={showSearchModal}
@@ -571,7 +588,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '0.1em', letterSpacing: '0.1em',
borderBottom: '1px solid #1a1a1a', borderBottom: '1px solid #1a1a1a',
background: '#0a0a0a', background: 'transparent',
border: 'none', border: 'none',
cursor: creating ? 'not-allowed' : 'pointer', cursor: creating ? 'not-allowed' : 'pointer',
textAlign: 'left', textAlign: 'left',
@@ -662,7 +679,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '0.1em', letterSpacing: '0.1em',
borderRadius: '4px', /* Added border radius */ borderRadius: '4px', /* Added border radius */
background: '#0f0f0f', /* Slightly different background */ background: 'transparent', /* Transparent to show panel color */
border: 'none', border: 'none',
cursor: 'pointer', cursor: 'pointer',
textAlign: 'left', textAlign: 'left',
@@ -696,11 +713,10 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
style={{ style={{
width: '100%', width: '100%',
padding: '14px 18px', padding: '14px 18px',
fontSize: '11px', fontSize: '14px',
fontWeight: 700, fontWeight: 600,
color: dimensionsSectionCollapsed ? '#94a3b8' : '#f8fafc', color: dimensionsSectionCollapsed ? '#94a3b8' : '#f8fafc',
textTransform: 'uppercase', letterSpacing: '0.01em',
letterSpacing: '0.18em',
borderBottom: '1px solid #151515', borderBottom: '1px solid #151515',
borderLeft: '3px solid transparent', borderLeft: '3px solid transparent',
background: dimensionsSectionCollapsed ? '#050505' : '#0e1811', background: dimensionsSectionCollapsed ? '#050505' : '#0e1811',
@@ -715,7 +731,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
> >
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<Layers size={14} color={dimensionsSectionCollapsed ? '#94a3b8' : '#f8fafc'} /> <Layers size={14} color={dimensionsSectionCollapsed ? '#94a3b8' : '#f8fafc'} />
<span>Dimensions ({lockedDimensions.length})</span> <span>Dimensions</span>
</div> </div>
{dimensionsSectionCollapsed ? ( {dimensionsSectionCollapsed ? (
<ChevronRight size={14} strokeWidth={2.5} color="#94a3b8" /> <ChevronRight size={14} strokeWidth={2.5} color="#94a3b8" />
@@ -763,10 +779,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
) : ( ) : (
<Folder size={16} color="#64748b" /> <Folder size={16} color="#64748b" />
)} )}
<span style={{ <span>
textTransform: 'uppercase',
letterSpacing: '0.08em'
}}>
{lockedDim.dimension} {lockedDim.dimension}
</span> </span>
</div> </div>
@@ -813,11 +826,10 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
style={{ style={{
width: '100%', width: '100%',
padding: '14px 18px', padding: '14px 18px',
fontSize: '11px', fontSize: '14px',
fontWeight: 700, fontWeight: 600,
color: allNodesSectionCollapsed ? '#94a3b8' : '#f8fafc', color: allNodesSectionCollapsed ? '#94a3b8' : '#f8fafc',
textTransform: 'uppercase', letterSpacing: '0.01em',
letterSpacing: '0.18em',
borderBottom: '1px solid #151515', borderBottom: '1px solid #151515',
borderLeft: '3px solid transparent', borderLeft: '3px solid transparent',
background: allNodesSectionCollapsed ? '#050505' : '#0e1811', background: allNodesSectionCollapsed ? '#050505' : '#0e1811',
+185 -150
View File
@@ -154,52 +154,16 @@ export default function SearchModal({ isOpen, onClose, onNodeSelect, existingFil
const modalContent = ( const modalContent = (
<div <div
style={{ className="search-backdrop"
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'
}}
onClick={handleBackdropClick} onClick={handleBackdropClick}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-label="Search nodes" aria-label="Search nodes"
> >
<div <div ref={modalRef} className="search-container">
ref={modalRef}
style={{
background: '#050505',
border: '1px solid #1f1f1f',
borderRadius: '12px',
width: '90%',
maxWidth: '600px',
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'
}}
>
{/* Search Input */} {/* Search Input */}
<div style={{ <div className="search-input-wrapper">
padding: '16px', <svg className="search-icon" viewBox="0 0 20 20" fill="currentColor">
borderBottom: suggestions.length > 0 ? '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" /> <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> </svg>
@@ -213,7 +177,6 @@ export default function SearchModal({ isOpen, onClose, onNodeSelect, existingFil
/> />
))} ))}
{/* Input */}
<input <input
ref={inputRef} ref={inputRef}
type="text" type="text"
@@ -221,140 +184,212 @@ export default function SearchModal({ isOpen, onClose, onNodeSelect, existingFil
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={existingFilters.length === 0 ? "Search nodes..." : ""} placeholder={existingFilters.length === 0 ? "Search nodes..." : ""}
style={{ className="search-input"
flex: 1,
background: 'none',
border: 'none',
outline: 'none',
color: '#fff',
fontSize: '14px',
fontFamily: 'inherit'
}}
/> />
{/* Close Button */} <div className="search-shortcut">
<button <kbd>esc</kbd>
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> </div>
</div> </div>
{/* Suggestions */} {/* Results */}
{suggestions.length > 0 && ( {suggestions.length > 0 && (
<div style={{ <div className="search-results">
maxHeight: '300px', {suggestions.map((suggestion, index) => (
overflowY: 'auto'
}}>
{suggestions.map((suggestion, index) => {
const primaryDimension = suggestion.dimensions && suggestion.dimensions.length > 0
? suggestion.dimensions[0]
: '';
return (
<button <button
key={index} key={suggestion.id}
onClick={() => handleSelectSuggestion(suggestion)} onClick={() => handleSelectSuggestion(suggestion)}
onMouseEnter={() => setSelectedIndex(index)} onMouseEnter={() => setSelectedIndex(index)}
style={{ className={`search-result-item ${index === selectedIndex ? 'selected' : ''}`}
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 #1f1f1f' : 'none',
color: '#ccc',
cursor: 'pointer',
transition: 'background 0.1s',
fontFamily: 'inherit'
}}
> >
<span style={{ <span className="result-id">#{suggestion.id}</span>
display: 'inline-block', <span className="result-title">{suggestion.title}</span>
fontSize: '10px', {index === selectedIndex && (
fontWeight: 700, <span className="result-hint"></span>
color: '#000',
background: '#22c55e',
padding: '2px 6px',
borderRadius: '6px',
minWidth: '20px',
textAlign: 'center',
letterSpacing: '0.05em',
flexShrink: 0
}}>
{suggestion.id}
</span>
{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> </button>
); ))}
})}
</div> </div>
)} )}
{/* Keyboard Hint */} {/* Empty state */}
<div style={{ {searchQuery && suggestions.length === 0 && (
padding: '12px 16px', <div className="search-empty">
borderTop: '1px solid #1f1f1f', No results for "{searchQuery}"
display: 'flex',
justifyContent: 'flex-end',
gap: '16px',
fontSize: '11px',
color: '#666'
}}>
<span> Navigate</span>
<span> Select</span>
<span>Esc Close</span>
</div> </div>
)}
</div> </div>
<style jsx>{` <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; } from { opacity: 0; }
to { opacity: 1; } to { opacity: 1; }
} }
@keyframes slideIn { @keyframes containerIn {
from { from {
opacity: 0; 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 { to {
opacity: 1; opacity: 1;