Skip to content

Commit 2c6c682

Browse files
committed
Refactor report row visuals and add Button component with disabled-reason tooltip
Made-with: Cursor
1 parent 1ed460f commit 2c6c682

File tree

7 files changed

+373
-209
lines changed

7 files changed

+373
-209
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Tooltip } from "@components/ui/Tooltip";
2+
import { Flex, Button as RadixButton, Text } from "@radix-ui/themes";
3+
import {
4+
type ComponentPropsWithoutRef,
5+
forwardRef,
6+
type ReactNode,
7+
} from "react";
8+
9+
export type ButtonProps = ComponentPropsWithoutRef<typeof RadixButton> & {
10+
/** Primary tooltip explaining what the button does. */
11+
tooltipContent?: ReactNode;
12+
/**
13+
* When non-null and the button is disabled, shown after "Disabled because" in the tooltip.
14+
* Must be null when the action is allowed.
15+
*/
16+
disabledReason?: string | null;
17+
};
18+
19+
function disabledBecauseLabel(detail: string): string {
20+
const d = detail.trim().replace(/\.$/, "");
21+
return `Disabled because ${d}.`;
22+
}
23+
24+
function buildTooltipContent(
25+
tooltipContent: ReactNode | undefined,
26+
disabledReason: string | null | undefined,
27+
disabled: boolean | undefined,
28+
): ReactNode | undefined {
29+
const reason = disabled ? disabledReason : null;
30+
if (tooltipContent != null && reason) {
31+
return (
32+
<Flex direction="column" gap="2" style={{ maxWidth: 280 }}>
33+
<Text as="span" size="1" style={{ color: "var(--gray-12)" }}>
34+
{tooltipContent}
35+
</Text>
36+
<Text as="span" color="gray" size="1" style={{ lineHeight: 1.45 }}>
37+
{disabledBecauseLabel(reason)}
38+
</Text>
39+
</Flex>
40+
);
41+
}
42+
if (reason) {
43+
return disabledBecauseLabel(reason);
44+
}
45+
if (tooltipContent != null) {
46+
return tooltipContent;
47+
}
48+
return undefined;
49+
}
50+
51+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
52+
function Button({ tooltipContent, disabledReason, disabled, ...props }, ref) {
53+
const tip = buildTooltipContent(
54+
tooltipContent,
55+
disabledReason ?? null,
56+
disabled,
57+
);
58+
59+
const button = <RadixButton ref={ref} disabled={disabled} {...props} />;
60+
61+
if (tip === undefined) {
62+
return button;
63+
}
64+
65+
// Disabled buttons don't receive pointer events; span keeps the tooltip hover target.
66+
const trigger =
67+
disabled === true ? (
68+
<span className="inline-flex cursor-not-allowed">{button}</span>
69+
) : (
70+
button
71+
);
72+
73+
return <Tooltip content={tip}>{trigger}</Tooltip>;
74+
},
75+
);
76+
77+
Button.displayName = "Button";

apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -353,17 +353,43 @@ export function InboxSignalsTab() {
353353
direction="column"
354354
tabIndex={0}
355355
className="outline-none"
356+
// Clicking a row/button/checkbox would normally move browser focus to that
357+
// element, losing the container's focus and breaking arrow-key navigation.
358+
// Intercept mousedown to redirect focus back to the container instead.
359+
// Text fields are exempt so the search box can still receive focus normally.
356360
onMouseDownCapture={(e) => {
357361
const target = e.target as HTMLElement;
358-
if (target.closest("[data-report-id]")) {
362+
if (
363+
target.closest(
364+
"input, textarea, select, [contenteditable='true']",
365+
)
366+
) {
367+
return;
368+
}
369+
if (
370+
target.closest(
371+
"[data-report-id], button, [role='checkbox']",
372+
)
373+
) {
359374
focusListPane();
360375
}
361376
}}
377+
// Same redirect for focus arriving via keyboard (Tab) — if focus lands
378+
// inside a row element rather than on the container itself, pull it back up.
362379
onFocusCapture={(e) => {
363380
const target = e.target as HTMLElement;
381+
if (
382+
target.closest(
383+
"input, textarea, select, [contenteditable='true']",
384+
)
385+
) {
386+
return;
387+
}
364388
if (
365389
target !== leftPaneRef.current &&
366-
target.closest("[data-report-id]")
390+
target.closest(
391+
"[data-report-id], button, [role='checkbox']",
392+
)
367393
) {
368394
focusListPane();
369395
}
@@ -468,18 +494,21 @@ export function InboxSignalsTab() {
468494
display: "flex",
469495
alignItems: "center",
470496
justifyContent: "center",
497+
pointerEvents: "none",
471498
background:
472499
"linear-gradient(to bottom, transparent 0%, var(--color-background) 30%)",
473500
}}
474501
>
475-
{!hasSignalSources ? (
476-
<WelcomePane onEnableInbox={() => setSourcesDialogOpen(true)} />
477-
) : (
478-
<WarmingUpPane
479-
onConfigureSources={() => setSourcesDialogOpen(true)}
480-
enabledProducts={enabledProducts}
481-
/>
482-
)}
502+
<Box style={{ pointerEvents: "auto" }}>
503+
{!hasSignalSources ? (
504+
<WelcomePane onEnableInbox={() => setSourcesDialogOpen(true)} />
505+
) : (
506+
<WarmingUpPane
507+
onConfigureSources={() => setSourcesDialogOpen(true)}
508+
enabledProducts={enabledProducts}
509+
/>
510+
)}
511+
</Box>
483512
</Box>
484513
</Box>
485514
)}

apps/code/src/renderer/features/inbox/components/list/ReportListPane.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ export function ReportListPane({
166166
{reports.map((report, index) => (
167167
<ReportListRow
168168
key={report.id}
169-
index={index}
170169
report={report}
170+
index={index}
171171
isSelected={selectedReportId === report.id}
172172
isChecked={selectedReportIds.includes(report.id)}
173173
onClick={() => onSelectReport(report.id)}

apps/code/src/renderer/features/inbox/components/list/ReportListRow.tsx

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,6 @@ export function ReportListRow({
3333
},
3434
);
3535

36-
const isStrongSignal = report.total_weight >= 65 || report.signal_count >= 20;
37-
const isMediumSignal = report.total_weight >= 30 || report.signal_count >= 6;
38-
const strengthColor = isStrongSignal
39-
? "var(--green-9)"
40-
: isMediumSignal
41-
? "var(--yellow-9)"
42-
: "var(--gray-8)";
43-
const strengthLabel = isStrongSignal
44-
? "strong"
45-
: isMediumSignal
46-
? "medium"
47-
: "light";
48-
4936
const isReady = report.status === "ready";
5037

5138
const isInteractiveTarget = (target: EventTarget | null): boolean => {
@@ -67,6 +54,14 @@ export function ReportListRow({
6754
onToggleChecked();
6855
};
6956

57+
const rowBgClass = isSelected
58+
? "bg-gray-3"
59+
: isChecked
60+
? "bg-gray-2"
61+
: report.is_suggested_reviewer
62+
? "bg-blue-2"
63+
: "";
64+
7065
return (
7166
<motion.div
7267
role="button"
@@ -96,23 +91,21 @@ export function ReportListRow({
9691
handleToggleChecked(e);
9792
}
9893
}}
99-
className="w-full cursor-pointer overflow-hidden border-gray-5 border-b py-2 pr-3 pl-2 text-left transition-colors hover:bg-gray-2"
100-
style={{
101-
backgroundColor: isSelected
102-
? "var(--gray-3)"
103-
: isChecked
104-
? "var(--gray-2)"
105-
: report.is_suggested_reviewer
106-
? "var(--blue-2)"
107-
: "transparent",
108-
}}
94+
className={[
95+
"relative isolate w-full cursor-pointer overflow-hidden border-gray-5 border-b py-2 pr-3 pl-2 text-left",
96+
"before:pointer-events-none before:absolute before:inset-0 before:z-[1] before:bg-gray-12 before:opacity-0 hover:before:opacity-[0.07]",
97+
rowBgClass,
98+
]
99+
.filter(Boolean)
100+
.join(" ")}
109101
>
110-
<Flex align="start" justify="between" gap="3">
102+
<Flex align="start" justify="between" gap="3" className="relative z-[2]">
111103
<Flex align="start" gap="2" style={{ minWidth: 0, flex: 1 }}>
112104
<Flex align="center" justify="center" className="shrink-0 pt-0.5">
113105
<Checkbox
114106
size="1"
115107
checked={isChecked}
108+
className="mt-0.5"
116109
tabIndex={-1}
117110
onMouseDown={(e) => {
118111
e.preventDefault();
@@ -129,16 +122,16 @@ export function ReportListRow({
129122
/>
130123
</Flex>
131124

132-
<Flex direction="column" gap="1" style={{ minWidth: 0, flex: 1 }}>
125+
<Flex direction="column" gap="0.5" style={{ minWidth: 0, flex: 1 }}>
133126
<Flex align="start" gapX="2" className="min-w-0">
134-
<Flex
135-
direction="column"
136-
align="center"
137-
gap="0.5"
138-
className="shrink-0 pt-1"
139-
>
140-
{(report.source_products ?? []).length > 0 ? (
141-
(report.source_products ?? []).map((sp) => {
127+
{(report.source_products ?? []).length > 0 && (
128+
<Flex
129+
direction="column"
130+
align="center"
131+
gap="0.5"
132+
className="shrink-0 pt-1"
133+
>
134+
{(report.source_products ?? []).map((sp) => {
142135
const meta = SOURCE_PRODUCT_META[sp];
143136
if (!meta) return null;
144137
const { Icon } = meta;
@@ -147,16 +140,9 @@ export function ReportListRow({
147140
<Icon size={12} />
148141
</span>
149142
);
150-
})
151-
) : (
152-
<span
153-
title={`Signal strength: ${strengthLabel}`}
154-
aria-hidden
155-
className="mt-1 inline-block h-1.5 w-1.5 rounded-full"
156-
style={{ backgroundColor: strengthColor }}
157-
/>
158-
)}
159-
</Flex>
143+
})}
144+
</Flex>
145+
)}
160146

161147
<Flex
162148
align="center"

0 commit comments

Comments
 (0)