diff --git a/.claude/memory.md b/.claude/memory.md index 2fffb186c..da6e9f790 100644 --- a/.claude/memory.md +++ b/.claude/memory.md @@ -62,6 +62,15 @@ Quick reference for anyone starting with Claude on this project. Updated by the - **Patch is fragile** — Resets on `cargo clean`, crate version bump, or registry re-download. Deleting build cache alone (`target/debug/build/whisper-rs-sys-*`) is NOT enough — cmake regenerates with the same bad flags. - **Correct fix** — Needs an upstream patch in `whisper-rs-sys` or a Cargo feature to opt out of `GGML_NATIVE` on Apple Silicon cross-builds. +## UI Redesign (Light Theme — April 2026) + +- **Full dark-to-light redesign shipped** — All pages, components, and settings panels converted from dark glass-morphism to clean light theme based on Figma designs by Mithil (`OpenHuman-Prod` file, node `2094-250136` for tokens). +- **Design tokens saved** in `my_docs/figma-design-tokens.md` — neutral grayscale, primary blue `#2F6EF4`, success `#34C759`, alert `#E8A728`, error `#EF4444`, SF Pro typography scale. +- **Navigation changed**: Left `MiniSidebar` → bottom `BottomTabBar` (Home, Chat, Skills, Intelligence, Automation, Notification). Settings accessible via gear icon on Home page header. +- **MiniSidebar.tsx retained** (not deleted) as backup. `BottomTabBar.tsx` is the active nav component. +- **Agent message bubbles** need `bg-stone-200/80` (not `bg-stone-100`) on `#F5F5F5` background — `bg-stone-100` is nearly invisible. +- **~55 files touched** — purely CSS class changes, zero logic/handler/state changes. + ## Environment - **Core sidecar port** — `7788` (default). Check with `lsof -i :7788`. diff --git a/app/src/App.tsx b/app/src/App.tsx index 2eb7bdf21..2af959f70 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -4,11 +4,11 @@ import { HashRouter as Router } from 'react-router-dom'; import { PersistGate } from 'redux-persist/integration/react'; import AppRoutes from './AppRoutes'; +import BottomTabBar from './components/BottomTabBar'; import ServiceBlockingGate from './components/daemon/ServiceBlockingGate'; import DictationOverlay from './components/dictation/DictationOverlay'; import ErrorFallbackScreen from './components/ErrorFallbackScreen'; import LocalAIDownloadSnackbar from './components/LocalAIDownloadSnackbar'; -import MiniSidebar from './components/MiniSidebar'; import OnboardingOverlay from './components/OnboardingOverlay'; import SocketProvider from './providers/SocketProvider'; import UserProvider from './providers/UserProvider'; @@ -41,20 +41,11 @@ function App() { -
-
- -
-
- -
-
-
- OpenHuman is in early beta -
-
-
+
+
+
+
diff --git a/app/src/components/BottomTabBar.tsx b/app/src/components/BottomTabBar.tsx new file mode 100644 index 000000000..894777971 --- /dev/null +++ b/app/src/components/BottomTabBar.tsx @@ -0,0 +1,170 @@ +import { useLocation, useNavigate } from 'react-router-dom'; + +import { useAppSelector } from '../store/hooks'; + +const tabs = [ + { + id: 'home', + label: 'Home', + path: '/home', + icon: ( + + + + ), + }, + { + id: 'chat', + label: 'Chat', + path: '/conversations', + icon: ( + + + + ), + }, + { + id: 'skills', + label: 'Skills', + path: '/skills', + icon: ( + + + + ), + }, + { + id: 'intelligence', + label: 'Intelligence', + path: '/intelligence', + icon: ( + + + + ), + }, + { + id: 'automation', + label: 'Automation', + path: '/settings/cron-jobs', + icon: ( + + + + + ), + }, + { + id: 'notification', + label: 'Notification', + path: '/settings/messaging', + icon: ( + + + + ), + }, +]; + +const BottomTabBar = () => { + const location = useLocation(); + const navigate = useNavigate(); + const token = useAppSelector(state => state.auth.token); + + const conversationsUnreadCount = useAppSelector(state => { + const { threads, lastViewedAt } = state.thread; + if (threads.length === 0) return 0; + return threads.filter(t => { + const viewed = lastViewedAt[t.id]; + const lastMsg = new Date(t.lastMessageAt || t.createdAt).getTime(); + return viewed == null || lastMsg > viewed; + }).length; + }); + + const hiddenPaths = ['/', '/login']; + if ( + !token || + hiddenPaths.some(path => location.pathname === path || location.pathname.startsWith(`${path}/`)) + ) { + return null; + } + + const isActive = (path: string) => { + if (path === '/conversations') return location.pathname.startsWith('/conversations'); + if (path === '/settings/cron-jobs') return location.pathname.startsWith('/settings/cron-jobs'); + if (path === '/settings/messaging') return location.pathname.startsWith('/settings/messaging'); + return location.pathname === path; + }; + + return ( +
+
+ {tabs.map(tab => { + const active = isActive(tab.path); + const showBadge = tab.id === 'chat' && conversationsUnreadCount > 0; + return ( + + ); + })} +
+
+ ); +}; + +export default BottomTabBar; diff --git a/app/src/components/ConnectionIndicator.tsx b/app/src/components/ConnectionIndicator.tsx index 64ea6bb07..7db936be2 100644 --- a/app/src/components/ConnectionIndicator.tsx +++ b/app/src/components/ConnectionIndicator.tsx @@ -3,13 +3,11 @@ import { selectSocketStatus } from '../store/socketSelectors'; interface ConnectionIndicatorProps { status?: 'connected' | 'disconnected' | 'connecting'; - description?: string; className?: string; } const ConnectionIndicator = ({ status: overrideStatus, - description = 'Your device is now connected to the OpenHuman AI. Keep the app running to keep the connection alive. You can message your assistant with the button below.', className = '', }: ConnectionIndicatorProps) => { // Use socket store status, but allow override via props @@ -28,15 +26,13 @@ const ConnectionIndicator = ({ const config = statusConfig[status]; return ( -
-
+
+
- {config.text} + className={`w-2 h-2 ${config.color} rounded-full ${status === 'connected' ? 'animate-pulse' : ''}`} + /> + {config.text}
- {description && ( -

{description}

- )}
); }; diff --git a/app/src/components/OnboardingOverlay.tsx b/app/src/components/OnboardingOverlay.tsx index ff928b8ad..0187cf8f7 100644 --- a/app/src/components/OnboardingOverlay.tsx +++ b/app/src/components/OnboardingOverlay.tsx @@ -85,7 +85,7 @@ const OnboardingOverlay = () => { if (!shouldShow) return null; return createPortal( -
+
, document.body diff --git a/app/src/components/ProgressIndicator.tsx b/app/src/components/ProgressIndicator.tsx index b6dc6dd4a..d730fd61a 100644 --- a/app/src/components/ProgressIndicator.tsx +++ b/app/src/components/ProgressIndicator.tsx @@ -10,7 +10,7 @@ const ProgressIndicator = ({ currentStep, totalSteps }: ProgressIndicatorProps)
))} diff --git a/app/src/components/RouteLoadingScreen.tsx b/app/src/components/RouteLoadingScreen.tsx index e22d9c79a..f06cdb3b2 100644 --- a/app/src/components/RouteLoadingScreen.tsx +++ b/app/src/components/RouteLoadingScreen.tsx @@ -5,7 +5,7 @@ interface RouteLoadingScreenProps { const RouteLoadingScreen = ({ label = 'Initializing OpenHuman...' }: RouteLoadingScreenProps) => { return (
-
+
{label}
diff --git a/app/src/components/SkillsGrid.tsx b/app/src/components/SkillsGrid.tsx index 806351493..6decd7db6 100644 --- a/app/src/components/SkillsGrid.tsx +++ b/app/src/components/SkillsGrid.tsx @@ -36,13 +36,13 @@ function SkillRow({ skillId, name, icon, skillType, syncSummaryText, onConnect } return ( + className="skill-row group hover:bg-stone-50 transition-all duration-300 cursor-pointer border-b border-stone-200 last:border-0">
-
+
{icon || }
- {name} + {name}
@@ -67,7 +67,7 @@ function SkillRow({ skillId, name, icon, skillType, syncSummaryText, onConnect } @@ -144,7 +144,7 @@ export default function SkillsGrid() { <>
-

Available Skills

+

Available Skills

- +
Skill @@ -193,7 +193,7 @@ export default function SkillsGrid() {
-
+
View all skills
diff --git a/app/src/components/__tests__/ConnectionIndicator.test.tsx b/app/src/components/__tests__/ConnectionIndicator.test.tsx index cab8dd8c8..d436d5183 100644 --- a/app/src/components/__tests__/ConnectionIndicator.test.tsx +++ b/app/src/components/__tests__/ConnectionIndicator.test.tsx @@ -20,17 +20,10 @@ describe('ConnectionIndicator', () => { expect(screen.getByText('Connecting')).toBeInTheDocument(); }); - it('renders description text when provided', () => { - renderWithProviders( - - ); - expect(screen.getByText('Custom description')).toBeInTheDocument(); - }); - - it('does not render description when empty string', () => { - renderWithProviders(); - // Default description should not appear - expect(screen.queryByText(/Keep the app running/)).not.toBeInTheDocument(); + it('renders as a pill badge', () => { + renderWithProviders(); + // The indicator renders as an inline pill — status text is visible + expect(screen.getByText(/Connected to OpenHuman AI/)).toBeInTheDocument(); }); it('falls back to store socket status when no override', () => { diff --git a/app/src/components/daemon/ServiceBlockingGate.tsx b/app/src/components/daemon/ServiceBlockingGate.tsx index ff58ea575..f160b4ebc 100644 --- a/app/src/components/daemon/ServiceBlockingGate.tsx +++ b/app/src/components/daemon/ServiceBlockingGate.tsx @@ -173,21 +173,21 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => { const canUninstall = !isOperating && installed; return ( -
-
+
+

OpenHuman Service Required

-

+

The desktop service must be installed and running before the app can continue. Use the buttons below to set up or restart the service.

-
-
Service
+
+
Service
{serviceStateText}
-
-
Agent Server
+
+
Agent Server
{agentRunning ? 'Running' : 'Not Running'}
@@ -251,7 +251,7 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => { className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${ canStop ? 'bg-red-600 hover:bg-red-500 text-white cursor-pointer' - : 'bg-white/5 text-white/30 cursor-not-allowed' + : 'bg-stone-50 text-stone-300 cursor-not-allowed' }`}> Stop Service @@ -265,7 +265,7 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => { className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${ canRestart ? 'bg-cyan-700 hover:bg-cyan-600 text-white cursor-pointer' - : 'bg-white/5 text-white/30 cursor-not-allowed' + : 'bg-stone-50 text-stone-300 cursor-not-allowed' }`}> Restart Service @@ -279,7 +279,7 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => { className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${ canUninstall ? 'bg-amber-700 hover:bg-amber-600 text-white cursor-pointer' - : 'bg-white/5 text-white/30 cursor-not-allowed' + : 'bg-stone-50 text-stone-300 cursor-not-allowed' }`}> Uninstall Service @@ -289,7 +289,7 @@ const ServiceBlockingGate = ({ children }: ServiceBlockingGateProps) => { console.warn('[ServiceGate] REFRESH clicked'); void refreshStatus({ showChecking: true, clearError: true }); }} - className="px-3 py-2 rounded-lg text-sm font-medium transition-colors bg-white/10 hover:bg-white/20 text-white cursor-pointer"> + className="px-3 py-2 rounded-lg text-sm font-medium transition-colors bg-stone-100 hover:bg-stone-200 text-stone-900 cursor-pointer"> Refresh
diff --git a/app/src/components/intelligence/ActionableCard.tsx b/app/src/components/intelligence/ActionableCard.tsx index 0ec0dfcf3..fb262c86a 100644 --- a/app/src/components/intelligence/ActionableCard.tsx +++ b/app/src/components/intelligence/ActionableCard.tsx @@ -87,13 +87,13 @@ function SnoozeDropdownPortal({ isOpen, buttonRef, onClose, onSnooze }: SnoozeDr
{SNOOZE_OPTIONS.map(option => ( ))} @@ -235,7 +235,7 @@ export function ActionableCard({ const priorityClasses = { critical: 'border-coral-500/30 bg-coral-500/5', important: 'border-amber-500/30 bg-amber-500/5', - normal: 'border-white/10 bg-white/[0.02]', + normal: 'border-stone-200 bg-stone-50', }; const priorityDotClasses = { @@ -258,13 +258,13 @@ export function ActionableCard({
{/* Main content row */}
{/* Icon */} -
+
{sourceIcon}
@@ -272,7 +272,7 @@ export function ActionableCard({
-

{item.title}

+

{item.title}

{item.description && (

{item.description}

)} diff --git a/app/src/components/intelligence/ConfirmationModal.tsx b/app/src/components/intelligence/ConfirmationModal.tsx index 361a2a07c..41d08ecf4 100644 --- a/app/src/components/intelligence/ConfirmationModal.tsx +++ b/app/src/components/intelligence/ConfirmationModal.tsx @@ -28,10 +28,10 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) { return (
e.stopPropagation()}> {/* Header */}
@@ -53,8 +53,8 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) {
)}
-

{modal.title}

-

{modal.message}

+

{modal.title}

+

{modal.message}

@@ -62,12 +62,12 @@ export function ConfirmationModal({ modal, onClose }: ConfirmationModalProps) { {/* Don't show again option */} {modal.showDontShowAgain && (
-