- React Flow replaces custom SVG map (node dragging, edge creation, SSE sync) - Redesigned QuickAddInput and EdgeExplanationModal (match SearchModal design) - Selection highlighting with edge/node dimming - Added tabBar prop to BasePaneProps and PaneHeaderProps - Removed stale activeDelegations prop from QuickAddInput callers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
263 lines
7.2 KiB
TypeScript
263 lines
7.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
interface EdgeExplanationModalProps {
|
|
sourceTitle: string;
|
|
targetTitle: string;
|
|
onSubmit: (explanation: string) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export default function EdgeExplanationModal({
|
|
sourceTitle,
|
|
targetTitle,
|
|
onSubmit,
|
|
onCancel,
|
|
}: EdgeExplanationModalProps) {
|
|
const [explanation, setExplanation] = useState('');
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
|
|
useEffect(() => {
|
|
textareaRef.current?.focus();
|
|
document.body.style.overflow = 'hidden';
|
|
return () => { document.body.style.overflow = ''; };
|
|
}, []);
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
onCancel();
|
|
} else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && explanation.trim()) {
|
|
e.preventDefault();
|
|
onSubmit(explanation.trim());
|
|
}
|
|
};
|
|
|
|
const modalContent = (
|
|
<div
|
|
className="edge-modal-backdrop"
|
|
onClick={onCancel}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Connect nodes"
|
|
>
|
|
<div className="edge-modal-container">
|
|
<div
|
|
className="edge-modal-card"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Connection visualization */}
|
|
<div className="edge-modal-connection">
|
|
<span className="edge-modal-node-badge">{sourceTitle.length > 24 ? sourceTitle.slice(0, 22) + '\u2026' : sourceTitle}</span>
|
|
<svg width="20" height="12" viewBox="0 0 20 12" fill="none" style={{ flexShrink: 0 }}>
|
|
<path d="M0 6h16M12 1l5 5-5 5" stroke="#22c55e" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
<span className="edge-modal-node-badge">{targetTitle.length > 24 ? targetTitle.slice(0, 22) + '\u2026' : targetTitle}</span>
|
|
</div>
|
|
|
|
{/* Textarea */}
|
|
<div className="edge-modal-input-wrapper">
|
|
<textarea
|
|
ref={textareaRef}
|
|
value={explanation}
|
|
onChange={(e) => setExplanation(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="How are these connected?"
|
|
rows={2}
|
|
className="edge-modal-textarea"
|
|
/>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="edge-modal-footer">
|
|
<span className="edge-modal-hint">
|
|
<kbd>⌘↵</kbd> connect · <kbd>esc</kbd> cancel
|
|
</span>
|
|
<div className="edge-modal-actions">
|
|
<button onClick={onCancel} className="edge-modal-btn edge-modal-btn--cancel">
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={() => explanation.trim() && onSubmit(explanation.trim())}
|
|
disabled={!explanation.trim()}
|
|
className={`edge-modal-btn edge-modal-btn--submit ${explanation.trim() ? 'active' : ''}`}
|
|
>
|
|
Connect
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style jsx>{`
|
|
.edge-modal-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
backdrop-filter: blur(8px);
|
|
display: flex;
|
|
justify-content: center;
|
|
padding-top: 18vh;
|
|
z-index: 9999;
|
|
animation: edgeBackdropIn 200ms ease-out;
|
|
}
|
|
|
|
.edge-modal-container {
|
|
width: 100%;
|
|
max-width: 480px;
|
|
animation: edgeContainerIn 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.edge-modal-card {
|
|
background: #141414;
|
|
border: 1px solid #262626;
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
box-shadow:
|
|
0 0 0 1px rgba(255, 255, 255, 0.04),
|
|
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
|
}
|
|
|
|
.edge-modal-connection {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
justify-content: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.edge-modal-node-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 6px 12px;
|
|
background: rgba(34, 197, 94, 0.08);
|
|
border: 1px solid rgba(34, 197, 94, 0.15);
|
|
border-radius: 8px;
|
|
color: #86efac;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
max-width: 180px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.edge-modal-input-wrapper {
|
|
border: 1px solid #262626;
|
|
border-radius: 12px;
|
|
background: #0a0a0a;
|
|
transition: border-color 0.15s ease;
|
|
}
|
|
|
|
.edge-modal-input-wrapper:focus-within {
|
|
border-color: #333;
|
|
}
|
|
|
|
.edge-modal-textarea {
|
|
width: 100%;
|
|
background: transparent;
|
|
border: none;
|
|
padding: 16px 18px;
|
|
color: #fafafa;
|
|
font-size: 15px;
|
|
font-family: inherit;
|
|
resize: none;
|
|
outline: none;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.edge-modal-textarea::placeholder {
|
|
color: #525252;
|
|
}
|
|
|
|
.edge-modal-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.edge-modal-hint {
|
|
font-size: 11px;
|
|
color: #525252;
|
|
}
|
|
|
|
.edge-modal-hint kbd {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 2px 6px;
|
|
background: #262626;
|
|
border-radius: 4px;
|
|
font-size: 10px;
|
|
font-family: inherit;
|
|
color: #737373;
|
|
border: 1px solid #333;
|
|
}
|
|
|
|
.edge-modal-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.edge-modal-btn {
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.edge-modal-btn--cancel {
|
|
background: transparent;
|
|
border: 1px solid #262626;
|
|
color: #737373;
|
|
}
|
|
|
|
.edge-modal-btn--cancel:hover {
|
|
border-color: #333;
|
|
color: #a3a3a3;
|
|
}
|
|
|
|
.edge-modal-btn--submit {
|
|
background: #262626;
|
|
border: 1px solid transparent;
|
|
color: #525252;
|
|
cursor: default;
|
|
}
|
|
|
|
.edge-modal-btn--submit.active {
|
|
background: #22c55e;
|
|
color: #052e16;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.edge-modal-btn--submit.active:hover {
|
|
background: #16a34a;
|
|
}
|
|
|
|
@keyframes edgeBackdropIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes edgeContainerIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.96) translateY(-8px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
}
|
|
`}</style>
|
|
</div>
|
|
);
|
|
|
|
return typeof window !== 'undefined' ? createPortal(modalContent, document.body) : null;
|
|
}
|