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.
- package/apps/lib/components/ActionButton.tsx +39 -6
- package/apps/lib/components/Badge.tsx +5 -2
- package/apps/lib/components/BadgeField.tsx +71 -67
- package/apps/lib/components/Button.tsx +4 -4
- package/apps/lib/components/Calendar.tsx +7 -17
- package/apps/lib/components/Card.tsx +2 -1
- package/apps/lib/components/ColorPicker.tsx +1 -1
- package/apps/lib/components/ContextMenu.tsx +65 -22
- package/apps/lib/components/DataViews/context.ts +2 -2
- package/apps/lib/components/DataViews/data-views.tsx +3 -3
- package/apps/lib/components/DataViews/filters/filters.tsx +0 -2
- package/apps/lib/components/DataViews/views/table-view.tsx +190 -153
- package/apps/lib/components/Drawer.tsx +18 -1
- package/apps/lib/components/DropdownMenu.tsx +65 -22
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +19 -5
- package/apps/lib/components/FormBuilder/types.ts +16 -0
- package/apps/lib/components/FormRenderer/form-renderer.tsx +16 -5
- package/apps/lib/components/FormRenderer/stepper.tsx +22 -16
- package/apps/lib/components/HeaderBar.tsx +51 -53
- package/apps/lib/components/Input.tsx +19 -4
- package/apps/lib/components/InputField.tsx +46 -47
- package/apps/lib/components/Popover.tsx +108 -56
- package/apps/lib/components/SearchableSelect.tsx +22 -17
- package/apps/lib/components/SearchableTree.tsx +33 -17
- package/apps/lib/components/SearchableTreeDialog.tsx +21 -12
- package/apps/lib/components/SectionBlock.tsx +3 -1
- package/apps/lib/components/Select.tsx +86 -54
- package/apps/lib/components/SimpleSelect.tsx +2 -2
- package/apps/lib/components/SlideDatePicker.tsx +15 -4
- package/apps/lib/components/Stepper.tsx +329 -180
- package/apps/lib/components/Switch.tsx +2 -2
- package/apps/lib/components/TabSwitch.tsx +18 -12
- package/apps/lib/components/Table.tsx +1 -1
- package/apps/lib/components/Textarea.tsx +29 -4
- package/apps/lib/components/TreeDropDown.tsx +18 -6
- package/apps/lib/layouts/FieldSection.tsx +28 -2
- package/apps/lib/registry.json +7 -17
- package/apps/lib/tsconfig.tsbuildinfo +1 -1
- package/docs/components/action-button.md +3 -1
- package/docs/components/badge-field.md +4 -4
- package/docs/components/context-menu.md +3 -1
- package/docs/components/data-views/examples/filters.md +0 -1
- package/docs/components/data-views/index.md +1 -17
- package/docs/components/dropdown-menu.md +3 -0
- package/docs/components/form-builder.md +27 -1
- package/docs/components/header-bar.md +3 -2
- package/docs/components/input-field.md +3 -3
- package/docs/components/select.md +1 -1
- package/docs/components/stepper.md +119 -24
- package/docs/migration/changelog.md +13 -0
- package/docs/reference/tailwind-plugins.md +42 -0
- package/docs/tutorials/getting-started.md +32 -11
- package/package.json +1 -1
- package/apps/lib/components/DataViews/filters/summary.tsx +0 -65
- package/apps/lib/components/FormStepper.tsx +0 -272
- 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
|
-
|
|
36
|
-
|
|
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 = "
|
|
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 = "
|
|
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={
|
|
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-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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-
|
|
150
|
-
"bg-
|
|
151
|
-
"hover:
|
|
152
|
-
"
|
|
153
|
-
"focus:
|
|
154
|
-
"
|
|
155
|
-
"disabled:
|
|
156
|
-
"disabled:
|
|
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-
|
|
160
|
-
"hover:bg-
|
|
161
|
-
"
|
|
162
|
-
"focus:
|
|
163
|
-
"
|
|
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-
|
|
167
|
-
"hover:bg-
|
|
168
|
-
"hover:text-
|
|
169
|
-
"focus:bg-
|
|
170
|
-
"focus:text-
|
|
171
|
-
"
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
243
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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 {
|
|
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
|
-
|
|
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 —
|
|
216
|
-
<
|
|
220
|
+
{/* Chevron toggle — the shared in-field action button (Figma `ActionButton` M). */}
|
|
221
|
+
<ActionButton
|
|
217
222
|
as="span"
|
|
218
|
-
|
|
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
|
-
</
|
|
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
|
|
257
|
-
|
|
258
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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 —
|
|
266
|
-
<
|
|
269
|
+
{/* Chevron toggle — the shared in-field action button (Figma `ActionButton` M). */}
|
|
270
|
+
<ActionButton
|
|
267
271
|
as="span"
|
|
268
|
-
|
|
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-[
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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 {
|
|
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
|
-
|
|
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 —
|
|
241
|
-
<
|
|
246
|
+
{/* Chevron toggle — the shared in-field action button (Figma `ActionButton` M). */}
|
|
247
|
+
<ActionButton
|
|
242
248
|
as="span"
|
|
243
|
-
|
|
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-[
|
|
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
|
-
</
|
|
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
|
|
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
|
-
|
|
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: "",
|