merge: ui polish pass sync

This commit is contained in:
“BeeRad”
2026-03-23 08:53:27 +11:00
6 changed files with 269 additions and 155 deletions
+15 -2
View File
@@ -139,9 +139,9 @@ html, body {
color: var(--rah-text-base); color: var(--rah-text-base);
/* Geist Sans for optimal reading/writing experience */ /* Geist Sans for optimal reading/writing experience */
font-family: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-family: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px; font-size: 15px;
line-height: 1.5; line-height: 1.5;
letter-spacing: -0.011em; /* Optimized letter spacing for Geist */ letter-spacing: -0.012em; /* Optimized letter spacing for Geist */
font-weight: 400; font-weight: 400;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@@ -220,6 +220,19 @@ html, body {
} }
} }
@keyframes rah-shimmer {
0%, 100% {
opacity: 0.4;
}
50% {
opacity: 0.8;
}
}
.rah-skeleton {
animation: rah-shimmer 1.5s ease-in-out infinite;
}
/* Modal/Popup animations */ /* Modal/Popup animations */
@keyframes modalFadeIn { @keyframes modalFadeIn {
from { from {
+5 -3
View File
@@ -52,6 +52,7 @@ function NavButton({
onClick, onClick,
trailing, trailing,
activeTone = 'neutral', activeTone = 'neutral',
title,
}: { }: {
icon: typeof Search; icon: typeof Search;
label: string; label: string;
@@ -60,6 +61,7 @@ function NavButton({
onClick: () => void; onClick: () => void;
trailing?: ReactNode; trailing?: ReactNode;
activeTone?: 'neutral' | 'green'; activeTone?: 'neutral' | 'green';
title?: string;
}) { }) {
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
const activeColor = activeTone === 'green' ? 'var(--rah-accent-green)' : 'var(--rah-text-active)'; const activeColor = activeTone === 'green' ? 'var(--rah-accent-green)' : 'var(--rah-text-active)';
@@ -69,7 +71,7 @@ function NavButton({
onClick={onClick} onClick={onClick}
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
title={expanded ? undefined : label} title={title ?? (expanded ? undefined : label)}
style={{ style={{
width: '100%', width: '100%',
height: '36px', height: '36px',
@@ -136,8 +138,8 @@ export default function LeftToolbar({
onClick={onToggleExpanded} onClick={onToggleExpanded}
/> />
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<NavButton icon={Search} label="Search" expanded={isExpanded} onClick={onSearchClick} /> <NavButton icon={Search} label="Search" title="Search (⌘K)" expanded={isExpanded} onClick={onSearchClick} />
<NavButton icon={Plus} label="Add Stuff" expanded={isExpanded} onClick={onAddStuffClick} /> <NavButton icon={Plus} label="Add Stuff" title="New node (⌘N)" expanded={isExpanded} onClick={onAddStuffClick} />
<NavButton icon={RefreshCw} label="Refresh" expanded={isExpanded} onClick={onRefreshClick} /> <NavButton icon={RefreshCw} label="Refresh" expanded={isExpanded} onClick={onRefreshClick} />
</div> </div>
</div> </div>
+4 -24
View File
@@ -4,6 +4,7 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Filter, ChevronDown, ChevronLeft, ChevronRight, X, ArrowUpDown, Search, ExternalLink } from 'lucide-react'; import { Filter, ChevronDown, ChevronLeft, ChevronRight, X, ArrowUpDown, Search, ExternalLink } from 'lucide-react';
import type { Node } from '@/types/database'; import type { Node } from '@/types/database';
import { formatRelativeDate } from '@/utils/formatDate';
type SortOrder = 'updated' | 'edges' | 'created' | 'event_date'; type SortOrder = 'updated' | 'edges' | 'created' | 'event_date';
@@ -28,27 +29,6 @@ interface DatabaseTableViewProps {
toolbarHost?: HTMLDivElement | null; toolbarHost?: HTMLDivElement | null;
} }
function formatRelativeTime(dateStr: string): string {
const now = Date.now();
const then = new Date(dateStr).getTime();
const diff = now - then;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days < 30) return `${days}d ago`;
const months = Math.floor(days / 30);
if (months < 12) return `${months}mo ago`;
return `${Math.floor(months / 12)}y ago`;
}
function formatDate(dateStr: string | null | undefined): string { function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return '\u2014'; if (!dateStr) return '\u2014';
try { try {
@@ -552,14 +532,14 @@ export default function DatabaseTableView({ onNodeClick, refreshToken = 0, toolb
{/* Updated */} {/* Updated */}
<td style={tdStyle()}> <td style={tdStyle()}>
<span style={{ fontSize: '11px', color: 'var(--rah-text-muted)' }}> <span style={{ fontSize: '11px', color: 'var(--rah-text-muted)' }}>
{formatRelativeTime(node.updated_at)} {formatRelativeDate(node.updated_at)}
</span> </span>
</td> </td>
{/* Created */} {/* Created */}
<td style={tdStyle()}> <td style={tdStyle()}>
<span style={{ fontSize: '11px', color: 'var(--rah-text-muted)' }}> <span style={{ fontSize: '11px', color: 'var(--rah-text-muted)' }}>
{formatRelativeTime(node.created_at)} {formatRelativeDate(node.created_at)}
</span> </span>
</td> </td>
@@ -594,7 +574,7 @@ export default function DatabaseTableView({ onNodeClick, refreshToken = 0, toolb
{/* Embedding Updated */} {/* Embedding Updated */}
<td style={tdStyle()}> <td style={tdStyle()}>
<span style={{ fontSize: '11px', color: 'var(--rah-text-muted)' }}> <span style={{ fontSize: '11px', color: 'var(--rah-text-muted)' }}>
{node.embedding_updated_at ? formatRelativeTime(node.embedding_updated_at) : '\u2014'} {node.embedding_updated_at ? formatRelativeDate(node.embedding_updated_at) : '\u2014'}
</span> </span>
</td> </td>
</tr> </tr>
+42 -36
View File
@@ -1,8 +1,10 @@
"use client"; "use client";
import { Inbox } from 'lucide-react';
import { Node } from '@/types/database'; import { Node } from '@/types/database';
import { getNodeIcon } from '@/utils/nodeIcons'; import { getNodeIcon } from '@/utils/nodeIcons';
import { useDimensionIcons } from '@/context/DimensionIconsContext'; import { useDimensionIcons } from '@/context/DimensionIconsContext';
import { formatRelativeDate } from '@/utils/formatDate';
interface ListViewProps { interface ListViewProps {
nodes: Node[]; nodes: Node[];
@@ -11,15 +13,6 @@ interface ListViewProps {
export default function ListView({ nodes, onNodeClick }: ListViewProps) { export default function ListView({ nodes, onNodeClick }: ListViewProps) {
const { dimensionIcons } = useDimensionIcons(); const { dimensionIcons } = useDimensionIcons();
const formatDate = (dateString?: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const truncateContent = (content?: string, maxLength: number = 100) => { const truncateContent = (content?: string, maxLength: number = 100) => {
if (!content) return ''; if (!content) return '';
@@ -31,13 +24,20 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
return ( return (
<div style={{ <div style={{
display: 'flex', display: 'flex',
flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: '8px',
height: '100%', height: '100%',
color: '#666', color: 'var(--rah-text-muted)',
fontSize: '13px' padding: '40px 20px',
textAlign: 'center',
}}> }}>
No nodes match the current filters <Inbox size={28} strokeWidth={1.5} style={{ opacity: 0.4 }} />
<span style={{ fontSize: '14px', color: 'var(--rah-text-secondary)' }}>Nothing here yet</span>
<span style={{ fontSize: '12px', opacity: 0.7 }}>
Try adjusting your filters, or add a node with N
</span>
</div> </div>
); );
} }
@@ -59,20 +59,25 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
gap: '12px', gap: '12px',
padding: '12px', padding: '12px',
marginBottom: '4px', marginBottom: '4px',
background: '#0a0a0a', background: 'var(--rah-bg-base)',
border: '1px solid #1a1a1a', border: '1px solid var(--rah-border)',
borderLeft: '2px solid var(--rah-border-stronger)',
borderRadius: '6px', borderRadius: '6px',
cursor: 'pointer', cursor: 'pointer',
textAlign: 'left', textAlign: 'left',
transition: 'all 0.2s' transition: 'all 0.15s ease'
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = '#111'; e.currentTarget.style.background = 'var(--rah-bg-surface)';
e.currentTarget.style.borderColor = '#333'; e.currentTarget.style.borderColor = 'var(--rah-border-strong)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = 'var(--rah-shadow-floating)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = '#0a0a0a'; e.currentTarget.style.background = 'var(--rah-bg-base)';
e.currentTarget.style.borderColor = '#1a1a1a'; e.currentTarget.style.borderColor = 'var(--rah-border)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}} }}
> >
{/* Icon */} {/* Icon */}
@@ -82,7 +87,7 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
background: '#1a1a1a', background: 'var(--rah-bg-active)',
borderRadius: '6px', borderRadius: '6px',
flexShrink: 0 flexShrink: 0
}}> }}>
@@ -93,9 +98,9 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
{/* Title */} {/* Title */}
<div style={{ <div style={{
fontSize: '13px', fontSize: '14px',
fontWeight: 500, fontWeight: 500,
color: '#e5e5e5', color: 'var(--rah-text-base)',
marginBottom: '4px', marginBottom: '4px',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
@@ -107,8 +112,8 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
{/* Description or Content Preview */} {/* Description or Content Preview */}
{(node.description || node.source) && ( {(node.description || node.source) && (
<div style={{ <div style={{
fontSize: '12px', fontSize: '13px',
color: '#666', color: 'var(--rah-text-muted)',
marginBottom: '8px', marginBottom: '8px',
lineHeight: '1.4', lineHeight: '1.4',
display: '-webkit-box', display: '-webkit-box',
@@ -138,11 +143,12 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
<span <span
key={dim} key={dim}
style={{ style={{
padding: '2px 6px', padding: '2px 8px',
background: '#1a1a1a', background: 'var(--rah-bg-active)',
borderRadius: '3px', border: '1px solid var(--rah-border-strong)',
fontSize: '10px', borderRadius: '8px',
color: '#888' fontSize: '11px',
color: 'var(--rah-text-base)'
}} }}
> >
{dim} {dim}
@@ -151,8 +157,8 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
{node.dimensions.length > 3 && ( {node.dimensions.length > 3 && (
<span style={{ <span style={{
padding: '2px 6px', padding: '2px 6px',
fontSize: '10px', fontSize: '11px',
color: '#666' color: 'var(--rah-text-muted)'
}}> }}>
+{node.dimensions.length - 3} +{node.dimensions.length - 3}
</span> </span>
@@ -162,17 +168,17 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
{/* Date */} {/* Date */}
<span style={{ <span style={{
fontSize: '10px', fontSize: '11px',
color: '#555' color: 'var(--rah-text-muted)'
}}> }}>
{formatDate(node.updated_at || node.created_at)} {formatRelativeDate(node.updated_at || node.created_at)}
</span> </span>
{/* Edge count */} {/* Edge count */}
{node.edge_count !== undefined && node.edge_count > 0 && ( {node.edge_count !== undefined && node.edge_count > 0 && (
<span style={{ <span style={{
fontSize: '10px', fontSize: '11px',
color: '#555' color: 'var(--rah-text-muted)'
}}> }}>
{node.edge_count} connections {node.edge_count} connections
</span> </span>
+178 -89
View File
@@ -2,12 +2,13 @@
import { useEffect, useMemo, useState, useRef, useCallback } from 'react'; import { useEffect, useMemo, useState, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Filter, ChevronDown, X, ArrowUpDown, GripVertical } from 'lucide-react'; import { Filter, ChevronDown, X, ArrowUpDown, GripVertical, Inbox } from 'lucide-react';
import type { Node } from '@/types/database'; import type { Node } from '@/types/database';
import { getNodeIcon } from '@/utils/nodeIcons'; import { getNodeIcon } from '@/utils/nodeIcons';
import { useDimensionIcons } from '@/context/DimensionIconsContext'; import { useDimensionIcons } from '@/context/DimensionIconsContext';
import { usePersistentState } from '@/hooks/usePersistentState'; import { usePersistentState } from '@/hooks/usePersistentState';
import type { PendingNode } from '../layout/ThreePanelLayout'; import type { PendingNode } from '../layout/ThreePanelLayout';
import { formatRelativeDate } from '@/utils/formatDate';
type SortOrder = 'updated' | 'edges' | 'created' | 'custom'; type SortOrder = 'updated' | 'edges' | 'created' | 'custom';
@@ -18,6 +19,8 @@ const SORT_LABELS: Record<SortOrder, string> = {
custom: 'Custom', custom: 'Custom',
}; };
const DOCUMENT_MAX_WIDTH = '980px';
interface ColumnFilter { interface ColumnFilter {
id: string; id: string;
dimension: string; dimension: string;
@@ -127,6 +130,39 @@ function PendingNodeCard({ pending, onDismiss }: { pending: PendingNode; onDismi
); );
} }
function SkeletonCard() {
return (
<div
className="rah-skeleton"
style={{
padding: '12px 14px',
border: '1px solid var(--rah-border)',
borderLeft: '2px solid var(--rah-border)',
borderRadius: '10px',
background: 'var(--rah-bg-base)',
display: 'flex',
gap: '12px',
alignItems: 'flex-start',
}}
>
<div
style={{
width: '32px',
height: '32px',
borderRadius: '8px',
background: 'var(--rah-bg-elevated)',
flexShrink: 0,
}}
/>
<div style={{ flex: 1 }}>
<div style={{ height: '14px', width: '58%', borderRadius: '4px', background: 'var(--rah-bg-elevated)', marginBottom: '8px' }} />
<div style={{ height: '12px', width: '82%', borderRadius: '4px', background: 'var(--rah-bg-elevated)', marginBottom: '6px' }} />
<div style={{ height: '11px', width: '28%', borderRadius: '4px', background: 'var(--rah-bg-elevated)' }} />
</div>
</div>
);
}
interface ViewsOverlayProps { interface ViewsOverlayProps {
onNodeClick: (nodeId: number) => void; onNodeClick: (nodeId: number) => void;
onNodeOpenInOtherPane?: (nodeId: number) => void; onNodeOpenInOtherPane?: (nodeId: number) => void;
@@ -418,19 +454,26 @@ export default function ViewsOverlay({
}} }}
style={{ style={{
padding: '10px 12px', padding: '10px 12px',
background: 'transparent', background: 'var(--rah-bg-base)',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.15s ease', transition: 'all 0.15s ease',
borderBottom: '1px solid var(--rah-bg-panel)', border: '1px solid var(--rah-border)',
borderLeft: '3px solid transparent', borderLeft: '2px solid var(--rah-border-stronger)',
borderRadius: '10px',
opacity: isDragSource ? 0.4 : 1, opacity: isDragSource ? 0.4 : 1,
borderTop: isDropTarget ? '2px solid #22c55e' : '2px solid transparent', borderTop: isDropTarget ? '2px solid var(--rah-accent-green)' : '2px solid transparent',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255,255,255,0.02)'; e.currentTarget.style.background = 'var(--rah-bg-surface)';
e.currentTarget.style.borderColor = 'var(--rah-border-strong)';
e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = 'var(--rah-shadow-floating)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'; e.currentTarget.style.background = 'var(--rah-bg-base)';
e.currentTarget.style.borderColor = 'var(--rah-border)';
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}} }}
onContextMenu={(e) => { onContextMenu={(e) => {
e.preventDefault(); e.preventDefault();
@@ -460,13 +503,13 @@ export default function ViewsOverlay({
justifyContent: 'center', justifyContent: 'center',
width: '16px', width: '16px',
cursor: 'grab', cursor: 'grab',
color: '#444', color: 'var(--rah-text-muted)',
flexShrink: 0, flexShrink: 0,
alignSelf: 'center', alignSelf: 'center',
transition: 'color 0.15s ease', transition: 'color 0.15s ease',
}} }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#888'; }} onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--rah-text-soft)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#444'; }} onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--rah-text-muted)'; }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<GripVertical size={14} /> <GripVertical size={14} />
@@ -476,8 +519,8 @@ export default function ViewsOverlay({
width: '32px', width: '32px',
height: '32px', height: '32px',
borderRadius: '8px', borderRadius: '8px',
background: 'var(var(--rah-bg-panel))', background: 'var(--rah-bg-panel)',
border: '1px solid var(--rah-bg-active)', border: '1px solid var(--rah-border)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -493,9 +536,9 @@ export default function ViewsOverlay({
marginBottom: descPreview ? '2px' : '4px' marginBottom: descPreview ? '2px' : '4px'
}}> }}>
<span style={{ <span style={{
fontSize: '13px', fontSize: '14px',
fontWeight: 500, fontWeight: 500,
color: 'var(var(--rah-text-active))', color: 'var(--rah-text-active)',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
@@ -506,21 +549,27 @@ export default function ViewsOverlay({
</span> </span>
{node.edge_count != null && node.edge_count > 0 && ( {node.edge_count != null && node.edge_count > 0 && (
<span style={{ <span style={{
fontSize: '10px', minWidth: '18px',
color: 'var(var(--rah-text-muted))', height: '18px',
padding: '0 5px',
borderRadius: '999px',
background: 'var(--rah-accent-green-soft)',
border: '1px solid var(--rah-accent-green-soft-strong)',
color: 'var(--rah-accent-green)',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '2px', justifyContent: 'center',
flexShrink: 0, flexShrink: 0,
fontSize: '11px',
fontWeight: 600,
}}> }}>
<span style={{ fontSize: '9px' }}>🔗</span>
{node.edge_count} {node.edge_count}
</span> </span>
)} )}
<span style={{ <span style={{
fontSize: '10px', fontSize: '11px',
color: '#444', color: 'var(--rah-text-muted)',
background: 'var(var(--rah-bg-panel))', background: 'var(--rah-bg-panel)',
padding: '2px 6px', padding: '2px 6px',
borderRadius: '4px', borderRadius: '4px',
fontFamily: 'monospace', fontFamily: 'monospace',
@@ -531,8 +580,8 @@ export default function ViewsOverlay({
</div> </div>
{descPreview && ( {descPreview && (
<div style={{ <div style={{
fontSize: '11px', fontSize: '13px',
color: 'var(var(--rah-text-muted))', color: 'var(--rah-text-muted)',
lineHeight: '1.4', lineHeight: '1.4',
marginBottom: '4px', marginBottom: '4px',
overflow: 'hidden', overflow: 'hidden',
@@ -553,24 +602,36 @@ export default function ViewsOverlay({
<span <span
key={d} key={d}
style={{ style={{
fontSize: '9px', fontSize: '11px',
padding: '1px 5px', padding: '2px 8px',
background: 'rgba(34, 197, 94, 0.08)', background: 'var(--rah-bg-active)',
color: '#4a9', border: '1px solid var(--rah-border-strong)',
borderRadius: '3px' color: 'var(--rah-text-base)',
borderRadius: '8px'
}} }}
> >
{d} {d}
</span> </span>
))} ))}
{node.dimensions.length > 3 && ( {node.dimensions.length > 3 && (
<span style={{ fontSize: '9px', color: 'var(var(--rah-text-muted))' }}> <span style={{ fontSize: '11px', color: 'var(--rah-text-muted)' }}>
+{node.dimensions.length - 3} +{node.dimensions.length - 3}
</span> </span>
)} )}
</> </>
) : null} ) : null}
</div> </div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
marginTop: '6px',
fontSize: '11px',
color: 'var(--rah-text-muted)',
}}>
<span>{formatRelativeDate(node.updated_at || node.created_at)}</span>
{node.edge_count != null && node.edge_count > 0 ? <span>{node.edge_count} connections</span> : null}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -578,14 +639,15 @@ export default function ViewsOverlay({
}; };
const toolbar = ( const toolbar = (
<div style={{ <div style={{ width: '100%', maxWidth: DOCUMENT_MAX_WIDTH, margin: '0 auto' }}>
width: '100%', <div style={{
display: 'flex', width: '100%',
alignItems: 'center', display: 'flex',
gap: '10px', alignItems: 'center',
flexWrap: 'wrap' gap: '10px',
}}> flexWrap: 'wrap'
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap', flex: 1 }}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap', flex: 1 }}>
{selectedFilters.map(filter => ( {selectedFilters.map(filter => (
<div <div
key={filter} key={filter}
@@ -622,7 +684,7 @@ export default function ViewsOverlay({
))} ))}
{externalDimensionFilter && ( {externalDimensionFilter && (
<span style={{ fontSize: '11px', color: 'var(var(--rah-text-muted))' }}> <span style={{ fontSize: '11px', color: 'var(--rah-text-muted)' }}>
Sidebar filter Sidebar filter
</span> </span>
)} )}
@@ -635,15 +697,16 @@ export default function ViewsOverlay({
} }
}} }}
disabled={!!externalDimensionFilter} disabled={!!externalDimensionFilter}
title="Filter (⌘F)"
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '4px', gap: '4px',
padding: '4px 8px', padding: '4px 8px',
background: 'transparent', background: 'transparent',
border: '1px solid var(--rah-border-strong)', border: '1px solid var(--rah-border)',
borderRadius: '5px', borderRadius: '5px',
color: externalDimensionFilter ? '#4b4b4b' : '#888', color: externalDimensionFilter ? 'var(--rah-text-muted)' : 'var(--rah-text-soft)',
fontSize: '11px', fontSize: '11px',
cursor: externalDimensionFilter ? 'not-allowed' : 'pointer', cursor: externalDimensionFilter ? 'not-allowed' : 'pointer',
transition: 'all 0.15s ease' transition: 'all 0.15s ease'
@@ -651,12 +714,12 @@ export default function ViewsOverlay({
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!externalDimensionFilter) { if (!externalDimensionFilter) {
e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; e.currentTarget.style.background = 'rgba(255,255,255,0.04)';
e.currentTarget.style.borderColor = '#333'; e.currentTarget.style.borderColor = 'var(--rah-border-strong)';
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'; e.currentTarget.style.background = 'transparent';
e.currentTarget.style.borderColor = '#222'; e.currentTarget.style.borderColor = 'var(--rah-border)';
}} }}
> >
<Filter size={11} /> <Filter size={11} />
@@ -670,8 +733,8 @@ export default function ViewsOverlay({
top: '100%', top: '100%',
left: 0, left: 0,
marginTop: '4px', marginTop: '4px',
background: 'var(var(--rah-bg-panel))', background: 'var(--rah-bg-panel)',
border: '1px solid var(--rah-border-strong)', border: '1px solid var(--rah-border)',
borderRadius: '10px', borderRadius: '10px',
padding: '6px', padding: '6px',
minWidth: '220px', minWidth: '220px',
@@ -689,23 +752,23 @@ export default function ViewsOverlay({
style={{ style={{
width: '100%', width: '100%',
padding: '7px 10px', padding: '7px 10px',
background: 'var(var(--rah-bg-surface))', background: 'var(--rah-bg-base)',
border: '1px solid transparent', border: '1px solid transparent',
borderRadius: '6px', borderRadius: '6px',
color: 'var(var(--rah-text-active))', color: 'var(--rah-text-active)',
fontSize: '12px', fontSize: '12px',
marginBottom: '4px', marginBottom: '4px',
outline: 'none', outline: 'none',
}} }}
onFocus={(e) => { e.currentTarget.style.borderColor = '#333'; }} onFocus={(e) => { e.currentTarget.style.borderColor = 'var(--rah-border-strong)'; }}
onBlur={(e) => { e.currentTarget.style.borderColor = 'transparent'; }} onBlur={(e) => { e.currentTarget.style.borderColor = 'transparent'; }}
/> />
{dimensionsLoading ? ( {dimensionsLoading ? (
<div style={{ padding: '12px', color: 'var(var(--rah-text-muted))', fontSize: '12px', textAlign: 'center' }}> <div style={{ padding: '12px', color: 'var(--rah-text-muted)', fontSize: '12px', textAlign: 'center' }}>
Loading dimensions... Loading dimensions...
</div> </div>
) : filterPickerDimensions.length === 0 ? ( ) : filterPickerDimensions.length === 0 ? (
<div style={{ padding: '12px', color: 'var(var(--rah-text-muted))', fontSize: '12px', textAlign: 'center' }}> <div style={{ padding: '12px', color: 'var(--rah-text-muted)', fontSize: '12px', textAlign: 'center' }}>
{filterSearchQuery ? 'No matching dimensions' : 'No dimensions available'} {filterSearchQuery ? 'No matching dimensions' : 'No dimensions available'}
</div> </div>
) : ( ) : (
@@ -722,7 +785,7 @@ export default function ViewsOverlay({
background: 'transparent', background: 'transparent',
border: 'none', border: 'none',
borderRadius: '5px', borderRadius: '5px',
color: 'var(var(--rah-text-secondary))', color: 'var(--rah-text-secondary)',
fontSize: '12px', fontSize: '12px',
cursor: 'pointer', cursor: 'pointer',
textAlign: 'left' textAlign: 'left'
@@ -732,9 +795,9 @@ export default function ViewsOverlay({
> >
<span>{d.dimension}</span> <span>{d.dimension}</span>
<span style={{ <span style={{
color: 'var(var(--rah-text-muted))', color: 'var(--rah-text-muted)',
fontSize: '10px', fontSize: '10px',
background: 'var(var(--rah-bg-active))', background: 'var(--rah-bg-active)',
padding: '1px 6px', padding: '1px 6px',
borderRadius: '10px', borderRadius: '10px',
}}> }}>
@@ -754,12 +817,12 @@ export default function ViewsOverlay({
padding: '4px 8px', padding: '4px 8px',
background: 'transparent', background: 'transparent',
border: 'none', border: 'none',
color: 'var(var(--rah-text-muted))', color: 'var(--rah-text-muted)',
fontSize: '11px', fontSize: '11px',
cursor: 'pointer' cursor: 'pointer'
}} }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#ef4444'; }} onMouseEnter={(e) => { e.currentTarget.style.color = '#ef4444'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#666'; }} onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--rah-text-muted)'; }}
> >
Clear all Clear all
</button> </button>
@@ -768,29 +831,29 @@ export default function ViewsOverlay({
{/* Sort dropdown */} {/* Sort dropdown */}
<div style={{ position: 'relative' }} ref={sortDropdownRef}> <div style={{ position: 'relative' }} ref={sortDropdownRef}>
<button <button
onClick={() => setShowSortDropdown(!showSortDropdown)} onClick={() => setShowSortDropdown(!showSortDropdown)}
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '4px', gap: '4px',
padding: '5px 8px', padding: '5px 8px',
background: 'transparent', background: 'transparent',
border: '1px solid var(--rah-border-strong)', border: '1px solid var(--rah-border)',
borderRadius: '5px', borderRadius: '5px',
color: 'var(var(--rah-text-muted))', color: 'var(--rah-text-soft)',
fontSize: '11px', fontSize: '11px',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.15s ease', transition: 'all 0.15s ease',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; e.currentTarget.style.background = 'rgba(255,255,255,0.04)';
e.currentTarget.style.borderColor = '#333'; e.currentTarget.style.borderColor = 'var(--rah-border-strong)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'; e.currentTarget.style.background = 'transparent';
e.currentTarget.style.borderColor = '#222'; e.currentTarget.style.borderColor = 'var(--rah-border)';
}} }}
> >
<ArrowUpDown size={11} /> <ArrowUpDown size={11} />
@@ -804,8 +867,8 @@ export default function ViewsOverlay({
top: '100%', top: '100%',
right: 0, right: 0,
marginTop: '4px', marginTop: '4px',
background: 'var(var(--rah-bg-panel))', background: 'var(--rah-bg-panel)',
border: '1px solid var(--rah-border-strong)', border: '1px solid var(--rah-border)',
borderRadius: '10px', borderRadius: '10px',
padding: '4px', padding: '4px',
minWidth: '140px', minWidth: '140px',
@@ -828,7 +891,7 @@ export default function ViewsOverlay({
background: sortOrder === key ? 'rgba(255,255,255,0.04)' : 'transparent', background: sortOrder === key ? 'rgba(255,255,255,0.04)' : 'transparent',
border: 'none', border: 'none',
borderRadius: '5px', borderRadius: '5px',
color: sortOrder === key ? '#f0f0f0' : '#999', color: sortOrder === key ? 'var(--rah-text-active)' : 'var(--rah-text-soft)',
fontSize: '12px', fontSize: '12px',
cursor: 'pointer', cursor: 'pointer',
textAlign: 'left', textAlign: 'left',
@@ -836,7 +899,7 @@ export default function ViewsOverlay({
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; }} onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = sortOrder === key ? 'rgba(255,255,255,0.04)' : 'transparent'; }} onMouseLeave={(e) => { e.currentTarget.style.background = sortOrder === key ? 'rgba(255,255,255,0.04)' : 'transparent'; }}
> >
{sortOrder === key && <span style={{ color: '#22c55e', fontSize: '12px' }}></span>} {sortOrder === key && <span style={{ color: 'var(--rah-accent-green)', fontSize: '12px' }}></span>}
{SORT_LABELS[key]} {SORT_LABELS[key]}
</button> </button>
))} ))}
@@ -844,6 +907,7 @@ export default function ViewsOverlay({
)} )}
</div> </div>
</div> </div>
</div>
); );
return ( return (
@@ -868,30 +932,55 @@ export default function ViewsOverlay({
{/* Content area — list view */} {/* Content area — list view */}
{filteredNodesLoading ? ( {filteredNodesLoading ? (
<div style={{ padding: '40px', color: 'var(var(--rah-text-muted))', textAlign: 'center' }}> <div style={{
Loading... flex: 1,
overflowY: 'auto',
padding: '16px',
}}>
<div style={{ width: '100%', maxWidth: DOCUMENT_MAX_WIDTH, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '8px' }}>
{Array.from({ length: 6 }, (_, index) => (
<SkeletonCard key={index} />
))}
</div>
</div> </div>
) : filteredNodes.length === 0 ? ( ) : filteredNodes.length === 0 ? (
<div style={{ padding: '40px', color: 'var(var(--rah-text-muted))', textAlign: 'center' }}> <div style={{
{selectedFilters.length > 0 ? 'No nodes match the selected filters.' : 'No nodes yet. Add some content to get started.'} flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
padding: '40px 20px',
color: 'var(--rah-text-muted)',
textAlign: 'center',
}}>
<div style={{ width: '100%', maxWidth: DOCUMENT_MAX_WIDTH, margin: '0 auto', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<Inbox size={28} strokeWidth={1.5} style={{ opacity: 0.4 }} />
<span style={{ fontSize: '14px', color: 'var(--rah-text-secondary)' }}>
{selectedFilters.length > 0 ? 'Nothing matches these filters' : 'Nothing here yet'}
</span>
<span style={{ fontSize: '12px', opacity: 0.7 }}>
{selectedFilters.length > 0 ? 'Try adjusting your filters, or add a node with ⌘N' : 'Add a node with ⌘N to get started'}
</span>
</div>
</div> </div>
) : ( ) : (
<div style={{ <div style={{
flex: 1, flex: 1,
overflowY: 'auto', overflowY: 'auto',
padding: '16px', padding: '16px',
display: 'flex',
flexDirection: 'column',
gap: '8px'
}}> }}>
{pendingNodes && pendingNodes.length > 0 && pendingNodes.map(p => ( <div style={{ width: '100%', maxWidth: DOCUMENT_MAX_WIDTH, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '8px' }}>
<PendingNodeCard {pendingNodes && pendingNodes.length > 0 && pendingNodes.map(p => (
key={p.id} <PendingNodeCard
pending={p} key={p.id}
onDismiss={onDismissPending ? () => onDismissPending(p.id) : undefined} pending={p}
/> onDismiss={onDismissPending ? () => onDismissPending(p.id) : undefined}
))} />
{filteredNodes.map((node, index) => renderNodeCard(node, index))} ))}
{filteredNodes.map((node, index) => renderNodeCard(node, index))}
</div>
</div> </div>
)} )}
</div> </div>
+24
View File
@@ -0,0 +1,24 @@
export function formatRelativeDate(dateString?: string | null): string {
if (!dateString) return '';
const then = new Date(dateString).getTime();
if (Number.isNaN(then)) return '';
const diff = Date.now() - then;
if (diff < 0) return 'today';
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days === 1) return 'yesterday';
if (days < 7) return `${days}d ago`;
if (days < 30) return `${Math.floor(days / 7)}w ago`;
if (days < 365) return `${Math.floor(days / 30)}mo ago`;
return `${Math.floor(days / 365)}y ago`;
}