Skip to content

Commit 999aadf

Browse files
samejrclaude
andcommitted
feat(webapp): put the smart column tooltip on the bolt, fix light mode actions
- The runs table header tooltip now hangs off the bolt itself rather than the info icon TableHeaderCell adds alongside it, and reads as one paragraph. - Cap the full-value tooltip and let it scroll (max-w-sm / max-h-64); a whole payload string is unbounded, so the box could grow arbitrarily tall. MiddleTruncate gains an optional tooltipContentClassName for this. - Wrap long preview text instead of forcing one line. Rows grow with their content now, so items-center no longer pushes overflow above the header -- the reason the previous fix had to disable wrapping. - Match the runs table in the preview header: label first, then the bolt. - The row edit/remove/reorder actions were raw buttons with a hardcoded charcoal hover, so light mode got a dark background. They now use Button variant="minimal/small", whose hover is the bg-tertiary token (verified rgb(39,42,46) dark / rgb(238,240,243) light), still square at 24px. Button forwards no onKeyDown, so the reorder arrow keys listen on a wrapper and catch the event bubbling from the focused button. - Even out the three footer icons at 1.15rem and give the outline star 2px strokes, matching the custom icons beside it (it defaults to 1.5px). - Replace the reset arrow with a custom ResetIcon. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9da734f commit 999aadf

7 files changed

Lines changed: 100 additions & 49 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export function ResetIcon({ className }: { className?: string }) {
2+
return (
3+
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4+
<path
5+
d="M7 3L4 6L7 9"
6+
stroke="currentColor"
7+
strokeWidth="2"
8+
strokeLinecap="round"
9+
strokeLinejoin="round"
10+
/>
11+
<path
12+
d="M5 6H13.5C17.0899 6 20 8.91015 20 12.5C20 16.0899 17.0899 19 13.5 19H6"
13+
stroke="currentColor"
14+
strokeWidth="2"
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
/>
18+
</svg>
19+
);
20+
}

apps/webapp/app/components/primitives/MiddleTruncate.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ type MiddleTruncateProps = {
77
className?: string;
88
/** Hover delay before the full-text tooltip opens. Defaults to the tooltip default (0). */
99
tooltipDelay?: number;
10+
/** Merged onto the tooltip body, for callers whose text needs a bigger or scrollable box. */
11+
tooltipContentClassName?: string;
1012
};
1113

1214
/**
@@ -15,7 +17,12 @@ type MiddleTruncateProps = {
1517
*
1618
* Example: "namespace:category:subcategory:task-name" becomes "namespace:cat…task-name"
1719
*/
18-
export function MiddleTruncate({ text, className, tooltipDelay }: MiddleTruncateProps) {
20+
export function MiddleTruncate({
21+
text,
22+
className,
23+
tooltipDelay,
24+
tooltipContentClassName,
25+
}: MiddleTruncateProps) {
1926
const containerRef = useRef<HTMLSpanElement>(null);
2027
const measureRef = useRef<HTMLSpanElement>(null);
2128
const [displayText, setDisplayText] = useState(text);
@@ -153,7 +160,11 @@ export function MiddleTruncate({ text, className, tooltipDelay }: MiddleTruncate
153160
return (
154161
<SimpleTooltip
155162
button={content}
156-
content={<span className="max-w-xs break-all font-mono text-xs">{text}</span>}
163+
content={
164+
<span className={cn("max-w-xs break-all font-mono text-xs", tooltipContentClassName)}>
165+
{text}
166+
</span>
167+
}
157168
side="top"
158169
asChild
159170
delayDuration={tooltipDelay}

apps/webapp/app/components/runs/v3/AddSmartColumnDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,10 +356,10 @@ function SmartColumnPreview({
356356
return (
357357
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border border-grid-dimmed">
358358
<div className="flex flex-none items-center gap-1 border-b border-grid-dimmed bg-background-dimmed px-2.5 py-1.5">
359-
<SmartColumnIcon className="size-3.5 flex-none text-text-dimmed" />
360359
<span className="truncate text-xs font-medium text-text-bright">
361360
{def.label || "Column"}
362361
</span>
362+
<SmartColumnIcon className="size-4 flex-none text-text-dimmed" />
363363
</div>
364364
<div className="flex-1 overflow-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
365365
{!loaded ? (
@@ -375,7 +375,7 @@ function SmartColumnPreview({
375375
<div
376376
key={index}
377377
className={cn(
378-
"flex h-8 items-center whitespace-nowrap border-b border-grid-dimmed/60 px-2.5 text-sm last:border-b-0",
378+
"flex min-h-8 items-center break-all border-b border-grid-dimmed/60 px-2.5 py-1.5 text-sm last:border-b-0",
379379
alignClass
380380
)}
381381
>

apps/webapp/app/components/runs/v3/RunsDisplayOptions.tsx

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import {
2-
ArrowUturnLeftIcon,
3-
PencilSquareIcon,
4-
StarIcon as StarIconSolid,
5-
XMarkIcon,
6-
} from "@heroicons/react/20/solid";
1+
import { PencilSquareIcon, StarIcon as StarIconSolid, XMarkIcon } from "@heroicons/react/20/solid";
72
import { StarIcon as StarIconOutline } from "@heroicons/react/24/outline";
83
import { GripVerticalIcon } from "lucide-react";
94
import { useMemo, useRef, useState } from "react";
105
import { ColumnsIcon } from "~/assets/icons/ColumnsIcon";
6+
import { ResetIcon } from "~/assets/icons/ResetIcon";
117
import { SmartColumnIcon } from "~/assets/icons/SmartColumnIcon";
128
import { useFavoritePageToggle } from "~/components/navigation/favoritePages";
139
import { Button } from "~/components/primitives/Buttons";
@@ -43,6 +39,9 @@ function keyFor(col: ResolvedColumn): string {
4339

4440
type SmartEditTarget = { index: number; def: SmartColumnDef };
4541

42+
/** The three footer actions share one icon size so the mixed icon sets line up. */
43+
const FOOTER_ICON_CLASS = "size-[1.15rem]";
44+
4645
/** Opens the Columns popover. "l" is free on every list this control appears on. */
4746
export const COLUMNS_SHORTCUT = { key: "l" as const };
4847

@@ -240,14 +239,17 @@ export function RunsDisplayOptions({
240239
title="Add smart column…"
241240
onClick={() => setAddOpen(true)}
242241
className="h-8"
242+
leadingIconClassName={FOOTER_ICON_CLASS}
243243
/>
244244
{canFavorite && (
245245
<PopoverMenuItem
246246
icon={
247247
isFavorited ? (
248-
<StarIconSolid className="size-4 text-yellow-500" />
248+
<StarIconSolid className={cn(FOOTER_ICON_CLASS, "text-yellow-500")} />
249249
) : (
250-
<StarIconOutline className="size-4" />
250+
// The outline star is 1.5px by default, noticeably thinner than the
251+
// custom 2px icons beside it.
252+
<StarIconOutline className={FOOTER_ICON_CLASS} strokeWidth={2} />
251253
)
252254
}
253255
title={isFavorited ? "Remove from favorites" : "Save to favorites"}
@@ -258,7 +260,7 @@ export function RunsDisplayOptions({
258260
{/* Wrapper carries the cursor: the disabled button has pointer-events-none. */}
259261
<div className={cn("flex", !layout.isCustomized && "cursor-not-allowed")}>
260262
<PopoverMenuItem
261-
icon={ArrowUturnLeftIcon}
263+
icon={ResetIcon}
262264
title="Reset to default"
263265
onClick={reset}
264266
disabled={!layout.isCustomized}
@@ -285,6 +287,13 @@ export function RunsDisplayOptions({
285287
);
286288
}
287289

290+
/**
291+
* The row's hover-revealed actions. Square, and hidden until the row is hovered or the
292+
* control itself takes keyboard focus (a checkbox click must not reveal them).
293+
*/
294+
const ROW_ACTION_CLASS =
295+
"aspect-square h-6 p-1 opacity-0 transition group-hover:opacity-100 group-focus-visible/button:opacity-100";
296+
288297
function ColumnRow({
289298
col,
290299
checked,
@@ -358,28 +367,27 @@ function ColumnRow({
358367
</label>
359368
<div className="flex flex-none items-center gap-0.5 pr-1">
360369
{onEdit && (
361-
<button
362-
type="button"
370+
<Button
371+
variant="minimal/small"
363372
onClick={onEdit}
364373
aria-label={`Edit ${col.def.label}`}
365-
className="flex size-6 cursor-pointer items-center justify-center rounded-sm text-text-dimmed opacity-0 transition hover:bg-charcoal-700 hover:text-text-bright focus-custom focus-visible:opacity-100 group-hover:opacity-100"
366-
>
367-
<PencilSquareIcon className="size-4" />
368-
</button>
374+
LeadingIcon={<PencilSquareIcon className="size-4" />}
375+
className={ROW_ACTION_CLASS}
376+
/>
369377
)}
370378
{onRemove && (
371-
<button
372-
type="button"
379+
<Button
380+
variant="minimal/small"
373381
onClick={onRemove}
374382
aria-label={`Remove ${col.def.label}`}
375-
className="flex size-6 cursor-pointer items-center justify-center rounded-sm text-text-dimmed opacity-0 transition hover:bg-charcoal-700 hover:text-error focus-custom focus-visible:opacity-100 group-hover:opacity-100"
376-
>
377-
<XMarkIcon className="size-4" />
378-
</button>
383+
LeadingIcon={<XMarkIcon className="size-4" />}
384+
className={cn(ROW_ACTION_CLASS, "group-hover/button:text-error")}
385+
/>
379386
)}
380-
<button
381-
type="button"
382-
aria-label={`Reorder ${col.def.label} (use arrow up and down)`}
387+
{/* Button forwards no onKeyDown, so the arrow-key reorder listens on the wrapper
388+
and catches the event bubbling up from the focused button. */}
389+
<span
390+
role="presentation"
383391
onKeyDown={(e) => {
384392
if (e.key === "ArrowUp") {
385393
e.preventDefault();
@@ -389,10 +397,14 @@ function ColumnRow({
389397
onMove(1);
390398
}
391399
}}
392-
className="flex size-6 cursor-grab items-center justify-center rounded-sm text-text-dimmed opacity-0 transition hover:bg-charcoal-700 hover:text-text-bright focus-custom focus-visible:opacity-100 group-hover:opacity-100 active:cursor-grabbing"
393400
>
394-
<GripVerticalIcon className="size-4" />
395-
</button>
401+
<Button
402+
variant="minimal/small"
403+
aria-label={`Reorder ${col.def.label} (use arrow up and down)`}
404+
LeadingIcon={<GripVerticalIcon className="size-4" />}
405+
className={cn(ROW_ACTION_CLASS, "cursor-grab active:cursor-grabbing")}
406+
/>
407+
</span>
396408
</div>
397409
</div>
398410
);

apps/webapp/app/components/runs/v3/TaskRunsTable.tsx

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -494,25 +494,24 @@ const SMART_SOURCE_LABELS: Record<SmartColumnSource, string> = {
494494

495495
function SmartColumnHeader({ def }: { def: SmartColumnDef }) {
496496
return (
497-
<TableHeaderCell
498-
tooltip={
499-
<div className="flex max-w-xs flex-col gap-1 p-1">
500-
<Paragraph variant="small" className="text-text-bright">
501-
Smart column
502-
</Paragraph>
503-
<Paragraph variant="extra-small" className="text-wrap! text-text-dimmed">
504-
Reads <span className="font-mono text-text-bright">{def.path}</span> from each run's{" "}
505-
{SMART_SOURCE_LABELS[def.source]}, shown as {def.displayAs}.
506-
</Paragraph>
507-
<Paragraph variant="extra-small" className="text-wrap! text-text-dimmed">
508-
Display only, so this column can't be sorted or filtered.
509-
</Paragraph>
510-
</div>
511-
}
512-
>
497+
<TableHeaderCell>
513498
<span className="flex items-center gap-1">
514499
<span className="truncate">{def.label}</span>
515-
<SmartColumnIcon className="size-4 flex-none text-text-dimmed" />
500+
{/* The bolt is the tooltip trigger, so the cell doesn't also get an info icon. */}
501+
<SimpleTooltip
502+
disableHoverableContent
503+
button={<SmartColumnIcon className="size-4 flex-none text-text-dimmed" />}
504+
content={
505+
<Paragraph
506+
variant="extra-small"
507+
className="max-w-xs text-wrap! normal-case tracking-normal text-text-dimmed"
508+
>
509+
Reads <span className="font-mono text-text-bright">{def.path}</span> from each run's{" "}
510+
{SMART_SOURCE_LABELS[def.source]}, shown as {def.displayAs}. Display only, so this
511+
column can't be sorted or filtered.
512+
</Paragraph>
513+
}
514+
/>
516515
</span>
517516
</TableHeaderCell>
518517
);

apps/webapp/app/components/runs/v3/smartColumnCell.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ function toFiniteNumber(value: unknown): number {
3737
const TEXT_CELL_MAX_WIDTH = "max-w-[600px]";
3838
/** Long values are common enough that an instant tooltip would fire while just scanning rows. */
3939
const TEXT_CELL_TOOLTIP_DELAY_MS = 500;
40+
/** A whole payload string can be arbitrarily long, so the tooltip is capped and scrolls. */
41+
const TEXT_CELL_TOOLTIP_CLASS =
42+
"block max-w-sm max-h-64 overflow-y-auto whitespace-pre-wrap scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control";
4043

4144
function renderSmartValue(
4245
value: unknown,
@@ -62,7 +65,11 @@ function renderSmartValue(
6265
// MiddleTruncate measures against its parent, so it needs the width cap around it.
6366
return (
6467
<span className={cn("block min-w-0", TEXT_CELL_MAX_WIDTH)}>
65-
<MiddleTruncate text={text} tooltipDelay={TEXT_CELL_TOOLTIP_DELAY_MS} />
68+
<MiddleTruncate
69+
text={text}
70+
tooltipDelay={TEXT_CELL_TOOLTIP_DELAY_MS}
71+
tooltipContentClassName={TEXT_CELL_TOOLTIP_CLASS}
72+
/>
6673
</span>
6774
);
6875
}

apps/webapp/app/routes/storybook.icons/route.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import { QuestionMarkIcon } from "~/assets/icons/QuestionMarkIcon";
105105
import { QueuesIcon } from "~/assets/icons/QueuesIcon";
106106
import { RadarPulseIcon } from "~/assets/icons/RadarPulseIcon";
107107
import { FlagEurope, FlagUSA } from "~/assets/icons/RegionIcons";
108+
import { ResetIcon } from "~/assets/icons/ResetIcon";
108109
import { RightSideMenuIcon } from "~/assets/icons/RightSideMenuIcon";
109110
import { RolesIcon } from "~/assets/icons/RolesIcon";
110111
import { RunFunctionIcon } from "~/assets/icons/RunFunctionIcon";
@@ -251,6 +252,7 @@ const icons: IconEntry[] = [
251252
{ name: "QuestionMarkIcon", render: simple(QuestionMarkIcon) },
252253
{ name: "QueuesIcon", render: simple(QueuesIcon) },
253254
{ name: "RadarPulseIcon", render: simple(RadarPulseIcon) },
255+
{ name: "ResetIcon", render: simple(ResetIcon) },
254256
{ name: "RightSideMenuIcon", render: simple(RightSideMenuIcon) },
255257
{ name: "RolesIcon", render: simple(RolesIcon) },
256258
{ name: "RunFunctionIcon", render: simple(RunFunctionIcon) },

0 commit comments

Comments
 (0)