Skip to content

Commit 8217cf3

Browse files
committed
style: reduce visual weight of suggested actions cards on dashboard
Add variant prop to ActionCardItem with "secondary" styling for suggested actions β€” lighter borders, no hover elevation, muted category badges and icons β€” creating clear visual hierarchy between Quick Actions (primary) and Suggested Actions (secondary).
1 parent 51039bc commit 8217cf3

File tree

3 files changed

+85
-11
lines changed

3 files changed

+85
-11
lines changed

β€Žapps/desktop/src/features/dashboard/components/ActionCardItem.tsxβ€Ž

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ export interface ActionCardItemProps {
1919
isLoading?: boolean | undefined;
2020
specialistId?: string | undefined;
2121
specialistName?: string | undefined;
22+
/** "primary" (default) = full-weight card; "secondary" = lighter, muted card */
23+
variant?: "primary" | "secondary" | undefined;
2224
onClick?: ((actionId: string, prompt: string, label: string) => void) | undefined;
2325
onViewResults?: ((actionId: string) => void) | undefined;
2426
}
2527

26-
export function ActionCardItem({ card, isCompleted, isHero, isLoading, specialistId, specialistName, onClick, onViewResults }: ActionCardItemProps) {
28+
export function ActionCardItem({ card, isCompleted, isHero, isLoading, specialistId, specialistName, variant = "primary", onClick, onViewResults }: ActionCardItemProps) {
2729
const Icon = card.icon;
2830
const config = categoryConfig[card.category];
2931
const specColors = specialistId ? getSpecialistColors(specialistId) : undefined;
32+
const isSecondary = variant === "secondary";
3033

3134
return (
3235
<Card
@@ -37,9 +40,11 @@ export function ActionCardItem({ card, isCompleted, isHero, isLoading, specialis
3740
? "pointer-events-none border-border/30 bg-card/50 opacity-60"
3841
: isCompleted
3942
? "cursor-pointer border-primary/15 bg-card hover:border-primary/25 hover:shadow-sm"
40-
: isHero
41-
? "cursor-pointer border-border/30 bg-card hover:-translate-y-0.5 hover:border-primary/25 hover:shadow-lg hover:shadow-black/5 dark:border-white/[0.08] dark:hover:shadow-black/20"
42-
: "cursor-pointer border-border/20 bg-card hover:-translate-y-0.5 hover:border-primary/20 hover:shadow-lg hover:shadow-black/5 dark:hover:shadow-black/20"
43+
: isSecondary
44+
? "cursor-pointer border-border/10 bg-card/50 hover:border-border/25 hover:bg-card hover:shadow-sm dark:border-white/[0.04] dark:hover:border-white/[0.08]"
45+
: isHero
46+
? "cursor-pointer border-border/30 bg-card hover:-translate-y-0.5 hover:border-primary/25 hover:shadow-lg hover:shadow-black/5 dark:border-white/[0.08] dark:hover:shadow-black/20"
47+
: "cursor-pointer border-border/20 bg-card hover:-translate-y-0.5 hover:border-primary/20 hover:shadow-lg hover:shadow-black/5 dark:hover:shadow-black/20"
4348
}`}
4449
onClick={() => {
4550
if (!isLoading) {
@@ -51,13 +56,13 @@ export function ActionCardItem({ card, isCompleted, isHero, isLoading, specialis
5156
}
5257
}}
5358
>
54-
{/* Accent left bar for hero cards */}
55-
{isHero && !isLoading && !isCompleted ? (
59+
{/* Accent left bar for hero cards only (not secondary) */}
60+
{isHero && !isSecondary && !isLoading && !isCompleted ? (
5661
<div className={`absolute inset-y-0 left-0 w-[2px] rounded-l-[inherit] transition-colors ${config.accentColor} group-hover/action:opacity-100 opacity-60`} />
5762
) : null}
5863
<CardHeader className="flex-1">
5964
<div className="flex items-center gap-2">
60-
<Badge variant="outline" className={`text-[10px] ${config.className}`}>
65+
<Badge variant="outline" className={`text-[10px] ${isSecondary ? "opacity-60" : ""} ${config.className}`}>
6166
{config.label}
6267
</Badge>
6368
{isCompleted ? (
@@ -67,7 +72,7 @@ export function ActionCardItem({ card, isCompleted, isHero, isLoading, specialis
6772
</span>
6873
) : null}
6974
</div>
70-
<CardTitle className={`leading-snug transition-colors group-hover/action:text-primary ${isHero ? "text-[15px] font-bold" : "text-sm"}`}>
75+
<CardTitle className={`leading-snug transition-colors ${isSecondary ? "group-hover/action:text-foreground" : "group-hover/action:text-primary"} ${isHero ? "text-[15px] font-bold" : "text-sm"}`}>
7176
{card.title}
7277
</CardTitle>
7378
<CardDescription className={`leading-relaxed line-clamp-2 ${isHero ? "text-[13px] text-muted-foreground/70" : "text-xs"}`}>
@@ -77,7 +82,9 @@ export function ActionCardItem({ card, isCompleted, isHero, isLoading, specialis
7782
<div className={`rounded-lg p-2 transition-all duration-100 ${
7883
isCompleted
7984
? "bg-emerald-500/10 text-emerald-600 group-hover/action:bg-emerald-500 group-hover/action:text-white dark:text-emerald-400"
80-
: `${config.iconBg} ${config.iconText} ${config.iconHoverBg} ${config.iconHoverText}`
85+
: isSecondary
86+
? `${config.iconBg} ${config.iconText} opacity-50 group-hover/action:opacity-75`
87+
: `${config.iconBg} ${config.iconText} ${config.iconHoverBg} ${config.iconHoverText}`
8188
}`}>
8289
{isLoading ? (
8390
<LoaderCircleIcon className="size-4 animate-spin" />
@@ -89,7 +96,7 @@ export function ActionCardItem({ card, isCompleted, isHero, isLoading, specialis
8996
</div>
9097
</CardAction>
9198
</CardHeader>
92-
<div className={`flex items-center gap-3 border-t py-2.5 px-4 ${isHero ? "border-border/30 dark:border-white/[0.06]" : "border-border/20"}`}>
99+
<div className={`flex items-center gap-3 border-t py-2.5 px-4 ${isSecondary ? "border-border/10 dark:border-white/[0.03]" : isHero ? "border-border/30 dark:border-white/[0.06]" : "border-border/20"}`}>
93100
{isLoading ? (
94101
<span className="animate-pulse font-mono text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70">Starting...</span>
95102
) : isCompleted ? (
@@ -112,7 +119,7 @@ export function ActionCardItem({ card, isCompleted, isHero, isLoading, specialis
112119
</>
113120
) : (
114121
<div className="flex flex-1 items-center justify-between">
115-
<span className={`flex items-center gap-1.5 font-mono text-[10px] font-semibold uppercase tracking-wider transition-colors group-hover/action:text-primary ${isHero ? "text-muted-foreground/60" : "text-muted-foreground/50"}`}>
122+
<span className={`flex items-center gap-1.5 font-mono text-[10px] font-semibold uppercase tracking-wider transition-colors ${isSecondary ? "text-muted-foreground/35 group-hover/action:text-muted-foreground/60" : `group-hover/action:text-primary ${isHero ? "text-muted-foreground/60" : "text-muted-foreground/50"}`}`}>
116123
Run
117124
<ArrowRightIcon className="size-3 transition-transform duration-150 group-hover/action:translate-x-0.5" />
118125
</span>

β€Žapps/desktop/src/features/dashboard/components/SuggestedActionGrid.tsxβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export function SuggestedActionGrid({
6262
card={card}
6363
isCompleted={completedActions?.has(card.id)}
6464
isLoading={isActionLoading}
65+
variant="secondary"
6566
specialistId={card.specialistId}
6667
specialistName={specialists ? getSpecialistName(specialists, card.specialistId) : undefined}
6768
onClick={onActionClick}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it } from "vitest";
2+
import { readFileSync } from "node:fs";
3+
import { resolve } from "node:path";
4+
5+
const dashDir = resolve(
6+
__dirname,
7+
"../../apps/desktop/src/features/dashboard",
8+
);
9+
const readSrc = (path: string) =>
10+
readFileSync(resolve(dashDir, path), "utf-8");
11+
12+
describe("Suggested Actions visual weight reduction", () => {
13+
describe("ActionCardItem supports variant prop", () => {
14+
const cardSrc = readSrc("components/ActionCardItem.tsx");
15+
16+
it("accepts a variant prop in its interface", () => {
17+
expect(cardSrc).toContain("variant");
18+
});
19+
20+
it("applies lighter border styling for secondary variant", () => {
21+
// Secondary cards should have a more muted/transparent border
22+
expect(cardSrc).toMatch(/variant.*secondary|secondary.*variant/s);
23+
});
24+
25+
it("does not apply hover elevation for secondary variant", () => {
26+
// Secondary cards should not translate-y on hover β€” differentiation
27+
// The code should conditionally apply translate based on variant
28+
expect(cardSrc).toMatch(/variant/);
29+
});
30+
});
31+
32+
describe("SuggestedActionGrid passes secondary variant", () => {
33+
const suggestedSrc = readSrc("components/SuggestedActionGrid.tsx");
34+
35+
it("passes variant='secondary' to ActionCardItem", () => {
36+
expect(suggestedSrc).toContain('variant="secondary"');
37+
});
38+
});
39+
40+
describe("ActionCardGrid does NOT pass secondary variant", () => {
41+
const quickSrc = readSrc("components/ActionCardGrid.tsx");
42+
43+
it("does not pass variant='secondary' to ActionCardItem", () => {
44+
expect(quickSrc).not.toContain('variant="secondary"');
45+
});
46+
47+
it("still renders ActionCardItem with default (primary) styling", () => {
48+
expect(quickSrc).toContain("<ActionCardItem");
49+
});
50+
});
51+
52+
describe("Visual differentiation details", () => {
53+
const cardSrc = readSrc("components/ActionCardItem.tsx");
54+
55+
it("secondary variant has distinct card styling from default", () => {
56+
// The card should have different className logic based on variant
57+
// Secondary should use bg-card/50 or similar muted background
58+
expect(cardSrc).toMatch(/secondary/);
59+
});
60+
61+
it("secondary variant mutes the footer run label", () => {
62+
// The secondary variant should produce lighter footer text
63+
expect(cardSrc).toMatch(/variant/);
64+
});
65+
});
66+
});

0 commit comments

Comments
Β (0)