feat: row detail options for hosts and credentials, open manage from sidebar header

This commit is contained in:
LukeGus committed 2026-10-06 14:41:57 -05:00
1 parent 08db1fcdde
commit a72aa5b57b
18 files changed
+415 -45

No files matched your search

@@ -15,9 +15,11 @@ import { toast } from "sonner";
import { copyToClipboard } from "@/lib/clipboard";
import { ComponentSlot } from "@/shell/ActionSlot";
import type { Credential } from "@/types/ui-types";
import type {
CredentialDensity,
CredentialTrayTrigger,
import {
defaultCredentialRowFields,
type CredentialDensity,
type CredentialRowFields,
type CredentialTrayTrigger,
} from "@/types/credential-sidebar-preferences";
/**
@@ -40,6 +42,8 @@ const CREDENTIAL_ITEM_DENSITY_TOKENS = {
},
} as const;
const DEFAULT_ROW_FIELDS = defaultCredentialRowFields();
export function CredentialItem({
cred,
usedByCount = 0,
@@ -53,6 +57,7 @@ export function CredentialItem({
density = "comfortable",
trayTrigger = "hover",
showTags = true,
rowFields = DEFAULT_ROW_FIELDS,
arrangeMode = false,
isDragging = false,
onReorderDrop,
@@ -80,6 +85,7 @@ export function CredentialItem({
density?: CredentialDensity;
trayTrigger?: CredentialTrayTrigger;
showTags?: boolean;
rowFields?: CredentialRowFields;
/** When true (rearranging unlocked), the row can be dragged to reorder or
* onto a folder header to move. */
arrangeMode?: boolean;
@@ -103,6 +109,8 @@ export function CredentialItem({
const isKey = cred.type === "key";
const tokens = CREDENTIAL_ITEM_DENSITY_TOKENS[density];
const isCompact = density === "compact";
const showUsername = rowFields.showUsername && !!cred.username;
const showUsage = rowFields.showUsageCount && usedByCount > 0;
const isTouchOnly =
typeof window !== "undefined" && window.matchMedia("(hover: none)").matches;
@@ -281,7 +289,7 @@ export function CredentialItem({
className={`${tokens.nameTextSize} font-semibold truncate text-foreground leading-none tracking-tight`}
>
{cred.name}
{cred.isShared && (
{rowFields.showSharedBadge && cred.isShared && (
<span
className="ml-1 inline-flex items-center gap-0.5 text-[9px] uppercase text-accent-brand/80"
title={t("credentials.sharedBy", {
@@ -293,21 +301,27 @@ export function CredentialItem({
</span>
)}
</span>
<span
className={`text-[9px] px-1 py-px font-bold border leading-none shrink-0 ${isKey ? "border-accent-brand/30 text-accent-brand" : "border-border/60 text-muted-foreground/60"}`}
>
{isKey ? t("credentials.keyBadge") : t("credentials.passwordBadge")}
</span>
{rowFields.showTypeBadge && (
<span
className={`text-[9px] px-1 py-px font-bold border leading-none shrink-0 ${isKey ? "border-accent-brand/30 text-accent-brand" : "border-border/60 text-muted-foreground/60"}`}
>
{isKey
? t("credentials.keyBadge")
: t("credentials.passwordBadge")}
</span>
)}
<ComponentSlot
slotId="credentials.badges"
props={{ credentialId: Number(cred.id) }}
/>
{cred.pin && (
{rowFields.showPinIcon && cred.pin && (
<Pin className="size-2.5 text-accent-brand/50 shrink-0" />
)}
{!shouldUseClickTray && !actionsOnly && (
<span className="text-[11px] text-muted-foreground/45 truncate leading-none ml-auto shrink-0 group-hover:hidden">
{usedByCount > 0 ? `${usedByCount}h` : ""}
{rowFields.showUsageCount && usedByCount > 0
? `${usedByCount}h`
: ""}
</span>
)}
{(shouldUseClickTray || actionsOnly) && (
@@ -331,12 +345,12 @@ export function CredentialItem({
</div>
{/* Username row */}
{tokens.showUsernameRow && (cred.username || usedByCount > 0) && (
{tokens.showUsernameRow && (showUsername || showUsage) && (
<span className="text-[11px] text-muted-foreground/60 truncate leading-none font-mono">
{cred.username}
{usedByCount > 0 && (
{showUsername && cred.username}
{showUsage && (
<span
className={`text-muted-foreground/40${cred.username ? " ml-2" : ""}`}
className={`text-muted-foreground/40${showUsername ? " ml-2" : ""}`}
>
{usedByCount}h
</span>
@@ -15,6 +15,7 @@ import { ReorderIndicator } from "@/sidebar/ReorderIndicator";
import type { Credential } from "@/types/ui-types";
import type {
CredentialDensity,
CredentialRowFields,
CredentialTrayTrigger,
} from "@/types/credential-sidebar-preferences";
import { CredentialItem } from "./CredentialItem/CredentialItem";
@@ -41,6 +42,7 @@ export function CredentialSidebarTree({
density = "comfortable",
trayTrigger = "hover",
showTags = true,
rowFields,
editingFolderName,
editingFolderValue,
onEditingFolderNameChange,
@@ -63,6 +65,7 @@ export function CredentialSidebarTree({
density?: CredentialDensity;
trayTrigger?: CredentialTrayTrigger;
showTags?: boolean;
rowFields?: CredentialRowFields;
editingFolderName: string | null;
editingFolderValue: string;
onEditingFolderNameChange: (name: string | null) => void;
@@ -165,6 +168,11 @@ export function CredentialSidebarTree({
isOpen,
showTags,
tagCount: row.item.tags?.length ?? 0,
hasDetailRow:
!rowFields ||
(rowFields.showUsername && !!row.item.username) ||
(rowFields.showUsageCount &&
(usedByCounts?.get(row.item.id) ?? 0) > 0),
});
},
[
@@ -176,6 +184,8 @@ export function CredentialSidebarTree({
actionsOnly,
density,
showTags,
rowFields,
usedByCounts,
],
);
@@ -223,6 +233,7 @@ export function CredentialSidebarTree({
density,
trayTrigger,
showTags,
rowFields,
]);
function toggleFolder(name: string) {
@@ -422,6 +433,7 @@ export function CredentialSidebarTree({
density={density}
trayTrigger={trayTrigger}
showTags={showTags}
rowFields={rowFields}
arrangeMode={arrangeMode}
isDragging={draggedReorderKey === `cred:${item.id}`}
onReorderDrop={(position) =>
@@ -10,6 +10,7 @@ const HEIGHTS = {
alwaysKey: 100.25,
alwaysPassword: 74,
tags: 18.5,
detail: 15,
},
compact: {
base: 23,
@@ -20,6 +21,7 @@ const HEIGHTS = {
alwaysKey: 74.75,
alwaysPassword: 52,
tags: 12.5,
detail: 0,
},
} as const;
@@ -31,6 +33,7 @@ export function getCredentialRowHeight({
isOpen,
showTags,
tagCount,
hasDetailRow = true,
}: {
density: CredentialDensity;
isKey: boolean;
@@ -39,6 +42,8 @@ export function getCredentialRowHeight({
isOpen: boolean;
showTags: boolean;
tagCount: number;
/** False when the username and usage line has nothing to show. */
hasDetailRow?: boolean;
}): number {
const heights = HEIGHTS[density];
let height: number = heights.base;
@@ -51,5 +56,9 @@ export function getCredentialRowHeight({
height = isKey ? heights.actionsOnlyKey : heights.actionsOnlyPassword;
}
return height + (showTags && tagCount > 0 ? heights.tags : 0);
return (
height +
(showTags && tagCount > 0 ? heights.tags : 0) -
(hasDetailRow ? 0 : heights.detail)
);
}