Skip to content

Commit 5fa4f36

Browse files
committed
feat: ui improvements
1 parent 7e42aad commit 5fa4f36

30 files changed

Lines changed: 2440 additions & 898 deletions

‎src/renderer/App.tsx‎

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,40 @@
22
* @file src/renderer/App.tsx
33
*
44
* @created 07.03.2026
5-
* @modified 10.03.2026
5+
* @modified 17.03.2026
66
*
77
* @author Christian Blank <aidrelay@proton.me>
88
* @copyright 2026
99
*
1010
* @description Root React component. Sets up the TanStack Router with a
1111
* root route that renders the Shell layout and child routes for each page.
12-
* Placeholder routes are used for pages not yet implemented so navigation
13-
* links work end-to-end from the start. Rules page wired in Step 22;
14-
* Profiles page wired in Step 30; Registry + Stacks pages wired in Step 43/45;
15-
* Settings + History pages wired in Phase 6.
12+
* Navigation was simplified in the UI/UX redesign: Activity Log merged into
13+
* History, Sync Center merged into Dashboard/Settings, Skills merged into Rules.
14+
* Stacks remains navigable but is no longer in the sidebar.
1615
*/
1716

1817
import { useEffect } from 'react'
19-
import { RouterProvider, createRouter, createRootRoute, createRoute } from '@tanstack/react-router'
18+
import {
19+
RouterProvider,
20+
createRouter,
21+
createRootRoute,
22+
createRoute,
23+
createMemoryHistory,
24+
redirect,
25+
} from '@tanstack/react-router'
2026
import { Toaster, toast } from 'sonner'
2127
import { useTranslation } from 'react-i18next'
2228
import { TooltipProvider } from '@/components/ui/tooltip'
2329
import { Shell } from '@/components/layout/Shell'
2430
import { DashboardPage } from '@/pages/DashboardPage'
2531
import { ServersPage } from '@/pages/ServersPage'
2632
import { RulesPage } from '@/pages/RulesPage'
27-
import { ActivityLogPage } from '@/pages/ActivityLogPage'
2833
import { ProfilesPage } from '@/pages/ProfilesPage'
2934
import { RegistryPage } from '@/pages/RegistryPage'
3035
import { StacksPage } from '@/pages/StacksPage'
3136
import { SettingsPage } from '@/pages/SettingsPage'
3237
import { HistoryPage } from '@/pages/HistoryPage'
3338
import { ClientsPage } from '@/pages/ClientsPage'
34-
import { SyncCenterPage } from '@/pages/SyncCenterPage'
35-
import { SkillsPage } from '@/pages/SkillsPage'
3639
import { StartupSplash } from '@/components/layout/StartupSplash'
3740
import { useStartupSplash } from '@/hooks/useStartupSplash'
3841
import { clientsService } from '@/services/clients.service'
@@ -72,12 +75,6 @@ const profilesRoute = createRoute({
7275
component: ProfilesPage,
7376
})
7477

75-
const activityRoute = createRoute({
76-
getParentRoute: () => rootRoute,
77-
path: '/activity',
78-
component: ActivityLogPage,
79-
})
80-
8178
const registryRoute = createRoute({
8279
getParentRoute: () => rootRoute,
8380
path: '/registry',
@@ -102,16 +99,14 @@ const historyRoute = createRoute({
10299
component: HistoryPage,
103100
})
104101

105-
const syncCenterRoute = createRoute({
102+
// Redirect legacy activity routes to the unified history page
103+
const activityRedirectRoute = createRoute({
106104
getParentRoute: () => rootRoute,
107-
path: '/sync-center',
108-
component: SyncCenterPage,
109-
})
110-
111-
const skillsRoute = createRoute({
112-
getParentRoute: () => rootRoute,
113-
path: '/skills',
114-
component: SkillsPage,
105+
path: '/activity',
106+
beforeLoad: () => {
107+
// eslint-disable-next-line @typescript-eslint/only-throw-error -- TanStack Router expects redirect to be thrown
108+
throw redirect({ to: '/history' })
109+
},
115110
})
116111

117112
const routeTree = rootRoute.addChildren([
@@ -120,18 +115,22 @@ const routeTree = rootRoute.addChildren([
120115
rulesRoute,
121116
clientsRoute,
122117
profilesRoute,
123-
activityRoute,
124118
registryRoute,
125119
stacksRoute,
126120
settingsRoute,
127121
historyRoute,
128-
syncCenterRoute,
129-
skillsRoute,
122+
activityRedirectRoute,
130123
])
131124

132125
// ─── Router ───────────────────────────────────────────────────────────────────
133126

134-
const router = createRouter({ routeTree })
127+
// Use in-memory history so routing works correctly under Electron's file:// protocol.
128+
// The default browser history breaks in production because the URL path is the
129+
// path to the asar bundle, not "/", so no route ever matches.
130+
const router = createRouter({
131+
routeTree,
132+
history: createMemoryHistory({ initialEntries: ['/'] }),
133+
})
135134

136135
// Register the router's type for TypeScript inference across the app
137136
declare module '@tanstack/react-router' {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @file src/renderer/components/common/EmptyState.tsx
3+
*
4+
* @created 17.03.2026
5+
* @modified 17.03.2026
6+
*
7+
* @author Christian Blank <aidrelay@proton.me>
8+
* @copyright 2026
9+
*
10+
* @description Reusable empty state component displayed when a page or section
11+
* has no data. Shows a muted icon, descriptive text, and one or two call-to-action
12+
* buttons to guide the user toward the next step.
13+
*/
14+
15+
import { type LucideIcon } from 'lucide-react'
16+
import { Button } from '@/components/ui/button'
17+
18+
// ─── Types ───────────────────────────────────────────────────────────────────
19+
20+
interface EmptyStateAction {
21+
readonly label: string
22+
readonly onClick: () => void
23+
}
24+
25+
interface EmptyStateProps {
26+
/** Large muted icon rendered above the title */
27+
readonly icon?: LucideIcon
28+
/** Primary message displayed to the user */
29+
readonly title: string
30+
/** Supporting description below the title */
31+
readonly description?: string
32+
/** Primary call-to-action button */
33+
readonly action?: EmptyStateAction
34+
/** Secondary call-to-action button rendered as outline variant */
35+
readonly secondaryAction?: EmptyStateAction
36+
/** Test identifier */
37+
readonly testId?: string
38+
}
39+
40+
// ─── Component ───────────────────────────────────────────────────────────────
41+
42+
/**
43+
* Renders a centered empty state panel with optional icon, description,
44+
* and action buttons.
45+
*
46+
* @param props - Empty state configuration.
47+
* @returns The empty state element.
48+
*/
49+
const EmptyState = ({
50+
icon: Icon,
51+
title,
52+
description,
53+
action,
54+
secondaryAction,
55+
testId = 'empty-state',
56+
}: Readonly<EmptyStateProps>) => (
57+
<div
58+
className="flex flex-col items-center gap-3 rounded-lg border border-dashed border-border p-12 text-center"
59+
data-testid={testId}
60+
>
61+
{Icon ? <Icon size={40} className="text-text-secondary/50" aria-hidden="true" /> : null}
62+
<p className="text-sm font-medium text-text-primary">{title}</p>
63+
{description ? <p className="max-w-sm text-xs text-text-secondary">{description}</p> : null}
64+
{action || secondaryAction ? (
65+
<div className="mt-2 flex items-center gap-2">
66+
{action ? (
67+
<Button size="sm" onClick={action.onClick} data-testid={`${testId}-action`}>
68+
{action.label}
69+
</Button>
70+
) : null}
71+
{secondaryAction ? (
72+
<Button
73+
variant="outline"
74+
size="sm"
75+
onClick={secondaryAction.onClick}
76+
data-testid={`${testId}-secondary-action`}
77+
>
78+
{secondaryAction.label}
79+
</Button>
80+
) : null}
81+
</div>
82+
) : null}
83+
</div>
84+
)
85+
86+
export { EmptyState }
87+
export type { EmptyStateProps, EmptyStateAction }
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @file src/renderer/components/common/PageHeader.tsx
3+
*
4+
* @created 17.03.2026
5+
* @modified 17.03.2026
6+
*
7+
* @author Christian Blank <aidrelay@proton.me>
8+
* @copyright 2026
9+
*
10+
* @description Standardized page header component used across all pages.
11+
* Renders a consistent title, optional subtitle, and right-aligned action
12+
* buttons. Supports an optional sticky mode with backdrop blur.
13+
*/
14+
15+
import { type ReactNode } from 'react'
16+
17+
// ─── Types ───────────────────────────────────────────────────────────────────
18+
19+
interface PageHeaderProps {
20+
/** Unique ID used for aria-labelledby linkage */
21+
readonly id?: string
22+
/** Page title displayed as h1 */
23+
readonly title: string
24+
/** Optional subtitle below the title */
25+
readonly subtitle?: string
26+
/** Right-aligned action buttons or controls */
27+
readonly actions?: ReactNode
28+
/** When true, the header sticks to the top with backdrop blur */
29+
readonly sticky?: boolean
30+
/** Test identifier */
31+
readonly testId?: string
32+
}
33+
34+
// ─── Component ───────────────────────────────────────────────────────────────
35+
36+
/**
37+
* Renders a consistent page header with title, subtitle, and action slot.
38+
*
39+
* @param props - Header configuration props.
40+
* @returns The page header element.
41+
*/
42+
const PageHeader = ({
43+
id,
44+
title,
45+
subtitle,
46+
actions,
47+
sticky = false,
48+
testId = 'page-header',
49+
}: Readonly<PageHeaderProps>) => {
50+
const stickyClasses = sticky
51+
? 'sticky top-0 z-20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60'
52+
: ''
53+
54+
return (
55+
<header
56+
className={`flex items-start justify-between gap-4 px-6 py-4 ${stickyClasses}`.trim()}
57+
data-testid={testId}
58+
>
59+
<div>
60+
<h1 id={id} className="text-2xl font-bold tracking-tight text-text-primary">
61+
{title}
62+
</h1>
63+
{subtitle ? <p className="mt-1 text-sm text-text-secondary">{subtitle}</p> : null}
64+
</div>
65+
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
66+
</header>
67+
)
68+
}
69+
70+
export { PageHeader }
71+
export type { PageHeaderProps }

0 commit comments

Comments
 (0)