From b3e3e9bb96cd0592d71873e099313ddd7c490e35 Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:05:59 +0530 Subject: [PATCH] feat(ui): full light theme redesign based on Figma design tokens (#307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ui): full light theme redesign based on Figma design tokens Redesign the entire app from dark glass-morphism to a clean light theme per the Figma reference (OpenHuman-Prod / Ref for mithil). This is a purely visual change — zero logic, handlers, state, or API changes. - Update Tailwind color tokens: neutral grayscale, primary #2F6EF4, success #34C759, alert #E8A728, error #EF4444 - Switch global CSS from dark (black bg, white text) to light (#F5F5F5 bg, #171717 text) - Replace left MiniSidebar with bottom BottomTabBar (Home, Chat, Skills, Intelligence, Automation, Notification) - Add Settings gear icon on Home page header for persistent access - Restyle Home: white card, Welcome Onboard title, connection badge pill - Restyle Chat: gray agent bubbles, blue user bubbles, white input with mic icon, circular send button, model pill selector - Restyle Skills: white card, filter tabs, + Add button, skill rows - Restyle Intelligence: light containers, filters, cards - Convert all 25+ settings panels from dark to light theme - Convert all shared components (modals, cards, inputs, status badges) * fix(test): update ConnectionIndicator tests for pill badge redesign The component no longer renders a description prop — it's now a compact pill badge. Remove description-related test cases, add pill badge test. * fix(ui): address CodeRabbit review — contrast, dead controls, regressions - Remove unused description prop from ConnectionIndicator interface - Fix Save Note button hover affordance (MemoryWorkspace) - Fix low-contrast text in AccessibilityPanel vision summary - Fix AIPanel metadata text contrast (gray-400 → stone-500) - Fix ActionPanel spinner visibility on light bg variants - Fix LocalModelPanel ollama link hover color - Fix MessagingPanel error chip to light theme tokens - Fix RecoveryPhrasePanel import inputs from dark to light tokens - Fix TeamMembersPanel avatar initial contrast + modal name text - Fix Intelligence refresh button text contrast - Remove non-functional + Add button and filter tabs from Skills page * feat(ui): center conversation page in a white card container Match the Figma design where the chat is inside a centered white card with rounded corners and shadow, consistent with the Skills page layout. * feat(ui): wrap all remaining pages in centered white card containers Apply the centered card layout (max-w-2xl, white bg, rounded-2xl, shadow-soft) to every page for visual consistency with Figma designs: - Settings: wrap Routes container in centered card with max-height - Intelligence: centered card with padding - Agents: centered card with empty state - Invites: centered card, replace glass with white cards - Webhooks: centered cards for tunnel list and activity - Mnemonic: centered card, replace glass with white card - Welcome: light bg, updated text colors * fix(ui): stretch settings menu items to fill card container Remove max-w-md constraint from SettingsHome scrollable area so menu items fill the full width of the centered card wrapper. * fix(ui): convert onboarding screens from dark to light theme All onboarding steps (Welcome, LocalAI, ScreenPermissions, Analytics, Tools, Skills, Connect, InviteCode, Mnemonic) and the overlay container now use white cards with light backgrounds, matching the app redesign. - Card containers: bg-black → bg-white with border-stone-200 - Info boxes: dark tinted → bg-stone-50 with border-stone-200 - Text: text-white → text-stone-900, secondary to text-stone-600 - Progress dots: inactive bg-stone-700 → bg-stone-300 - Toggle rows, permission badges, inputs all converted to light tokens * fix(ui): fix onboarding Continue button hover turning green Replace sage hover styles with proper primary blue hover (bg-primary-600) on the shared OnboardingNextButton component. * fix(ui): reapply light theme to Home.tsx after upstream merge overwrite The merge with upstream (Local AI preset bootstrap PR #304) reverted Home.tsx to the dark theme. Reapply the Figma-based light redesign while preserving the new first-run bootstrap logic from upstream. * fix(ui): light theme cleanup for MemoryWorkspace component Remaining dark-theme classes from upstream merge — update borders, text colors, and backgrounds to match light theme. --- .claude/memory.md | 9 + app/src/App.tsx | 19 +- app/src/components/BottomTabBar.tsx | 170 +++++++++++ app/src/components/ConnectionIndicator.tsx | 14 +- app/src/components/OnboardingOverlay.tsx | 2 +- app/src/components/ProgressIndicator.tsx | 2 +- app/src/components/RouteLoadingScreen.tsx | 2 +- app/src/components/SkillsGrid.tsx | 14 +- .../__tests__/ConnectionIndicator.test.tsx | 15 +- .../components/daemon/ServiceBlockingGate.tsx | 22 +- .../intelligence/ActionableCard.tsx | 12 +- .../intelligence/ConfirmationModal.tsx | 16 +- .../intelligence/MemoryGraphMap.tsx | 34 +-- .../components/intelligence/MemoryHeatmap.tsx | 16 +- .../intelligence/MemoryInsights.tsx | 56 ++-- .../intelligence/MemoryStatsBar.tsx | 16 +- .../intelligence/MemoryWorkspace.tsx | 32 +- app/src/components/settings/SettingsHome.tsx | 18 +- .../components/SettingsBackButton.tsx | 4 +- .../settings/components/SettingsHeader.tsx | 6 +- .../settings/components/SettingsMenuItem.tsx | 8 +- .../components/settings/panels/AIPanel.tsx | 92 +++--- .../settings/panels/AccessibilityPanel.tsx | 74 ++--- .../settings/panels/AdvancedPanel.tsx | 10 +- .../settings/panels/AgentChatPanel.tsx | 16 +- .../settings/panels/AutocompletePanel.tsx | 112 +++---- .../settings/panels/BillingPanel.tsx | 66 ++-- .../settings/panels/ConnectionsPanel.tsx | 18 +- .../settings/panels/CronJobsPanel.tsx | 72 ++--- .../settings/panels/DictationPanel.tsx | 48 +-- .../settings/panels/LocalModelPanel.tsx | 262 ++++++++-------- .../settings/panels/MessagingPanel.tsx | 41 ++- .../settings/panels/PrivacyPanel.tsx | 10 +- .../settings/panels/ProfilePanel.tsx | 8 +- .../settings/panels/RecoveryPhrasePanel.tsx | 22 +- .../panels/ScreenIntelligencePanel.tsx | 106 +++---- .../settings/panels/SkillsPanel.tsx | 30 +- .../settings/panels/TauriCommandsPanel.tsx | 60 ++-- .../settings/panels/TeamInvitesPanel.tsx | 20 +- .../settings/panels/TeamManagementPanel.tsx | 52 ++-- .../settings/panels/TeamMembersPanel.tsx | 28 +- .../components/settings/panels/TeamPanel.tsx | 38 +-- .../panels/components/ActionPanel.tsx | 20 +- .../settings/panels/components/InputGroup.tsx | 16 +- .../panels/components/SectionCard.tsx | 24 +- .../panels/components/ValidatedField.tsx | 28 +- .../components/skills/SetupFormRenderer.tsx | 22 +- app/src/components/skills/SkillDebugModal.tsx | 44 +-- app/src/components/skills/SkillSetupModal.tsx | 10 +- app/src/index.css | 93 +++--- app/src/pages/Agents.tsx | 40 ++- app/src/pages/Conversations.tsx | 102 ++++--- app/src/pages/Home.tsx | 282 ++++++++++-------- app/src/pages/Intelligence.tsx | 48 +-- app/src/pages/Invites.tsx | 12 +- app/src/pages/Mnemonic.tsx | 6 +- app/src/pages/Settings.tsx | 62 ++-- app/src/pages/Skills.tsx | 57 ++-- app/src/pages/Webhooks.tsx | 36 +-- app/src/pages/Welcome.tsx | 10 +- app/src/pages/onboarding/Onboarding.tsx | 12 +- .../components/OnboardingNextButton.tsx | 2 +- .../pages/onboarding/steps/AnalyticsStep.tsx | 38 +-- .../pages/onboarding/steps/ConnectStep.tsx | 24 +- .../pages/onboarding/steps/InviteCodeStep.tsx | 12 +- .../pages/onboarding/steps/LocalAIStep.tsx | 24 +- .../pages/onboarding/steps/MnemonicStep.tsx | 42 +-- .../steps/ScreenPermissionsStep.tsx | 26 +- app/src/pages/onboarding/steps/SkillsStep.tsx | 16 +- app/src/pages/onboarding/steps/ToolsStep.tsx | 18 +- .../pages/onboarding/steps/WelcomeStep.tsx | 10 +- app/src/styles/theme.css | 25 +- app/tailwind.config.js | 150 +++++----- 73 files changed, 1607 insertions(+), 1376 deletions(-) create mode 100644 app/src/components/BottomTabBar.tsx 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 && (
-