torch-glare 2.5.4 → 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/BadgeField.tsx +70 -66
- package/apps/lib/components/Card.tsx +2 -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 +184 -176
- 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/HeaderBar.tsx +51 -53
- package/apps/lib/components/InputField.tsx +46 -47
- package/apps/lib/components/Popover.tsx +23 -9
- package/apps/lib/components/SearchableSelect.tsx +10 -6
- package/apps/lib/components/SearchableTree.tsx +23 -6
- package/apps/lib/components/SearchableTreeDialog.tsx +11 -1
- package/apps/lib/components/Select.tsx +56 -48
- package/apps/lib/components/SlideDatePicker.tsx +5 -7
- package/apps/lib/components/TabSwitch.tsx +18 -12
- package/apps/lib/layouts/FieldSection.tsx +28 -2
- package/apps/lib/registry.json +1 -2
- 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/migration/changelog.md +13 -0
- package/package.json +1 -1
- package/apps/lib/components/DataViews/filters/summary.tsx +0 -65
|
@@ -174,11 +174,22 @@ function FormRendererRoot<T extends FieldValues = FieldValues>({
|
|
|
174
174
|
// and the rail still has room to be legible (a plain `minmax(0,…)` let it collapse to 0 in a
|
|
175
175
|
// narrow preview frame). Past that floor the labels truncate instead of pushing.
|
|
176
176
|
const bodyInner = isStepper ? (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
// `@container` on the wrapper, not on the grid: an element cannot query its own size, so the
|
|
178
|
+
// measured box has to be an ancestor. It sits here rather than on the scroll body because
|
|
179
|
+
// `bodyInner` is also used directly when there is no header, and this way both paths get a
|
|
180
|
+
// container. The form then reacts to the width it is actually given — a rail, a drawer, a
|
|
181
|
+
// narrow preview frame — instead of the viewport's.
|
|
182
|
+
<div className="@container w-full">
|
|
183
|
+
{/* Below `@lg` the balancing column is dropped: it is empty, and in a narrow container its
|
|
184
|
+
180px floor plus a 32px gap is width the fields column needs more than the centring.
|
|
185
|
+
Both halves are required — a `display:none` child is not a grid item, but the third
|
|
186
|
+
track would still be reserved if the template kept describing it. */}
|
|
187
|
+
<div className="grid w-full grid-cols-[minmax(180px,1fr)_minmax(0,1100px)] gap-8 @lg:grid-cols-[minmax(180px,1fr)_minmax(0,1100px)_minmax(180px,1fr)]">
|
|
188
|
+
<StepperNav control={formInstance.control as Control<FieldValues>} />
|
|
189
|
+
{fieldsColumn}
|
|
190
|
+
{/* Empty third column — balances the rail's gutter so the middle column is centred. */}
|
|
191
|
+
<div className="hidden @lg:block" />
|
|
192
|
+
</div>
|
|
182
193
|
</div>
|
|
183
194
|
) : (
|
|
184
195
|
fieldsColumn
|
|
@@ -22,13 +22,18 @@ import { Themes } from "../utils/types";
|
|
|
22
22
|
* (renders plain "sales iNVOICE" on the LEFT, colored "de-344" on the RIGHT)
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
|
|
25
|
+
// 28px / weight 510 — the design's `Font/Size/Display/Medium`, which is exactly what this class
|
|
26
|
+
// carries. Shared by the badge and the plain title so the two pieces cannot drift apart.
|
|
27
|
+
const headerTextStyles = "typography-display-medium-medium uppercase [font-feature-settings:'cv05'_on]";
|
|
28
|
+
|
|
29
|
+
// Colored pill holding `label`. 32px tall, 4px side padding, 8px radius — Figma `Header.Badge`.
|
|
30
|
+
const badgeStyles = cva(["flex", "h-8", "items-center", "justify-center", "rounded-lg", "px-1"], {
|
|
27
31
|
variants: {
|
|
28
32
|
variant: {
|
|
29
|
-
new: "
|
|
30
|
-
edit: "
|
|
31
|
-
detail
|
|
33
|
+
new: "bg-blue-sparkle-alpha-50",
|
|
34
|
+
edit: "bg-orange-alpha-50",
|
|
35
|
+
// `White Alpha/15`, not /30 — the detail chip is the faintest of the three.
|
|
36
|
+
detail: "bg-white-alpha-15",
|
|
32
37
|
},
|
|
33
38
|
},
|
|
34
39
|
defaultVariants: {
|
|
@@ -36,49 +41,22 @@ const rowStyles = cva(["flex", "items-center"], {
|
|
|
36
41
|
},
|
|
37
42
|
});
|
|
38
43
|
|
|
39
|
-
// Colored pill holding `label`.
|
|
40
|
-
const badgeStyles = cva(
|
|
41
|
-
["flex", "h-8", "items-center", "justify-center", "gap-2.5", "rounded-lg", "px-1"],
|
|
42
|
-
{
|
|
43
|
-
variants: {
|
|
44
|
-
variant: {
|
|
45
|
-
new: "bg-blue-sparkle-alpha-50",
|
|
46
|
-
edit: "bg-orange-alpha-50",
|
|
47
|
-
detail: "bg-white-alpha-30",
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
defaultVariants: {
|
|
51
|
-
variant: "new",
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
);
|
|
55
|
-
|
|
56
44
|
// Text inside the colored pill.
|
|
57
|
-
const badgeTextStyles = cva(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"uppercase",
|
|
64
|
-
"[font-feature-settings:'cv05'_on]",
|
|
65
|
-
],
|
|
66
|
-
{
|
|
67
|
-
variants: {
|
|
68
|
-
variant: {
|
|
69
|
-
new: "text-blue-sparkle-200",
|
|
70
|
-
edit: "text-orange-200",
|
|
71
|
-
detail: "text-white-00",
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
defaultVariants: {
|
|
75
|
-
variant: "new",
|
|
45
|
+
const badgeTextStyles = cva([headerTextStyles], {
|
|
46
|
+
variants: {
|
|
47
|
+
variant: {
|
|
48
|
+
new: "text-blue-sparkle-200",
|
|
49
|
+
edit: "text-orange-200",
|
|
50
|
+
detail: "text-white-00",
|
|
76
51
|
},
|
|
77
52
|
},
|
|
78
|
-
|
|
53
|
+
defaultVariants: {
|
|
54
|
+
variant: "new",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
79
57
|
|
|
80
58
|
interface HeaderBarProps
|
|
81
|
-
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof
|
|
59
|
+
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeStyles> {
|
|
82
60
|
theme?: Themes;
|
|
83
61
|
/** The colored emphasis pill text. */
|
|
84
62
|
label: string;
|
|
@@ -88,25 +66,45 @@ interface HeaderBarProps
|
|
|
88
66
|
|
|
89
67
|
const HeaderBar = forwardRef<HTMLDivElement, HeaderBarProps>(
|
|
90
68
|
({ variant = "new", label, title, theme, className, ...props }, ref) => {
|
|
69
|
+
const badge = (
|
|
70
|
+
<div className={cn(badgeStyles({ variant }))}>
|
|
71
|
+
<p className={cn(badgeTextStyles({ variant }))}>{label}</p>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// The plain title sits in its own 32px box with 6px side padding — that padding, not a row
|
|
76
|
+
// `gap`, is what separates it from the badge (matching the design's `padding` frame).
|
|
77
|
+
const plain = (
|
|
78
|
+
<div className="flex h-8 items-center justify-center px-1.5">
|
|
79
|
+
<p className={cn(headerTextStyles, "text-white-00")}>{title}</p>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
|
|
91
83
|
return (
|
|
92
84
|
<div
|
|
93
85
|
ref={ref}
|
|
94
86
|
data-theme={theme}
|
|
95
87
|
className={cn(
|
|
96
|
-
"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)]",
|
|
88
|
+
"inline-flex flex-col items-start overflow-hidden 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)]",
|
|
97
89
|
className,
|
|
98
90
|
)}
|
|
99
91
|
{...props}
|
|
100
92
|
>
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{
|
|
108
|
-
|
|
109
|
-
|
|
93
|
+
{/* `detail` genuinely swaps the two children rather than using `flex-row-reverse`. The
|
|
94
|
+
visual result is the same in LTR, but reversing the row would flip the pair the wrong
|
|
95
|
+
way under `dir="rtl"` and would read out of order to a screen reader. */}
|
|
96
|
+
<div className="flex items-center">
|
|
97
|
+
{variant === "detail" ? (
|
|
98
|
+
<>
|
|
99
|
+
{plain}
|
|
100
|
+
{badge}
|
|
101
|
+
</>
|
|
102
|
+
) : (
|
|
103
|
+
<>
|
|
104
|
+
{badge}
|
|
105
|
+
{plain}
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
110
108
|
</div>
|
|
111
109
|
</div>
|
|
112
110
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { forwardRef, InputHTMLAttributes, ReactNode, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { cn } from "../utils/cn";
|
|
4
|
-
import {
|
|
4
|
+
import { ToolTipSide } from "./Tooltip";
|
|
5
5
|
import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
|
|
6
6
|
import { ActionButton } from "./ActionButton";
|
|
7
7
|
import { Themes } from "../utils/types";
|
|
@@ -13,8 +13,13 @@ export interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, "size
|
|
|
13
13
|
icon?: ReactNode; // to add left side icon if you pass it
|
|
14
14
|
childrenSide?: ReactNode; // to add action button to the end of the input
|
|
15
15
|
popoverChildren?: ReactNode; // to add drop down list if you pass it
|
|
16
|
-
|
|
16
|
+
/** Marks the field invalid: any non-undefined value turns on the negative border. */
|
|
17
|
+
errorMessage?: string;
|
|
17
18
|
onTable?: boolean; // to change the border style of the component when it is on table
|
|
19
|
+
/**
|
|
20
|
+
* @deprecated Ignored. The error tooltip was removed — an invalid field is shown by its negative
|
|
21
|
+
* border alone. Kept so existing call sites keep compiling; it will go in a future major.
|
|
22
|
+
*/
|
|
18
23
|
toolTipSide?: ToolTipSide;
|
|
19
24
|
theme?: Themes;
|
|
20
25
|
}
|
|
@@ -29,6 +34,7 @@ export const InputField = forwardRef<HTMLInputElement, Props>(
|
|
|
29
34
|
errorMessage,
|
|
30
35
|
onTable,
|
|
31
36
|
variant = "PresentationStyle",
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- deprecated no-op, destructured to keep it out of the {...props} spread
|
|
32
38
|
toolTipSide,
|
|
33
39
|
theme,
|
|
34
40
|
className,
|
|
@@ -49,54 +55,47 @@ export const InputField = forwardRef<HTMLInputElement, Props>(
|
|
|
49
55
|
// TODO: make the user input visible when input is focused
|
|
50
56
|
return (
|
|
51
57
|
<Popover onOpenChange={setIsPopoverOpen}>
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
<PopoverTrigger ref={triggerRef} asChild>
|
|
59
|
+
<Group
|
|
60
|
+
error={errorMessage !== undefined}
|
|
61
|
+
onTable={onTable}
|
|
62
|
+
size={size}
|
|
63
|
+
variant={variant}
|
|
64
|
+
data-theme={theme}
|
|
65
|
+
onFocus={(e) => {
|
|
66
|
+
setPopoverWidth(e.currentTarget.offsetWidth);
|
|
67
|
+
setIsPopoverOpen(!isPopoverOpen);
|
|
68
|
+
inputRef.current?.focus();
|
|
69
|
+
}}
|
|
70
|
+
className={className}
|
|
71
|
+
>
|
|
72
|
+
{icon && <Icon>{icon}</Icon>}
|
|
73
|
+
<Input
|
|
74
|
+
{...props}
|
|
65
75
|
onFocus={(e) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
setIsPopoverOpen(true);
|
|
77
|
+
props.onFocus?.(e);
|
|
78
|
+
}}
|
|
79
|
+
onBlur={(e) => {
|
|
80
|
+
setIsPopoverOpen(false);
|
|
81
|
+
props.onBlur?.(e);
|
|
69
82
|
}}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{icon && <Icon>{icon}</Icon>}
|
|
73
|
-
<Input
|
|
74
|
-
{...props}
|
|
75
|
-
onFocus={(e) => {
|
|
76
|
-
setIsPopoverOpen(true);
|
|
77
|
-
props.onFocus?.(e);
|
|
78
|
-
}}
|
|
79
|
-
onBlur={(e) => {
|
|
80
|
-
setIsPopoverOpen(false);
|
|
81
|
-
props.onBlur?.(e);
|
|
82
|
-
}}
|
|
83
|
-
ref={inputRef}
|
|
84
|
-
/>
|
|
83
|
+
ref={inputRef}
|
|
84
|
+
/>
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
</Tooltip>
|
|
86
|
+
<Trilling>
|
|
87
|
+
{childrenSide}
|
|
88
|
+
{popoverChildren && (
|
|
89
|
+
<PopoverActionButton
|
|
90
|
+
size={size}
|
|
91
|
+
variant={variant}
|
|
92
|
+
isPopoverOpen={isPopoverOpen}
|
|
93
|
+
disabled={props.disabled}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</Trilling>
|
|
97
|
+
</Group>
|
|
98
|
+
</PopoverTrigger>
|
|
100
99
|
|
|
101
100
|
{popoverChildren && !props.disabled && (
|
|
102
101
|
<PopoverContent
|
|
@@ -141,10 +141,9 @@ export { Popover, PopoverTrigger, PopoverContent, PopoverItem };
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
const PopoverItemStyles = cva(
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
// `PopoverItem` always renders it, including through `asChild`.
|
|
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`.
|
|
148
147
|
[
|
|
149
148
|
"text-content-presentation-global-primary-light typography-body-medium-regular",
|
|
150
149
|
"outline-none",
|
|
@@ -158,6 +157,8 @@ const PopoverItemStyles = cva(
|
|
|
158
157
|
"overflow-hidden",
|
|
159
158
|
"p-[2px]",
|
|
160
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.
|
|
161
162
|
"ease-in-out",
|
|
162
163
|
"duration-300",
|
|
163
164
|
"[&>div]:flex",
|
|
@@ -172,8 +173,9 @@ const PopoverItemStyles = cva(
|
|
|
172
173
|
{
|
|
173
174
|
variants: {
|
|
174
175
|
variant: {
|
|
175
|
-
// The menu
|
|
176
|
-
//
|
|
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.
|
|
177
179
|
Default: [
|
|
178
180
|
"text-content-presentation-global-primary-light",
|
|
179
181
|
"[&>div]:hover:bg-white-50 [&>div]:hover:shadow-[0_0_16px_0_rgba(0,0,0,0.36)]",
|
|
@@ -182,6 +184,7 @@ const PopoverItemStyles = cva(
|
|
|
182
184
|
"[&:focus>div]:text-black-1000",
|
|
183
185
|
"[&:disabled>div]:text-content-presentation-global-primary-light",
|
|
184
186
|
"[&:disabled>div]:opacity-50",
|
|
187
|
+
"[&:disabled>div]:hover:text-content-presentation-global-primary-light",
|
|
185
188
|
"[&:disabled>div]:hover:bg-transparent",
|
|
186
189
|
"[&:disabled>div]:hover:shadow-none",
|
|
187
190
|
],
|
|
@@ -191,6 +194,11 @@ const PopoverItemStyles = cva(
|
|
|
191
194
|
"[&>div]:hover:text-blue-sparkle-700",
|
|
192
195
|
"[&:focus>div]:bg-white-alpha-75",
|
|
193
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",
|
|
194
202
|
],
|
|
195
203
|
Negative: [
|
|
196
204
|
"text-medium-red-200",
|
|
@@ -198,6 +206,11 @@ const PopoverItemStyles = cva(
|
|
|
198
206
|
"[&>div]:hover:text-medium-red-600",
|
|
199
207
|
"[&:focus>div]:bg-white-alpha-75",
|
|
200
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",
|
|
201
214
|
],
|
|
202
215
|
SystemStyle: [
|
|
203
216
|
"bg-background-system-body-primary",
|
|
@@ -247,7 +260,10 @@ const PopoverItemStyles = cva(
|
|
|
247
260
|
|
|
248
261
|
const popoverStyles = cva(
|
|
249
262
|
[
|
|
250
|
-
|
|
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",
|
|
251
267
|
"rounded-[8px]",
|
|
252
268
|
"border",
|
|
253
269
|
"min-w-[240px]",
|
|
@@ -281,8 +297,6 @@ const popoverStyles = cva(
|
|
|
281
297
|
// translucent: over the previous opaque `form-base` the backdrop-blur painted nothing.
|
|
282
298
|
"bg-[rgba(61,64,69,0.72)]",
|
|
283
299
|
"shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]",
|
|
284
|
-
// Figma's group-container stacks its rows with a 4px gutter.
|
|
285
|
-
"flex flex-col gap-1",
|
|
286
300
|
],
|
|
287
301
|
},
|
|
288
302
|
overlayBlur: {
|
|
@@ -249,19 +249,23 @@ export function SearchableSelect({
|
|
|
249
249
|
data-theme={theme}
|
|
250
250
|
ref={popoverContentRef}
|
|
251
251
|
variant={variant}
|
|
252
|
-
style={{ width: dropdownWidth || undefined }}
|
|
253
252
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
254
253
|
onWheel={(e) => e.stopPropagation()}
|
|
255
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
|
+
}}
|
|
256
261
|
>
|
|
257
|
-
{/* Dedicated scroll viewport
|
|
258
|
-
|
|
259
|
-
|
|
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. */}
|
|
260
265
|
<div
|
|
261
266
|
ref={scrollRef}
|
|
262
267
|
onScroll={handleScroll}
|
|
263
|
-
className="overflow-y-auto overflow-x-hidden rounded-[10px] scrollbar-hide"
|
|
264
|
-
style={{ maxHeight: maxVisibleItems * ROW_HEIGHT }}
|
|
268
|
+
className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden rounded-[10px] scrollbar-hide"
|
|
265
269
|
>
|
|
266
270
|
{filteredOptions.length > 0 && (
|
|
267
271
|
// Boxed group container — matches the DropdownMenu's auto-grouped look.
|
|
@@ -72,6 +72,8 @@ interface Props<T> {
|
|
|
72
72
|
*/
|
|
73
73
|
selectableFolders?: boolean;
|
|
74
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. */
|
|
75
77
|
maxBodyHeight?: number;
|
|
76
78
|
|
|
77
79
|
// --- Async (optional; static `nodes` still works) ---
|
|
@@ -112,7 +114,7 @@ export function SearchableTree<T>({
|
|
|
112
114
|
className,
|
|
113
115
|
defaultExpanded = true,
|
|
114
116
|
selectableFolders = false,
|
|
115
|
-
maxBodyHeight =
|
|
117
|
+
maxBodyHeight = 200,
|
|
116
118
|
filterClientSide = true,
|
|
117
119
|
onSearchChange,
|
|
118
120
|
searchDebounceMs = 300,
|
|
@@ -299,16 +301,31 @@ export function SearchableTree<T>({
|
|
|
299
301
|
variant={variant}
|
|
300
302
|
align="start"
|
|
301
303
|
sideOffset={4}
|
|
302
|
-
|
|
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
|
+
}}
|
|
303
310
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
304
311
|
onWheel={(e) => e.stopPropagation()}
|
|
305
|
-
|
|
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"
|
|
306
318
|
>
|
|
307
319
|
{/* SearchResult body — frosted surface holding the "Tree" label + rows. */}
|
|
308
|
-
|
|
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">
|
|
309
323
|
<div
|
|
310
|
-
|
|
311
|
-
|
|
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]"
|
|
312
329
|
>
|
|
313
330
|
{visibleTree.length > 0 ? (
|
|
314
331
|
<div className="flex flex-col gap-[4px]">
|
|
@@ -59,6 +59,9 @@ interface Props<T> {
|
|
|
59
59
|
searchPlaceholder?: string;
|
|
60
60
|
/** Label shown on the dialog's search field. */
|
|
61
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;
|
|
62
65
|
|
|
63
66
|
size?: "XS" | "S" | "M";
|
|
64
67
|
variant?: "SystemStyle" | "PresentationStyle";
|
|
@@ -109,6 +112,7 @@ export function SearchableTreeDialog<T>({
|
|
|
109
112
|
placeholder = "Select…",
|
|
110
113
|
searchPlaceholder = "Search…",
|
|
111
114
|
title = "Select an item",
|
|
115
|
+
maxBodyHeight = 200,
|
|
112
116
|
size = "M",
|
|
113
117
|
variant = "PresentationStyle",
|
|
114
118
|
icon,
|
|
@@ -283,7 +287,13 @@ export function SearchableTreeDialog<T>({
|
|
|
283
287
|
{/* SearchResult.Container — inset 12px each side, tucked under the bar (z-1). */}
|
|
284
288
|
<div className="flex w-full items-start px-[12px]">
|
|
285
289
|
{/* Body — frosted surface that connects to the bar with rounded-bottom. */}
|
|
286
|
-
<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
|
+
>
|
|
287
297
|
{visibleTree.length > 0 ? (
|
|
288
298
|
<div className="flex flex-col gap-[4px]">
|
|
289
299
|
{/* "Tree" section label. */}
|
|
@@ -4,7 +4,6 @@ import * as SelectPrimitive from "@radix-ui/react-select";
|
|
|
4
4
|
import { cn } from "../utils/cn";
|
|
5
5
|
import { cva, VariantProps } from "class-variance-authority";
|
|
6
6
|
import { ActionButton } from "./ActionButton";
|
|
7
|
-
import { Tooltip } from "./Tooltip";
|
|
8
7
|
import { MenuItemStyles } from "./DropdownMenu";
|
|
9
8
|
import { Themes } from "../utils/types";
|
|
10
9
|
|
|
@@ -18,6 +17,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
18
17
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
19
18
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> &
|
|
20
19
|
VariantProps<typeof PopoverTriggerStyles> & {
|
|
20
|
+
/** Marks the trigger invalid: any non-undefined value turns on the negative border. */
|
|
21
21
|
errors?: string;
|
|
22
22
|
icon?: string;
|
|
23
23
|
theme?: Themes;
|
|
@@ -38,52 +38,50 @@ const SelectTrigger = React.forwardRef<
|
|
|
38
38
|
ref,
|
|
39
39
|
) => {
|
|
40
40
|
return (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
<SelectPrimitive.Trigger
|
|
42
|
+
data-theme={theme}
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn(
|
|
45
|
+
PopoverTriggerStyles({
|
|
46
|
+
size,
|
|
47
|
+
variant,
|
|
48
|
+
error: errors !== undefined,
|
|
49
|
+
onTable,
|
|
50
|
+
}),
|
|
51
|
+
className,
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
<p
|
|
56
|
+
className={cn({
|
|
57
|
+
"[&_span]:text-[#A0A0A0]": !props.value,
|
|
58
|
+
})}
|
|
55
59
|
>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"[&_span]:text-[#A0A0A0]": !props.value,
|
|
59
|
-
})}
|
|
60
|
-
>
|
|
61
|
-
{children}
|
|
62
|
-
</p>
|
|
60
|
+
{children}
|
|
61
|
+
</p>
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
</Tooltip>
|
|
63
|
+
{/* The shared in-field action button. Its box tracks the trigger height the same way
|
|
64
|
+
InputField's does — a 30px field takes the 22px button, a 40px field the 32px one. */}
|
|
65
|
+
<ActionButton
|
|
66
|
+
as={"span"}
|
|
67
|
+
size={size === "XL" ? "M" : size === "S" ? "XS" : "S"}
|
|
68
|
+
className={cn([
|
|
69
|
+
"group-aria-expanded:bg-background-presentation-action-hover",
|
|
70
|
+
"group-aria-expanded:text-white",
|
|
71
|
+
])}
|
|
72
|
+
>
|
|
73
|
+
<i
|
|
74
|
+
className={cn(
|
|
75
|
+
"ri-arrow-down-s-line transition-all duration-100 ease-in-out group-aria-expanded:rotate-180",
|
|
76
|
+
{ "!text-[12px]": size === "S" },
|
|
77
|
+
{ "!text-[16px]": size === "M" },
|
|
78
|
+
{ "!text-[18px]": size === "L" },
|
|
79
|
+
{ "!text-[26px]": size === "XL" },
|
|
80
|
+
{ icon: icon },
|
|
81
|
+
)}
|
|
82
|
+
/>
|
|
83
|
+
</ActionButton>
|
|
84
|
+
</SelectPrimitive.Trigger>
|
|
87
85
|
);
|
|
88
86
|
},
|
|
89
87
|
);
|
|
@@ -140,10 +138,19 @@ const SelectContent = React.forwardRef<
|
|
|
140
138
|
ref={ref}
|
|
141
139
|
className={cn(SelectContentStyles({ variant }), className)}
|
|
142
140
|
position={position}
|
|
141
|
+
// Never taller than 368px, and never taller than the space Radix has on screen. Valid because
|
|
142
|
+
// `position` defaults to "popper", which is what publishes the variable.
|
|
143
|
+
style={{
|
|
144
|
+
maxHeight: "min(368px, var(--radix-select-content-available-height, 100vh))",
|
|
145
|
+
...props.style,
|
|
146
|
+
}}
|
|
143
147
|
{...props}
|
|
144
148
|
>
|
|
145
|
-
{/* Dedicated scroll viewport + boxed group, matching SearchableSelect's menu surface.
|
|
146
|
-
|
|
149
|
+
{/* Dedicated scroll viewport + boxed group, matching SearchableSelect's menu surface.
|
|
150
|
+
`flex-1 min-h-0` is what makes it scroll: without `min-h-0` a flex item refuses to shrink
|
|
151
|
+
below its content, so the list grew past the panel and `overflow-hidden` simply clipped
|
|
152
|
+
the rows off with no scrollbar. */}
|
|
153
|
+
<SelectPrimitive.Viewport className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden rounded-[10px] scrollbar-hide">
|
|
147
154
|
<div className="flex flex-col gap-[1px] overflow-hidden rounded-[10px]">{children}</div>
|
|
148
155
|
</SelectPrimitive.Viewport>
|
|
149
156
|
</SelectPrimitive.Content>
|
|
@@ -231,7 +238,8 @@ const SelectContentStyles = cva(
|
|
|
231
238
|
"data-[state=open]:animate-in",
|
|
232
239
|
"data-[state=open]:fade-in-0",
|
|
233
240
|
"z-[1000]",
|
|
234
|
-
|
|
241
|
+
// Panel height is set inline below from `min(368px, available-height)`; the class is kept off
|
|
242
|
+
// deliberately so the inline value governs.
|
|
235
243
|
],
|
|
236
244
|
{
|
|
237
245
|
variants: {
|