refactor(web): enhance ui visual polish and ux

- Replace native selects with Shadcn UI Select in Settings
- Increase ActivityChart height for better visibility
- specific Economy Overview card height to fill column
- Add hover/active scale animations to sidebar items
This commit is contained in:
syntaxbullet
2026-01-08 23:10:14 +01:00
parent 713ea07040
commit 238d9a8803
5 changed files with 227 additions and 29 deletions

View File

@@ -59,7 +59,7 @@ export const ActivityChart: React.FC<ActivityChartProps> = ({ data, loading }) =
}));
return (
<div className="w-full h-[300px] mt-4">
<div className="w-full h-[400px] mt-4">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={chartData}

View File

@@ -46,7 +46,7 @@ export function AppSidebar() {
<SidebarHeader className="p-4">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild className="hover:bg-white/5 transition-all duration-300 rounded-xl">
<SidebarMenuButton size="lg" asChild className="hover:bg-white/5 transition-all duration-300 rounded-xl hover:scale-[1.02] active:scale-[0.98]">
<Link to="/" className="flex items-center gap-3">
<div className="flex aspect-square size-10 items-center justify-center rounded-xl bg-gradient-to-br from-primary to-purple-600 text-primary-foreground shadow-lg shadow-primary/20 overflow-hidden border border-white/10">
{botAvatar ? (
@@ -74,7 +74,7 @@ export function AppSidebar() {
<SidebarMenuButton
asChild
isActive={location.pathname === item.url}
className={`transition-all duration-200 rounded-lg px-4 py-6 ${location.pathname === item.url
className={`transition-all duration-200 rounded-lg px-4 py-6 hover:scale-[1.02] active:scale-[0.98] ${location.pathname === item.url
? "bg-primary/10 text-primary border border-primary/20 shadow-lg shadow-primary/5"
: "hover:bg-white/5 text-white/60 hover:text-white"
}`}

View File

@@ -0,0 +1,188 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -100,7 +100,7 @@ export function Dashboard() {
</Card>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4 glass border-white/5">
<Card className="col-span-4 glass border-white/5 h-full">
<CardHeader className="flex flex-row items-start justify-between">
<div>
<CardTitle className="text-xl font-bold flex items-center gap-2">

View File

@@ -5,6 +5,13 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// Types matching the backend response
interface RoleOption { id: string; name: string; color: string; }
@@ -358,20 +365,28 @@ export function Settings() {
})}
</div>
<div className="mt-2">
<select
className="w-full bg-black/20 border border-white/10 rounded-md p-2 text-sm text-white mt-2"
onChange={(e) => {
if (e.target.value && !(config?.colorRoles || []).includes(e.target.value)) {
updateConfig("colorRoles", [...(config?.colorRoles || []), e.target.value]);
<Select
value=""
onValueChange={(value) => {
if (value && !(config?.colorRoles || []).includes(value)) {
updateConfig("colorRoles", [...(config?.colorRoles || []), value]);
}
e.target.value = "";
}}
>
<option value="">+ Add Color Role</option>
{meta.roles.map(r => (
<option key={r.id} value={r.id} style={{ color: r.color }}>{r.name}</option>
))}
</select>
<SelectTrigger className="w-full bg-black/20 border-white/10 text-white/50 h-9">
<SelectValue placeholder="+ Add Color Role" />
</SelectTrigger>
<SelectContent>
{meta.roles.map((r) => (
<SelectItem key={r.id} value={r.id}>
<span className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: r.color || '#999' }} />
{r.name}
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
@@ -477,23 +492,18 @@ function SelectField({ label, value, options, onChange }: { label: string, value
return (
<div className="space-y-1.5">
<label className="text-xs font-medium text-white/60 ml-1">{label}</label>
<div className="relative">
<select
value={value || ""}
onChange={(e) => onChange(e.target.value)}
className="flex h-10 w-full rounded-md border border-white/10 bg-black/20 px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 text-white appearance-none"
>
<option value="" className="bg-zinc-900 text-white/50">Select...</option>
<Select value={value || ""} onValueChange={onChange}>
<SelectTrigger className="w-full bg-black/20 border-white/10 text-white">
<SelectValue placeholder="Select..." />
</SelectTrigger>
<SelectContent>
{options.map((opt) => (
<option key={opt.id} value={opt.id} className="bg-zinc-900">
<SelectItem key={opt.id} value={opt.id}>
{opt.name}
</option>
</SelectItem>
))}
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-white/50">
<svg className="h-4 w-4 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" /></svg>
</div>
</div>
</SelectContent>
</Select>
</div>
);
}