Skip to content

Commit 40d13aa

Browse files
fix: scale selection label arrow proportionally for narrow labels (aidenybai#165)
The arrow (16px wide) overflowed labels with short tag names like "p". Arrow size now scales based on the inner panel width via a shared getArrowSize utility, and label positioning uses the actual arrow height to eliminate gaps. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 41cdb21 commit 40d13aa

File tree

5 files changed

+36
-7
lines changed

5 files changed

+36
-7
lines changed

packages/react-grab/src/components/selection-label/arrow.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { Component } from "solid-js";
22
import type { ArrowProps } from "../../types.js";
3+
import { getArrowSize } from "../../utils/get-arrow-size.js";
34

45
export const Arrow: Component<ArrowProps> = (props) => {
56
const arrowColor = () => props.color ?? "white";
67
const isBottom = () => props.position === "bottom";
8+
const arrowSize = () => getArrowSize(props.labelWidth ?? 0);
79

810
return (
911
<div
@@ -16,10 +18,14 @@ export const Arrow: Component<ArrowProps> = (props) => {
1618
transform: isBottom()
1719
? "translateX(-50%) translateY(-100%)"
1820
: "translateX(-50%) translateY(100%)",
19-
"border-left": "8px solid transparent",
20-
"border-right": "8px solid transparent",
21-
"border-bottom": isBottom() ? `8px solid ${arrowColor()}` : undefined,
22-
"border-top": isBottom() ? undefined : `8px solid ${arrowColor()}`,
21+
"border-left": `${arrowSize()}px solid transparent`,
22+
"border-right": `${arrowSize()}px solid transparent`,
23+
"border-bottom": isBottom()
24+
? `${arrowSize()}px solid ${arrowColor()}`
25+
: undefined,
26+
"border-top": isBottom()
27+
? undefined
28+
: `${arrowSize()}px solid ${arrowColor()}`,
2329
filter: isBottom()
2430
? "drop-shadow(-1px -1px 0 rgba(0,0,0,0.06)) drop-shadow(1px -1px 0 rgba(0,0,0,0.06))"
2531
: "drop-shadow(-1px 1px 0 rgba(0,0,0,0.06)) drop-shadow(1px 1px 0 rgba(0,0,0,0.06))",

packages/react-grab/src/components/selection-label/index.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import type { Component } from "solid-js";
1010
import type { ArrowPosition, SelectionLabelProps } from "../../types.js";
1111
import {
1212
VIEWPORT_MARGIN_PX,
13-
ARROW_HEIGHT_PX,
1413
ARROW_CENTER_PERCENT,
1514
ARROW_LABEL_MARGIN_PX,
1615
LABEL_GAP_PX,
1716
PANEL_STYLES,
1817
} from "../../constants.js";
18+
import { getArrowSize } from "../../utils/get-arrow-size.js";
1919
import { isKeyboardEventTriggeredByInput } from "../../utils/is-keyboard-event-triggered-by-input.js";
2020
import { cn } from "../../utils/cn.js";
2121
import { getTagDisplay } from "../../utils/get-tag-display.js";
@@ -40,6 +40,7 @@ const DEFAULT_OFFSCREEN_POSITION = {
4040

4141
export const SelectionLabel: Component<SelectionLabelProps> = (props) => {
4242
let containerRef: HTMLDivElement | undefined;
43+
let panelRef: HTMLDivElement | undefined;
4344
let inputRef: HTMLTextAreaElement | undefined;
4445
let isTagCurrentlyHovered = false;
4546
let lastValidPosition: {
@@ -53,6 +54,7 @@ export const SelectionLabel: Component<SelectionLabelProps> = (props) => {
5354

5455
const [measuredWidth, setMeasuredWidth] = createSignal(0);
5556
const [measuredHeight, setMeasuredHeight] = createSignal(0);
57+
const [panelWidth, setPanelWidth] = createSignal(0);
5658
const [arrowPosition, setArrowPosition] =
5759
createSignal<ArrowPosition>("bottom");
5860
const [viewportVersion, setViewportVersion] = createSignal(0);
@@ -89,6 +91,9 @@ export const SelectionLabel: Component<SelectionLabelProps> = (props) => {
8991
setMeasuredWidth(rect.width);
9092
setMeasuredHeight(rect.height);
9193
}
94+
if (panelRef) {
95+
setPanelWidth(panelRef.getBoundingClientRect().width);
96+
}
9297
};
9398

9499
const handleTagHoverChange = (hovered: boolean) => {
@@ -215,11 +220,13 @@ export const SelectionLabel: Component<SelectionLabelProps> = (props) => {
215220
const selectionBottom = bounds.y + bounds.height;
216221
const selectionTop = bounds.y;
217222

223+
const actualArrowHeight = getArrowSize(panelWidth());
224+
218225
// HACK: Use cursorX as anchor point, CSS transform handles centering via translateX(-50%)
219226
// This avoids the flicker when content changes because centering doesn't depend on JS measurement
220227
const anchorX = cursorX;
221228
let edgeOffsetX = 0;
222-
let positionTop = selectionBottom + ARROW_HEIGHT_PX + LABEL_GAP_PX;
229+
let positionTop = selectionBottom + actualArrowHeight + LABEL_GAP_PX;
223230

224231
// Calculate edge clamping offset (only applied when we have valid measurements)
225232
if (labelWidth > 0) {
@@ -234,7 +241,7 @@ export const SelectionLabel: Component<SelectionLabelProps> = (props) => {
234241
}
235242
}
236243

237-
const totalHeightNeeded = labelHeight + ARROW_HEIGHT_PX + LABEL_GAP_PX;
244+
const totalHeightNeeded = labelHeight + actualArrowHeight + LABEL_GAP_PX;
238245
const fitsBelow =
239246
positionTop + labelHeight <= viewportHeight - VIEWPORT_MARGIN_PX;
240247

@@ -371,6 +378,7 @@ export const SelectionLabel: Component<SelectionLabelProps> = (props) => {
371378
position={arrowPosition()}
372379
leftPercent={computedPosition().arrowLeftPercent}
373380
leftOffsetPx={computedPosition().arrowLeftOffset}
381+
labelWidth={panelWidth()}
374382
/>
375383

376384
<Show when={isCompletedStatus() && !props.error}>
@@ -394,6 +402,7 @@ export const SelectionLabel: Component<SelectionLabelProps> = (props) => {
394402
</Show>
395403

396404
<div
405+
ref={panelRef}
397406
class={cn(
398407
"contain-layout flex items-center gap-[5px] rounded-[10px] antialiased w-fit h-fit p-0 [font-synthesis:none] [corner-shape:superellipse(1.25)]",
399408
PANEL_STYLES,

packages/react-grab/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export const FROZEN_GLOW_COLOR = `rgba(${GRAB_PURPLE_RGB}, 0.15)`;
5757
export const FROZEN_GLOW_EDGE_PX = 50;
5858

5959
export const ARROW_HEIGHT_PX = 8;
60+
export const ARROW_MIN_SIZE_PX = 4;
61+
export const ARROW_MAX_LABEL_WIDTH_RATIO = 0.2;
6062
export const ARROW_CENTER_PERCENT = 50;
6163
export const ARROW_LABEL_MARGIN_PX = 16;
6264
export const LABEL_GAP_PX = 4;

packages/react-grab/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ export interface ArrowProps {
544544
leftPercent: number;
545545
leftOffsetPx: number;
546546
color?: string;
547+
labelWidth?: number;
547548
}
548549

549550
export interface TagBadgeProps {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {
2+
ARROW_HEIGHT_PX,
3+
ARROW_MIN_SIZE_PX,
4+
ARROW_MAX_LABEL_WIDTH_RATIO,
5+
} from "../constants.js";
6+
7+
export const getArrowSize = (labelWidth: number): number => {
8+
if (labelWidth <= 0) return ARROW_HEIGHT_PX;
9+
const scaledSize = labelWidth * ARROW_MAX_LABEL_WIDTH_RATIO;
10+
return Math.max(ARROW_MIN_SIZE_PX, Math.min(ARROW_HEIGHT_PX, scaledSize));
11+
};

0 commit comments

Comments
 (0)