feat(design): cyan accent, dialog overflow fixes, unified delete buttons, nav polish
This commit is contained in:
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user