sync: Major UI updates from private repo (Jan 16-24)

Features synced:
- UI Panels Refactor: flexible pane system with ChatPane, NodePane,
  DimensionsPane, WorkflowsPane, ViewsPane, MapPane
- Source Content Reader: content type detection + 4 formatters
  (transcript, book, markdown, raw)
- Source Content Search: Cmd+F search in Source Reader
- Feed Layout Overhaul: LeftToolbar, SplitHandle, CollapsedRail
- Map Panel: promoted to first-class pane
- Edge policy simplification: auto-inference + direction correction

New files:
- src/components/panes/* (pane system)
- src/components/layout/CollapsedRail.tsx
- src/components/layout/LeftToolbar.tsx
- src/components/layout/SplitHandle.tsx
- src/components/focus/source/* (Source Reader + formatters)
- src/components/views/ViewsOverlay.tsx

Updated:
- ThreePanelLayout.tsx (complete refactor)
- FocusPanel.tsx (Source tab integration)
- API routes (dimensions GET, edges auto-inference)
- Database services (nodes, edges, dimensions)

Dependencies added:
- react-markdown
- remark-gfm

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-01-24 21:54:21 +11:00
co-authored by Claude Opus 4.5
parent cea1df7f1f
commit 05523b7cb2
34 changed files with 7895 additions and 991 deletions
+111
View File
@@ -0,0 +1,111 @@
"use client";
import { ChevronLeft, ChevronRight, FileText, MessageSquare, Workflow, FolderOpen, Map, LayoutList } from 'lucide-react';
type RailPosition = 'left' | 'right';
type PaneType = 'nodes' | 'node' | 'chat' | 'workflows' | 'dimensions' | 'map' | 'views';
interface CollapsedRailProps {
position: RailPosition;
paneType: PaneType;
onExpand: () => void;
shortcut?: string;
}
const PANE_ICONS: Record<PaneType, React.ReactNode> = {
nodes: <FileText size={18} />,
node: <FileText size={18} />,
chat: <MessageSquare size={18} />,
workflows: <Workflow size={18} />,
dimensions: <FolderOpen size={18} />,
map: <Map size={18} />,
views: <LayoutList size={18} />,
};
const PANE_LABELS: Record<PaneType, string> = {
nodes: 'Nodes',
node: 'Focus',
chat: 'Chat',
workflows: 'Workflows',
dimensions: 'Dimensions',
map: 'Map',
views: 'Feed',
};
export default function CollapsedRail({ position, paneType, onExpand, shortcut }: CollapsedRailProps) {
const isLeft = position === 'left';
const ChevronIcon = isLeft ? ChevronRight : ChevronLeft;
return (
<div
onClick={onExpand}
style={{
width: '40px',
height: '100%',
flexShrink: 0,
borderLeft: isLeft ? 'none' : '1px solid #1f1f1f',
borderRight: isLeft ? '1px solid #1f1f1f' : 'none',
background: '#0c0c0c',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
paddingTop: '12px',
gap: '8px',
cursor: 'pointer',
transition: 'background 0.15s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#141414';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#0c0c0c';
}}
title={`Expand ${PANE_LABELS[paneType]}${shortcut ? ` (${shortcut})` : ''}`}
>
{/* Icon representing the collapsed panel */}
<div
style={{
width: '32px',
height: '32px',
borderRadius: '6px',
background: '#1a1a1a',
border: '1px solid #333',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#888',
transition: 'all 0.15s ease',
}}
>
{PANE_ICONS[paneType]}
</div>
{/* Chevron indicator */}
<div
style={{
color: '#666',
marginTop: '4px',
}}
>
<ChevronIcon size={14} />
</div>
{/* Vertical label */}
<div
style={{
writingMode: 'vertical-rl',
textOrientation: 'mixed',
transform: isLeft ? 'rotate(180deg)' : 'none',
fontSize: '10px',
fontWeight: 500,
color: '#777',
letterSpacing: '0.05em',
textTransform: 'uppercase',
marginTop: '8px',
}}
>
{PANE_LABELS[paneType]}
</div>
</div>
);
}
+231
View File
@@ -0,0 +1,231 @@
"use client";
import { useState, useCallback } from 'react';
import {
Search,
Plus,
LayoutList,
MessageSquare,
Map,
Folder,
Workflow,
Settings,
} from 'lucide-react';
import type { PaneType } from '../panes/types';
interface LeftToolbarProps {
onSearchClick: () => void;
onAddStuffClick: () => void;
onSettingsClick: () => void;
onPaneTypeClick: (paneType: PaneType) => void;
activePane: 'A' | 'B';
slotAType: PaneType | null;
slotBType: PaneType | null;
}
// Map pane types to their icons
const PANE_TYPE_ICONS: Record<string, typeof LayoutList> = {
views: LayoutList,
chat: MessageSquare,
map: Map,
dimensions: Folder,
workflows: Workflow,
};
const PANE_TYPE_LABELS: Record<string, string> = {
views: 'Feed',
chat: 'Chat',
map: 'Map',
dimensions: 'Dimensions',
workflows: 'Workflows',
};
// Pane types shown in the toolbar (excludes 'node' which is opened via Feed)
const TOOLBAR_PANE_TYPES: PaneType[] = ['views', 'chat', 'map', 'dimensions', 'workflows'];
interface ToolbarButtonProps {
icon: typeof Search;
label: string;
shortcut?: string;
onClick: () => void;
disabled?: boolean;
isActive?: boolean;
}
function ToolbarButton({ icon: Icon, label, shortcut, onClick, disabled, isActive }: ToolbarButtonProps) {
const [isHovered, setIsHovered] = useState(false);
return (
<button
onClick={onClick}
disabled={disabled}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
title={shortcut ? `${label} (${shortcut})` : label}
style={{
width: '36px',
height: '36px',
borderRadius: '8px',
border: 'none',
background: isHovered ? '#1a1a1a' : 'transparent',
color: isActive ? '#22c55e' : (isHovered ? '#aaa' : '#666'),
cursor: disabled ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.15s ease',
opacity: disabled ? 0.5 : 1,
}}
>
<Icon size={18} />
</button>
);
}
interface PaneTypeButtonProps {
icon: typeof LayoutList;
label: string;
paneType: PaneType;
isOpen: boolean;
isActivePane: boolean;
onClick: () => void;
}
function PaneTypeButton({ icon: Icon, label, paneType, isOpen, isActivePane, onClick }: PaneTypeButtonProps) {
const [isHovered, setIsHovered] = useState(false);
// Determine color: green if open, brighter if it's the active pane
const getColor = () => {
if (isOpen) {
return isActivePane ? '#4ade80' : '#22c55e'; // Brighter green for active
}
return isHovered ? '#aaa' : '#666';
};
return (
<button
onClick={onClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
title={label}
style={{
width: '36px',
height: '36px',
borderRadius: '8px',
border: 'none',
background: isOpen ? '#1a1a1a' : (isHovered ? '#151515' : 'transparent'),
color: getColor(),
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.15s ease',
position: 'relative',
}}
>
<Icon size={18} />
{/* Active pane indicator dot */}
{isActivePane && isOpen && (
<div
style={{
position: 'absolute',
bottom: '4px',
left: '50%',
transform: 'translateX(-50%)',
width: '4px',
height: '4px',
borderRadius: '50%',
background: '#4ade80',
}}
/>
)}
</button>
);
}
export default function LeftToolbar({
onSearchClick,
onAddStuffClick,
onSettingsClick,
onPaneTypeClick,
activePane,
slotAType,
slotBType,
}: LeftToolbarProps) {
// Determine which pane types are currently open
const openPaneTypes = new Set<PaneType>(
[slotAType, slotBType].filter((t): t is PaneType => t !== null)
);
// Determine which pane type is in the active pane (null if pane is closed)
const activePaneType = activePane === 'A' ? slotAType : slotBType;
return (
<div
style={{
width: '50px',
height: '100%',
background: '#0a0a0a',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '12px 0',
flexShrink: 0,
}}
>
{/* Top section - Actions */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<ToolbarButton
icon={Search}
label="Search"
shortcut="⌘K"
onClick={onSearchClick}
/>
<ToolbarButton
icon={Plus}
label="Add Stuff"
onClick={onAddStuffClick}
/>
</div>
{/* Middle section - Pane Types */}
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4px',
padding: '8px 0',
}}
>
{TOOLBAR_PANE_TYPES.map((paneType) => {
const Icon = PANE_TYPE_ICONS[paneType];
const label = PANE_TYPE_LABELS[paneType];
const isOpen = openPaneTypes.has(paneType);
const isActivePane = activePaneType === paneType;
return (
<PaneTypeButton
key={paneType}
icon={Icon}
label={label}
paneType={paneType}
isOpen={isOpen}
isActivePane={isActivePane}
onClick={() => onPaneTypeClick(paneType)}
/>
);
})}
</div>
{/* Bottom section - Settings */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<ToolbarButton
icon={Settings}
label="Settings"
onClick={onSettingsClick}
/>
</div>
</div>
);
}
+135
View File
@@ -0,0 +1,135 @@
"use client";
import { useState, useCallback, useEffect, useRef } from 'react';
import { GripVertical } from 'lucide-react';
interface SplitHandleProps {
isSecondPaneOpen: boolean;
onOpenSecondPane: () => void;
onResize: (newWidthPercent: number) => void;
onCloseSecondPane: () => void;
containerRef: React.RefObject<HTMLDivElement>;
toolbarWidth?: number;
}
export default function SplitHandle({
isSecondPaneOpen,
onOpenSecondPane,
onResize,
onCloseSecondPane,
containerRef,
toolbarWidth = 50,
}: SplitHandleProps) {
const [isDragging, setIsDragging] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const startXRef = useRef<number>(0);
const startWidthRef = useRef<number>(50);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
if (!isSecondPaneOpen) {
// First drag opens the second pane at 50%
onOpenSecondPane();
}
setIsDragging(true);
startXRef.current = e.clientX;
// Calculate current width from container
if (containerRef.current) {
const containerWidth = containerRef.current.offsetWidth - toolbarWidth;
// Assume current position is at the split point
startWidthRef.current = 50; // Default to 50% for new split
}
}, [isSecondPaneOpen, onOpenSecondPane, containerRef, toolbarWidth]);
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
if (!containerRef.current) return;
const containerRect = containerRef.current.getBoundingClientRect();
const containerWidth = containerRect.width - toolbarWidth;
const mouseX = e.clientX - containerRect.left - toolbarWidth;
// Calculate what percentage of the available space should be Slot B
// mouseX is distance from left edge, so slotA width = mouseX
// slotB width = containerWidth - mouseX
const slotBWidthPercent = ((containerWidth - mouseX) / containerWidth) * 100;
// Clamp between 20% and 70%
const clampedWidth = Math.max(20, Math.min(70, slotBWidthPercent));
// If dragged to less than 15%, close the pane
if (slotBWidthPercent < 15) {
onCloseSecondPane();
setIsDragging(false);
return;
}
onResize(clampedWidth);
};
const handleMouseUp = () => {
setIsDragging(false);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [isDragging, containerRef, toolbarWidth, onResize, onCloseSecondPane]);
// When second pane is closed, show a wider handle with grip icon
if (!isSecondPaneOpen) {
return (
<div
onMouseDown={handleMouseDown}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
width: '12px',
cursor: 'col-resize',
background: isHovered ? '#1a1a1a' : 'transparent',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background 0.15s ease',
flexShrink: 0,
}}
title="Drag to split view (⌘\)"
>
<GripVertical
size={12}
color={isHovered ? '#888' : '#444'}
style={{ transition: 'color 0.15s ease' }}
/>
</div>
);
}
// When second pane is open, show resize handle
return (
<div
onMouseDown={handleMouseDown}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
width: '8px',
cursor: 'col-resize',
background: isDragging ? '#22c55e' : (isHovered ? '#1a1a1a' : 'transparent'),
transition: isDragging ? 'none' : 'background 0.15s ease',
flexShrink: 0,
}}
/>
);
}
File diff suppressed because it is too large Load Diff