Skip to content

Commit f96bd7a

Browse files
committed
Refactor report row visuals and add Button component with disabled-reason tooltip
Made-with: Cursor
1 parent 49f9daf commit f96bd7a

7 files changed

Lines changed: 261 additions & 111 deletions

File tree

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: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -324,20 +324,35 @@ export function InboxSignalsTab() {
324324
className="outline-none"
325325
onMouseDownCapture={(e) => {
326326
const target = e.target as HTMLElement;
327+
// Don't steal focus from text fields — we need the search box to stay focused.
327328
if (
328329
target.closest(
329-
"[data-report-id], button, input, select, textarea, [role='checkbox']",
330+
"input, textarea, select, [contenteditable='true']",
331+
)
332+
) {
333+
return;
334+
}
335+
if (
336+
target.closest(
337+
"[data-report-id], button, [role='checkbox']",
330338
)
331339
) {
332340
focusListPane();
333341
}
334342
}}
335343
onFocusCapture={(e) => {
336344
const target = e.target as HTMLElement;
345+
if (
346+
target.closest(
347+
"input, textarea, select, [contenteditable='true']",
348+
)
349+
) {
350+
return;
351+
}
337352
if (
338353
target !== leftPaneRef.current &&
339354
target.closest(
340-
"[data-report-id], button, input, select, textarea, [role='checkbox']",
355+
"[data-report-id], button, [role='checkbox']",
341356
)
342357
) {
343358
focusListPane();
@@ -441,18 +456,21 @@ export function InboxSignalsTab() {
441456
display: "flex",
442457
alignItems: "center",
443458
justifyContent: "center",
459+
pointerEvents: "none",
444460
background:
445461
"linear-gradient(to bottom, transparent 0%, var(--color-background) 30%)",
446462
}}
447463
>
448-
{!hasSignalSources ? (
449-
<WelcomePane onEnableInbox={() => setSourcesDialogOpen(true)} />
450-
) : (
451-
<WarmingUpPane
452-
onConfigureSources={() => setSourcesDialogOpen(true)}
453-
enabledProducts={enabledProducts}
454-
/>
455-
)}
464+
<Box style={{ pointerEvents: "auto" }}>
465+
{!hasSignalSources ? (
466+
<WelcomePane onEnableInbox={() => setSourcesDialogOpen(true)} />
467+
) : (
468+
<WarmingUpPane
469+
onConfigureSources={() => setSourcesDialogOpen(true)}
470+
enabledProducts={enabledProducts}
471+
/>
472+
)}
473+
</Box>
456474
</Box>
457475
</Box>
458476
)}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,9 @@ export function ReportListPane({
163163
// ── Report list ─────────────────────────────────────────────────────────
164164
return (
165165
<>
166-
{reports.map((report, index) => (
166+
{reports.map((report) => (
167167
<ReportListRow
168168
key={report.id}
169-
index={index}
170169
report={report}
171170
isSelected={selectedReportId === report.id}
172171
isChecked={selectedReportIds.includes(report.id)}

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

Lines changed: 33 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { SignalReportPriorityBadge } from "@features/inbox/components/utils/Sign
22
import { SignalReportStatusBadge } from "@features/inbox/components/utils/SignalReportStatusBadge";
33
import { SignalReportSummaryMarkdown } from "@features/inbox/components/utils/SignalReportSummaryMarkdown";
44
import { SOURCE_PRODUCT_META } from "@features/inbox/components/utils/source-product-icons";
5+
import { INBOX_LIST_CHECKBOX_CLASSNAME } from "@features/inbox/utils/inboxConstants";
56
import { EyeIcon } from "@phosphor-icons/react";
67
import { Checkbox, Flex, Text, Tooltip } from "@radix-ui/themes";
78
import type { SignalReport } from "@shared/types";
8-
import { motion } from "framer-motion";
99
import type { KeyboardEvent, MouseEvent } from "react";
1010

1111
interface ReportListRowProps {
@@ -14,7 +14,6 @@ interface ReportListRowProps {
1414
isChecked: boolean;
1515
onClick: () => void;
1616
onToggleChecked: () => void;
17-
index: number;
1817
}
1918

2019
export function ReportListRow({
@@ -23,7 +22,6 @@ export function ReportListRow({
2322
isChecked,
2423
onClick,
2524
onToggleChecked,
26-
index,
2725
}: ReportListRowProps) {
2826
const updatedAtLabel = new Date(report.updated_at).toLocaleDateString(
2927
undefined,
@@ -33,19 +31,6 @@ export function ReportListRow({
3331
},
3432
);
3533

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-
4934
const isReady = report.status === "ready";
5035

5136
const isInteractiveTarget = (target: EventTarget | null): boolean => {
@@ -67,18 +52,20 @@ export function ReportListRow({
6752
onToggleChecked();
6853
};
6954

55+
const rowBgClass = isSelected
56+
? "bg-gray-3"
57+
: isChecked
58+
? "bg-gray-2"
59+
: report.is_suggested_reviewer
60+
? "bg-blue-2"
61+
: "";
62+
7063
return (
71-
<motion.div
64+
// biome-ignore lint/a11y/useSemanticElements: whole row is clickable; nested checkbox cannot live inside <button>
65+
<div
7266
role="button"
7367
tabIndex={-1}
7468
data-report-id={report.id}
75-
initial={{ opacity: 0, y: 6 }}
76-
animate={{ opacity: 1, y: 0 }}
77-
transition={{
78-
duration: 0.22,
79-
delay: Math.min(index * 0.035, 0.35),
80-
ease: [0.22, 1, 0.36, 1],
81-
}}
8269
onMouseDown={(e) => {
8370
e.preventDefault();
8471
}}
@@ -96,23 +83,21 @@ export function ReportListRow({
9683
handleToggleChecked(e);
9784
}
9885
}}
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-
}}
86+
className={[
87+
"relative isolate w-full cursor-pointer overflow-hidden border-gray-5 border-b py-2 pr-3 pl-2 text-left",
88+
"before:pointer-events-none before:absolute before:inset-0 before:z-[1] before:bg-gray-12 before:opacity-0 hover:before:opacity-[0.07]",
89+
rowBgClass,
90+
]
91+
.filter(Boolean)
92+
.join(" ")}
10993
>
110-
<Flex align="start" justify="between" gap="3">
94+
<Flex align="start" justify="between" gap="3" className="relative z-[2]">
11195
<Flex align="start" gap="2" style={{ minWidth: 0, flex: 1 }}>
11296
<Flex align="center" justify="center" className="shrink-0 pt-0.5">
11397
<Checkbox
11498
size="1"
11599
checked={isChecked}
100+
className={INBOX_LIST_CHECKBOX_CLASSNAME}
116101
tabIndex={-1}
117102
onMouseDown={(e) => {
118103
e.preventDefault();
@@ -129,16 +114,16 @@ export function ReportListRow({
129114
/>
130115
</Flex>
131116

132-
<Flex direction="column" gap="1" style={{ minWidth: 0, flex: 1 }}>
117+
<Flex direction="column" gap="0.5" style={{ minWidth: 0, flex: 1 }}>
133118
<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) => {
119+
{(report.source_products ?? []).length > 0 && (
120+
<Flex
121+
direction="column"
122+
align="center"
123+
gap="0.5"
124+
className="shrink-0 pt-1"
125+
>
126+
{(report.source_products ?? []).map((sp) => {
142127
const meta = SOURCE_PRODUCT_META[sp];
143128
if (!meta) return null;
144129
const { Icon } = meta;
@@ -147,16 +132,9 @@ export function ReportListRow({
147132
<Icon size={12} />
148133
</span>
149134
);
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>
135+
})}
136+
</Flex>
137+
)}
160138

161139
<Flex
162140
align="center"
@@ -210,6 +188,6 @@ export function ReportListRow({
210188
</Text>
211189
</Flex>
212190
</Flex>
213-
</motion.div>
191+
</div>
214192
);
215193
}

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)