Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/web/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ResetPassword } from './pages/ResetPassword';
import { InteractiveGraphVisualization } from './components/InteractiveGraphVisualization';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { GraphProvider } from './contexts/GraphContext';
import { ViewModeProvider } from './contexts/ViewModeContext';
import { NotificationProvider } from './contexts/NotificationContext';

function AuthenticatedApp() {
Expand Down Expand Up @@ -96,6 +97,7 @@ function AuthenticatedApp() {
return (
<NotificationProvider>
<GraphProvider>
<ViewModeProvider>
<Layout>
<Routes>
<Route path="/" element={<Workspace />} />
Expand All @@ -109,6 +111,7 @@ function AuthenticatedApp() {
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Layout>
</ViewModeProvider>
</GraphProvider>
</NotificationProvider>
);
Expand Down
20 changes: 10 additions & 10 deletions packages/web/src/components/CardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ const CardView: React.FC<CardViewProps> = ({ filteredNodes, handleEditNode, edge
<div
key={node.id}
onClick={() => handleEditNode(node)}
className={`${getNodeTypeCardBackground(node.type)} rounded-xl p-6 shadow-lg hover:shadow-xl hover:shadow-white/10 transition-all duration-200 cursor-pointer border border-gray-600/50 hover:border-gray-500/70 hover:scale-[1.02] hover:-translate-y-1 hover:brightness-125 group backdrop-blur-sm`}
className={`${getNodeTypeCardBackground(node.type)} rounded-xl p-3 sm:p-4 shadow-lg hover:shadow-xl hover:shadow-white/10 transition-all duration-200 cursor-pointer border border-gray-600/50 hover:border-gray-500/70 hover:scale-[1.02] hover:-translate-y-1 hover:brightness-125 group backdrop-blur-sm`}
style={{
borderLeft: `4px solid ${getNodeTypeCardBorderColor(node.type)}`,
borderLeftWidth: '4px',
borderLeftStyle: 'solid',
borderLeftColor: getNodeTypeCardBorderColor(node.type)
}}
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-start justify-between mb-2">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold shadow-sm ${getNodeTypeColor(node.type)}`}>
{getTypeIconElement(node.type as WorkItemType, "w-3 h-3")}
<span className="ml-1">{formatLabel(node.type)}</span>
Expand Down Expand Up @@ -171,18 +171,18 @@ const CardView: React.FC<CardViewProps> = ({ filteredNodes, handleEditNode, edge
})()}
</div>

<h3 className="text-gray-900 dark:text-white font-semibold mb-3 text-lg leading-tight break-words">{node.title}</h3>
<h3 className="text-gray-900 dark:text-white font-semibold mb-1.5 text-base sm:text-lg leading-tight break-words">{node.title}</h3>

{node.description && (
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4 leading-relaxed break-words">{node.description}</p>
<p className="text-gray-600 dark:text-gray-400 text-sm mb-3 leading-snug break-words line-clamp-2">{node.description}</p>
)}

{/* Tags */}
<TagDisplay tags={node.tags} className="mb-4" />
<TagDisplay tags={node.tags} className="mb-3" />

{/* Priority and Due Date */}
<div className="mb-4 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
<div className="flex items-center justify-between mb-2">
<div className="mb-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg border border-gray-200 dark:border-gray-600">
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">Priority</span>
</div>
<div className="flex items-center justify-between">
Expand All @@ -193,7 +193,7 @@ const CardView: React.FC<CardViewProps> = ({ filteredNodes, handleEditNode, edge
className="text-sm font-semibold"
renderBar={(animatedValue, animatedColor) => (
<div className="flex items-center relative">
<div className="w-3 h-12 bg-gray-300 dark:bg-gray-600 rounded overflow-hidden flex flex-col justify-end relative">
<div className="w-3 h-9 bg-gray-300 dark:bg-gray-600 rounded overflow-hidden flex flex-col justify-end relative">
<div
className="w-full transition-colors duration-300"
style={{
Expand Down Expand Up @@ -262,7 +262,7 @@ const CardView: React.FC<CardViewProps> = ({ filteredNodes, handleEditNode, edge
</div>

{/* Footer */}
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-600">
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-600">
{/* Contributor */}
<div className="flex items-center">
{node.assignedTo ? (
Expand Down
8 changes: 6 additions & 2 deletions packages/web/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAuth } from '../contexts/AuthContext';
import { McpHealthIndicator } from './McpHealthIndicator';
import FloatingConsole from './FloatingConsole';
import { InsecureConnectionBanner } from './TlsStatusIndicator';
import { MobileBottomNav } from './MobileBottomNav';
import { APP_VERSION } from '../utils/version';

interface LayoutProps {
Expand Down Expand Up @@ -302,13 +303,16 @@ export function Layout({ children }: LayoutProps) {
)}

{/* Main content */}
<div className="flex-1 flex flex-col min-w-0 relative z-20">
<div className="flex-1 flex flex-col min-w-0 min-h-0 relative z-20">
<main className="flex-1 select-none min-h-0">
{children}
</main>
</div>
</div>


{/* Consistent mobile footer across every page */}
<MobileBottomNav />

{/* Global Floating Console */}
<FloatingConsole
isVisible={showFloatingConsole}
Expand Down
130 changes: 130 additions & 0 deletions packages/web/src/components/MobileBottomNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import {
CreditCard, Network, MoreHorizontal, LayoutDashboard, Table, Columns,
GanttChartSquare, CalendarDays, Activity, Brain, Settings as SettingsIcon,
Shield, Server, Bot, BarChart3,
} from 'lucide-react';
import { useViewMode, ViewMode } from '../contexts/ViewModeContext';
import { useAuth } from '../contexts/AuthContext';

/**
* The app-wide mobile footer. Present on every page so navigation is consistent:
* List and Graph jump to the workspace in that view; More opens a sheet with the
* remaining views and the other pages. Desktop/tablet keep their own chrome.
*/
export function MobileBottomNav() {
const { viewMode, setViewMode } = useViewMode();
const { currentUser } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [moreOpen, setMoreOpen] = useState(false);

const onWorkspace = location.pathname === '/' || location.pathname === '/workspace';
const goView = (m: ViewMode) => {
setViewMode(m);
if (!onWorkspace) navigate('/');
setMoreOpen(false);
};

const VIEWS: { mode: ViewMode; label: string; Icon: typeof Table }[] = [
{ mode: 'dashboard', label: 'Dashboard', Icon: LayoutDashboard },
{ mode: 'table', label: 'Table', Icon: Table },
{ mode: 'kanban', label: 'Board', Icon: Columns },
{ mode: 'gantt', label: 'Gantt', Icon: GanttChartSquare },
{ mode: 'calendar', label: 'Calendar', Icon: CalendarDays },
{ mode: 'activity', label: 'Activity', Icon: Activity },
];

const role = currentUser?.role;
const PAGES = [
{ href: '/ontology', label: 'Ontology', Icon: Brain, show: true },
{ href: '/agents', label: 'AI & Agents', Icon: Bot, show: true },
{ href: '/analytics', label: 'Analytics', Icon: BarChart3, show: true },
{ href: '/settings', label: 'Settings', Icon: SettingsIcon, show: true },
{ href: '/admin', label: 'Admin', Icon: Shield, show: role === 'ADMIN' },
{ href: '/backend', label: 'System', Icon: Server, show: role !== 'VIEWER' && role !== 'GUEST' },
].filter((p) => p.show);

const tabActive = (active: boolean) =>
`flex-1 flex flex-col items-center justify-center gap-0.5 py-2 min-h-[3.25rem] transition-colors ${
active ? 'text-green-400' : 'text-gray-400 hover:text-gray-200'
}`;

return (
<>
<nav
data-testid="mobile-bottom-nav"
className="md:hidden flex-shrink-0 relative z-30 flex items-stretch border-t border-gray-700/60 bg-gray-900/95 backdrop-blur-md pb-safe"
>
<button onClick={() => goView('cards')} className={tabActive(onWorkspace && viewMode === 'cards')}>
<CreditCard className="h-5 w-5" strokeWidth={1.75} />
<span className="text-[11px] font-medium">List</span>
</button>
<button onClick={() => goView('graph')} className={tabActive(onWorkspace && viewMode === 'graph')}>
<Network className="h-5 w-5" strokeWidth={1.75} />
<span className="text-[11px] font-medium">Graph</span>
</button>
<button
onClick={() => setMoreOpen(true)}
className={tabActive(moreOpen || (onWorkspace && !['cards', 'graph'].includes(viewMode)) || !onWorkspace)}
>
<MoreHorizontal className="h-5 w-5" strokeWidth={1.75} />
<span className="text-[11px] font-medium">More</span>
</button>
</nav>

{moreOpen && createPortal(
<div
className="md:hidden fixed inset-0 z-[100] flex flex-col justify-end"
onClick={() => setMoreOpen(false)}
>
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
<div
data-testid="mobile-more-sheet"
className="relative bg-gray-900 border-t border-gray-700 rounded-t-2xl p-4 pb-[calc(1rem+env(safe-area-inset-bottom))] shadow-2xl max-h-[80vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="mx-auto mb-3 h-1 w-10 rounded-full bg-gray-600" />
<h3 className="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2 px-1">Views</h3>
<div className="grid grid-cols-3 gap-2 mb-4">
{VIEWS.map(({ mode, label, Icon }) => (
<button
key={mode}
onClick={() => goView(mode)}
className={`flex flex-col items-center justify-center gap-1.5 py-4 rounded-xl border transition-colors ${
onWorkspace && viewMode === mode
? 'bg-green-600/20 border-green-500/40 text-green-300'
: 'bg-gray-800/60 border-gray-700/60 text-gray-300 active:bg-gray-700'
}`}
>
<Icon className="h-6 w-6" strokeWidth={1.5} />
<span className="text-xs font-medium">{label}</span>
</button>
))}
</div>
<h3 className="text-xs font-semibold uppercase tracking-wide text-gray-500 mb-2 px-1">Go to</h3>
<div className="grid grid-cols-3 gap-2">
{PAGES.map(({ href, label, Icon }) => (
<button
key={href}
onClick={() => { navigate(href); setMoreOpen(false); }}
className={`flex flex-col items-center justify-center gap-1.5 py-4 rounded-xl border transition-colors ${
location.pathname === href
? 'bg-green-600/20 border-green-500/40 text-green-300'
: 'bg-gray-800/60 border-gray-700/60 text-gray-300 active:bg-gray-700'
}`}
>
<Icon className="h-6 w-6" strokeWidth={1.5} />
<span className="text-xs font-medium text-center leading-tight">{label}</span>
</button>
))}
</div>
</div>
</div>,
document.body
)}
</>
);
}
12 changes: 6 additions & 6 deletions packages/web/src/components/ViewManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -395,14 +395,14 @@ const ViewManager: React.FC<ViewManagerProps> = ({ viewMode }) => {
)}
</div>

{/* Filter Dropdowns */}
<div className="flex gap-3">
{/* Filter Dropdowns — 2-up grid on phones so all filters fit; inline on desktop */}
<div className="grid grid-cols-2 gap-2 sm:flex sm:gap-3">
{/* Type Filter */}
<div className="relative" ref={typeDropdownRef}>
<button
type="button"
onClick={() => setIsTypeDropdownOpen(!isTypeDropdownOpen)}
className="flex items-center justify-between px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent hover:border-gray-500 transition-all duration-200 min-w-[160px]"
className="flex items-center justify-between px-2.5 sm:px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-xs sm:text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent hover:border-gray-500 transition-all duration-200 w-full sm:w-auto sm:min-w-[150px] min-w-0"
>
<div className="flex items-center space-x-2">
{(() => {
Expand Down Expand Up @@ -476,7 +476,7 @@ const ViewManager: React.FC<ViewManagerProps> = ({ viewMode }) => {
<button
type="button"
onClick={() => setIsStatusDropdownOpen(!isStatusDropdownOpen)}
className="flex items-center justify-between px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent hover:border-gray-500 transition-all duration-200 min-w-[150px]"
className="flex items-center justify-between px-2.5 sm:px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-xs sm:text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent hover:border-gray-500 transition-all duration-200 w-full sm:w-auto sm:min-w-[150px] min-w-0"
>
<div className="flex items-center space-x-2">
{(() => {
Expand Down Expand Up @@ -548,7 +548,7 @@ const ViewManager: React.FC<ViewManagerProps> = ({ viewMode }) => {
<button
type="button"
onClick={() => setIsPriorityDropdownOpen(!isPriorityDropdownOpen)}
className="flex items-center justify-between px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent hover:border-gray-500 transition-all duration-200 min-w-[140px]"
className="flex items-center justify-between px-2.5 sm:px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-xs sm:text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent hover:border-gray-500 transition-all duration-200 w-full sm:w-auto sm:min-w-[150px] min-w-0"
>
<div className="flex items-center space-x-2">
{(() => {
Expand Down Expand Up @@ -620,7 +620,7 @@ const ViewManager: React.FC<ViewManagerProps> = ({ viewMode }) => {
<button
type="button"
onClick={() => setIsContributorDropdownOpen(!isContributorDropdownOpen)}
className="flex items-center justify-between px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent hover:border-gray-500 transition-all duration-200 min-w-[180px]"
className="flex items-center justify-between px-2.5 sm:px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white text-xs sm:text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent hover:border-gray-500 transition-all duration-200 w-full sm:w-auto sm:min-w-[150px] min-w-0"
>
<div className="flex items-center space-x-2">
{(() => {
Expand Down
36 changes: 36 additions & 0 deletions packages/web/src/contexts/ViewModeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';

export type ViewMode = 'graph' | 'dashboard' | 'table' | 'cards' | 'kanban' | 'gantt' | 'calendar' | 'activity';

interface ViewModeContextType {
viewMode: ViewMode;
setViewMode: (m: ViewMode) => void;
}

const ViewModeContext = createContext<ViewModeContextType | undefined>(undefined);

export function ViewModeProvider({ children }: { children: ReactNode }) {
const [viewMode, setViewMode] = useState<ViewMode>(() => {
if (typeof window === 'undefined') return 'graph';
const saved = window.localStorage.getItem('graphdone:viewMode');
if (saved) return saved as ViewMode;
// Phones land on the readable card list, not the graph.
return window.matchMedia('(max-width: 767px)').matches ? 'cards' : 'graph';
});

useEffect(() => {
try { window.localStorage.setItem('graphdone:viewMode', viewMode); } catch { /* private mode */ }
}, [viewMode]);

return (
<ViewModeContext.Provider value={{ viewMode, setViewMode }}>
{children}
</ViewModeContext.Provider>
);
}

export function useViewMode() {
const ctx = useContext(ViewModeContext);
if (!ctx) throw new Error('useViewMode must be used within a ViewModeProvider');
return ctx;
}
2 changes: 1 addition & 1 deletion packages/web/src/pages/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function Admin() {
return (
<div className="h-screen flex flex-col">
{/* Header */}
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-6 py-4">
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-3 py-3 sm:px-6 sm:py-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center space-x-3">
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/pages/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function Analytics() {
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-6 py-4">
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-3 py-3 sm:px-6 sm:py-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center space-x-3">
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/pages/Backend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ export function Backend() {
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-6 py-4">
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-3 py-3 sm:px-6 sm:py-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center space-x-3">
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/pages/GraphManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ export function GraphManagement() {
return (
<div className="h-screen flex flex-col">
{/* Header */}
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-6 py-4">
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-3 py-3 sm:px-6 sm:py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-100">Graph Management</h1>
Expand Down Expand Up @@ -325,7 +325,7 @@ export function GraphManagement() {
</div>

{/* Filters and controls */}
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-6 py-4">
<div className="bg-gray-900/30 backdrop-blur-md border-b border-gray-700/30 px-3 py-3 sm:px-6 sm:py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex-1 relative">
Expand Down
Loading
Loading