feat(design): cyan accent, dialog overflow fixes, unified delete buttons, nav polish

This commit is contained in:
opencode
2026-08-06 23:03:14 +02:00
parent e65daea8dd
commit 54364d7a88
49 changed files with 4776 additions and 31 deletions
@@ -134,7 +134,8 @@ export function AdminToolsTab() {
</div>
<Button
size="sm"
variant="destructive"
variant="outline"
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
disabled={selectedIds.length === 0 || trash.isPending}
onClick={() => setConfirmTrash(true)}
>
+17 -2
View File
@@ -1,5 +1,5 @@
import { Link, useLocation } from "wouter";
import { LayoutDashboard, Wrench, BarChart3, FileText, LogIn, LogOut, ShieldCheck, AlertTriangle, Search } from "lucide-react";
import { LayoutDashboard, Wrench, BarChart3, LogIn, LogOut, ShieldCheck, AlertTriangle, Search, Trash2, HelpCircle } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useAuth } from "@/hooks/use-auth";
import { useGetVersion, getGetVersionQueryKey } from "@workspace/api-client-react";
@@ -39,12 +39,12 @@ export function Layout({ children }: { children: React.ReactNode }) {
{ href: "/", label: t("nav.home"), icon: LayoutDashboard },
{ href: "/tools", label: t("nav.browseTools"), icon: Wrench },
{ href: "/analytics", label: t("nav.analytics"), icon: BarChart3 },
{ href: "/docs", label: t("nav.docs"), icon: FileText },
];
const adminLinks = [
{ href: "/admin", label: t("nav.admin"), icon: ShieldCheck },
{ href: "/admin/redundancy", label: t("nav.redundancy"), icon: AlertTriangle },
{ href: "/trash", label: t("nav.trash"), icon: Trash2 },
];
function isActive(href: string) {
@@ -107,6 +107,21 @@ export function Layout({ children }: { children: React.ReactNode }) {
)}
</SidebarContent>
<SidebarFooter className="gap-2">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip={t("nav.docs")}
className="text-muted-foreground"
data-testid="button-help"
>
<Link href="/docs">
<HelpCircle />
<span>{t("nav.docs")}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<UserMenu />
<div className="border-t pt-2 flex items-center justify-between text-[11px] text-muted-foreground px-2 group-data-[collapsible=icon]:justify-center">
<span className="font-medium group-data-[collapsible=icon]:hidden">toolr</span>
@@ -31,7 +31,7 @@ export function RatingStars({ value, max = 5, size = "md", interactive = false,
data-testid={`star-${starValue}`}
>
<Star
className={`${sizeClasses[size]} ${isFilled ? "fill-primary text-primary" : "text-muted-foreground/30"} transition-colors`}
className={`${sizeClasses[size]} ${isFilled ? "fill-primary text-primary [filter:drop-shadow(0_0_4px_hsl(var(--primary)/0.45))]" : "text-muted-foreground/30"} transition-colors group-hover:brightness-110 ${interactive ? "hover:[filter:drop-shadow(0_0_6px_hsl(var(--glow-1)/0.6))]" : ""}`}
/>
</button>
);
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary focus-visible:neon-glow disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:shadow-[0_0_0_1px_hsl(var(--ring)/0.2)] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary focus-visible:neon-glow disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:shadow-[0_0_0_1px_hsl(var(--ring)/0.2)] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
@@ -20,7 +20,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 origin-[--radix-tooltip-content-transform-origin]",
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 origin-[--radix-tooltip-content-transform-origin]",
className
)}
{...props}
+7 -7
View File
@@ -204,8 +204,8 @@
--popover-foreground: 210 40% 98%;
--popover-border: 217 33% 17%;
--primary: 38 92% 50%;
--primary-foreground: 222 47% 11%;
--primary: 187 95% 55%;
--primary-foreground: 210 40% 5%;
--secondary: 217 33% 17%;
--secondary-foreground: 210 40% 98%;
@@ -220,8 +220,8 @@
--destructive-foreground: 210 40% 98%;
--input: 217 33% 17%;
--ring: 38 92% 50%;
--chart-1: 38 92% 50%;
--ring: 187 95% 55%;
--chart-1: 187 95% 55%;
--chart-2: 210 40% 98%;
--chart-3: 187 100% 42%;
--chart-4: 43 100% 50%;
@@ -392,9 +392,9 @@
.neon-glow-primary {
box-shadow:
0 0 0 1px hsl(var(--glow-3) / 0.4),
0 0 16px -2px hsl(var(--glow-3) / 0.38),
0 0 34px -8px hsl(var(--glow-3) / 0.3);
0 0 0 1px hsl(var(--primary) / 0.4),
0 0 16px -2px hsl(var(--primary) / 0.38),
0 0 34px -8px hsl(var(--primary) / 0.3);
}
/* Solid neon fill for progress/rating bars — high contrast, cyan → violet */
+6 -6
View File
@@ -361,7 +361,7 @@ export default function Admin() {
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader className="flex flex-row items-center justify-between gap-2">
<DialogTitle>{t("admin.createNewUser")}</DialogTitle>
<DialogTitle className="min-w-0 truncate">{t("admin.createNewUser")}</DialogTitle>
<GuideHelp guide="administration" label={t("admin.createNewUser")} />
</DialogHeader>
<div className="space-y-4 py-2">
@@ -403,7 +403,7 @@ export default function Admin() {
</Select>
</div>
</div>
<DialogFooter>
<DialogFooter className="flex-col-reverse sm:flex-row sm:flex-wrap sm:justify-end gap-2">
<Button variant="outline" onClick={() => setCreateOpen(false)}>{t("common.cancel")}</Button>
<Button onClick={handleCreateUser} disabled={createUser.isPending || !newUsername || !newPassword}>
{createUser.isPending ? t("admin.creating") : t("admin.createUser")}
@@ -413,9 +413,9 @@ export default function Admin() {
</Dialog>
<Dialog open={!!editUser} onOpenChange={(open) => { if (!open) { setEditUser(null); setEditPassword(""); } }}>
<DialogContent className="sm:max-w-sm">
<DialogContent className="sm:max-w-md">
<DialogHeader className="flex flex-row items-center justify-between gap-2">
<DialogTitle>{t("admin.editUser", { username: editUser?.username })}</DialogTitle>
<DialogTitle className="min-w-0 truncate">{t("admin.editUser", { username: editUser?.username })}</DialogTitle>
<GuideHelp guide="administration" label={t("admin.editUser", { username: editUser?.username })} />
</DialogHeader>
<div className="space-y-4 py-2">
@@ -467,7 +467,7 @@ export default function Admin() {
</div>
)}
</div>
<DialogFooter className="flex-col sm:flex-row sm:justify-end gap-2">
<DialogFooter className="flex-col-reverse sm:flex-row sm:flex-wrap sm:justify-end gap-2">
<Button variant="outline" onClick={() => { setEditUser(null); setEditPassword(""); }}>{t("common.cancel")}</Button>
<Button
onClick={handleUpdateUser}
@@ -498,7 +498,7 @@ export default function Admin() {
</p>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteConfirm(null)}>{t("common.cancel")}</Button>
<Button variant="destructive" onClick={handleDeleteUser} disabled={deleteUser.isPending}>
<Button variant="outline" className="text-destructive hover:bg-destructive hover:text-destructive-foreground" onClick={handleDeleteUser} disabled={deleteUser.isPending}>
{deleteUser.isPending ? t("detail.deleting") : t("common.delete")}
</Button>
</DialogFooter>
+25 -7
View File
@@ -127,9 +127,15 @@ export default function Analytics() {
<YAxis domain={[0, 5]} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}`} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<RechartsTooltip
cursor={{ fill: 'hsl(var(--muted))' }}
contentStyle={{ borderRadius: '8px', border: '1px solid hsl(var(--border))' }}
contentStyle={{ backgroundColor: 'hsl(var(--popover))', borderRadius: '8px', border: '1px solid hsl(var(--border))', color: 'hsl(var(--foreground))' }}
/>
<Bar dataKey="score" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} maxBarSize={50} />
<defs>
<linearGradient id="neonBarFillA" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="hsl(var(--glow-1))" />
<stop offset="100%" stopColor="hsl(var(--glow-2))" />
</linearGradient>
</defs>
<Bar dataKey="score" fill="url(#neonBarFillA)" radius={[4, 4, 0, 0]} maxBarSize={50} />
</BarChart>
</ResponsiveContainer>
</div>
@@ -154,7 +160,7 @@ export default function Analytics() {
<PolarAngleAxis dataKey="category" tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<PolarRadiusAxis angle={30} domain={[0, 'auto']} tick={false} axisLine={false} />
<Radar name={t("analytics.radarTools")} dataKey="tools" stroke="hsl(var(--primary))" fill="hsl(var(--primary))" fillOpacity={0.3} />
<RechartsTooltip />
<RechartsTooltip contentStyle={{ backgroundColor: 'hsl(var(--popover))', borderRadius: '8px', border: '1px solid hsl(var(--border))', color: 'hsl(var(--foreground))' }} />
</RadarChart>
</ResponsiveContainer>
</div>
@@ -181,8 +187,14 @@ export default function Analytics() {
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis dataKey="score" tickLine={false} axisLine={false} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<YAxis hide />
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ borderRadius: '6px', border: '1px solid hsl(var(--border))' }} />
<Bar dataKey="count" fill="hsl(var(--primary))" radius={[4, 4, 0, 0]} maxBarSize={40} />
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ backgroundColor: 'hsl(var(--popover))', borderRadius: '6px', border: '1px solid hsl(var(--border))', color: 'hsl(var(--foreground))' }} />
<defs>
<linearGradient id="neonBarFillB" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="hsl(var(--glow-1))" />
<stop offset="100%" stopColor="hsl(var(--glow-2))" />
</linearGradient>
</defs>
<Bar dataKey="count" fill="url(#neonBarFillB)" radius={[4, 4, 0, 0]} maxBarSize={40} />
</BarChart>
</ResponsiveContainer>
</div>
@@ -195,8 +207,14 @@ export default function Analytics() {
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis dataKey="score" tickLine={false} axisLine={false} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<YAxis hide />
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ borderRadius: '6px', border: '1px solid hsl(var(--border))' }} />
<Bar dataKey="count" fill="hsl(var(--chart-3))" radius={[4, 4, 0, 0]} maxBarSize={40} />
<RechartsTooltip cursor={{ fill: 'hsl(var(--muted))' }} contentStyle={{ backgroundColor: 'hsl(var(--popover))', borderRadius: '6px', border: '1px solid hsl(var(--border))', color: 'hsl(var(--foreground))' }} />
<defs>
<linearGradient id="neonBarFillC" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="hsl(var(--glow-1))" />
<stop offset="100%" stopColor="hsl(var(--glow-2))" />
</linearGradient>
</defs>
<Bar dataKey="count" fill="url(#neonBarFillC)" radius={[4, 4, 0, 0]} maxBarSize={40} />
</BarChart>
</ResponsiveContainer>
</div>
+9 -3
View File
@@ -717,8 +717,14 @@ export default function ToolDetail() {
<BarChart data={usefulnessData} layout="vertical" margin={{ top: 0, right: 0, bottom: 0, left: -20 }}>
<XAxis type="number" hide />
<YAxis dataKey="score" type="category" axisLine={false} tickLine={false} tickFormatter={(val) => `${val}`} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} />
<Tooltip cursor={{ fill: 'transparent' }} />
<Bar dataKey="count" fill="hsl(var(--primary))" radius={[0, 4, 4, 0]} barSize={12} />
<Tooltip cursor={{ fill: 'transparent' }} contentStyle={{ backgroundColor: 'hsl(var(--popover))', border: '1px solid hsl(var(--border))', borderRadius: '8px', color: 'hsl(var(--foreground))' }} />
<defs>
<linearGradient id="neonBarFill" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="hsl(var(--glow-1))" />
<stop offset="100%" stopColor="hsl(var(--glow-2))" />
</linearGradient>
</defs>
<Bar dataKey="count" fill="url(#neonBarFill)" radius={[0, 4, 4, 0]} barSize={12} />
</BarChart>
</ResponsiveContainer>
</div>
@@ -740,7 +746,7 @@ export default function ToolDetail() {
<CartesianGrid strokeDasharray="3 3" opacity={0.2} />
<XAxis dataKey="date" hide />
<YAxis domain={[0, 5]} tick={{ fill: 'hsl(var(--foreground))', fontSize: 12 }} axisLine={false} tickLine={false} />
<Tooltip labelFormatter={(_, payload) => (payload?.[0] ? format(new Date(payload[0].payload.date), "dd.MM.yyyy HH:mm") : "")} />
<Tooltip labelFormatter={(_, payload) => (payload?.[0] ? format(new Date(payload[0].payload.date), "dd.MM.yyyy HH:mm") : "")} contentStyle={{ backgroundColor: 'hsl(var(--popover))', border: '1px solid hsl(var(--border))', borderRadius: '8px', color: 'hsl(var(--foreground))' }} />
<Legend wrapperStyle={{ fontSize: 12 }} />
<Line type="monotone" dataKey="combined" name={t("detail.score")} stroke="hsl(var(--primary))" strokeWidth={2} dot={false} />
<Line type="monotone" dataKey="usefulness" name={t("detail.usefulness")} stroke="hsl(var(--success))" strokeWidth={1.5} dot={false} strokeDasharray="4 3" />
+2 -1
View File
@@ -192,7 +192,8 @@ export default function Trash() {
<>
<Button
size="sm"
variant="destructive"
variant="outline"
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
disabled={selectedIds.length === 0 || deletePermanent.isPending}
onClick={() => setConfirmDelete(true)}
>