Skip to content

Commit 78bc002

Browse files
committed
feat: implement theme provider and mode toggle for improved UI theming
1 parent 7a2bc0b commit 78bc002

File tree

5 files changed

+179
-56
lines changed

5 files changed

+179
-56
lines changed

apps/console/src/App.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,14 @@ function findFirstRoute(items: any[]): string {
169169
return '/';
170170
}
171171

172+
import { ThemeProvider } from './components/theme-provider';
173+
172174
export function App() {
173175
return (
174-
<BrowserRouter>
175-
<AppContent />
176-
</BrowserRouter>
176+
<ThemeProvider defaultTheme="system" storageKey="object-ui-theme">
177+
<BrowserRouter>
178+
<AppContent />
179+
</BrowserRouter>
180+
</ThemeProvider>
177181
);
178182
}

apps/console/src/components/AppHeader.tsx

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
BreadcrumbSeparator,
99
} from '@object-ui/components';
1010

11+
import { ModeToggle } from './mode-toggle';
12+
1113
export function AppHeader({ appName, objects }: { appName: string, objects: any[] }) {
1214
const location = useLocation();
1315

@@ -18,24 +20,29 @@ export function AppHeader({ appName, objects }: { appName: string, objects: any[
1820
const currentObject = isObjectPage ? objects.find((o: any) => o.name === location.pathname.substring(1)) : null;
1921

2022
return (
21-
<div className="flex items-center gap-2 px-4 h-full">
22-
<Breadcrumb>
23-
<BreadcrumbList>
24-
<BreadcrumbItem className="hidden md:block">
25-
<BreadcrumbLink href="#">
26-
{appName}
27-
</BreadcrumbLink>
28-
</BreadcrumbItem>
29-
{currentObject && (
30-
<>
31-
<BreadcrumbSeparator className="hidden md:block" />
32-
<BreadcrumbItem>
33-
<BreadcrumbPage>{currentObject.label}</BreadcrumbPage>
34-
</BreadcrumbItem>
35-
</>
36-
)}
37-
</BreadcrumbList>
38-
</Breadcrumb>
23+
<div className="flex items-center justify-between w-full h-full px-4">
24+
<div className="flex items-center gap-2">
25+
<Breadcrumb>
26+
<BreadcrumbList>
27+
<BreadcrumbItem className="hidden md:block">
28+
<BreadcrumbLink href="#">
29+
{appName}
30+
</BreadcrumbLink>
31+
</BreadcrumbItem>
32+
{currentObject && (
33+
<>
34+
<BreadcrumbSeparator className="hidden md:block" />
35+
<BreadcrumbItem>
36+
<BreadcrumbPage>{currentObject.label}</BreadcrumbPage>
37+
</BreadcrumbItem>
38+
</>
39+
)}
40+
</BreadcrumbList>
41+
</Breadcrumb>
42+
</div>
43+
<div>
44+
<ModeToggle />
45+
</div>
3946
</div>
4047
);
4148
}

apps/console/src/components/ObjectView.tsx

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -165,49 +165,58 @@ export function ObjectView({ dataSource, objects, onEdit }: any) {
165165
};
166166

167167
return (
168-
<div className="h-full flex flex-col gap-4">
168+
<div className="h-full flex flex-col">
169169
{/* Header Section */}
170-
<div className="flex justify-between items-start">
171-
<div className="space-y-1">
172-
<h1 className="text-2xl font-bold tracking-tight text-slate-900">{objectDef.label}</h1>
173-
<div className="flex items-center gap-2">
174-
{/* View Switcher Tabs */}
175-
{views.length > 1 && (
176-
<Tabs value={activeView.id} onValueChange={handleViewChange} className="h-8">
177-
<TabsList className="h-8 p-0 bg-transparent border-0 gap-2">
178-
{views.map((v: any) => (
179-
<TabsTrigger
180-
key={v.id}
181-
value={v.id}
182-
className="h-8 px-3 data-[state=active]:bg-muted data-[state=active]:shadow-none border border-transparent data-[state=active]:border-border rounded-md transition-all"
183-
>
184-
{v.type === 'kanban' && <KanbanIcon className="mr-2 h-3.5 w-3.5" />}
185-
{v.type === 'calendar' && <CalendarIcon className="mr-2 h-3.5 w-3.5" />}
186-
{v.type === 'grid' && <TableIcon className="mr-2 h-3.5 w-3.5" />}
187-
{v.type === 'gantt' && <AlignLeft className="mr-2 h-3.5 w-3.5" />}
188-
{v.label}
189-
</TabsTrigger>
190-
))}
191-
</TabsList>
192-
</Tabs>
193-
)}
194-
{views.length <= 1 && (
195-
<p className="text-slate-500 text-sm">
196-
{objectDef.description || 'Manage your records'}
197-
</p>
198-
)}
170+
<div className="flex justify-between items-center py-4 px-6 border-b shrink-0 bg-background/95 backdrop-blur z-10 sticky top-0">
171+
<div className="flex items-center gap-4">
172+
<div className="flex items-center gap-3">
173+
<div className="bg-primary/10 p-2 rounded-lg shrink-0">
174+
{/* Map icons based on object name if possible, fallback to generic */}
175+
<TableIcon className="h-5 w-5 text-primary" />
176+
</div>
177+
<div>
178+
<h1 className="text-xl font-bold tracking-tight text-foreground">{objectDef.label}</h1>
179+
<p className="text-sm text-muted-foreground">{activeView.label}</p>
180+
</div>
199181
</div>
182+
183+
{/* View Switcher - Re-styled as refined segmented control */}
184+
{views.length > 1 && (
185+
<div className="ml-4 h-8 bg-muted/50 rounded-lg p-1 flex items-center">
186+
{views.map((v: any) => (
187+
<button
188+
key={v.id}
189+
onClick={() => handleViewChange(v.id)}
190+
className={`
191+
flex items-center gap-2 px-3 h-full rounded-md text-sm font-medium transition-all
192+
${activeView.id === v.id
193+
? 'bg-background shadow-sm text-foreground'
194+
: 'text-muted-foreground hover:bg-background/50 hover:text-foreground/80'
195+
}
196+
`}
197+
>
198+
{v.type === 'kanban' && <KanbanIcon className="h-3.5 w-3.5" />}
199+
{v.type === 'calendar' && <CalendarIcon className="h-3.5 w-3.5" />}
200+
{v.type === 'grid' && <TableIcon className="h-3.5 w-3.5" />}
201+
{v.type === 'gantt' && <AlignLeft className="h-3.5 w-3.5" />}
202+
<span>{v.label}</span>
203+
</button>
204+
))}
205+
</div>
206+
)}
200207
</div>
201-
<div className="flex gap-2">
202-
<Button onClick={() => onEdit(null)} className="shadow-none">
203-
<Plus className="mr-2 h-4 w-4" /> New {objectDef.label}
208+
209+
<div className="flex items-center gap-2">
210+
<Button onClick={() => onEdit(null)} className="shadow-none gap-2">
211+
<Plus className="h-4 w-4" />
212+
<span className="hidden sm:inline">New</span>
204213
</Button>
205214
</div>
206215
</div>
207216

208217
{/* Content Area */}
209-
<div className="flex-1 rounded-xl border bg-card text-card-foreground shadow-sm overflow-hidden p-0 relative">
210-
<div className="absolute inset-0">
218+
<div className="flex-1 bg-muted/5 p-4 sm:p-6 overflow-hidden">
219+
<div className="h-full w-full rounded-xl border bg-card text-card-foreground shadow-sm overflow-hidden relative">
211220
{renderCurrentView()}
212221
</div>
213222
</div>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Moon, Sun } from "lucide-react"
2+
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@object-ui/components"
3+
import { useTheme } from "./theme-provider"
4+
5+
export function ModeToggle() {
6+
const { setTheme } = useTheme()
7+
8+
return (
9+
<DropdownMenu>
10+
<DropdownMenuTrigger asChild>
11+
<Button variant="ghost" size="icon">
12+
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
13+
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
14+
<span className="sr-only">Toggle theme</span>
15+
</Button>
16+
</DropdownMenuTrigger>
17+
<DropdownMenuContent align="end">
18+
<DropdownMenuItem onClick={() => setTheme("light")}>
19+
Light
20+
</DropdownMenuItem>
21+
<DropdownMenuItem onClick={() => setTheme("dark")}>
22+
Dark
23+
</DropdownMenuItem>
24+
<DropdownMenuItem onClick={() => setTheme("system")}>
25+
System
26+
</DropdownMenuItem>
27+
</DropdownMenuContent>
28+
</DropdownMenu>
29+
)
30+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { createContext, useContext, useEffect, useState } from "react"
2+
3+
type Theme = "dark" | "light" | "system"
4+
5+
type ThemeProviderProps = {
6+
children: React.ReactNode
7+
defaultTheme?: Theme
8+
storageKey?: string
9+
}
10+
11+
type ThemeProviderState = {
12+
theme: Theme
13+
setTheme: (theme: Theme) => void
14+
}
15+
16+
const initialState: ThemeProviderState = {
17+
theme: "system",
18+
setTheme: () => null,
19+
}
20+
21+
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
22+
23+
export function ThemeProvider({
24+
children,
25+
defaultTheme = "system",
26+
storageKey = "vite-ui-theme",
27+
...props
28+
}: ThemeProviderProps) {
29+
const [theme, setTheme] = useState<Theme>(
30+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31+
)
32+
33+
useEffect(() => {
34+
const root = window.document.documentElement
35+
36+
root.classList.remove("light", "dark")
37+
38+
if (theme === "system") {
39+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40+
.matches
41+
? "dark"
42+
: "light"
43+
44+
root.classList.add(systemTheme)
45+
return
46+
}
47+
48+
root.classList.add(theme)
49+
}, [theme])
50+
51+
const value = {
52+
theme,
53+
setTheme: (theme: Theme) => {
54+
localStorage.setItem(storageKey, theme)
55+
setTheme(theme)
56+
},
57+
}
58+
59+
return (
60+
<ThemeProviderContext.Provider {...props} value={value}>
61+
{children}
62+
</ThemeProviderContext.Provider>
63+
)
64+
}
65+
66+
export const useTheme = () => {
67+
const context = useContext(ThemeProviderContext)
68+
69+
if (context === undefined)
70+
throw new Error("useTheme must be used within a ThemeProvider")
71+
72+
return context
73+
}

0 commit comments

Comments
 (0)