From 3b8fa75b70317b0d4390abda360ccfc2da17fa27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Mon, 23 Mar 2026 08:53:19 +1100 Subject: [PATCH] sync: ui polish pass from private repo - increase base typography and relative date formatting - add card polish, empty states, and skeleton loaders - center the nodes pane content like the focus panel --- app/globals.css | 19 +- src/components/layout/LeftToolbar.tsx | 8 +- src/components/views/DatabaseTableView.tsx | 28 +-- src/components/views/ListView.tsx | 78 +++--- src/components/views/ViewsOverlay.tsx | 267 ++++++++++++++------- src/utils/formatDate.ts | 24 ++ 6 files changed, 269 insertions(+), 155 deletions(-) create mode 100644 src/utils/formatDate.ts diff --git a/app/globals.css b/app/globals.css index 1f50907..e902559 100644 --- a/app/globals.css +++ b/app/globals.css @@ -139,9 +139,9 @@ html, body { color: var(--rah-text-base); /* Geist Sans for optimal reading/writing experience */ font-family: 'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - font-size: 14px; - line-height: 1.5; - letter-spacing: -0.011em; /* Optimized letter spacing for Geist */ + font-size: 15px; + line-height: 1.5; + letter-spacing: -0.012em; /* Optimized letter spacing for Geist */ font-weight: 400; -webkit-font-smoothing: antialiased; -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 */ @keyframes modalFadeIn { from { diff --git a/src/components/layout/LeftToolbar.tsx b/src/components/layout/LeftToolbar.tsx index 8d749ee..0f3dfbd 100644 --- a/src/components/layout/LeftToolbar.tsx +++ b/src/components/layout/LeftToolbar.tsx @@ -52,6 +52,7 @@ function NavButton({ onClick, trailing, activeTone = 'neutral', + title, }: { icon: typeof Search; label: string; @@ -60,6 +61,7 @@ function NavButton({ onClick: () => void; trailing?: ReactNode; activeTone?: 'neutral' | 'green'; + title?: string; }) { const [hovered, setHovered] = useState(false); const activeColor = activeTone === 'green' ? 'var(--rah-accent-green)' : 'var(--rah-text-active)'; @@ -69,7 +71,7 @@ function NavButton({ onClick={onClick} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} - title={expanded ? undefined : label} + title={title ?? (expanded ? undefined : label)} style={{ width: '100%', height: '36px', @@ -136,8 +138,8 @@ export default function LeftToolbar({ onClick={onToggleExpanded} />
- - + +
diff --git a/src/components/views/DatabaseTableView.tsx b/src/components/views/DatabaseTableView.tsx index ecfcc17..d46de78 100644 --- a/src/components/views/DatabaseTableView.tsx +++ b/src/components/views/DatabaseTableView.tsx @@ -4,6 +4,7 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { Filter, ChevronDown, ChevronLeft, ChevronRight, X, ArrowUpDown, Search, ExternalLink } from 'lucide-react'; import type { Node } from '@/types/database'; +import { formatRelativeDate } from '@/utils/formatDate'; type SortOrder = 'updated' | 'edges' | 'created' | 'event_date'; @@ -28,27 +29,6 @@ interface DatabaseTableViewProps { 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 { if (!dateStr) return '\u2014'; try { @@ -552,14 +532,14 @@ export default function DatabaseTableView({ onNodeClick, refreshToken = 0, toolb {/* Updated */} - {formatRelativeTime(node.updated_at)} + {formatRelativeDate(node.updated_at)} {/* Created */} - {formatRelativeTime(node.created_at)} + {formatRelativeDate(node.created_at)} @@ -594,7 +574,7 @@ export default function DatabaseTableView({ onNodeClick, refreshToken = 0, toolb {/* Embedding Updated */} - {node.embedding_updated_at ? formatRelativeTime(node.embedding_updated_at) : '\u2014'} + {node.embedding_updated_at ? formatRelativeDate(node.embedding_updated_at) : '\u2014'} diff --git a/src/components/views/ListView.tsx b/src/components/views/ListView.tsx index 5153aa5..dd8fc4c 100644 --- a/src/components/views/ListView.tsx +++ b/src/components/views/ListView.tsx @@ -1,8 +1,10 @@ "use client"; +import { Inbox } from 'lucide-react'; import { Node } from '@/types/database'; import { getNodeIcon } from '@/utils/nodeIcons'; import { useDimensionIcons } from '@/context/DimensionIconsContext'; +import { formatRelativeDate } from '@/utils/formatDate'; interface ListViewProps { nodes: Node[]; @@ -11,15 +13,6 @@ interface ListViewProps { export default function ListView({ nodes, onNodeClick }: ListViewProps) { 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) => { if (!content) return ''; @@ -31,13 +24,20 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) { return (
- No nodes match the current filters + + Nothing here yet + + Try adjusting your filters, or add a node with ⌘N +
); } @@ -59,20 +59,25 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) { gap: '12px', padding: '12px', marginBottom: '4px', - background: '#0a0a0a', - border: '1px solid #1a1a1a', + background: 'var(--rah-bg-base)', + border: '1px solid var(--rah-border)', + borderLeft: '2px solid var(--rah-border-stronger)', borderRadius: '6px', cursor: 'pointer', textAlign: 'left', - transition: 'all 0.2s' + transition: 'all 0.15s ease' }} onMouseEnter={(e) => { - e.currentTarget.style.background = '#111'; - e.currentTarget.style.borderColor = '#333'; + 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) => { - e.currentTarget.style.background = '#0a0a0a'; - e.currentTarget.style.borderColor = '#1a1a1a'; + 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'; }} > {/* Icon */} @@ -82,7 +87,7 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) { display: 'flex', alignItems: 'center', justifyContent: 'center', - background: '#1a1a1a', + background: 'var(--rah-bg-active)', borderRadius: '6px', flexShrink: 0 }}> @@ -93,9 +98,9 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
{/* Title */}
{dim} @@ -151,8 +157,8 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) { {node.dimensions.length > 3 && ( +{node.dimensions.length - 3} @@ -162,17 +168,17 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) { {/* Date */} - {formatDate(node.updated_at || node.created_at)} + {formatRelativeDate(node.updated_at || node.created_at)} {/* Edge count */} {node.edge_count !== undefined && node.edge_count > 0 && ( {node.edge_count} connections diff --git a/src/components/views/ViewsOverlay.tsx b/src/components/views/ViewsOverlay.tsx index cd284fc..217f881 100644 --- a/src/components/views/ViewsOverlay.tsx +++ b/src/components/views/ViewsOverlay.tsx @@ -2,12 +2,13 @@ import { useEffect, useMemo, useState, useRef, useCallback } from 'react'; 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 { getNodeIcon } from '@/utils/nodeIcons'; import { useDimensionIcons } from '@/context/DimensionIconsContext'; import { usePersistentState } from '@/hooks/usePersistentState'; import type { PendingNode } from '../layout/ThreePanelLayout'; +import { formatRelativeDate } from '@/utils/formatDate'; type SortOrder = 'updated' | 'edges' | 'created' | 'custom'; @@ -18,6 +19,8 @@ const SORT_LABELS: Record = { custom: 'Custom', }; +const DOCUMENT_MAX_WIDTH = '980px'; + interface ColumnFilter { id: string; dimension: string; @@ -127,6 +130,39 @@ function PendingNodeCard({ pending, onDismiss }: { pending: PendingNode; onDismi ); } +function SkeletonCard() { + return ( +
+
+
+
+
+
+
+
+ ); +} + interface ViewsOverlayProps { onNodeClick: (nodeId: number) => void; onNodeOpenInOtherPane?: (nodeId: number) => void; @@ -418,19 +454,26 @@ export default function ViewsOverlay({ }} style={{ padding: '10px 12px', - background: 'transparent', + background: 'var(--rah-bg-base)', cursor: 'pointer', transition: 'all 0.15s ease', - borderBottom: '1px solid var(--rah-bg-panel)', - borderLeft: '3px solid transparent', + border: '1px solid var(--rah-border)', + borderLeft: '2px solid var(--rah-border-stronger)', + borderRadius: '10px', 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) => { - 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) => { - 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) => { e.preventDefault(); @@ -460,13 +503,13 @@ export default function ViewsOverlay({ justifyContent: 'center', width: '16px', cursor: 'grab', - color: '#444', + color: 'var(--rah-text-muted)', flexShrink: 0, alignSelf: 'center', transition: 'color 0.15s ease', }} - onMouseEnter={(e) => { e.currentTarget.style.color = '#888'; }} - onMouseLeave={(e) => { e.currentTarget.style.color = '#444'; }} + onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--rah-text-soft)'; }} + onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--rah-text-muted)'; }} onClick={(e) => e.stopPropagation()} > @@ -476,8 +519,8 @@ export default function ViewsOverlay({ width: '32px', height: '32px', borderRadius: '8px', - background: 'var(var(--rah-bg-panel))', - border: '1px solid var(--rah-bg-active)', + background: 'var(--rah-bg-panel)', + border: '1px solid var(--rah-border)', display: 'flex', alignItems: 'center', justifyContent: 'center', @@ -493,9 +536,9 @@ export default function ViewsOverlay({ marginBottom: descPreview ? '2px' : '4px' }}> {node.edge_count != null && node.edge_count > 0 && ( - 🔗 {node.edge_count} )} {descPreview && (
{d} ))} {node.dimensions.length > 3 && ( - + +{node.dimensions.length - 3} )} ) : null}
+
+ {formatRelativeDate(node.updated_at || node.created_at)} + {node.edge_count != null && node.edge_count > 0 ? {node.edge_count} connections : null} +
@@ -578,14 +639,15 @@ export default function ViewsOverlay({ }; const toolbar = ( -
-
+
+
+
{selectedFilters.map(filter => (
+ Sidebar filter )} @@ -635,15 +697,16 @@ export default function ViewsOverlay({ } }} disabled={!!externalDimensionFilter} + title="Filter (⌘F)" style={{ display: 'flex', alignItems: 'center', gap: '4px', padding: '4px 8px', background: 'transparent', - border: '1px solid var(--rah-border-strong)', + border: '1px solid var(--rah-border)', borderRadius: '5px', - color: externalDimensionFilter ? '#4b4b4b' : '#888', + color: externalDimensionFilter ? 'var(--rah-text-muted)' : 'var(--rah-text-soft)', fontSize: '11px', cursor: externalDimensionFilter ? 'not-allowed' : 'pointer', transition: 'all 0.15s ease' @@ -651,12 +714,12 @@ export default function ViewsOverlay({ onMouseEnter={(e) => { if (!externalDimensionFilter) { 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) => { e.currentTarget.style.background = 'transparent'; - e.currentTarget.style.borderColor = '#222'; + e.currentTarget.style.borderColor = 'var(--rah-border)'; }} > @@ -670,8 +733,8 @@ export default function ViewsOverlay({ top: '100%', left: 0, marginTop: '4px', - background: 'var(var(--rah-bg-panel))', - border: '1px solid var(--rah-border-strong)', + background: 'var(--rah-bg-panel)', + border: '1px solid var(--rah-border)', borderRadius: '10px', padding: '6px', minWidth: '220px', @@ -689,23 +752,23 @@ export default function ViewsOverlay({ style={{ width: '100%', padding: '7px 10px', - background: 'var(var(--rah-bg-surface))', + background: 'var(--rah-bg-base)', border: '1px solid transparent', borderRadius: '6px', - color: 'var(var(--rah-text-active))', + color: 'var(--rah-text-active)', fontSize: '12px', marginBottom: '4px', 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'; }} /> {dimensionsLoading ? ( -
+
Loading dimensions...
) : filterPickerDimensions.length === 0 ? ( -
+
{filterSearchQuery ? 'No matching dimensions' : 'No dimensions available'}
) : ( @@ -722,7 +785,7 @@ export default function ViewsOverlay({ background: 'transparent', border: 'none', borderRadius: '5px', - color: 'var(var(--rah-text-secondary))', + color: 'var(--rah-text-secondary)', fontSize: '12px', cursor: 'pointer', textAlign: 'left' @@ -732,9 +795,9 @@ export default function ViewsOverlay({ > {d.dimension} @@ -754,12 +817,12 @@ export default function ViewsOverlay({ padding: '4px 8px', background: 'transparent', border: 'none', - color: 'var(var(--rah-text-muted))', + color: 'var(--rah-text-muted)', fontSize: '11px', cursor: 'pointer' }} 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 @@ -768,29 +831,29 @@ export default function ViewsOverlay({ {/* Sort dropdown */}
- ))} @@ -844,6 +907,7 @@ export default function ViewsOverlay({ )}
+
); return ( @@ -868,30 +932,55 @@ export default function ViewsOverlay({ {/* Content area — list view */} {filteredNodesLoading ? ( -
- Loading... +
+
+ {Array.from({ length: 6 }, (_, index) => ( + + ))} +
) : filteredNodes.length === 0 ? ( -
- {selectedFilters.length > 0 ? 'No nodes match the selected filters.' : 'No nodes yet. Add some content to get started.'} +
+
+ + + {selectedFilters.length > 0 ? 'Nothing matches these filters' : 'Nothing here yet'} + + + {selectedFilters.length > 0 ? 'Try adjusting your filters, or add a node with ⌘N' : 'Add a node with ⌘N to get started'} + +
) : (
- {pendingNodes && pendingNodes.length > 0 && pendingNodes.map(p => ( - onDismissPending(p.id) : undefined} - /> - ))} - {filteredNodes.map((node, index) => renderNodeCard(node, index))} +
+ {pendingNodes && pendingNodes.length > 0 && pendingNodes.map(p => ( + onDismissPending(p.id) : undefined} + /> + ))} + {filteredNodes.map((node, index) => renderNodeCard(node, index))} +
)}
diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts new file mode 100644 index 0000000..f06221d --- /dev/null +++ b/src/utils/formatDate.ts @@ -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`; +}