chore: split dashboard quick actions into quarters and drop host status row lines

This commit is contained in:
LukeGus committed 2026-10-06 14:30:45 -05:00
1 parent 46b3adeb57
commit dbf6c0117f
3 files changed
+78 -31

No files matched your search

+43 -31
View File
@@ -24,6 +24,7 @@ import {
import { getDefaultConnectionTab } from "@/lib/host-connection-tabs"; import { getDefaultConnectionTab } from "@/lib/host-connection-tabs";
import type { RecentActivityItem } from "@/main-axios"; import type { RecentActivityItem } from "@/main-axios";
import type { Host, TabType } from "@/types/ui-types"; import type { Host, TabType } from "@/types/ui-types";
import { quarterBorders } from "./quarter-borders";
export type VersionStatus = export type VersionStatus =
"up_to_date" | "requires_update" | "beta" | "unknown"; "up_to_date" | "requires_update" | "beta" | "unknown";
@@ -161,17 +162,19 @@ function ActionButton({
label, label,
hint, hint,
onClick, onClick,
className,
}: { }: {
icon: React.ElementType; icon: React.ElementType;
label: string; label: string;
hint: string; hint: string;
onClick: () => void; onClick: () => void;
className?: string;
}) { }) {
return ( return (
<button <button
type="button" type="button"
onClick={onClick} onClick={onClick}
className="group/btn flex h-full min-w-0 items-center gap-2 border border-border/60 px-2 py-1.5 text-left transition-colors hover:border-accent-brand/40 hover:bg-muted/50" className={`group/btn flex h-full min-w-0 items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-muted/50 ${className ?? ""}`}
> >
<span className="flex size-6 shrink-0 items-center justify-center border border-border bg-muted transition-colors group-hover/btn:border-accent-brand/40 group-hover/btn:bg-accent-brand/20"> <span className="flex size-6 shrink-0 items-center justify-center border border-border bg-muted transition-colors group-hover/btn:border-accent-brand/40 group-hover/btn:bg-accent-brand/20">
<Icon className="size-3 text-accent-brand" /> <Icon className="size-3 text-accent-brand" />
@@ -208,39 +211,48 @@ export function QuickActions({
: host.port; : host.port;
return `${host.ip}:${port ?? host.port}`; return `${host.ip}:${port ?? host.port}`;
}; };
const actions = [
{
icon: Plus,
label: t("dashboard.addHost"),
hint: t("dashboardTab.registerNewServer"),
onClick: () =>
onOpenSingletonTab("host-manager", "host-manager:add-host"),
},
{
icon: KeyRound,
label: t("dashboard.addCredential"),
hint: t("dashboardTab.storeSshKeysOrPasswords"),
onClick: () =>
onOpenSingletonTab("host-manager", "host-manager:add-credential"),
},
...(isAdmin
? [
{
icon: Settings,
label: t("dashboard.adminSettings"),
hint: t("dashboardTab.manageUsersAndRoles"),
onClick: () => onOpenSingletonTab("admin-settings"),
},
]
: []),
{
icon: User,
label: t("dashboard.userProfile"),
hint: t("dashboardTab.manageYourAccount"),
onClick: () => onOpenSingletonTab("user-profile"),
},
];
return ( return (
<div className="flex min-h-full flex-1 flex-col"> <div className="flex min-h-full flex-1 flex-col">
<div className="grid flex-1 auto-rows-fr grid-cols-1 gap-1 p-2 sm:grid-cols-2"> <div className="grid flex-1 auto-rows-fr grid-cols-1 sm:grid-cols-2">
<ActionButton {actions.map((action, i) => (
icon={Plus}
label={t("dashboard.addHost")}
hint={t("dashboardTab.registerNewServer")}
onClick={() =>
onOpenSingletonTab("host-manager", "host-manager:add-host")
}
/>
<ActionButton
icon={KeyRound}
label={t("dashboard.addCredential")}
hint={t("dashboardTab.storeSshKeysOrPasswords")}
onClick={() =>
onOpenSingletonTab("host-manager", "host-manager:add-credential")
}
/>
{isAdmin && (
<ActionButton <ActionButton
icon={Settings} key={action.label}
label={t("dashboard.adminSettings")} {...action}
hint={t("dashboardTab.manageUsersAndRoles")} className={quarterBorders(i, actions.length)}
onClick={() => onOpenSingletonTab("admin-settings")}
/> />
)} ))}
<ActionButton
icon={User}
label={t("dashboard.userProfile")}
hint={t("dashboardTab.manageYourAccount")}
onClick={() => onOpenSingletonTab("user-profile")}
/>
</div> </div>
{pinnedHosts.length > 0 && ( {pinnedHosts.length > 0 && (
<div className="flex flex-col border-t border-border"> <div className="flex flex-col border-t border-border">
@@ -308,7 +320,7 @@ export function HostStatusList({
defaultConnectAction(actions, host)?.tabType; defaultConnectAction(actions, host)?.tabType;
if (target) onOpenTab(host, target); if (target) onOpenTab(host, target);
}} }}
className="group/row flex min-w-0 cursor-pointer items-center gap-2 border-b border-border/60 px-3 py-1.5 last:border-0 hover:bg-muted/50" className="group/row flex min-w-0 cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-muted/50"
> >
<span <span
className={`size-1.5 shrink-0 rounded-full ${getStatusClasses(availability, scheme, "dot", statusLoading)}`} className={`size-1.5 shrink-0 rounded-full ${getStatusClasses(availability, scheme, "dot", statusLoading)}`}
+15
View File
@@ -0,0 +1,15 @@
/**
* Lines only between cells, so the actions read as quarters: one column on
* small screens, two above. An odd last cell spans the full row.
*/
export function quarterBorders(index: number, count: number): string {
const classes = ["border-border"];
const last = index === count - 1;
if (!last) classes.push("border-b");
if (Math.floor(index / 2) === Math.floor((count - 1) / 2)) {
classes.push("sm:border-b-0");
}
if (index % 2 === 0 && !last) classes.push("sm:border-r");
if (last && count % 2 === 1) classes.push("sm:col-span-2");
return classes.join(" ");
}
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import { quarterBorders } from "@/dashboard/quarter-borders";
describe("quarterBorders", () => {
it("draws a cross between four cells", () => {
expect([0, 1, 2, 3].map((i) => quarterBorders(i, 4))).toEqual([
"border-border border-b sm:border-r",
"border-border border-b",
"border-border border-b sm:border-b-0 sm:border-r",
"border-border sm:border-b-0",
]);
});
it("stretches an odd last cell across the row", () => {
expect(quarterBorders(2, 3)).toBe(
"border-border sm:border-b-0 sm:col-span-2",
);
expect(quarterBorders(1, 3)).toBe("border-border border-b");
});
});