torch-glare 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -122,6 +122,12 @@ export const badgeStyles = cva(
122
122
  "transition-all duration-200 ease-in-out",
123
123
  "whitespace-nowrap",
124
124
  "[&_i]:!leading-none",
125
+ // The subtle variants blend their label/icon with `mix-blend-luminosity`.
126
+ // `isolate` opens a new stacking context so that blend only composites
127
+ // against the badge's own background — not against arbitrary page layers
128
+ // behind it (semi-transparent rows, gradients, masks), which otherwise
129
+ // ghosts/double-strokes the text in "random" places.
130
+ "isolate",
125
131
  ],
126
132
  {
127
133
  variants: {
@@ -122,7 +122,7 @@ ContextMenuRadioGroup.displayName = ContextMenuPrimitive.RadioGroup.displayName;
122
122
  const ContextMenuContent = React.forwardRef<
123
123
  React.ElementRef<typeof ContextMenuPrimitive.Content>,
124
124
  React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content> &
125
- ContextMenuContentProps & { autoGroup?: boolean }
125
+ ContextMenuContentProps & { autoGroup?: boolean; maxHeight?: number }
126
126
  >(
127
127
  (
128
128
  {
@@ -131,6 +131,8 @@ const ContextMenuContent = React.forwardRef<
131
131
  variant = "PresentationStyle",
132
132
  autoGroup = true,
133
133
  collisionPadding = 8,
134
+ maxHeight = 320,
135
+ style,
134
136
  children,
135
137
  ...props
136
138
  },
@@ -141,13 +143,13 @@ const ContextMenuContent = React.forwardRef<
141
143
  data-theme={theme}
142
144
  ref={ref}
143
145
  collisionPadding={collisionPadding}
144
- className={cn(
145
- menuContentStyles({ variant }),
146
- // Cap to the space Radix has after collision handling so a tall menu
147
- // scrolls instead of overflowing off-screen.
148
- "max-h-[var(--radix-context-menu-content-available-height)]",
149
- className
150
- )}
146
+ // Cap at maxHeight, but never exceed the space Radix has after collision
147
+ // handling. The menu scrolls (overflow on the surface) past this height.
148
+ style={{
149
+ maxHeight: `min(${maxHeight}px, var(--radix-context-menu-content-available-height))`,
150
+ ...style,
151
+ }}
152
+ className={cn(menuContentStyles({ variant }), className)}
151
153
  {...props}
152
154
  >
153
155
  {autoGroup ? autoGroupChildren(children) : children}
@@ -388,6 +390,7 @@ const MenuItemStyles = cva(
388
390
  "border",
389
391
  "border-transparent",
390
392
  "flex",
393
+ "shrink-0", // keep full row height so the menu scrolls instead of squishing
391
394
  "items-center",
392
395
  "justify-start",
393
396
  "text-overflow",
@@ -483,13 +486,13 @@ const menuContentStyles = cva(
483
486
  "rounded-[14px]",
484
487
  "min-w-[240px]",
485
488
  "outline-none",
486
- "overflow-scroll",
489
+ "overflow-y-auto",
490
+ "overflow-x-hidden",
487
491
  // Only animate the OPEN (enter) state. An exit animation on [data-state=closed]
488
492
  // holds the old DOM node during close, which breaks close/reposition on a
489
493
  // second right-click (Radix issue #2572).
490
494
  "data-[state=open]:animate-in",
491
495
  "data-[state=open]:fade-in-0",
492
- "overflow-x-hidden",
493
496
  "scrollbar-hide",
494
497
  "backdrop-blur-[21px]",
495
498
  "flex gap-1 flex-col",
@@ -509,7 +512,7 @@ const menuContentStyles = cva(
509
512
  }
510
513
  );
511
514
 
512
- const menuGroupStyles = cva(["flex", "flex-col"], {
515
+ const menuGroupStyles = cva(["flex", "flex-col", "shrink-0"], {
513
516
  variants: {
514
517
  variant: {
515
518
  // Visually contains its items in a bordered card.
@@ -30,9 +30,16 @@ export function DataViewRadio({
30
30
  <span
31
31
  className={cn(
32
32
  "flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full",
33
- "border border-border-presentation-action-primary bg-background-presentation-form-field-primary transition-colors",
33
+ // Hardcoded literals so the indicator always renders dark inside the
34
+ // DataView regardless of the host app's data-theme (matches the
35
+ // always-dark panel chrome convention). Unselected ring = the spec
36
+ // CheckBox-Primary border (#626467) + BorderStyle fill
37
+ // (rgba(255,255,255,0.05)); same tokens the shared Checkbox uses.
38
+ "border border-[#626467] bg-white/5 transition-colors",
34
39
  "group-data-[state=checked]:border-transparent",
35
- "group-data-[state=checked]:bg-border-presentation-state-focus",
40
+ // #0075FF = the previous selected-fill value (border-presentation-state-focus),
41
+ // now literal so the blue is theme-independent and visually unchanged.
42
+ "group-data-[state=checked]:bg-[#0075FF]",
36
43
  )}
37
44
  >
38
45
  <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
@@ -18,6 +18,7 @@ import type {
18
18
  } from "./types";
19
19
 
20
20
  import { RadioGroup } from "../Radio";
21
+ import { Button } from "../Button";
21
22
  import { FilterPanel } from "./FilterPanel";
22
23
  import { RadioRow, DataViewsSwitch } from "./PanelControls";
23
24
  import { cn } from "../../utils/cn";
@@ -234,7 +235,7 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
234
235
  type="button"
235
236
  onClick={onClose}
236
237
  aria-label="Close panel"
237
- className="flex h-7 w-7 items-center justify-center rounded-[8px] bg-white/[0.15] text-white transition-colors hover:bg-white/25"
238
+ className="flex h-7 w-7 items-center justify-center rounded-[8px] bg-white/[0.15] text-white transition-colors hover:bg-background-presentation-state-negative-primary hover:text-white"
238
239
  >
239
240
  <X className="h-[18px] w-[18px]" />
240
241
  </button>
@@ -262,14 +263,16 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
262
263
  </div>
263
264
  ))}
264
265
  </RadioGroup>
265
- <button
266
+ <Button
266
267
  type="button"
268
+ variant="BorderStyle"
269
+ size="M"
267
270
  onClick={onSaveNewView}
268
- className="flex w-full items-center justify-center gap-1.5 rounded-[4px] bg-white/[0.15] px-1.5 py-0.5 text-[12px] font-[510] text-white transition-colors hover:bg-white/25"
271
+ className="w-full"
269
272
  >
270
- <Plus className="h-3 w-3" />
273
+ <Plus className="h-4 w-4" />
271
274
  Save a New View
272
- </button>
275
+ </Button>
273
276
  </div>
274
277
 
275
278
  <div className="h-px w-full bg-[#2C2D2E]" />
@@ -277,9 +280,6 @@ export function DataViewsConfigPanel(props: DataViewsConfigPanelProps) {
277
280
  {/* Table Columns */}
278
281
  <div className="space-y-3">
279
282
  <SectionHeader title="Table Columns" />
280
- <p className="text-[12px] leading-[1.475] text-content-presentation-global-tertiary">
281
- Show or hide columns in table view
282
- </p>
283
283
  {orderedColumns.length === 0 ? (
284
284
  <p className="text-xs text-content-presentation-global-tertiary">
285
285
  No fields detected.
@@ -30,9 +30,11 @@ import {
30
30
  resolvePresets,
31
31
  } from "../../utils/dataViews/rangeUtils"
32
32
  import { RangeSliderWithInputs } from "./filters/RangeSliderWithInputs"
33
- import { DateRangePopover } from "./filters/DateRangePopover"
33
+ import { DatePickerRangeFilter } from "./filters/DatePickerRangeFilter"
34
34
  import { PresetChips } from "./filters/PresetChips"
35
35
  import { resolveBadgeVariant } from "./badgeAdapter"
36
+ import { SearchableSelect } from "../SearchableSelect"
37
+ import type { SearchableSelectOption } from "../SearchableSelect"
36
38
 
37
39
  type FilterPanelProps = {
38
40
  data: DynamicRecord[]
@@ -346,21 +348,36 @@ function FilterBody({
346
348
  }
347
349
 
348
350
  if (entry.kind === "date-range" && entry.field) {
349
- const presets = resolvePresets(entry.field)
350
351
  const dateValue: DateRangeFilter | undefined = isDateRange(value) ? value : undefined
351
- return (
352
- <DateRangePopover
353
- value={dateValue}
354
- onChange={onSetFilter}
355
- presets={presets.length > 0 ? presets : undefined}
356
- />
357
- )
352
+ // Glare DatePicker in range mode — pick a from→to span. (No quick-preset
353
+ // chips: the calendar range selection is the single intended interaction.)
354
+ return <DatePickerRangeFilter value={dateValue} onChange={onSetFilter} />
358
355
  }
359
356
 
360
357
  const opts = getCategoricalOptions(data, entry.path, entry.field, entry.legacy)
361
358
  const selected = Array.isArray(value) ? value : []
362
359
  const isSingle = entry.field?.filterMode === "single"
363
360
 
361
+ // Searchable single-select dropdown — for fields with many options. Stores
362
+ // the chosen value as a 1-element array (empty when cleared) to stay
363
+ // consistent with the categorical FilterValue shape.
364
+ if (entry.field?.filterVariant === "searchable-select") {
365
+ const selectOptions: SearchableSelectOption[] = opts.map((opt) => ({
366
+ value: opt,
367
+ label: opt,
368
+ }))
369
+ const current = selected[0] ?? null
370
+ return (
371
+ <SearchableSelect
372
+ options={selectOptions}
373
+ value={current}
374
+ onValueChange={(v) => onSetFilter(v ? [v] : [])}
375
+ placeholder={`Select ${entry.label}…`}
376
+ icon={<i className="ri-search-line" />}
377
+ />
378
+ )
379
+ }
380
+
364
381
  if (isSingle) {
365
382
  const current = selected[0] ?? ""
366
383
  const onSingleChange = (next: string) => onSetFilter(next ? [next] : [])
@@ -0,0 +1,80 @@
1
+ "use client"
2
+
3
+ import { type ChangeEventHandler } from "react"
4
+ import { DatePicker } from "../../DatePicker"
5
+ import { InputField } from "../../InputField"
6
+ import { ActionButton } from "../../ActionButton"
7
+ import type { DateRangeFilter } from "../types"
8
+ import { toIsoDate } from "../../../utils/dataViews/rangeUtils"
9
+
10
+ type Props = {
11
+ value: DateRangeFilter | undefined
12
+ onChange: (next: DateRangeFilter) => void
13
+ }
14
+
15
+ /** Parse an ISO `yyyy-MM-dd` string to a local Date (noon-safe, no TZ drift). */
16
+ function parseIso(iso?: string): Date | undefined {
17
+ return iso ? new Date(iso + "T00:00:00") : undefined
18
+ }
19
+
20
+ /** One labeled single-date Glare DatePicker bound to one end of the range. */
21
+ function DateBound({
22
+ caption,
23
+ iso,
24
+ placeholder,
25
+ onPick,
26
+ }: {
27
+ caption: string
28
+ iso?: string
29
+ placeholder: string
30
+ onPick: (next?: string) => void
31
+ }) {
32
+ return (
33
+ <label className="flex items-center gap-2">
34
+ <span className="w-9 shrink-0 typography-body-small-regular text-content-presentation-global-secondary">
35
+ {caption}
36
+ </span>
37
+ <div className="flex-1 min-w-0">
38
+ <DatePicker
39
+ mode="single"
40
+ size="S"
41
+ value={parseIso(iso) ?? (undefined as never)}
42
+ dateFormat="yyyy-MM-dd"
43
+ // Single mode reports `{ target: { value: Date } }` (it casts internally).
44
+ onChange={((e: { target: { value: Date | undefined } }) => {
45
+ const d = e?.target?.value
46
+ onPick(d instanceof Date ? toIsoDate(d) : undefined)
47
+ }) as unknown as ChangeEventHandler<HTMLInputElement>}
48
+ >
49
+ <InputField
50
+ readOnly
51
+ value={iso ?? ""}
52
+ placeholder={placeholder}
53
+ childrenSide={
54
+ <ActionButton type="button" size="S" aria-label={`Pick ${caption} date`}>
55
+ <i className="ri-calendar-event-line" />
56
+ </ActionButton>
57
+ }
58
+ />
59
+ </DatePicker>
60
+ </div>
61
+ </label>
62
+ )
63
+ }
64
+
65
+ /**
66
+ * Date-range filter as two separate Glare DatePickers — a "From" and a "To"
67
+ * single-date picker. Each writes only its own bound, preserving the other, so
68
+ * the result is still a `DateRangeFilter` ({ from, to }).
69
+ */
70
+ export function DatePickerRangeFilter({ value, onChange }: Props) {
71
+ const setBound = (key: "from" | "to") => (next?: string) =>
72
+ onChange({ kind: "date", from: value?.from, to: value?.to, [key]: next })
73
+
74
+ return (
75
+ <div className="space-y-2">
76
+ <DateBound caption="From" iso={value?.from} placeholder="Start date" onPick={setBound("from")} />
77
+ <DateBound caption="To" iso={value?.to} placeholder="End date" onPick={setBound("to")} />
78
+ </div>
79
+ )
80
+ }
@@ -160,6 +160,14 @@ export type FieldConfig = {
160
160
  * - "single": radios, single-select. FilterValue is a 1-element array.
161
161
  */
162
162
  filterMode?: "single" | "multi"
163
+ /**
164
+ * Categorical filter control style.
165
+ * - "checkbox" (default): inline list of checkboxes (multi) or radios (single).
166
+ * - "searchable-select": a single-select SearchableSelect dropdown — useful
167
+ * when a field has many options. Implies single-select; the FilterValue is
168
+ * a 1-element array (or empty when cleared).
169
+ */
170
+ filterVariant?: "checkbox" | "searchable-select"
163
171
  presets?: FieldPreset[]
164
172
  rangeMin?: number
165
173
  rangeMax?: number
@@ -63,7 +63,12 @@ export const DatePicker = forwardRef(
63
63
  ? value
64
64
  : [value]
65
65
  : mode == "range"
66
- ? { from: value, to: value }
66
+ // Accept a DateRange ({ from, to }) directly, or wrap a single
67
+ // Date into a one-day range.
68
+ ? value && typeof value === "object" && !(value instanceof Date) &&
69
+ ("from" in value || "to" in value)
70
+ ? (value as DateRange)
71
+ : { from: value, to: value }
67
72
  : value;
68
73
  const [date, setDate] = useState<Date[] | Date | DateRange | undefined>(initialDate);
69
74
  const [pickerValue, setPickerValue] = useState<TimePickerValue>({
@@ -50,7 +50,7 @@ DropdownMenuRadioGroup.displayName =
50
50
  const DropdownMenuContent = React.forwardRef<
51
51
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
52
52
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> &
53
- DropdownMenuProps & { autoGroup?: boolean }
53
+ DropdownMenuProps & { autoGroup?: boolean; maxHeight?: number }
54
54
  >(
55
55
  (
56
56
  {
@@ -60,6 +60,8 @@ const DropdownMenuContent = React.forwardRef<
60
60
  variant = "PresentationStyle",
61
61
  autoGroup = true,
62
62
  collisionPadding = 8,
63
+ maxHeight = 320,
64
+ style,
63
65
  children,
64
66
  ...props
65
67
  },
@@ -71,13 +73,13 @@ const DropdownMenuContent = React.forwardRef<
71
73
  ref={ref}
72
74
  sideOffset={sideOffset}
73
75
  collisionPadding={collisionPadding}
74
- className={cn(
75
- menuContentStyles({ variant }),
76
- // Cap to the space Radix has after collision handling so a tall menu
77
- // scrolls instead of overflowing off-screen.
78
- "max-h-[var(--radix-dropdown-menu-content-available-height)]",
79
- className
80
- )}
76
+ // Cap at maxHeight, but never exceed the space Radix has after collision
77
+ // handling. The menu scrolls (overflow on the surface) past this height.
78
+ style={{
79
+ maxHeight: `min(${maxHeight}px, var(--radix-dropdown-menu-content-available-height))`,
80
+ ...style,
81
+ }}
82
+ className={cn(menuContentStyles({ variant }), className)}
81
83
  {...props}
82
84
  >
83
85
  {autoGroup ? autoGroupChildren(children) : children}
@@ -398,6 +400,7 @@ export const MenuItemStyles = cva(
398
400
  "border",
399
401
  "border-transparent",
400
402
  "flex",
403
+ "shrink-0", // keep full row height so the menu scrolls instead of squishing
401
404
  "items-center",
402
405
  "justify-start",
403
406
  "text-overflow",
@@ -493,13 +496,13 @@ export const menuContentStyles = cva(
493
496
  "rounded-[14px]",
494
497
  "min-w-[240px]",
495
498
  "outline-none",
496
- "overflow-scroll",
499
+ "overflow-y-auto",
500
+ "overflow-x-hidden",
497
501
  // Only animate the OPEN (enter) state. An exit animation on [data-state=closed]
498
502
  // holds the old DOM node during close, which breaks the context menu's
499
503
  // close/reposition on a second right-click (Radix issue #2572).
500
504
  "data-[state=open]:animate-in",
501
505
  "data-[state=open]:fade-in-0",
502
- "overflow-x-hidden",
503
506
  "scrollbar-hide",
504
507
  "backdrop-blur-[21px]",
505
508
  "flex gap-1 flex-col",
@@ -519,7 +522,7 @@ export const menuContentStyles = cva(
519
522
  }
520
523
  );
521
524
 
522
- export const menuGroupStyles = cva(["flex", "flex-col"], {
525
+ export const menuGroupStyles = cva(["flex", "flex-col", "shrink-0"], {
523
526
  variants: {
524
527
  variant: {
525
528
  // Visually contains its items in a bordered card.
@@ -0,0 +1,127 @@
1
+ import React, { forwardRef } from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../utils/cn";
4
+ import { Themes } from "../utils/types";
5
+
6
+ /**
7
+ * HeaderBar
8
+ *
9
+ * A variant-driven header chip showing two text pieces inside a single
10
+ * rounded container:
11
+ * - `label` -> the colored emphasis pill (BADGE piece)
12
+ * - `title` -> the plain text (PLAIN piece)
13
+ *
14
+ * The `variant` controls BOTH the colors AND the DOM order:
15
+ * - "new" / "edit" -> [badge(label)] [plain(title)] (badge on the LEFT)
16
+ * - "detail" -> [plain(title)] [badge(label)] (badge on the RIGHT)
17
+ *
18
+ * Example usage:
19
+ * - new: <HeaderBar variant="new" label="New" title="sales iNVOICE" />
20
+ * - edit: <HeaderBar variant="edit" label="edit" title="sales iNVOICE" />
21
+ * - detail: <HeaderBar variant="detail" label="de-344" title="sales iNVOICE" />
22
+ * (renders plain "sales iNVOICE" on the LEFT, colored "de-344" on the RIGHT)
23
+ */
24
+
25
+ // Inner row: order is reversed for "detail" so the badge ends up on the right.
26
+ const rowStyles = cva(["flex", "items-center"], {
27
+ variants: {
28
+ variant: {
29
+ new: "flex-row",
30
+ edit: "flex-row",
31
+ detail: "flex-row-reverse",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: "new",
36
+ },
37
+ });
38
+
39
+ // Colored pill holding `label`.
40
+ const badgeStyles = cva(
41
+ [
42
+ "flex",
43
+ "h-8",
44
+ "items-center",
45
+ "justify-center",
46
+ "gap-2.5",
47
+ "rounded-lg",
48
+ "px-1",
49
+ ],
50
+ {
51
+ variants: {
52
+ variant: {
53
+ new: "bg-blue-sparkle-alpha-50",
54
+ edit: "bg-orange-alpha-50",
55
+ detail: "bg-white-alpha-30",
56
+ },
57
+ },
58
+ defaultVariants: {
59
+ variant: "new",
60
+ },
61
+ },
62
+ );
63
+
64
+ // Text inside the colored pill.
65
+ const badgeTextStyles = cva(
66
+ [
67
+ "font-sans",
68
+ "text-[28px]",
69
+ "font-[510]",
70
+ "leading-normal",
71
+ "uppercase",
72
+ "[font-feature-settings:'cv05'_on]",
73
+ ],
74
+ {
75
+ variants: {
76
+ variant: {
77
+ new: "text-blue-sparkle-200",
78
+ edit: "text-orange-200",
79
+ detail: "text-white-00",
80
+ },
81
+ },
82
+ defaultVariants: {
83
+ variant: "new",
84
+ },
85
+ },
86
+ );
87
+
88
+ interface HeaderBarProps
89
+ extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof rowStyles> {
90
+ theme?: Themes;
91
+ /** The colored emphasis pill text. */
92
+ label: string;
93
+ /** The plain text. */
94
+ title: string;
95
+ }
96
+
97
+ const HeaderBar = forwardRef<HTMLDivElement, HeaderBarProps>(
98
+ ({ variant = "new", label, title, theme, className, ...props }, ref) => {
99
+ return (
100
+ <div
101
+ ref={ref}
102
+ data-theme={theme}
103
+ className={cn(
104
+ "inline-flex flex-col items-start rounded-[14px] border border-black-600 bg-black-1000 p-1.5 shadow-[0_0_32px_2px_rgba(0,0,0,0.05),0_0_32px_2px_rgba(0,0,0,0.05)]",
105
+ className,
106
+ )}
107
+ {...props}
108
+ >
109
+ <div className={cn(rowStyles({ variant }))}>
110
+ <div className={cn(badgeStyles({ variant }))}>
111
+ <p className={cn(badgeTextStyles({ variant }))}>{label}</p>
112
+ </div>
113
+ <div className="flex h-8 items-center justify-center px-1.5">
114
+ <p className="font-sans text-[28px] font-[510] leading-normal uppercase text-white-00 [font-feature-settings:'cv05'_on]">
115
+ {title}
116
+ </p>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ );
121
+ },
122
+ );
123
+
124
+ HeaderBar.displayName = "HeaderBar";
125
+
126
+ export default HeaderBar;
127
+ export { HeaderBar };
@@ -251,53 +251,53 @@ export function SearchableSelect({
251
251
  <div
252
252
  ref={scrollRef}
253
253
  onScroll={handleScroll}
254
- className="overflow-y-auto overflow-x-hidden scrollbar-hide"
254
+ className="overflow-y-auto overflow-x-hidden rounded-[10px] scrollbar-hide"
255
255
  style={{ maxHeight: maxVisibleItems * ROW_HEIGHT }}
256
256
  >
257
- {filteredOptions.length > 0 && (
258
- // Boxed group container — matches the DropdownMenu's auto-grouped look.
259
- <div className="flex flex-col gap-[1px] rounded-[10px] overflow-hidden">
260
- {filteredOptions.map((option) => {
261
- const isSelected = option.value === value;
262
- return (
263
- // Same structure as DropdownMenuItem: MenuItemStyles on the
264
- // element + a single inner <div> the styles target via [&>div].
265
- <button
266
- type="button"
267
- key={option.value}
268
- onClick={() => handleSelect(option)}
269
- data-highlighted={isSelected ? "" : undefined}
270
- className={cn(
271
- MenuItemStyles({ variant: "Default", size: "M" }),
272
- "shrink-0" // keep full row height; the list scrolls instead of squishing
273
- )}
274
- >
275
- <div>
276
- {option.icon}
277
- <span className="flex-1 text-start">{option.label}</span>
278
- {isSelected && (
279
- <i className="ri-check-line text-[16px] shrink-0" />
257
+ {filteredOptions.length > 0 && (
258
+ // Boxed group container — matches the DropdownMenu's auto-grouped look.
259
+ <div className="flex flex-col gap-[1px] rounded-[10px] overflow-hidden">
260
+ {filteredOptions.map((option) => {
261
+ const isSelected = option.value === value;
262
+ return (
263
+ // Same structure as DropdownMenuItem: MenuItemStyles on the
264
+ // element + a single inner <div> the styles target via [&>div].
265
+ <button
266
+ type="button"
267
+ key={option.value}
268
+ onClick={() => handleSelect(option)}
269
+ data-highlighted={isSelected ? "" : undefined}
270
+ className={cn(
271
+ MenuItemStyles({ variant: "Default", size: "M" }),
272
+ "shrink-0" // keep full row height; the list scrolls instead of squishing
280
273
  )}
281
- </div>
282
- </button>
283
- );
284
- })}
285
- </div>
286
- )}
274
+ >
275
+ <div>
276
+ {option.icon}
277
+ <span className="flex-1 text-start">{option.label}</span>
278
+ {isSelected && (
279
+ <i className="ri-check-line text-[16px] shrink-0" />
280
+ )}
281
+ </div>
282
+ </button>
283
+ );
284
+ })}
285
+ </div>
286
+ )}
287
287
 
288
- {/* Empty state — only when nothing is loading. */}
289
- {filteredOptions.length === 0 && !loading && (
290
- <div className="px-3 py-2 typography-body-small-regular text-white-alpha-75">
291
- No results found
292
- </div>
293
- )}
288
+ {/* Empty state — only when nothing is loading. */}
289
+ {filteredOptions.length === 0 && !loading && (
290
+ <div className="px-3 py-2 typography-body-small-regular text-white-alpha-75">
291
+ No results found
292
+ </div>
293
+ )}
294
294
 
295
- {/* Loading row (initial load or fetching the next page). */}
296
- {loading && (
297
- <div className="flex items-center justify-center py-2">
298
- <LoadingIcon size="M" />
299
- </div>
300
- )}
295
+ {/* Loading row (initial load or fetching the next page). */}
296
+ {loading && (
297
+ <div className="flex items-center justify-center py-2">
298
+ <LoadingIcon size="M" />
299
+ </div>
300
+ )}
301
301
 
302
302
  </div>
303
303
  </PopoverContent>