diff --git a/src/components/FeatureIdeaCloud.tsx b/src/components/FeatureIdeaCloud.tsx index 20ff0eb..962ac03 100644 --- a/src/components/FeatureIdeaCloud.tsx +++ b/src/components/FeatureIdeaCloud.tsx @@ -30,8 +30,6 @@ import { ScrollArea } from '@/components/ui/scroll-area' import { Plus, Trash, Sparkle, DotsThree, Package } from '@phosphor-icons/react' import { toast } from 'sonner' -type ConnectionType = 'dependency' | 'association' | 'inheritance' | 'composition' | 'aggregation' - interface FeatureIdea { id: string title: string @@ -51,7 +49,6 @@ interface IdeaGroup { } interface IdeaEdgeData { - type: ConnectionType label?: string } @@ -151,22 +148,11 @@ const SEED_IDEAS: FeatureIdea[] = [ const CATEGORIES = ['AI/ML', 'Collaboration', 'Community', 'DevOps', 'Testing', 'Performance', 'Design', 'Database', 'Mobile', 'Accessibility', 'Productivity', 'Security', 'Analytics', 'Other'] const PRIORITIES = ['low', 'medium', 'high'] as const const STATUSES = ['idea', 'planned', 'in-progress', 'completed'] as const -const CONNECTION_TYPES = ['dependency', 'association', 'inheritance', 'composition', 'aggregation'] as const -const CONNECTION_STYLES = { - dependency: { stroke: '#70c0ff', strokeDasharray: '8,4', strokeWidth: 2.5, markerEnd: MarkerType.ArrowClosed }, - association: { stroke: '#a78bfa', strokeDasharray: '', strokeWidth: 2.5, markerEnd: MarkerType.Arrow }, - inheritance: { stroke: '#60d399', strokeDasharray: '', strokeWidth: 2.5, markerEnd: MarkerType.ArrowClosed }, - composition: { stroke: '#f87171', strokeDasharray: '', strokeWidth: 2.5, markerEnd: MarkerType.ArrowClosed }, - aggregation: { stroke: '#facc15', strokeDasharray: '', strokeWidth: 2.5, markerEnd: MarkerType.Arrow }, -} - -const CONNECTION_LABELS = { - dependency: 'depends on', - association: 'relates to', - inheritance: 'extends', - composition: 'contains', - aggregation: 'has', +const CONNECTION_STYLE = { + stroke: '#a78bfa', + strokeWidth: 2.5, + markerEnd: MarkerType.ArrowClosed } const STATUS_COLORS = { @@ -310,10 +296,10 @@ export function FeatureIdeaCloud() { sourceHandle: 'right', targetHandle: 'left', type: 'default', - animated: true, - data: { type: 'dependency', label: 'requires' }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#70c0ff', width: 20, height: 20 }, - style: { stroke: '#70c0ff', strokeDasharray: '8,4', strokeWidth: 2.5 }, + animated: false, + data: { label: 'requires' }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#a78bfa', width: 20, height: 20 }, + style: { stroke: '#a78bfa', strokeWidth: 2.5 }, }, { id: 'edge-2', @@ -322,8 +308,8 @@ export function FeatureIdeaCloud() { sourceHandle: 'bottom', targetHandle: 'top', type: 'default', - data: { type: 'association', label: 'works with' }, - markerEnd: { type: MarkerType.Arrow, color: '#a78bfa', width: 20, height: 20 }, + data: { label: 'works with' }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#a78bfa', width: 20, height: 20 }, style: { stroke: '#a78bfa', strokeWidth: 2.5 }, }, { @@ -333,9 +319,9 @@ export function FeatureIdeaCloud() { sourceHandle: 'bottom', targetHandle: 'left', type: 'default', - data: { type: 'composition', label: 'includes' }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#f87171', width: 20, height: 20 }, - style: { stroke: '#f87171', strokeWidth: 2.5 }, + data: { label: 'includes' }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#a78bfa', width: 20, height: 20 }, + style: { stroke: '#a78bfa', strokeWidth: 2.5 }, }, ]) const [savedNodePositions, setSavedNodePositions] = useKV>('feature-idea-node-positions', {}) @@ -350,7 +336,6 @@ export function FeatureIdeaCloud() { const [viewDialogOpen, setViewDialogOpen] = useState(false) const [edgeDialogOpen, setEdgeDialogOpen] = useState(false) const [debugPanelOpen, setDebugPanelOpen] = useState(false) - const [connectionType, setConnectionType] = useState('association') const edgeReconnectSuccessful = useRef(true) const safeIdeas = ideas || SEED_IDEAS @@ -523,7 +508,6 @@ export function FeatureIdeaCloud() { targetHandleId ) - const style = CONNECTION_STYLES[connectionType] const newEdge: Edge = { id: `edge-${Date.now()}`, source: sourceNodeId, @@ -531,19 +515,18 @@ export function FeatureIdeaCloud() { ...(params.sourceHandle && { sourceHandle: params.sourceHandle }), ...(params.targetHandle && { targetHandle: params.targetHandle }), type: 'default', - data: { type: connectionType, label: CONNECTION_LABELS[connectionType] }, + data: { label: 'relates to' }, markerEnd: { - type: style.markerEnd, - color: style.stroke, + type: CONNECTION_STYLE.markerEnd, + color: CONNECTION_STYLE.stroke, width: 20, height: 20 }, style: { - stroke: style.stroke, - strokeDasharray: style.strokeDasharray, - strokeWidth: style.strokeWidth + stroke: CONNECTION_STYLE.stroke, + strokeWidth: CONNECTION_STYLE.strokeWidth }, - animated: connectionType === 'dependency', + animated: false, } const updatedEdges = addEdge(newEdge, filteredEdges) @@ -573,7 +556,7 @@ export function FeatureIdeaCloud() { return updatedEdges }) }, - [connectionType, setEdges, setSavedEdges, validateAndRemoveConflicts] + [setEdges, setSavedEdges, validateAndRemoveConflicts] ) const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { @@ -809,22 +792,20 @@ export function FeatureIdeaCloud() { const handleSaveEdge = () => { if (selectedEdge) { - const style = CONNECTION_STYLES[selectedEdge.data!.type] const updatedEdge = { ...selectedEdge, data: selectedEdge.data, markerEnd: { - type: style.markerEnd, - color: style.stroke, + type: CONNECTION_STYLE.markerEnd, + color: CONNECTION_STYLE.stroke, width: 20, height: 20 }, style: { - stroke: style.stroke, - strokeDasharray: style.strokeDasharray, - strokeWidth: style.strokeWidth + stroke: CONNECTION_STYLE.stroke, + strokeWidth: CONNECTION_STYLE.strokeWidth }, - animated: selectedEdge.data!.type === 'dependency', + animated: false, } const updatedEdges = edges.map(e => e.id === selectedEdge.id ? updatedEdge : e) @@ -921,24 +902,6 @@ export function FeatureIdeaCloud() { > - - -
- Connection Type: - -
-
@@ -985,47 +948,6 @@ export function FeatureIdeaCloud() { - -
-

Connection Types:

-
- {CONNECTION_TYPES.map(type => { - const style = CONNECTION_STYLES[type] - return ( -
- - - - - - - - - {type} -
- ) - })} -
-
-
-

💡 Tip: Double-click ideas to view details

@@ -1076,28 +998,43 @@ export function FeatureIdeaCloud() {
{safeIdeas.slice(0, 10).map((idea) => { const nodeEdges = edges.filter(e => e.source === idea.id || e.target === idea.id) - const leftHandle = edges.find(e => e.target === idea.id && e.targetHandle === 'left') - const rightHandle = edges.find(e => e.source === idea.id && e.sourceHandle === 'right') - const topHandle = edges.find(e => e.target === idea.id && e.targetHandle === 'top') - const bottomHandle = edges.find(e => e.source === idea.id && e.sourceHandle === 'bottom') + const leftHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'left') + const rightHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'right') + const topHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'top') + const bottomHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'bottom') + + const hasViolation = leftHandles.length > 1 || rightHandles.length > 1 || topHandles.length > 1 || bottomHandles.length > 1 return ( -
-
+
+
+ {hasViolation && ⚠️} {idea.title}
-
- ← {leftHandle ? '✓' : '○'} +
1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : + leftHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' + }`}> + ← {leftHandles.length > 0 ? `✓${leftHandles.length > 1 ? `(${leftHandles.length})` : ''}` : '○'}
-
- → {rightHandle ? '✓' : '○'} +
1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : + rightHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' + }`}> + → {rightHandles.length > 0 ? `✓${rightHandles.length > 1 ? `(${rightHandles.length})` : ''}` : '○'}
-
- ↑ {topHandle ? '✓' : '○'} +
1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : + topHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' + }`}> + ↑ {topHandles.length > 0 ? `✓${topHandles.length > 1 ? `(${topHandles.length})` : ''}` : '○'}
-
- ↓ {bottomHandle ? '✓' : '○'} +
1 ? 'bg-red-500/40 text-red-900 dark:text-red-100 font-bold' : + bottomHandles.length === 1 ? 'bg-green-500/20 text-green-700 dark:text-green-300' : 'bg-muted' + }`}> + ↓ {bottomHandles.length > 0 ? `✓${bottomHandles.length > 1 ? `(${bottomHandles.length})` : ''}` : '○'}
@@ -1115,17 +1052,45 @@ export function FeatureIdeaCloud() {
-
-
- ✅ Test Status -
-
-
• Each handle can connect to exactly 1 other handle
-
• New connections automatically remove conflicts
-
• Remapping preserves 1:1 constraint
-
• Changes persist to database immediately
-
-
+ {(() => { + const violations: string[] = [] + safeIdeas.forEach(idea => { + const leftHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'left') + const rightHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'right') + const topHandles = edges.filter(e => e.target === idea.id && e.targetHandle === 'top') + const bottomHandles = edges.filter(e => e.source === idea.id && e.sourceHandle === 'bottom') + + if (leftHandles.length > 1) violations.push(`${idea.title}: Left handle has ${leftHandles.length} connections`) + if (rightHandles.length > 1) violations.push(`${idea.title}: Right handle has ${rightHandles.length} connections`) + if (topHandles.length > 1) violations.push(`${idea.title}: Top handle has ${topHandles.length} connections`) + if (bottomHandles.length > 1) violations.push(`${idea.title}: Bottom handle has ${bottomHandles.length} connections`) + }) + + return violations.length > 0 ? ( +
+
+ ❌ CONSTRAINT VIOLATIONS DETECTED +
+
+ {violations.map((v, i) => ( +
• {v}
+ ))} +
+
+ ) : ( +
+
+ ✅ All Constraints Satisfied +
+
+
• Each handle has at most 1 connection ✓
+
• New connections automatically remove conflicts ✓
+
• Remapping preserves 1:1 constraint ✓
+
• Changes persist to database immediately ✓
+
+
+ ) + })()}
@@ -1386,10 +1351,12 @@ export function FeatureIdeaCloud() { const isOutgoing = edge.source === selectedIdea.id return (
- {edge.data?.type || 'unknown'} {isOutgoing ? '→' : '←'} {otherIdea?.title || 'Unknown'} + {edge.data?.label && ( + {edge.data.label} + )}
) })} @@ -1441,32 +1408,10 @@ export function FeatureIdeaCloud() {
-
- - -
-
setSelectedEdge({ ...selectedEdge, data: { @@ -1474,19 +1419,15 @@ export function FeatureIdeaCloud() { label: e.target.value } })} - placeholder={CONNECTION_LABELS[selectedEdge.data.type]} + placeholder="relates to" />
-

Connection Type Guide:

-
    -
  • Dependency: One feature needs another to function
  • -
  • Association: Features work together or are related
  • -
  • Inheritance: One feature extends another
  • -
  • Composition: One feature contains another as essential part
  • -
  • Aggregation: One feature has another as optional part
  • -
+

💡 Connection Info:

+

+ Each connection point can have exactly one arrow. Creating a new connection from an occupied point will automatically remap the existing connection. +

)}