Skip to content

Commit 57e72a2

Browse files
committed
Refactor report row visuals and add Button component with disabled-reason tooltip
Made-with: Cursor
1 parent 6755a6a commit 57e72a2

File tree

7 files changed

+269
-98
lines changed

7 files changed

+269
-98
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: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -322,22 +322,42 @@ export function InboxSignalsTab() {
322322
direction="column"
323323
tabIndex={0}
324324
className="outline-none"
325+
// Clicking a row/button/checkbox would normally move browser focus to that
326+
// element, losing the container's focus and breaking arrow-key navigation.
327+
// Intercept mousedown to redirect focus back to the container instead.
328+
// Text fields are exempt so the search box can still receive focus normally.
325329
onMouseDownCapture={(e) => {
326330
const target = e.target as HTMLElement;
327331
if (
328332
target.closest(
329-
"[data-report-id], button, input, select, textarea, [role='checkbox']",
333+
"input, textarea, select, [contenteditable='true']",
334+
)
335+
) {
336+
return;
337+
}
338+
if (
339+
target.closest(
340+
"[data-report-id], button, [role='checkbox']",
330341
)
331342
) {
332343
focusListPane();
333344
}
334345
}}
346+
// Same redirect for focus arriving via keyboard (Tab) — if focus lands
347+
// inside a row element rather than on the container itself, pull it back up.
335348
onFocusCapture={(e) => {
336349
const target = e.target as HTMLElement;
350+
if (
351+
target.closest(
352+
"input, textarea, select, [contenteditable='true']",
353+
)
354+
) {
355+
return;
356+
}
337357
if (
338358
target !== leftPaneRef.current &&
339359
target.closest(
340-
"[data-report-id], button, input, select, textarea, [role='checkbox']",
360+
"[data-report-id], button, [role='checkbox']",
341361
)
342362
) {
343363
focusListPane();
@@ -441,18 +461,21 @@ export function InboxSignalsTab() {
441461
display: "flex",
442462
alignItems: "center",
443463
justifyContent: "center",
464+
pointerEvents: "none",
444465
background:
445466
"linear-gradient(to bottom, transparent 0%, var(--color-background) 30%)",
446467
}}
447468
>
448-
{!hasSignalSources ? (
449-
<WelcomePane onEnableInbox={() => setSourcesDialogOpen(true)} />
450-
) : (
451-
<WarmingUpPane
452-
onConfigureSources={() => setSourcesDialogOpen(true)}
453-
enabledProducts={enabledProducts}
454-
/>
455-
)}
469+
<Box style={{ pointerEvents: "auto" }}>
470+
{!hasSignalSources ? (
471+
<WelcomePane onEnableInbox={() => setSourcesDialogOpen(true)} />
472+
) : (
473+
<WarmingUpPane
474+
onConfigureSources={() => setSourcesDialogOpen(true)}
475+
enabledProducts={enabledProducts}
476+
/>
477+
)}
478+
</Box>
456479
</Box>
457480
</Box>
458481
)}

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"

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ export function SignalsToolbar({
128128

129129
const {
130130
selectedCount,
131-
canSuppress,
132-
canSnooze,
133-
canDelete,
134-
canReingest,
131+
snoozeDisabledReason,
132+
suppressDisabledReason,
133+
deleteDisabledReason,
134+
reingestDisabledReason,
135135
isSuppressing,
136136
isSnoozing,
137137
isDeleting,
@@ -277,7 +277,7 @@ export function SignalsToolbar({
277277
variant="soft"
278278
color="gray"
279279
className="text-[12px]"
280-
disabled={!canSnooze || isSnoozing}
280+
disabled={snoozeDisabledReason !== null || isSnoozing}
281281
onClick={() => void handleSnooze()}
282282
>
283283
{isSnoozing ? <Spinner size="1" /> : <PauseIcon size={12} />}
@@ -290,7 +290,7 @@ export function SignalsToolbar({
290290
variant="soft"
291291
color="red"
292292
className="text-[12px]"
293-
disabled={!canDelete || isDeleting}
293+
disabled={deleteDisabledReason !== null || isDeleting}
294294
onClick={() => setShowDeleteConfirm(true)}
295295
>
296296
{isDeleting ? <Spinner size="1" /> : <TrashIcon size={12} />}
@@ -302,7 +302,7 @@ export function SignalsToolbar({
302302
variant="soft"
303303
color="red"
304304
className="text-[12px]"
305-
disabled={!canSuppress || isSuppressing}
305+
disabled={suppressDisabledReason !== null || isSuppressing}
306306
onClick={() => setShowSuppressConfirm(true)}
307307
>
308308
{isSuppressing ? (
@@ -319,7 +319,7 @@ export function SignalsToolbar({
319319
variant="soft"
320320
color="gray"
321321
className="text-[12px]"
322-
disabled={!canReingest || isReingesting}
322+
disabled={reingestDisabledReason !== null || isReingesting}
323323
onClick={() => void handleReingest()}
324324
>
325325
{isReingesting ? (

0 commit comments

Comments
 (0)