torch-glare 2.5.4 → 2.5.6
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 +138 -69
- package/apps/lib/components/Button.tsx +10 -2
- 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 +20 -8
- package/apps/lib/components/DataViews/filters/filters.tsx +0 -2
- package/apps/lib/components/DataViews/index.ts +8 -4
- package/apps/lib/components/DataViews/slots.ts +9 -0
- package/apps/lib/components/DataViews/states.tsx +43 -8
- package/apps/lib/components/DataViews/views/table-view.tsx +184 -176
- package/apps/lib/components/Drawer.tsx +70 -39
- package/apps/lib/components/DropdownMenu.tsx +79 -22
- package/apps/lib/components/FormBuilder/context.ts +12 -0
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +38 -19
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +31 -8
- package/apps/lib/components/FormBuilder/submit.tsx +21 -1
- package/apps/lib/components/FormBuilder/types.ts +21 -0
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +139 -17
- package/apps/lib/components/FormRenderer/detail.tsx +57 -8
- package/apps/lib/components/FormRenderer/form-renderer.tsx +82 -10
- package/apps/lib/components/FormRenderer/index.ts +2 -0
- package/apps/lib/components/FormRenderer/notch-action.tsx +64 -0
- package/apps/lib/components/FormRenderer/stepper.tsx +56 -2
- package/apps/lib/components/FormRenderer/types.ts +37 -0
- 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/SectionBlock.tsx +24 -3
- package/apps/lib/components/Select.tsx +64 -56
- package/apps/lib/components/SlideDatePicker.tsx +5 -5
- package/apps/lib/components/TabSwitch.tsx +18 -12
- package/apps/lib/components/Table.tsx +15 -28
- package/apps/lib/hooks/useActiveTreeItem.ts +4 -1
- package/apps/lib/hooks/useHtmlDir.ts +31 -0
- package/apps/lib/hooks/useTagSelection.ts +95 -9
- package/apps/lib/layouts/FieldSection.tsx +28 -2
- package/apps/lib/registry.json +20 -5
- package/apps/lib/utils/scroller.ts +26 -0
- package/docs/components/badge-field.md +30 -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 +32 -22
- package/docs/components/data-views/migration.md +7 -5
- package/docs/components/drawer.md +5 -5
- package/docs/components/dropdown-menu.md +3 -0
- package/docs/components/form-builder.md +36 -2
- package/docs/components/form-renderer.md +71 -1
- package/docs/components/header-bar.md +3 -2
- package/docs/components/input-field.md +3 -3
- package/docs/components/section-block.md +6 -0
- package/docs/components/select.md +1 -1
- package/docs/migration/changelog.md +19 -0
- package/docs/reference/hooks.md +23 -0
- package/docs/reference/utilities.md +22 -0
- package/package.json +1 -1
- package/apps/lib/components/DataViews/filters/summary.tsx +0 -65
|
@@ -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. */}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { forwardRef, HTMLAttributes, ReactNode } from "react";
|
|
2
2
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
3
|
import { cn } from "../utils/cn";
|
|
4
|
+
import { horizontalScrollerStyles } from "../utils/scroller";
|
|
4
5
|
|
|
6
|
+
// LOCAL PATCH (Contact Center): the pill's side padding is asymmetric (16 leading / 22 trailing),
|
|
7
|
+
// so it must be logical — `ps`/`pe` rather than `pl`/`pr`. This pill heads every section card, so
|
|
8
|
+
// mirrored the wrong way it reads as a systematic misalignment across the whole page.
|
|
5
9
|
const titleBadge = cva(
|
|
6
|
-
"flex pt-2 pb-2
|
|
10
|
+
"flex pt-2 pb-2 ps-[16px] pe-[22px] justify-center items-center gap-[6px] rounded-[10px] self-start typography-headers-medium-medium text-[#F4F4F4]",
|
|
7
11
|
{
|
|
8
12
|
variants: {
|
|
9
13
|
color: {
|
|
@@ -56,12 +60,29 @@ const header = cva("flex px-[6px] justify-between gap-3", {
|
|
|
56
60
|
defaultVariants: { variant: "Default" },
|
|
57
61
|
});
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
// LOCAL PATCH (Contact Center): the body is the section's horizontal scrollport.
|
|
64
|
+
//
|
|
65
|
+
// `Table` renders `overflow-visible w-auto` (so its sticky header can reach a real scrollport), so
|
|
66
|
+
// a table wider than its card does not clip or scroll itself — it widens the card, and then the
|
|
67
|
+
// page. Only one call site in the app wraps its table in `TableScroller`; the rest drop a bare
|
|
68
|
+
// `<Table>` straight into a section. Owning the scroll here contains all of them at once, and
|
|
69
|
+
// replaces the `Table` variant's old `overflow-hidden`, which truncated instead of scrolling.
|
|
70
|
+
//
|
|
71
|
+
// `overflow-y-hidden` is required, not decorative: CSS computes `overflow-y: visible` to `auto`
|
|
72
|
+
// whenever `overflow-x` is not `visible`, so without it every section grows a spurious vertical
|
|
73
|
+
// scrollbar. The body is content-height, so nothing is clipped vertically. Same reasoning, and the
|
|
74
|
+
// same scrollbar styling, as `TableScroller`.
|
|
75
|
+
//
|
|
76
|
+
// The cost: a scrollport is the containing block for `position: sticky`, so a `<Table>`'s sticky
|
|
77
|
+
// header inside a section now pins to a box that never scrolls vertically — i.e. it stops
|
|
78
|
+
// sticking. Sections hold short tables, and in `variant="Table"` those headers were already inert
|
|
79
|
+
// under the old `overflow-hidden`.
|
|
80
|
+
const body = cva(`flex w-full flex-col ${horizontalScrollerStyles}`, {
|
|
60
81
|
variants: {
|
|
61
82
|
variant: {
|
|
62
83
|
Default: "px-[42px] gap-[2px]",
|
|
63
84
|
// Full bleed, with the rule that separates the header from the table.
|
|
64
|
-
Table: "mt-[6px] border-t border-border-presentation-global-primary
|
|
85
|
+
Table: "mt-[6px] border-t border-border-presentation-global-primary",
|
|
65
86
|
},
|
|
66
87
|
},
|
|
67
88
|
defaultVariants: { variant: "Default" },
|
|
@@ -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
|
|
|
@@ -17,11 +16,12 @@ const SelectValue = SelectPrimitive.Value;
|
|
|
17
16
|
const SelectTrigger = React.forwardRef<
|
|
18
17
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
19
18
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> &
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
VariantProps<typeof PopoverTriggerStyles> & {
|
|
20
|
+
/** Marks the trigger invalid: any non-undefined value turns on the negative border. */
|
|
21
|
+
errors?: string;
|
|
22
|
+
icon?: string;
|
|
23
|
+
theme?: Themes;
|
|
24
|
+
}
|
|
25
25
|
>(
|
|
26
26
|
(
|
|
27
27
|
{
|
|
@@ -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
|
);
|
|
@@ -126,9 +124,9 @@ SelectScrollDownButton.displayName = "SelectScrollDownButton";
|
|
|
126
124
|
const SelectContent = React.forwardRef<
|
|
127
125
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
128
126
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> &
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
VariantProps<typeof SelectContentStyles> & {
|
|
128
|
+
theme?: Themes;
|
|
129
|
+
}
|
|
132
130
|
>(
|
|
133
131
|
(
|
|
134
132
|
{ className, children, variant = "PresentationStyle", position = "popper", theme, ...props },
|
|
@@ -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: {
|
|
@@ -149,11 +149,11 @@ export const SlideDatePicker = forwardRef<HTMLInputElement, SlideDatePickerProps
|
|
|
149
149
|
|
|
150
150
|
return (
|
|
151
151
|
<Popover onOpenChange={setIsOpen}>
|
|
152
|
-
{/*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
{/* `theme` is deliberately NOT passed to the field: only the picker panel below is pinned
|
|
153
|
+
dark. The field follows the surrounding page instead, so it matches the other inputs
|
|
154
|
+
next to it. Note it must not be handed down as `data-theme` either — `asChild` merges
|
|
155
|
+
trigger props onto the child, which would land the attribute on the bare <input> and
|
|
156
|
+
leave its text resolving dark tokens inside a light field (white on white). */}
|
|
157
157
|
<PopoverTrigger ref={triggerRef} asChild className="w-full flex-1">
|
|
158
158
|
{isValidElement(children) ? (
|
|
159
159
|
cloneElement(children as React.ReactElement<HTMLInputElement & { theme?: string }>, {
|
|
@@ -31,13 +31,16 @@ const trackStyles = cva(
|
|
|
31
31
|
"rounded-[10px]",
|
|
32
32
|
"bg-background-presentation-body-primary",
|
|
33
33
|
"shadow-[inset_0_0_4px_0_rgba(0,0,0,0.08)]",
|
|
34
|
+
// Contains the active pill's drop-shadow: it is clipped at the track's rounded edge instead
|
|
35
|
+
// of bleeding onto whatever sits behind the switcher.
|
|
36
|
+
"overflow-hidden",
|
|
34
37
|
],
|
|
35
38
|
{
|
|
36
39
|
variants: {
|
|
37
40
|
size: {
|
|
38
|
-
S: ["
|
|
39
|
-
M: ["
|
|
40
|
-
L: ["
|
|
41
|
+
S: ["p-[2px]"],
|
|
42
|
+
M: ["p-[2px]"],
|
|
43
|
+
L: ["p-[3px] rounded-[12px]"],
|
|
41
44
|
},
|
|
42
45
|
},
|
|
43
46
|
defaultVariants: { size: "M" },
|
|
@@ -48,7 +51,7 @@ const trackStyles = cva(
|
|
|
48
51
|
// transparent and lighten on hover.
|
|
49
52
|
const optionStyles = cva(
|
|
50
53
|
[
|
|
51
|
-
"flex items-center justify-center
|
|
54
|
+
"flex items-center justify-center",
|
|
52
55
|
"rounded-[8px]",
|
|
53
56
|
"font-[510] leading-none",
|
|
54
57
|
"transition-all duration-200 ease-in-out",
|
|
@@ -61,7 +64,7 @@ const optionStyles = cva(
|
|
|
61
64
|
size: {
|
|
62
65
|
S: ["h-5 px-2 text-[12px]", "[&_svg]:h-3 [&_svg]:w-3", "[&_i]:text-[12px]"],
|
|
63
66
|
M: ["h-6 px-3 text-[14px]", "[&_svg]:h-[14px] [&_svg]:w-[14px]", "[&_i]:text-[14px]"],
|
|
64
|
-
L: ["h-8 px-4 text-[16px]", "[&_svg]:h-4 [&_svg]:w-4", "[&_i]:text-[16px]"],
|
|
67
|
+
L: ["h-8 px-4 text-[16px] rounded-[10px]", "[&_svg]:h-4 [&_svg]:w-4", "[&_i]:text-[16px]"],
|
|
65
68
|
},
|
|
66
69
|
active: {
|
|
67
70
|
// Active option = a solid raised WHITE pill with dark text, in every
|
|
@@ -78,6 +81,8 @@ const optionStyles = cva(
|
|
|
78
81
|
// keeps both states the same width — without it, selecting a tab resizes the pill and
|
|
79
82
|
// shoves its neighbours. The design's own 4px gap absorbs the 1px.
|
|
80
83
|
"border border-black/5",
|
|
84
|
+
// The pill casts outward to look raised; the track clips it (`overflow-hidden` on
|
|
85
|
+
// `trackStyles`) so the shadow never spills past the container.
|
|
81
86
|
"drop-shadow-[0px_0px_5px_rgba(0,0,0,0.25)]",
|
|
82
87
|
],
|
|
83
88
|
false: [
|
|
@@ -116,22 +121,23 @@ function TabSwitchInner<T extends string = string>(
|
|
|
116
121
|
>
|
|
117
122
|
{options.map((option, idx) => {
|
|
118
123
|
const active = option.value === value;
|
|
124
|
+
const isDisabled = disabled || option.disabled;
|
|
125
|
+
// A divider is drawn only between two INACTIVE options — the raised pill is never flanked
|
|
126
|
+
// by a rule, so the two dividers touching it fade out.
|
|
119
127
|
const prevActive = idx > 0 && options[idx - 1].value === value;
|
|
120
|
-
// A divider sits between two inactive options only — the active pill is
|
|
121
|
-
// never flanked by one.
|
|
122
128
|
const showDivider = idx > 0 && !active && !prevActive;
|
|
123
|
-
const isDisabled = disabled || option.disabled;
|
|
124
129
|
|
|
125
130
|
return (
|
|
126
131
|
<div key={option.value} className="flex items-center">
|
|
127
132
|
{idx > 0 && (
|
|
128
133
|
<div
|
|
129
134
|
aria-hidden
|
|
135
|
+
// The slot is ALWAYS rendered and only its ink changes. Unmounting it instead cost
|
|
136
|
+
// the track 7px (`w-px` + `mx-[3px]`) every time one went away, so picking a tab
|
|
137
|
+
// resized the track and shoved its neighbours — with three options, selecting the
|
|
138
|
+
// middle one dropped both dividers and moved the track 14px.
|
|
130
139
|
className={cn(
|
|
131
|
-
|
|
132
|
-
// the track 7px every time the rule below hid one, so picking a tab resized the
|
|
133
|
-
// track and shoved its neighbours sideways.
|
|
134
|
-
"mx-[3px] h-3 w-px",
|
|
140
|
+
"h-3 w-px",
|
|
135
141
|
showDivider ? "bg-border-presentation-action-disabled" : "bg-transparent",
|
|
136
142
|
)}
|
|
137
143
|
/>
|