From 086da56c8a089772ff3ee46ac1082cf6a83a03f1 Mon Sep 17 00:00:00 2001 From: Matthew Valancy Date: Tue, 16 Jun 2026 16:24:05 -0700 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20standardize=20the=20mobile=20ex?= =?UTF-8?q?perience=20=E2=80=94=20consistent=20footer,=20dark-mode=20fix,?= =?UTF-8?q?=20slimmer=20chrome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the whole app feel like one mobile product, not the desktop UI shrunk. - Consistent footer on EVERY page: the bottom nav (List · Graph · More) moves from the Workspace into the Layout shell via a shared MobileBottomNav + ViewModeContext (viewMode lifted out of Workspace). List/Graph jump to the workspace in that view from any page; More opens a sheet with the other views AND the other pages (Ontology/Settings/Admin/System/…). The desktop tab strip stays desktop-only. - Dark-mode fix (the "black text on dark background" report): tailwind darkMode was unset (→ 'media'), so on a light-OS device the app's `dark:` text variants never applied and `text-gray-900` rendered black on the always-dark surfaces. Force it: `darkMode: 'class'` + ``. Fixes readability across every component that used `dark:` variants (CardView, modals, Kanban, the mobile Table cards, …) at once. - Standardized, slimmer page headers on mobile (px-3 py-3 sm:px-6 sm:py-4) across Ontology/Settings/Admin/Backend/Analytics/Agents + sub-pages. - Skinnier list items: CardView is more compact (tighter padding/margins, smaller title, 2-line clamped description, shorter priority bar). - Refined filters: the 4 filter dropdowns become a 2-up grid on phones (were clipped in a single row) and are compact (text-xs, responsive width). - Fixed the flex chain (min-h-0) so the scroll area is bounded and the footer is never hidden behind overflowing content. mobile-experience + mobile-views 10/10 (navigate via the shared footer); smoke gate 5/5; web typecheck + prod build clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/web/index.html | 2 +- packages/web/src/App.tsx | 3 + packages/web/src/components/CardView.tsx | 20 +-- packages/web/src/components/Layout.tsx | 8 +- .../web/src/components/MobileBottomNav.tsx | 130 ++++++++++++++++++ packages/web/src/components/ViewManager.tsx | 12 +- packages/web/src/contexts/ViewModeContext.tsx | 36 +++++ packages/web/src/pages/Admin.tsx | 2 +- packages/web/src/pages/Analytics.tsx | 2 +- packages/web/src/pages/Backend.tsx | 2 +- packages/web/src/pages/GraphManagement.tsx | 4 +- packages/web/src/pages/GraphView.tsx | 2 +- packages/web/src/pages/NodeList.tsx | 4 +- packages/web/src/pages/Ontology.tsx | 2 +- packages/web/src/pages/Settings.tsx | 2 +- packages/web/src/pages/Workspace.tsx | 91 +----------- packages/web/tailwind.config.js | 4 + 17 files changed, 210 insertions(+), 116 deletions(-) create mode 100644 packages/web/src/components/MobileBottomNav.tsx create mode 100644 packages/web/src/contexts/ViewModeContext.tsx diff --git a/packages/web/index.html b/packages/web/index.html index 54bca434..c35af21c 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -1,5 +1,5 @@ - + diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx index 1a7838dd..9c8e0c54 100644 --- a/packages/web/src/App.tsx +++ b/packages/web/src/App.tsx @@ -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() { @@ -96,6 +97,7 @@ function AuthenticatedApp() { return ( + } /> @@ -109,6 +111,7 @@ function AuthenticatedApp() { } /> + ); diff --git a/packages/web/src/components/CardView.tsx b/packages/web/src/components/CardView.tsx index 291f2260..b02c298c 100644 --- a/packages/web/src/components/CardView.tsx +++ b/packages/web/src/components/CardView.tsx @@ -120,7 +120,7 @@ const CardView: React.FC = ({ filteredNodes, handleEditNode, edge
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', @@ -128,7 +128,7 @@ const CardView: React.FC = ({ filteredNodes, handleEditNode, edge borderLeftColor: getNodeTypeCardBorderColor(node.type) }} > -
+
{getTypeIconElement(node.type as WorkItemType, "w-3 h-3")} {formatLabel(node.type)} @@ -171,18 +171,18 @@ const CardView: React.FC = ({ filteredNodes, handleEditNode, edge })()}
-

{node.title}

- +

{node.title}

+ {node.description && ( -

{node.description}

+

{node.description}

)} {/* Tags */} - + {/* Priority and Due Date */} -
-
+
+
Priority
@@ -193,7 +193,7 @@ const CardView: React.FC = ({ filteredNodes, handleEditNode, edge className="text-sm font-semibold" renderBar={(animatedValue, animatedColor) => (
-
+
= ({ filteredNodes, handleEditNode, edge
{/* Footer */} -
+
{/* Contributor */}
{node.assignedTo ? ( diff --git a/packages/web/src/components/Layout.tsx b/packages/web/src/components/Layout.tsx index 63f193a0..2a580574 100644 --- a/packages/web/src/components/Layout.tsx +++ b/packages/web/src/components/Layout.tsx @@ -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 { @@ -302,13 +303,16 @@ export function Layout({ children }: LayoutProps) { )} {/* Main content */} -
+
{children}
- + + {/* Consistent mobile footer across every page */} + + {/* Global Floating Console */} { + 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 ( + <> + + + {moreOpen && createPortal( +
setMoreOpen(false)} + > +
+
e.stopPropagation()} + > +
+

Views

+
+ {VIEWS.map(({ mode, label, Icon }) => ( + + ))} +
+

Go to

+
+ {PAGES.map(({ href, label, Icon }) => ( + + ))} +
+
+
, + document.body + )} + + ); +} diff --git a/packages/web/src/components/ViewManager.tsx b/packages/web/src/components/ViewManager.tsx index 31d85a36..b0d6208b 100644 --- a/packages/web/src/components/ViewManager.tsx +++ b/packages/web/src/components/ViewManager.tsx @@ -395,14 +395,14 @@ const ViewManager: React.FC = ({ viewMode }) => { )}
- {/* Filter Dropdowns */} -
+ {/* Filter Dropdowns — 2-up grid on phones so all filters fit; inline on desktop */} +
{/* Type Filter */}
- ))} - - - )} - {/* Create FAB (phones) — primary action; hidden for read-only guests and in graph view (where the on-canvas "+" grow flow creates nodes). */} {currentGraph && currentUser?.role !== 'GUEST' && viewMode !== 'graph' && ( @@ -504,48 +463,6 @@ export function Workspace() { )} - {/* "More" views sheet (phones) */} - {showMoreSheet && createPortal( -
setShowMoreSheet(false)} - > -
-
e.stopPropagation()} - > -
-

More views

-
- {([ - { 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 }, - ] as const).map(({ mode, label, Icon }) => ( - - ))} -
-
-
, - document.body - )} - {/* Mini-Map Navigation - Bottom Right Corner (hidden on mobile: it covers too much of a phone screen, and panning the graph by touch is the primary nav there) */} {viewMode === 'graph' && currentGraph && showMiniMap && !isMobile && createPortal( diff --git a/packages/web/tailwind.config.js b/packages/web/tailwind.config.js index 1d355329..4c639461 100644 --- a/packages/web/tailwind.config.js +++ b/packages/web/tailwind.config.js @@ -1,5 +1,9 @@ /** @type {import('tailwindcss').Config} */ export default { + // The app is unconditionally dark-themed; force the `dark` class so every + // `dark:` text/color variant always applies (otherwise light-OS users get + // black text on the dark surfaces). Applied via . + darkMode: 'class', content: [ "./index.html", "./src/**/*.{js,ts,jsx,tsx}",