torch-glare 2.5.3 → 2.5.5

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.
Files changed (56) hide show
  1. package/apps/lib/components/ActionButton.tsx +39 -6
  2. package/apps/lib/components/Badge.tsx +5 -2
  3. package/apps/lib/components/BadgeField.tsx +71 -67
  4. package/apps/lib/components/Button.tsx +4 -4
  5. package/apps/lib/components/Calendar.tsx +7 -17
  6. package/apps/lib/components/Card.tsx +2 -1
  7. package/apps/lib/components/ColorPicker.tsx +1 -1
  8. package/apps/lib/components/ContextMenu.tsx +65 -22
  9. package/apps/lib/components/DataViews/context.ts +2 -2
  10. package/apps/lib/components/DataViews/data-views.tsx +3 -3
  11. package/apps/lib/components/DataViews/filters/filters.tsx +0 -2
  12. package/apps/lib/components/DataViews/views/table-view.tsx +190 -153
  13. package/apps/lib/components/Drawer.tsx +18 -1
  14. package/apps/lib/components/DropdownMenu.tsx +65 -22
  15. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +19 -5
  16. package/apps/lib/components/FormBuilder/types.ts +16 -0
  17. package/apps/lib/components/FormRenderer/form-renderer.tsx +16 -5
  18. package/apps/lib/components/FormRenderer/stepper.tsx +22 -16
  19. package/apps/lib/components/HeaderBar.tsx +51 -53
  20. package/apps/lib/components/Input.tsx +19 -4
  21. package/apps/lib/components/InputField.tsx +46 -47
  22. package/apps/lib/components/Popover.tsx +108 -56
  23. package/apps/lib/components/SearchableSelect.tsx +22 -17
  24. package/apps/lib/components/SearchableTree.tsx +33 -17
  25. package/apps/lib/components/SearchableTreeDialog.tsx +21 -12
  26. package/apps/lib/components/SectionBlock.tsx +3 -1
  27. package/apps/lib/components/Select.tsx +86 -54
  28. package/apps/lib/components/SimpleSelect.tsx +2 -2
  29. package/apps/lib/components/SlideDatePicker.tsx +15 -4
  30. package/apps/lib/components/Stepper.tsx +329 -180
  31. package/apps/lib/components/Switch.tsx +2 -2
  32. package/apps/lib/components/TabSwitch.tsx +18 -12
  33. package/apps/lib/components/Table.tsx +1 -1
  34. package/apps/lib/components/Textarea.tsx +29 -4
  35. package/apps/lib/components/TreeDropDown.tsx +18 -6
  36. package/apps/lib/layouts/FieldSection.tsx +28 -2
  37. package/apps/lib/registry.json +7 -17
  38. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  39. package/docs/components/action-button.md +3 -1
  40. package/docs/components/badge-field.md +4 -4
  41. package/docs/components/context-menu.md +3 -1
  42. package/docs/components/data-views/examples/filters.md +0 -1
  43. package/docs/components/data-views/index.md +1 -17
  44. package/docs/components/dropdown-menu.md +3 -0
  45. package/docs/components/form-builder.md +27 -1
  46. package/docs/components/header-bar.md +3 -2
  47. package/docs/components/input-field.md +3 -3
  48. package/docs/components/select.md +1 -1
  49. package/docs/components/stepper.md +119 -24
  50. package/docs/migration/changelog.md +13 -0
  51. package/docs/reference/tailwind-plugins.md +42 -0
  52. package/docs/tutorials/getting-started.md +32 -11
  53. package/package.json +1 -1
  54. package/apps/lib/components/DataViews/filters/summary.tsx +0 -65
  55. package/apps/lib/components/FormStepper.tsx +0 -272
  56. package/docs/components/form-stepper.md +0 -250
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
  import { cva, VariantProps } from "class-variance-authority";
3
3
  import { cn } from "../utils/cn";
4
- import React, { useEffect, useRef } from "react";
4
+ import React, { cloneElement, isValidElement, useEffect, useRef } from "react";
5
5
  import * as PopoverPrimitive from "@radix-ui/react-popover";
6
6
  import { Slot } from "@radix-ui/react-slot";
7
7
  import { Themes } from "../utils/types";
8
8
 
9
+
9
10
  interface LocalPopOverProps extends VariantProps<typeof popoverStyles> {
10
11
  variant?: "SystemStyle" | "PresentationStyle";
11
12
  className?: string;
@@ -32,18 +33,19 @@ PopoverTrigger.displayName = PopoverPrimitive.Trigger.displayName;
32
33
  const PopoverContent = React.forwardRef<
33
34
  React.ElementRef<typeof PopoverPrimitive.Content>,
34
35
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> &
35
- LocalPopOverProps & {
36
- theme?: Themes;
37
- }
36
+ LocalPopOverProps & {
37
+ theme?: Themes;
38
+ }
38
39
  >(
39
40
  (
40
41
  {
41
42
  className,
42
43
  align = "center",
43
44
  sideOffset = 4,
44
- variant = "SystemStyle",
45
+ variant = "PresentationStyle",
45
46
  overlayBlur = false,
46
47
  theme,
48
+ children,
47
49
  ...props
48
50
  },
49
51
  ref,
@@ -58,7 +60,9 @@ const PopoverContent = React.forwardRef<
58
60
  sideOffset={sideOffset}
59
61
  className={cn(popoverStyles({ variant, overlayBlur }), className)}
60
62
  {...props}
61
- />
63
+ >
64
+ {children}
65
+ </PopoverPrimitive.Content>
62
66
  </div>
63
67
  ) : (
64
68
  <PopoverPrimitive.Content
@@ -68,7 +72,9 @@ const PopoverContent = React.forwardRef<
68
72
  sideOffset={sideOffset}
69
73
  className={cn(popoverStyles({ variant, overlayBlur }), className)}
70
74
  {...props}
71
- />
75
+ >
76
+ {children}
77
+ </PopoverPrimitive.Content>
72
78
  )}
73
79
  </PopoverPrimitive.Portal>
74
80
  ),
@@ -84,7 +90,7 @@ interface Props<T extends React.ElementType = "li">
84
90
 
85
91
  // Define the PopoverItem component with a generic type parameter
86
92
  const PopoverItem = <T extends React.ElementType = "li">({
87
- variant = "SystemStyle",
93
+ variant = "Default",
88
94
  size = "M",
89
95
  asChild,
90
96
  className,
@@ -103,72 +109,108 @@ const PopoverItem = <T extends React.ElementType = "li">({
103
109
  }
104
110
  }, [active]);
105
111
 
112
+ const itemClassName = cn(PopoverItemStyles({ variant, size, active }), className);
113
+
114
+ // The styles are a two-layer chip (see `PopoverItemStyles`): this element is the container and
115
+ // the inner div is the row that lights up, which every `[&>div]:` rule targets. It therefore has
116
+ // to exist in both branches.
117
+ if (asChild && isValidElement(children)) {
118
+ // Slot merges the item's props onto the consumer's element (e.g. a Link), so the row div has to
119
+ // be injected as that element's child — wrapping the Link instead would move the class onto the
120
+ // wrapper and leave the anchor nested inside the item rather than being it.
121
+ const child = children as React.ReactElement<{ children?: React.ReactNode }>;
122
+ return (
123
+ <Slot className={itemClassName} ref={ref as React.Ref<HTMLElement>}>
124
+ {cloneElement(child, {}, <div>{child.props.children}</div>)}
125
+ </Slot>
126
+ );
127
+ }
128
+
106
129
  return (
107
130
  <Component
108
131
  {...(props as React.ComponentPropsWithoutRef<T>)} // Spread the props dynamically
109
- className={cn(
110
- PopoverItemStyles({
111
- variant,
112
- size,
113
- active,
114
- }),
115
- className,
116
- )}
117
- /// <reference path="" />
132
+ className={itemClassName}
118
133
  ref={ref}
119
134
  >
120
- {children}
135
+ <div>{children}</div>
121
136
  </Component>
122
137
  );
123
138
  };
124
139
 
125
140
  export { Popover, PopoverTrigger, PopoverContent, PopoverItem };
126
141
 
142
+
127
143
  const PopoverItemStyles = cva(
144
+ // `MenuItemStyles` (ContextMenu/DropdownMenu) minus its grey wrapper fill: same 2px gutter, same
145
+ // inner `<div>` that lights up on hover/focus, but the row itself stays transparent on the panel.
146
+ // `PopoverItem` always renders that inner div, including through `asChild`.
128
147
  [
129
- "text-content-presentation-action-light-primary",
148
+ "text-content-presentation-global-primary-light typography-body-medium-regular",
130
149
  "outline-none",
131
150
  "border",
132
151
  "border-transparent",
133
152
  "flex w-full",
134
- "gap-[8px]",
153
+ "shrink-0", // keep full row height so the popover scrolls instead of squishing
135
154
  "items-center",
136
155
  "justify-start",
137
156
  "text-overflow",
138
157
  "overflow-hidden",
139
- "px-[12px]",
140
- "rounded-[4px]",
158
+ "p-[2px]",
141
159
  "transition-all",
160
+ // No `bg-[rgba(184,192,204,0.36)]` here, unlike the menu's item: a popover row sits directly on
161
+ // the panel with no grey container behind it. Everything else is the menu's rule set.
142
162
  "ease-in-out",
143
163
  "duration-300",
164
+ "[&>div]:flex",
165
+ "[&>div]:px-[12px]",
166
+ "[&>div]:py-[4px]",
167
+ "[&>div]:gap-2",
168
+ "[&>div]:w-full",
169
+ "[&>div]:rounded-[8px]",
170
+ "[&>div]:items-center",
171
+ "group",
144
172
  ],
145
173
  {
146
174
  variants: {
147
175
  variant: {
176
+ // The menu marks its keyboard highlight with Radix's `data-highlighted` and its disabled
177
+ // rows with `data-disabled`; a PopoverItem is a plain <button>, so those become `:focus`
178
+ // and `:disabled` here. Everything else is the menu's rule set verbatim.
148
179
  Default: [
149
- "text-content-presentation-action-light-primary",
150
- "bg-background-presentation-action-dropdown-primary",
151
- "hover:bg-background-presentation-action-hover",
152
- "hover:text-content-presentation-global-primary-inverse",
153
- "focus:bg-background-presentation-action-hover",
154
- "focus:text-content-presentation-global-primary-inverse",
155
- "disabled:text-content-presentation-state-disabled",
156
- "disabled:bg-white-00",
180
+ "text-content-presentation-global-primary-light",
181
+ "[&>div]:hover:bg-white-50 [&>div]:hover:shadow-[0_0_16px_0_rgba(0,0,0,0.36)]",
182
+ "[&>div]:hover:text-black-1000",
183
+ "[&:focus>div]:bg-white-alpha-75",
184
+ "[&:focus>div]:text-black-1000",
185
+ "[&:disabled>div]:text-content-presentation-global-primary-light",
186
+ "[&:disabled>div]:opacity-50",
187
+ "[&:disabled>div]:hover:text-content-presentation-global-primary-light",
188
+ "[&:disabled>div]:hover:bg-transparent",
189
+ "[&:disabled>div]:hover:shadow-none",
157
190
  ],
158
191
  Warning: [
159
- "text-content-presentation-state-information",
160
- "hover:bg-background-presentation-state-information-primary",
161
- "focus:bg-background-presentation-state-information-primary",
162
- "focus:text-content-presentation-global-primary-inverse",
163
- "hover:text-content-presentation-global-primary-inverse",
192
+ "text-blue-sparkle-200",
193
+ "[&>div]:hover:bg-white-50 [&>div]:hover:shadow-[0_0_16px_0_rgba(0,0,0,0.36)]",
194
+ "[&>div]:hover:text-blue-sparkle-700",
195
+ "[&:focus>div]:bg-white-alpha-75",
196
+ "[&:focus>div]:text-blue-sparkle-700",
197
+ "[&:disabled>div]:text-content-presentation-global-primary-light",
198
+ "[&:disabled>div]:opacity-50",
199
+ "[&:disabled>div]:hover:text-content-presentation-global-primary-light",
200
+ "[&:disabled>div]:hover:bg-transparent",
201
+ "[&:disabled>div]:hover:shadow-none",
164
202
  ],
165
203
  Negative: [
166
- "text-content-presentation-state-negative",
167
- "hover:bg-background-presentation-state-negative-primary",
168
- "hover:text-content-presentation-global-primary-inverse",
169
- "focus:bg-background-presentation-state-negative-primary",
170
- "focus:text-content-presentation-global-primary-inverse",
171
- "active:text-content-presentation-state-negative",
204
+ "text-medium-red-200",
205
+ "[&>div]:hover:bg-white-50 [&>div]:hover:shadow-[0_0_16px_0_rgba(0,0,0,0.36)]",
206
+ "[&>div]:hover:text-medium-red-600",
207
+ "[&:focus>div]:bg-white-alpha-75",
208
+ "[&:focus>div]:text-medium-red-600",
209
+ "[&:disabled>div]:text-content-presentation-global-primary-light",
210
+ "[&:disabled>div]:opacity-50",
211
+ "[&:disabled>div]:hover:text-content-presentation-global-primary-light",
212
+ "[&:disabled>div]:hover:bg-transparent",
213
+ "[&:disabled>div]:hover:shadow-none",
172
214
  ],
173
215
  SystemStyle: [
174
216
  "bg-background-system-body-primary",
@@ -198,13 +240,13 @@ const PopoverItemStyles = cva(
198
240
  "text-content-presentation-action-light-primary",
199
241
  ],
200
242
  },
201
-
202
- defaultVariants: {
203
- variant: "Default",
204
- size: "M",
205
- active: false,
206
- disabled: false,
207
- },
243
+ },
244
+ // Same misplacement as `popoverStyles` — a sibling of `variants`, not one of them.
245
+ defaultVariants: {
246
+ variant: "Default",
247
+ size: "M",
248
+ active: false,
249
+ disabled: false,
208
250
  },
209
251
  compoundVariants: [
210
252
  {
@@ -218,7 +260,10 @@ const PopoverItemStyles = cva(
218
260
 
219
261
  const popoverStyles = cva(
220
262
  [
221
- "p-1 max-h-[200px] z-[1000] shrink-0",
263
+ // Cap at 200px but never exceed the room Radix actually has after collision handling, so a
264
+ // popover opened near a screen edge shrinks and scrolls instead of being cut off. Consumers that
265
+ // supply their own inner scroller override this with an inline `maxHeight`, which beats the class.
266
+ "p-1 max-h-[min(200px,var(--radix-popover-content-available-height,100vh))] z-[1000] shrink-0",
222
267
  "rounded-[8px]",
223
268
  "border",
224
269
  "min-w-[240px]",
@@ -239,22 +284,29 @@ const popoverStyles = cva(
239
284
  "bg-background-system-body-primary",
240
285
  "shadow-[0px_0px_18px_0px_rgba(0,0,0,0.75)]",
241
286
  ],
242
- // Adopts the DropdownMenu surface (`menuContentStyles`): backdrop-blurred, borderless
243
- // rounded-14 panel with a soft ambient shadow — keeping the original background color.
287
+ // Figma `Dropdown-Menu-1.0` (1735:159246) — the same surface `menuContentStyles` renders,
288
+ // so a popover and a dropdown menu are indistinguishable panels.
244
289
  PresentationStyle: [
245
- "border-transparent",
290
+ // `border-0`, not `border-transparent`: the base sets `border`, and with `border-box`
291
+ // that 1px sits on top of the 4px padding, making the panel 2px wider than the design.
292
+ "border-0",
246
293
  "rounded-[14px]",
247
294
  "backdrop-blur-[21px]",
248
- "bg-background-presentation-form-base",
295
+ // The design's fill is a raw `rgba(61,64,69,0.72)` with no Figma variable behind it, so it
296
+ // is spelled out exactly as DropdownMenu/ContextMenu already spell it. It has to be
297
+ // translucent: over the previous opaque `form-base` the backdrop-blur painted nothing.
298
+ "bg-[rgba(61,64,69,0.72)]",
249
299
  "shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]",
250
300
  ],
251
301
  },
252
302
  overlayBlur: {
253
303
  true: ["h-fit"],
254
304
  },
255
- defaultVariants: {
256
- variant: "PresentationStyle",
257
- },
305
+ },
306
+ // Was nested inside `variants`, where cva reads it as a variant group called
307
+ // "defaultVariants" and no default is ever applied.
308
+ defaultVariants: {
309
+ variant: "PresentationStyle",
258
310
  },
259
311
  },
260
312
  );
@@ -6,7 +6,8 @@ import { cn } from "../utils/cn";
6
6
  import { Themes } from "../utils/types";
7
7
  import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
8
8
  import { Icon, Input, Group } from "./Input";
9
- import { Button, LoadingIcon } from "./Button";
9
+ import { LoadingIcon } from "./Button";
10
+ import { ActionButton } from "./ActionButton";
10
11
  import { useClickOutside } from "../hooks/useClickOutside";
11
12
  // Reuse the exact menu-item styling so rows look identical to DropdownMenuItem.
12
13
  import { MenuItemStyles } from "./DropdownMenu";
@@ -189,7 +190,11 @@ export function SearchableSelect({
189
190
  setDropdownWidth(e.currentTarget.offsetWidth)
190
191
  }
191
192
  // Trigger styling mirrors the `Select` component's trigger so the two read identically.
192
- className={cn("flex w-full items-center gap-1 rounded-[6px] p-[4px]", className)}
193
+ // `p-[3px]` so the end button lands 4px from the outer edge once the 1px border is added.
194
+ // The old `rounded-[6px]` here was dead but dangerous: `twMerge` cannot dedupe it against
195
+ // the Group's `rounded-radius-xl` (custom scale key vs arbitrary value), so both shipped
196
+ // and the 12px only won on CSS source order.
197
+ className={cn("flex w-full items-center gap-1 p-[3px]", className)}
193
198
  >
194
199
  {icon && <Icon>{icon}</Icon>}
195
200
  <Input
@@ -212,10 +217,10 @@ export function SearchableSelect({
212
217
  "!h-[24px]": size === "M",
213
218
  })}
214
219
  />
215
- {/* Chevron toggle — boxed icon button matching the Select component. */}
216
- <Button
220
+ {/* Chevron toggle — the shared in-field action button (Figma `ActionButton` M). */}
221
+ <ActionButton
217
222
  as="span"
218
- buttonType="icon"
223
+ size="M"
219
224
  tabIndex={-1}
220
225
  aria-label={open ? "Close" : "Open"}
221
226
  onMouseDown={(e: React.MouseEvent) => {
@@ -227,11 +232,7 @@ export function SearchableSelect({
227
232
  setOpen(true);
228
233
  }
229
234
  }}
230
- className={cn(
231
- // Same box + open-state fill as the Select trigger's chevron.
232
- "h-[32px] w-[32px] shrink-0",
233
- open && "bg-background-presentation-action-hover text-white",
234
- )}
235
+ className={cn("shrink-0", open && "bg-background-presentation-action-hover text-white")}
235
236
  >
236
237
  <i
237
238
  className={cn(
@@ -239,7 +240,7 @@ export function SearchableSelect({
239
240
  open && "rotate-180",
240
241
  )}
241
242
  />
242
- </Button>
243
+ </ActionButton>
243
244
  </Group>
244
245
  </PopoverTrigger>
245
246
 
@@ -248,19 +249,23 @@ export function SearchableSelect({
248
249
  data-theme={theme}
249
250
  ref={popoverContentRef}
250
251
  variant={variant}
251
- style={{ width: dropdownWidth || undefined }}
252
252
  onOpenAutoFocus={(e) => e.preventDefault()}
253
253
  onWheel={(e) => e.stopPropagation()}
254
254
  className={cn(menuContentStyles({ variant }))}
255
+ // The panel carries the cap — `maxVisibleItems` rows, but never more than the room Radix has
256
+ // on screen. Inline beats the `max-h-[…]` class popoverStyles contributes.
257
+ style={{
258
+ width: dropdownWidth || undefined,
259
+ maxHeight: `min(${maxVisibleItems * ROW_HEIGHT}px, var(--radix-popover-content-available-height, 100vh))`,
260
+ }}
255
261
  >
256
- {/* Dedicated scroll viewport: caps height to ~maxVisibleItems rows and
257
- scrolls the rest. Keeping it separate from the popover padding avoids
258
- flex/overflow conflicts that block scrolling. */}
262
+ {/* Dedicated scroll viewport. The height cap lives on the panel above; this just fills what is
263
+ left and scrolls. `min-h-0` is required — a flex item will not shrink below its content, so
264
+ without it the list grew past the panel and was clipped instead of scrolling. */}
259
265
  <div
260
266
  ref={scrollRef}
261
267
  onScroll={handleScroll}
262
- className="overflow-y-auto overflow-x-hidden rounded-[10px] scrollbar-hide"
263
- style={{ maxHeight: maxVisibleItems * ROW_HEIGHT }}
268
+ className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden rounded-[10px] scrollbar-hide"
264
269
  >
265
270
  {filteredOptions.length > 0 && (
266
271
  // Boxed group container — matches the DropdownMenu's auto-grouped look.
@@ -5,7 +5,8 @@ import { cn } from "../utils/cn";
5
5
  import { Themes } from "../utils/types";
6
6
  import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
7
7
  import { Icon, Input, Group } from "./Input";
8
- import { Button, LoadingIcon } from "./Button";
8
+ import { LoadingIcon } from "./Button";
9
+ import { ActionButton } from "./ActionButton";
9
10
  import { useClickOutside } from "../hooks/useClickOutside";
10
11
 
11
12
  /**
@@ -71,6 +72,8 @@ interface Props<T> {
71
72
  */
72
73
  selectableFolders?: boolean;
73
74
  /** Max height (px) of the scrollable tree body before it scrolls (default 320). */
75
+ /** Height cap for the scrolling body, in px. The panel never exceeds this, nor the room Radix
76
+ * has on screen — past it the tree scrolls inside the panel. Default 200. */
74
77
  maxBodyHeight?: number;
75
78
 
76
79
  // --- Async (optional; static `nodes` still works) ---
@@ -111,7 +114,7 @@ export function SearchableTree<T>({
111
114
  className,
112
115
  defaultExpanded = true,
113
116
  selectableFolders = false,
114
- maxBodyHeight = 320,
117
+ maxBodyHeight = 200,
115
118
  filterClientSide = true,
116
119
  onSearchChange,
117
120
  searchDebounceMs = 300,
@@ -236,7 +239,8 @@ export function SearchableTree<T>({
236
239
  onFocus={(e: React.FocusEvent<HTMLDivElement>) =>
237
240
  setDropdownWidth(e.currentTarget.offsetWidth)
238
241
  }
239
- className={cn("flex w-full items-center gap-1 p-1", className)}
242
+ // 3px + the 1px border = Figma's 4px from the field's outer edge.
243
+ className={cn("flex w-full items-center gap-1 p-[3px]", className)}
240
244
  >
241
245
  {icon && <Icon>{icon}</Icon>}
242
246
  {/* The field itself is the search input — typing filters the tree. */}
@@ -262,10 +266,10 @@ export function SearchableTree<T>({
262
266
  "!h-[24px]": size === "M",
263
267
  })}
264
268
  />
265
- {/* Chevron toggle — boxed icon button matching the Select component. */}
266
- <Button
269
+ {/* Chevron toggle — the shared in-field action button (Figma `ActionButton` M). */}
270
+ <ActionButton
267
271
  as="span"
268
- buttonType="icon"
272
+ size="M"
269
273
  tabIndex={-1}
270
274
  aria-label={open ? "Close" : "Open"}
271
275
  onMouseDown={(e: React.MouseEvent) => {
@@ -278,18 +282,15 @@ export function SearchableTree<T>({
278
282
  setOpen(true);
279
283
  }
280
284
  }}
281
- className={cn(
282
- "shrink-0 h-[32px] w-[32px] rounded-[4px]",
283
- open && "bg-background-presentation-action-hover text-white",
284
- )}
285
+ className={cn("shrink-0", open && "bg-background-presentation-action-hover text-white")}
285
286
  >
286
287
  <i
287
288
  className={cn(
288
- "ri-arrow-down-s-line text-[20px] transition-all duration-100 ease-in-out",
289
+ "ri-arrow-down-s-line !text-[26px] transition-all duration-100 ease-in-out",
289
290
  open && "rotate-180",
290
291
  )}
291
292
  />
292
- </Button>
293
+ </ActionButton>
293
294
  </Group>
294
295
  </PopoverTrigger>
295
296
 
@@ -300,16 +301,31 @@ export function SearchableTree<T>({
300
301
  variant={variant}
301
302
  align="start"
302
303
  sideOffset={4}
303
- style={{ width: dropdownWidth || undefined }}
304
+ // The panel owns the height: `maxBodyHeight` at most, and never more than the room Radix has
305
+ // on screen. Inline beats popoverStyles' `max-h-[…]` class.
306
+ style={{
307
+ width: dropdownWidth || undefined,
308
+ maxHeight: `min(${maxBodyHeight}px, var(--radix-popover-content-available-height, 100vh))`,
309
+ }}
304
310
  onOpenAutoFocus={(e) => e.preventDefault()}
305
311
  onWheel={(e) => e.stopPropagation()}
306
- className="p-0 border-0 bg-transparent shadow-none rounded-[14px] flex flex-col gap-0 overflow-visible"
312
+ // No `bg-transparent`/`shadow-none`: PopoverContent IS the frosted surface. Forcing it
313
+ // transparent and moving the glass to the inner div double-blurred (an element with
314
+ // backdrop-filter forms a backdrop root for its descendants, so the inner one sampled
315
+ // nothing). `overflow-hidden`, not `overflow-visible` — the list must stay inside the
316
+ // frosted panel rather than spilling rows over bare page content.
317
+ className="p-0 border-0 rounded-[14px] flex flex-col gap-0 overflow-hidden"
307
318
  >
308
319
  {/* SearchResult body — frosted surface holding the "Tree" label + rows. */}
309
- <div className="flex w-full items-start">
320
+ {/* `flex-1 min-h-0 items-stretch`: this wrapper hands the panel's height down to the body.
321
+ With `items-start` the body sized to its own content and escaped the panel instead. */}
322
+ <div className="flex w-full flex-1 min-h-0 items-stretch">
310
323
  <div
311
- className="flex-1 min-w-px overflow-auto scrollbar-hide rounded-[14px] py-[8px] px-[4px] bg-[rgba(61,64,69,0.72)] backdrop-blur-[21px] shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]"
312
- style={{ maxHeight: maxBodyHeight }}
324
+ // Transparent: the frosted surface is the PopoverContent behind it. A second
325
+ // backdrop-filter here would sample nothing anyway.
326
+ // The cap now lives on the panel; this fills what is left and scrolls, which needs
327
+ // `min-h-0` — a flex item will not shrink below its content without it.
328
+ className="flex-1 min-w-px min-h-0 overflow-auto scrollbar-hide rounded-[14px] p-[4px]"
313
329
  >
314
330
  {visibleTree.length > 0 ? (
315
331
  <div className="flex flex-col gap-[4px]">
@@ -13,7 +13,8 @@ import { cn } from "../utils/cn";
13
13
  import { Themes } from "../utils/types";
14
14
  import { Dialog, DialogTrigger, DialogContent, DialogTitle } from "./Dialog";
15
15
  import { Icon, Input, Group } from "./Input";
16
- import { Button, LoadingIcon } from "./Button";
16
+ import { LoadingIcon } from "./Button";
17
+ import { ActionButton } from "./ActionButton";
17
18
 
18
19
  /**
19
20
  * SearchableTreeDialog — a field that opens a modal Dialog to pick a node from a tree.
@@ -58,6 +59,9 @@ interface Props<T> {
58
59
  searchPlaceholder?: string;
59
60
  /** Label shown on the dialog's search field. */
60
61
  title?: string;
62
+ /** Height cap for the scrolling body, in px. Also bounded by 55vh so it shrinks on short
63
+ * screens. Past it the tree scrolls inside the dialog. Default 200. */
64
+ maxBodyHeight?: number;
61
65
 
62
66
  size?: "XS" | "S" | "M";
63
67
  variant?: "SystemStyle" | "PresentationStyle";
@@ -108,6 +112,7 @@ export function SearchableTreeDialog<T>({
108
112
  placeholder = "Select…",
109
113
  searchPlaceholder = "Search…",
110
114
  title = "Select an item",
115
+ maxBodyHeight = 200,
111
116
  size = "M",
112
117
  variant = "PresentationStyle",
113
118
  icon,
@@ -224,7 +229,8 @@ export function SearchableTreeDialog<T>({
224
229
  size={size === "XS" ? "S" : size}
225
230
  role="button"
226
231
  tabIndex={0}
227
- className={cn("flex w-full items-center gap-1 p-1 cursor-pointer", className)}
232
+ // 3px + the 1px border = Figma's 4px from the field's outer edge.
233
+ className={cn("flex w-full items-center gap-1 p-[3px] cursor-pointer", className)}
228
234
  >
229
235
  {icon && <Icon>{icon}</Icon>}
230
236
  <span
@@ -237,24 +243,21 @@ export function SearchableTreeDialog<T>({
237
243
  >
238
244
  {triggerLabel}
239
245
  </span>
240
- {/* Chevron toggle — boxed icon button matching the Select component. */}
241
- <Button
246
+ {/* Chevron toggle — the shared in-field action button (Figma `ActionButton` M). */}
247
+ <ActionButton
242
248
  as="span"
243
- buttonType="icon"
249
+ size="M"
244
250
  tabIndex={-1}
245
251
  aria-label={open ? "Close" : "Open"}
246
- className={cn(
247
- "shrink-0 h-[32px] w-[32px] rounded-[4px]",
248
- open && "bg-background-presentation-action-hover text-white",
249
- )}
252
+ className={cn("shrink-0", open && "bg-background-presentation-action-hover text-white")}
250
253
  >
251
254
  <i
252
255
  className={cn(
253
- "ri-arrow-down-s-line text-[20px] transition-all duration-100 ease-in-out",
256
+ "ri-arrow-down-s-line !text-[26px] transition-all duration-100 ease-in-out",
254
257
  open && "rotate-180",
255
258
  )}
256
259
  />
257
- </Button>
260
+ </ActionButton>
258
261
  </Group>
259
262
  </DialogTrigger>
260
263
 
@@ -284,7 +287,13 @@ export function SearchableTreeDialog<T>({
284
287
  {/* SearchResult.Container — inset 12px each side, tucked under the bar (z-1). */}
285
288
  <div className="flex w-full items-start px-[12px]">
286
289
  {/* Body — frosted surface that connects to the bar with rounded-bottom. */}
287
- <div className="flex-1 min-w-px max-h-[55vh] overflow-auto scrollbar-hide rounded-b-[14px] pt-[8px] pb-[4px] px-[4px] bg-[rgba(61,64,69,0.72)] backdrop-blur-[21px] shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]">
290
+ <div
291
+ // `max-h-[55vh]` alone never engaged: at a 900px viewport that is 495px, far more than
292
+ // the tree needs, so the body simply grew to fit and there was nothing to scroll.
293
+ // `maxBodyHeight` is the real cap; 55vh still bounds it on short screens.
294
+ style={{ maxHeight: `min(${maxBodyHeight}px, 55vh)` }}
295
+ className="flex-1 min-w-px min-h-0 overflow-auto scrollbar-hide rounded-b-[14px] pt-[8px] pb-[4px] px-[4px] bg-[rgba(61,64,69,0.72)] backdrop-blur-[21px] shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]"
296
+ >
288
297
  {visibleTree.length > 0 ? (
289
298
  <div className="flex flex-col gap-[4px]">
290
299
  {/* "Tree" section label. */}
@@ -71,7 +71,9 @@ const rail = cva("flex w-full min-w-[300px] flex-col items-start", {
71
71
  variants: {
72
72
  variant: {
73
73
  // Hairline between each direct child (form rows).
74
- Default: "divide-y divide-gray-300",
74
+ // `divide-gray-300` was a hardcoded light gray, so in the dark theme every row separator
75
+ // rendered as a bright rgb(209,213,219) line. The token follows the theme.
76
+ Default: "divide-y divide-border-presentation-global-primary",
75
77
  // The table draws its own row borders — a divide rule would double up on the
76
78
  // table / scroller / end-action siblings.
77
79
  Table: "",