merge: ui polish pass sync
This commit is contained in:
+16
-3
@@ -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 {
|
||||||
|
|||||||
@@ -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,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user