@luxfi/ui 5.6.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/package.json +81 -278
- package/dist/accordion.cjs +0 -213
- package/dist/accordion.js +0 -186
- package/dist/alert.cjs +0 -553
- package/dist/alert.js +0 -531
- package/dist/avatar.cjs +0 -149
- package/dist/avatar.js +0 -125
- package/dist/badge.cjs +0 -611
- package/dist/badge.js +0 -589
- package/dist/button.cjs +0 -689
- package/dist/button.js +0 -664
- package/dist/checkbox.cjs +0 -265
- package/dist/checkbox.js +0 -241
- package/dist/close-button.cjs +0 -73
- package/dist/close-button.js +0 -51
- package/dist/collapsible.cjs +0 -702
- package/dist/collapsible.js +0 -679
- package/dist/color-mode.cjs +0 -96
- package/dist/color-mode.js +0 -72
- package/dist/dialog.cjs +0 -279
- package/dist/dialog.js +0 -246
- package/dist/drawer.cjs +0 -207
- package/dist/drawer.js +0 -175
- package/dist/empty-state.cjs +0 -93
- package/dist/empty-state.js +0 -71
- package/dist/field.cjs +0 -183
- package/dist/field.js +0 -160
- package/dist/heading.cjs +0 -46
- package/dist/heading.js +0 -40
- package/dist/icon-button.cjs +0 -491
- package/dist/icon-button.js +0 -470
- package/dist/image.cjs +0 -572
- package/dist/image.js +0 -551
- package/dist/index.cjs +0 -5779
- package/dist/index.js +0 -5619
- package/dist/input-group.cjs +0 -155
- package/dist/input-group.js +0 -133
- package/dist/input.cjs +0 -65
- package/dist/input.js +0 -59
- package/dist/link.cjs +0 -630
- package/dist/link.js +0 -606
- package/dist/menu.cjs +0 -305
- package/dist/menu.js +0 -269
- package/dist/pin-input.cjs +0 -182
- package/dist/pin-input.js +0 -160
- package/dist/popover.cjs +0 -327
- package/dist/popover.js +0 -294
- package/dist/progress-circle.cjs +0 -152
- package/dist/progress-circle.js +0 -128
- package/dist/progress.cjs +0 -117
- package/dist/progress.js +0 -94
- package/dist/provider.cjs +0 -62
- package/dist/provider.js +0 -40
- package/dist/radio.cjs +0 -177
- package/dist/radio.js +0 -153
- package/dist/rating.cjs +0 -80
- package/dist/rating.js +0 -58
- package/dist/select.cjs +0 -791
- package/dist/select.js +0 -757
- package/dist/separator.cjs +0 -57
- package/dist/separator.js +0 -51
- package/dist/skeleton.cjs +0 -370
- package/dist/skeleton.js +0 -346
- package/dist/slider.cjs +0 -138
- package/dist/slider.js +0 -115
- package/dist/switch.cjs +0 -163
- package/dist/switch.js +0 -140
- package/dist/table.cjs +0 -1044
- package/dist/table.js +0 -1013
- package/dist/tabs.cjs +0 -240
- package/dist/tabs.js +0 -213
- package/dist/tag.cjs +0 -651
- package/dist/tag.js +0 -628
- package/dist/textarea.cjs +0 -65
- package/dist/textarea.js +0 -59
- package/dist/toaster.cjs +0 -99
- package/dist/toaster.js +0 -96
- package/dist/tooltip.cjs +0 -171
- package/dist/tooltip.js +0 -148
- package/dist/utils.cjs +0 -11
- package/dist/utils.js +0 -9
- package/src/accordion.tsx +0 -285
- package/src/alert.tsx +0 -221
- package/src/avatar.tsx +0 -174
- package/src/badge.tsx +0 -158
- package/src/button.tsx +0 -411
- package/src/checkbox.tsx +0 -307
- package/src/close-button.tsx +0 -51
- package/src/collapsible.tsx +0 -126
- package/src/color-mode.tsx +0 -125
- package/src/dialog.tsx +0 -356
- package/src/drawer.tsx +0 -186
- package/src/empty-state.tsx +0 -97
- package/src/field.tsx +0 -202
- package/src/heading.tsx +0 -55
- package/src/icon-button.tsx +0 -192
- package/src/image.tsx +0 -280
- package/src/index.ts +0 -192
- package/src/input-group.tsx +0 -159
- package/src/input.tsx +0 -60
- package/src/link.tsx +0 -326
- package/src/menu.tsx +0 -471
- package/src/pin-input.tsx +0 -187
- package/src/popover.tsx +0 -400
- package/src/progress-circle.tsx +0 -180
- package/src/progress.tsx +0 -109
- package/src/provider.tsx +0 -12
- package/src/radio.tsx +0 -175
- package/src/rating.tsx +0 -79
- package/src/select.tsx +0 -696
- package/src/separator.tsx +0 -59
- package/src/skeleton.tsx +0 -302
- package/src/slider.tsx +0 -152
- package/src/switch.tsx +0 -158
- package/src/table.tsx +0 -621
- package/src/tabs.tsx +0 -354
- package/src/tag.tsx +0 -159
- package/src/textarea.tsx +0 -60
- package/src/toaster.tsx +0 -117
- package/src/tokens.css +0 -438
- package/src/tooltip.tsx +0 -184
- package/src/utils/cn.ts +0 -7
- package/src/utils.ts +0 -6
- package/tokens.css +0 -438
package/src/select.tsx
DELETED
|
@@ -1,696 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as RadixSelect from '@radix-ui/react-select';
|
|
4
|
-
import { useDebounce } from '@uidotdev/usehooks';
|
|
5
|
-
import * as React from 'react';
|
|
6
|
-
|
|
7
|
-
// Minimal ListCollection compatible with Chakra's API but without the dependency
|
|
8
|
-
export interface ListCollection<T> {
|
|
9
|
-
items: Array<T>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function createListCollection<T>(config: { items: Array<T> }): ListCollection<T> {
|
|
13
|
-
return { items: config.items };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
import { cn } from './utils';
|
|
17
|
-
|
|
18
|
-
import { Skeleton } from './skeleton';
|
|
19
|
-
|
|
20
|
-
// Inline chevron icon (east-mini arrow)
|
|
21
|
-
const ArrowIcon = ({ className }: { readonly className?: string }) => (
|
|
22
|
-
<svg className={ className } viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
23
|
-
<path d="M7.5 15L12.5 10L7.5 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
24
|
-
</svg>
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
// Inline check icon
|
|
28
|
-
const CheckIcon = ({ className }: { readonly className?: string }) => (
|
|
29
|
-
<svg className={ className } viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
30
|
-
<path d="M16.667 5L7.5 14.167 3.333 10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
31
|
-
</svg>
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
// Inline FilterInput for SelectAsync
|
|
35
|
-
interface FilterInputProps {
|
|
36
|
-
readonly placeholder?: string;
|
|
37
|
-
readonly initialValue?: string;
|
|
38
|
-
readonly onChange?: (value: string) => void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function FilterInput({ placeholder, initialValue = '', onChange }: FilterInputProps) {
|
|
42
|
-
const [ value, setValue ] = React.useState(initialValue);
|
|
43
|
-
const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
44
|
-
setValue(e.target.value);
|
|
45
|
-
onChange?.(e.target.value);
|
|
46
|
-
}, [ onChange ]);
|
|
47
|
-
return (
|
|
48
|
-
<input
|
|
49
|
-
type="text"
|
|
50
|
-
placeholder={ placeholder }
|
|
51
|
-
value={ value }
|
|
52
|
-
onChange={ handleChange }
|
|
53
|
-
className={ cn(
|
|
54
|
-
'w-full px-3 py-2 text-sm outline-none',
|
|
55
|
-
'bg-transparent border-b border-[var(--color-border-divider)]',
|
|
56
|
-
'placeholder:text-[var(--color-input-placeholder)]',
|
|
57
|
-
) }
|
|
58
|
-
/>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Types
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
export type ViewMode = 'default' | 'compact';
|
|
67
|
-
|
|
68
|
-
export interface SelectOption<Value extends string = string> {
|
|
69
|
-
label: string;
|
|
70
|
-
renderLabel?: (place: 'item' | 'value-text') => React.ReactNode;
|
|
71
|
-
value: Value;
|
|
72
|
-
icon?: React.ReactNode;
|
|
73
|
-
afterElement?: React.ReactNode;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Internal context — replaces Chakra's useSelectContext
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
interface SelectInternalContextValue {
|
|
81
|
-
value: Array<string>;
|
|
82
|
-
selectedItems: Array<SelectOption>;
|
|
83
|
-
collection: ListCollection<SelectOption>;
|
|
84
|
-
onValueChange: (details: { value: Array<string>; items: Array<SelectOption> }) => void;
|
|
85
|
-
open: boolean;
|
|
86
|
-
size?: 'sm' | 'lg';
|
|
87
|
-
variant?: string;
|
|
88
|
-
disabled?: boolean;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const SelectInternalContext = React.createContext<SelectInternalContextValue | null>(null);
|
|
92
|
-
|
|
93
|
-
function useSelectInternalContext(): SelectInternalContextValue {
|
|
94
|
-
const ctx = React.useContext(SelectInternalContext);
|
|
95
|
-
if (!ctx) {
|
|
96
|
-
throw new Error('Select compound components must be rendered inside <SelectRoot>');
|
|
97
|
-
}
|
|
98
|
-
return ctx;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// SelectRoot
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
export interface SelectRootProps {
|
|
106
|
-
children?: React.ReactNode;
|
|
107
|
-
collection?: ListCollection<SelectOption>;
|
|
108
|
-
defaultValue?: Array<string>;
|
|
109
|
-
value?: Array<string>;
|
|
110
|
-
onValueChange?: (details: { value: Array<string>; items: Array<SelectOption> }) => void;
|
|
111
|
-
onInteractOutside?: () => void;
|
|
112
|
-
name?: string;
|
|
113
|
-
disabled?: boolean;
|
|
114
|
-
readOnly?: boolean;
|
|
115
|
-
required?: boolean;
|
|
116
|
-
invalid?: boolean;
|
|
117
|
-
size?: 'sm' | 'lg';
|
|
118
|
-
variant?: string;
|
|
119
|
-
open?: boolean;
|
|
120
|
-
defaultOpen?: boolean;
|
|
121
|
-
onOpenChange?: (open: boolean) => void;
|
|
122
|
-
// Chakra compat — accepted but handled via className/style
|
|
123
|
-
positioning?: { sameWidth?: boolean; offset?: { mainAxis?: number; crossAxis?: number } };
|
|
124
|
-
lazyMount?: boolean;
|
|
125
|
-
unmountOnExit?: boolean;
|
|
126
|
-
asChild?: boolean;
|
|
127
|
-
className?: string;
|
|
128
|
-
style?: React.CSSProperties;
|
|
129
|
-
// Chakra style prop shims
|
|
130
|
-
w?: string | Record<string, string>;
|
|
131
|
-
maxW?: string | Record<string, string>;
|
|
132
|
-
minW?: string | Record<string, string>;
|
|
133
|
-
hideFrom?: string;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export const SelectRoot = React.forwardRef<HTMLDivElement, SelectRootProps>(
|
|
137
|
-
function SelectRoot(props, ref) {
|
|
138
|
-
const {
|
|
139
|
-
children,
|
|
140
|
-
collection: collectionProp,
|
|
141
|
-
defaultValue: defaultValueArr,
|
|
142
|
-
value: valueProp,
|
|
143
|
-
onValueChange: onValueChangeProp,
|
|
144
|
-
onInteractOutside,
|
|
145
|
-
name,
|
|
146
|
-
disabled,
|
|
147
|
-
readOnly,
|
|
148
|
-
required,
|
|
149
|
-
invalid,
|
|
150
|
-
size,
|
|
151
|
-
variant,
|
|
152
|
-
open: openProp,
|
|
153
|
-
defaultOpen,
|
|
154
|
-
onOpenChange,
|
|
155
|
-
positioning: _positioning,
|
|
156
|
-
lazyMount: _lazyMount,
|
|
157
|
-
unmountOnExit: _unmountOnExit,
|
|
158
|
-
asChild: _asChild,
|
|
159
|
-
className,
|
|
160
|
-
style,
|
|
161
|
-
w,
|
|
162
|
-
maxW,
|
|
163
|
-
minW,
|
|
164
|
-
hideFrom: _hideFrom,
|
|
165
|
-
} = props;
|
|
166
|
-
|
|
167
|
-
// Provide a fallback empty collection
|
|
168
|
-
const collection = collectionProp ?? createListCollection<SelectOption>({ items: [] });
|
|
169
|
-
|
|
170
|
-
// Controlled / uncontrolled value (always Array<string> externally)
|
|
171
|
-
const [ internalValue, setInternalValue ] = React.useState<Array<string>>(defaultValueArr ?? []);
|
|
172
|
-
const currentValue = valueProp ?? internalValue;
|
|
173
|
-
|
|
174
|
-
const selectedItems = React.useMemo(() => {
|
|
175
|
-
return currentValue
|
|
176
|
-
.map((v) => collection.items.find((item) => item.value === v))
|
|
177
|
-
.filter(Boolean) as Array<SelectOption>;
|
|
178
|
-
}, [ currentValue, collection.items ]);
|
|
179
|
-
|
|
180
|
-
const [ open, setOpen ] = React.useState(defaultOpen ?? false);
|
|
181
|
-
const isOpen = openProp ?? open;
|
|
182
|
-
|
|
183
|
-
const handleOpenChange = React.useCallback((nextOpen: boolean) => {
|
|
184
|
-
setOpen(nextOpen);
|
|
185
|
-
onOpenChange?.(nextOpen);
|
|
186
|
-
if (!nextOpen) {
|
|
187
|
-
onInteractOutside?.();
|
|
188
|
-
}
|
|
189
|
-
}, [ onOpenChange, onInteractOutside ]);
|
|
190
|
-
|
|
191
|
-
const handleRadixValueChange = React.useCallback((radixValue: string) => {
|
|
192
|
-
const nextArr = [ radixValue ];
|
|
193
|
-
if (!valueProp) {
|
|
194
|
-
setInternalValue(nextArr);
|
|
195
|
-
}
|
|
196
|
-
const items = collection.items.filter((item) => nextArr.includes(item.value));
|
|
197
|
-
onValueChangeProp?.({ value: nextArr, items });
|
|
198
|
-
}, [ valueProp, collection.items, onValueChangeProp ]);
|
|
199
|
-
|
|
200
|
-
const ctxValue = React.useMemo<SelectInternalContextValue>(() => ({
|
|
201
|
-
value: currentValue,
|
|
202
|
-
selectedItems,
|
|
203
|
-
collection,
|
|
204
|
-
onValueChange: (details) => {
|
|
205
|
-
if (!valueProp) {
|
|
206
|
-
setInternalValue(details.value);
|
|
207
|
-
}
|
|
208
|
-
onValueChangeProp?.(details);
|
|
209
|
-
},
|
|
210
|
-
open: isOpen,
|
|
211
|
-
size,
|
|
212
|
-
variant,
|
|
213
|
-
disabled,
|
|
214
|
-
}), [ currentValue, selectedItems, collection, valueProp, onValueChangeProp, isOpen, size, variant, disabled ]);
|
|
215
|
-
|
|
216
|
-
// Extract Chakra-style shorthand props into inline style
|
|
217
|
-
const resolveVal = (v: string | Record<string, string> | undefined) => {
|
|
218
|
-
if (!v) return undefined;
|
|
219
|
-
if (typeof v === 'string') return v;
|
|
220
|
-
return v.base ?? v.lg ?? Object.values(v)[0];
|
|
221
|
-
};
|
|
222
|
-
const inlineStyle = React.useMemo(() => {
|
|
223
|
-
const s: React.CSSProperties = { ...style };
|
|
224
|
-
const rw = resolveVal(w); if (rw) s.width = rw;
|
|
225
|
-
const rmw = resolveVal(maxW); if (rmw) s.maxWidth = rmw;
|
|
226
|
-
const rminw = resolveVal(minW); if (rminw) s.minWidth = rminw;
|
|
227
|
-
return s;
|
|
228
|
-
}, [ style, w, maxW, minW ]);
|
|
229
|
-
|
|
230
|
-
return (
|
|
231
|
-
<SelectInternalContext.Provider value={ ctxValue }>
|
|
232
|
-
<RadixSelect.Root
|
|
233
|
-
value={ currentValue[0] ?? '' }
|
|
234
|
-
defaultValue={ defaultValueArr?.[0] }
|
|
235
|
-
onValueChange={ handleRadixValueChange }
|
|
236
|
-
open={ isOpen }
|
|
237
|
-
onOpenChange={ handleOpenChange }
|
|
238
|
-
disabled={ disabled || readOnly }
|
|
239
|
-
name={ name }
|
|
240
|
-
required={ required }
|
|
241
|
-
>
|
|
242
|
-
<div
|
|
243
|
-
ref={ ref }
|
|
244
|
-
className={ cn('relative inline-flex', className) }
|
|
245
|
-
style={ inlineStyle }
|
|
246
|
-
data-invalid={ invalid || undefined }
|
|
247
|
-
data-disabled={ disabled || undefined }
|
|
248
|
-
data-variant={ variant || undefined }
|
|
249
|
-
data-size={ size || undefined }
|
|
250
|
-
>
|
|
251
|
-
{ children }
|
|
252
|
-
</div>
|
|
253
|
-
</RadixSelect.Root>
|
|
254
|
-
</SelectInternalContext.Provider>
|
|
255
|
-
);
|
|
256
|
-
},
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
// SelectControl
|
|
261
|
-
// ---------------------------------------------------------------------------
|
|
262
|
-
|
|
263
|
-
export interface SelectControlProps {
|
|
264
|
-
children?: React.ReactNode;
|
|
265
|
-
noIndicator?: boolean;
|
|
266
|
-
triggerProps?: React.ComponentPropsWithoutRef<typeof RadixSelect.Trigger> & {
|
|
267
|
-
asChild?: boolean;
|
|
268
|
-
px?: string | number;
|
|
269
|
-
className?: string;
|
|
270
|
-
};
|
|
271
|
-
loading?: boolean;
|
|
272
|
-
defaultValue?: Array<string>;
|
|
273
|
-
className?: string;
|
|
274
|
-
style?: React.CSSProperties;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export const SelectControl = React.forwardRef<HTMLButtonElement, SelectControlProps>(
|
|
278
|
-
function SelectControl(props, ref) {
|
|
279
|
-
const { children, noIndicator, triggerProps, loading, defaultValue } = props;
|
|
280
|
-
const ctx = useSelectInternalContext();
|
|
281
|
-
|
|
282
|
-
const isDefaultValue = Array.isArray(defaultValue) ?
|
|
283
|
-
ctx.value.every((item) => defaultValue.includes(item)) :
|
|
284
|
-
false;
|
|
285
|
-
|
|
286
|
-
const { asChild, px: _px, className: triggerClassName, ...radixTriggerProps } = triggerProps ?? {};
|
|
287
|
-
|
|
288
|
-
const trigger = (
|
|
289
|
-
<RadixSelect.Trigger
|
|
290
|
-
ref={ ref }
|
|
291
|
-
asChild={ asChild }
|
|
292
|
-
className={ cn(
|
|
293
|
-
'group peer inline-flex items-center gap-2 cursor-pointer',
|
|
294
|
-
'rounded-lg text-sm transition-colors outline-none',
|
|
295
|
-
'border border-[var(--color-input-border)] bg-[var(--color-input-bg)] text-[var(--color-input-fg)]',
|
|
296
|
-
'hover:border-[var(--color-input-border-hover)]',
|
|
297
|
-
'focus-visible:border-[var(--color-input-border-focus)]',
|
|
298
|
-
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
299
|
-
ctx.variant === 'plain' && 'border-transparent bg-transparent px-1 py-0.5',
|
|
300
|
-
ctx.variant !== 'plain' && ctx.size === 'lg' && 'px-4 py-2.5 min-h-[52px]',
|
|
301
|
-
ctx.variant !== 'plain' && ctx.size !== 'lg' && 'px-3 py-1.5 min-h-[36px]',
|
|
302
|
-
triggerClassName,
|
|
303
|
-
) }
|
|
304
|
-
data-default-value={ isDefaultValue || undefined }
|
|
305
|
-
{ ...radixTriggerProps }
|
|
306
|
-
>
|
|
307
|
-
{ asChild ? children : (
|
|
308
|
-
<>
|
|
309
|
-
{ children }
|
|
310
|
-
{ !noIndicator && (
|
|
311
|
-
<span className="ml-auto shrink-0 transition-transform data-[state=open]:rotate-180 text-[var(--color-icon-secondary)]">
|
|
312
|
-
<ArrowIcon className="h-5 w-5 -rotate-90"/>
|
|
313
|
-
</span>
|
|
314
|
-
) }
|
|
315
|
-
</>
|
|
316
|
-
) }
|
|
317
|
-
</RadixSelect.Trigger>
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
if (loading) {
|
|
321
|
-
return (
|
|
322
|
-
<Skeleton loading={ loading } asChild>
|
|
323
|
-
{ trigger }
|
|
324
|
-
</Skeleton>
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return trigger;
|
|
329
|
-
},
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// ---------------------------------------------------------------------------
|
|
333
|
-
// SelectClearTrigger
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
|
-
|
|
336
|
-
export interface SelectClearTriggerProps {
|
|
337
|
-
className?: string;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export const SelectClearTrigger = React.forwardRef<HTMLButtonElement, SelectClearTriggerProps>(
|
|
341
|
-
function SelectClearTrigger(props, ref) {
|
|
342
|
-
const { className, ...rest } = props;
|
|
343
|
-
const ctx = useSelectInternalContext();
|
|
344
|
-
|
|
345
|
-
const handleClick = React.useCallback(() => {
|
|
346
|
-
ctx.onValueChange({ value: [], items: [] });
|
|
347
|
-
}, [ ctx ]);
|
|
348
|
-
|
|
349
|
-
return (
|
|
350
|
-
<button
|
|
351
|
-
ref={ ref }
|
|
352
|
-
type="button"
|
|
353
|
-
onClick={ handleClick }
|
|
354
|
-
className={ cn('pointer-events-auto', className) }
|
|
355
|
-
aria-label="Clear selection"
|
|
356
|
-
{ ...rest }
|
|
357
|
-
>
|
|
358
|
-
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
359
|
-
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
|
|
360
|
-
</svg>
|
|
361
|
-
</button>
|
|
362
|
-
);
|
|
363
|
-
},
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
// ---------------------------------------------------------------------------
|
|
367
|
-
// SelectContent
|
|
368
|
-
// ---------------------------------------------------------------------------
|
|
369
|
-
|
|
370
|
-
export interface SelectContentProps {
|
|
371
|
-
children?: React.ReactNode;
|
|
372
|
-
portalled?: boolean;
|
|
373
|
-
portalRef?: React.RefObject<HTMLElement>;
|
|
374
|
-
className?: string;
|
|
375
|
-
style?: React.CSSProperties;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export const SelectContent = React.forwardRef<HTMLDivElement, SelectContentProps>(
|
|
379
|
-
function SelectContent(props, ref) {
|
|
380
|
-
const { portalled = true, portalRef, children, className, ...rest } = props;
|
|
381
|
-
|
|
382
|
-
const content = (
|
|
383
|
-
<RadixSelect.Content
|
|
384
|
-
ref={ ref }
|
|
385
|
-
position="popper"
|
|
386
|
-
sideOffset={ 4 }
|
|
387
|
-
className={ cn(
|
|
388
|
-
'z-50 min-w-[8rem] overflow-hidden rounded-lg',
|
|
389
|
-
'bg-[var(--color-popover-bg)] shadow-[var(--shadow-popover)]',
|
|
390
|
-
'border border-[var(--color-border-divider)]',
|
|
391
|
-
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
392
|
-
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
393
|
-
className,
|
|
394
|
-
) }
|
|
395
|
-
{ ...rest }
|
|
396
|
-
>
|
|
397
|
-
<RadixSelect.Viewport className="p-1">
|
|
398
|
-
{ children }
|
|
399
|
-
</RadixSelect.Viewport>
|
|
400
|
-
</RadixSelect.Content>
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
if (portalled) {
|
|
404
|
-
return (
|
|
405
|
-
<RadixSelect.Portal container={ portalRef?.current ?? undefined }>
|
|
406
|
-
{ content }
|
|
407
|
-
</RadixSelect.Portal>
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return content;
|
|
412
|
-
},
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
// ---------------------------------------------------------------------------
|
|
416
|
-
// SelectItem
|
|
417
|
-
// ---------------------------------------------------------------------------
|
|
418
|
-
|
|
419
|
-
export interface SelectItemProps {
|
|
420
|
-
item: SelectOption;
|
|
421
|
-
children?: React.ReactNode;
|
|
422
|
-
className?: string;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export const SelectItem = React.forwardRef<HTMLDivElement, SelectItemProps>(
|
|
426
|
-
function SelectItem(props, ref) {
|
|
427
|
-
const { item, children, className, ...rest } = props;
|
|
428
|
-
|
|
429
|
-
return (
|
|
430
|
-
<RadixSelect.Item
|
|
431
|
-
ref={ ref }
|
|
432
|
-
value={ item.value }
|
|
433
|
-
textValue={ item.label }
|
|
434
|
-
className={ cn(
|
|
435
|
-
'relative flex cursor-pointer select-none items-center gap-2',
|
|
436
|
-
'rounded-md px-3 py-2 text-sm outline-none',
|
|
437
|
-
'text-[var(--color-text-primary)]',
|
|
438
|
-
'data-[highlighted]:bg-[var(--color-selected-control-bg)]',
|
|
439
|
-
'data-[state=checked]:font-medium',
|
|
440
|
-
'data-[disabled]:opacity-50 data-[disabled]:pointer-events-none',
|
|
441
|
-
className,
|
|
442
|
-
) }
|
|
443
|
-
{ ...rest }
|
|
444
|
-
>
|
|
445
|
-
{ item.icon }
|
|
446
|
-
<RadixSelect.ItemText>
|
|
447
|
-
{ children }
|
|
448
|
-
</RadixSelect.ItemText>
|
|
449
|
-
<RadixSelect.ItemIndicator className="ml-auto shrink-0">
|
|
450
|
-
<CheckIcon className="h-5 w-5"/>
|
|
451
|
-
</RadixSelect.ItemIndicator>
|
|
452
|
-
</RadixSelect.Item>
|
|
453
|
-
);
|
|
454
|
-
},
|
|
455
|
-
);
|
|
456
|
-
|
|
457
|
-
// ---------------------------------------------------------------------------
|
|
458
|
-
// SelectValueText
|
|
459
|
-
// ---------------------------------------------------------------------------
|
|
460
|
-
|
|
461
|
-
interface SelectValueTextProps {
|
|
462
|
-
children?: (items: Array<SelectOption>) => React.ReactNode;
|
|
463
|
-
placeholder?: string;
|
|
464
|
-
size?: SelectRootProps['size'];
|
|
465
|
-
required?: boolean;
|
|
466
|
-
invalid?: boolean;
|
|
467
|
-
errorText?: string;
|
|
468
|
-
mode?: ViewMode;
|
|
469
|
-
className?: string;
|
|
470
|
-
style?: React.CSSProperties;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
export const SelectValueText = React.forwardRef<HTMLSpanElement, SelectValueTextProps>(
|
|
474
|
-
function SelectValueText(props, ref) {
|
|
475
|
-
const { children, size, required, invalid, errorText, mode, className, style, placeholder: placeholderProp, ...rest } = props;
|
|
476
|
-
const ctx = useSelectInternalContext();
|
|
477
|
-
|
|
478
|
-
const placeholderText = `${ placeholderProp ?? '' }${ required ? '*' : '' }${ invalid && errorText ? ` - ${ errorText }` : '' }`;
|
|
479
|
-
|
|
480
|
-
const content = (() => {
|
|
481
|
-
const items = ctx.selectedItems;
|
|
482
|
-
|
|
483
|
-
if (items.length === 0) return null;
|
|
484
|
-
|
|
485
|
-
if (children) return children(items);
|
|
486
|
-
|
|
487
|
-
if (items.length === 1) {
|
|
488
|
-
const item = items[0] as SelectOption;
|
|
489
|
-
|
|
490
|
-
if (!item) return null;
|
|
491
|
-
|
|
492
|
-
const label = size === 'lg' ? (
|
|
493
|
-
<span
|
|
494
|
-
className={ cn(
|
|
495
|
-
'text-xs block',
|
|
496
|
-
invalid ? 'text-[var(--color-text-error)]' : 'text-[var(--color-input-placeholder)]',
|
|
497
|
-
) }
|
|
498
|
-
style={{ display: '-webkit-box' }}
|
|
499
|
-
>
|
|
500
|
-
{ placeholderText }
|
|
501
|
-
</span>
|
|
502
|
-
) : null;
|
|
503
|
-
|
|
504
|
-
return (
|
|
505
|
-
<>
|
|
506
|
-
{ label }
|
|
507
|
-
<span className="inline-flex items-center flex-nowrap gap-1">
|
|
508
|
-
{ item.icon }
|
|
509
|
-
{ mode !== 'compact' && (
|
|
510
|
-
<span
|
|
511
|
-
style={{
|
|
512
|
-
WebkitLineClamp: 1,
|
|
513
|
-
WebkitBoxOrient: 'vertical',
|
|
514
|
-
display: '-webkit-box',
|
|
515
|
-
overflow: 'hidden',
|
|
516
|
-
}}
|
|
517
|
-
>
|
|
518
|
-
{ item.renderLabel ? item.renderLabel('value-text') : item.label }
|
|
519
|
-
</span>
|
|
520
|
-
) }
|
|
521
|
-
</span>
|
|
522
|
-
</>
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
return `${ items.length } selected`;
|
|
527
|
-
})();
|
|
528
|
-
|
|
529
|
-
// Radix SelectValue renders the selected value or placeholder.
|
|
530
|
-
// We override with our custom content.
|
|
531
|
-
return (
|
|
532
|
-
<RadixSelect.Value
|
|
533
|
-
ref={ ref }
|
|
534
|
-
placeholder={ placeholderText }
|
|
535
|
-
className={ cn('flex flex-col justify-center min-w-0 truncate', className) }
|
|
536
|
-
{ ...rest }
|
|
537
|
-
>
|
|
538
|
-
{ content }
|
|
539
|
-
</RadixSelect.Value>
|
|
540
|
-
);
|
|
541
|
-
},
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
// ---------------------------------------------------------------------------
|
|
545
|
-
// SelectItemGroup
|
|
546
|
-
// ---------------------------------------------------------------------------
|
|
547
|
-
|
|
548
|
-
interface SelectItemGroupProps {
|
|
549
|
-
children?: React.ReactNode;
|
|
550
|
-
label: React.ReactNode;
|
|
551
|
-
className?: string;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
export const SelectItemGroup = React.forwardRef<HTMLDivElement, SelectItemGroupProps>(
|
|
555
|
-
function SelectItemGroup(props, ref) {
|
|
556
|
-
const { children, label, className, ...rest } = props;
|
|
557
|
-
return (
|
|
558
|
-
<RadixSelect.Group ref={ ref } className={ cn('py-1', className) } { ...rest }>
|
|
559
|
-
<RadixSelect.Label className="px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)]">
|
|
560
|
-
{ label }
|
|
561
|
-
</RadixSelect.Label>
|
|
562
|
-
{ children }
|
|
563
|
-
</RadixSelect.Group>
|
|
564
|
-
);
|
|
565
|
-
},
|
|
566
|
-
);
|
|
567
|
-
|
|
568
|
-
// ---------------------------------------------------------------------------
|
|
569
|
-
// SelectLabel / SelectItemText
|
|
570
|
-
// ---------------------------------------------------------------------------
|
|
571
|
-
|
|
572
|
-
export const SelectLabel = RadixSelect.Label;
|
|
573
|
-
export const SelectItemText = RadixSelect.ItemText;
|
|
574
|
-
|
|
575
|
-
// ---------------------------------------------------------------------------
|
|
576
|
-
// Select (composite)
|
|
577
|
-
// ---------------------------------------------------------------------------
|
|
578
|
-
|
|
579
|
-
export interface SelectProps extends SelectRootProps {
|
|
580
|
-
collection: ListCollection<SelectOption>;
|
|
581
|
-
placeholder: string;
|
|
582
|
-
portalled?: boolean;
|
|
583
|
-
loading?: boolean;
|
|
584
|
-
errorText?: string;
|
|
585
|
-
contentProps?: SelectContentProps;
|
|
586
|
-
contentHeader?: React.ReactNode;
|
|
587
|
-
itemFilter?: (item: SelectOption) => boolean;
|
|
588
|
-
mode?: ViewMode;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
export const Select = React.forwardRef<HTMLDivElement, SelectProps>((props, ref) => {
|
|
592
|
-
const { collection, placeholder, portalled = true, loading, errorText, contentProps, contentHeader, itemFilter, mode, ...rest } = props;
|
|
593
|
-
return (
|
|
594
|
-
<SelectRoot
|
|
595
|
-
ref={ ref }
|
|
596
|
-
collection={ collection }
|
|
597
|
-
{ ...rest }
|
|
598
|
-
>
|
|
599
|
-
<SelectControl loading={ loading }>
|
|
600
|
-
<SelectValueText
|
|
601
|
-
placeholder={ placeholder }
|
|
602
|
-
size={ props.size }
|
|
603
|
-
required={ props.required }
|
|
604
|
-
invalid={ props.invalid }
|
|
605
|
-
errorText={ errorText }
|
|
606
|
-
mode={ mode }
|
|
607
|
-
/>
|
|
608
|
-
</SelectControl>
|
|
609
|
-
<SelectContent portalled={ portalled } { ...contentProps }>
|
|
610
|
-
{ contentHeader }
|
|
611
|
-
{ collection.items
|
|
612
|
-
.filter(itemFilter ?? (() => true))
|
|
613
|
-
.map((item: SelectOption) => (
|
|
614
|
-
<React.Fragment key={ item.value }>
|
|
615
|
-
<SelectItem item={ item }>
|
|
616
|
-
{ item.renderLabel ? item.renderLabel('item') : item.label }
|
|
617
|
-
</SelectItem>
|
|
618
|
-
{ item.afterElement }
|
|
619
|
-
</React.Fragment>
|
|
620
|
-
)) }
|
|
621
|
-
</SelectContent>
|
|
622
|
-
</SelectRoot>
|
|
623
|
-
);
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// ---------------------------------------------------------------------------
|
|
627
|
-
// SelectAsync
|
|
628
|
-
// ---------------------------------------------------------------------------
|
|
629
|
-
|
|
630
|
-
export interface SelectAsyncProps extends Omit<SelectProps, 'collection'> {
|
|
631
|
-
placeholder: string;
|
|
632
|
-
portalled?: boolean;
|
|
633
|
-
loading?: boolean;
|
|
634
|
-
loadOptions: (input: string, currentValue: Array<string>) => Promise<ListCollection<SelectOption>>;
|
|
635
|
-
extraControls?: React.ReactNode;
|
|
636
|
-
mode?: ViewMode;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
export const SelectAsync = React.forwardRef<HTMLDivElement, SelectAsyncProps>((props, ref) => {
|
|
640
|
-
const { placeholder, portalled = true, loading, loadOptions, extraControls, onValueChange, errorText, mode, contentHeader, ...rest } = props;
|
|
641
|
-
|
|
642
|
-
const [ collection, setCollection ] = React.useState<ListCollection<SelectOption>>(createListCollection<SelectOption>({ items: [] }));
|
|
643
|
-
const [ inputValue, setInputValue ] = React.useState('');
|
|
644
|
-
const [ value, setValue ] = React.useState<Array<string>>([]);
|
|
645
|
-
|
|
646
|
-
const debouncedInputValue = useDebounce(inputValue, 300);
|
|
647
|
-
|
|
648
|
-
React.useEffect(() => {
|
|
649
|
-
loadOptions(debouncedInputValue, value).then(setCollection);
|
|
650
|
-
}, [ debouncedInputValue, loadOptions, value ]);
|
|
651
|
-
|
|
652
|
-
const handleFilterChange = React.useCallback((val: string) => {
|
|
653
|
-
setInputValue(val);
|
|
654
|
-
}, []);
|
|
655
|
-
|
|
656
|
-
const handleValueChange = React.useCallback(({ value: v, items }: { value: Array<string>; items: Array<SelectOption> }) => {
|
|
657
|
-
setValue(v);
|
|
658
|
-
onValueChange?.({ value: v, items });
|
|
659
|
-
}, [ onValueChange ]);
|
|
660
|
-
|
|
661
|
-
return (
|
|
662
|
-
<SelectRoot
|
|
663
|
-
ref={ ref }
|
|
664
|
-
collection={ collection }
|
|
665
|
-
onValueChange={ handleValueChange }
|
|
666
|
-
{ ...rest }
|
|
667
|
-
>
|
|
668
|
-
<SelectControl loading={ loading }>
|
|
669
|
-
<SelectValueText
|
|
670
|
-
placeholder={ placeholder }
|
|
671
|
-
size={ props.size }
|
|
672
|
-
required={ props.required }
|
|
673
|
-
invalid={ props.invalid }
|
|
674
|
-
errorText={ errorText }
|
|
675
|
-
mode={ mode }
|
|
676
|
-
/>
|
|
677
|
-
</SelectControl>
|
|
678
|
-
<SelectContent portalled={ portalled }>
|
|
679
|
-
<div className="px-4">
|
|
680
|
-
<FilterInput
|
|
681
|
-
placeholder="Search"
|
|
682
|
-
initialValue={ inputValue }
|
|
683
|
-
onChange={ handleFilterChange }
|
|
684
|
-
/>
|
|
685
|
-
{ extraControls }
|
|
686
|
-
</div>
|
|
687
|
-
{ contentHeader }
|
|
688
|
-
{ collection.items.map((item) => (
|
|
689
|
-
<SelectItem item={ item } key={ item.value }>
|
|
690
|
-
{ item.renderLabel ? item.renderLabel('item') : item.label }
|
|
691
|
-
</SelectItem>
|
|
692
|
-
)) }
|
|
693
|
-
</SelectContent>
|
|
694
|
-
</SelectRoot>
|
|
695
|
-
);
|
|
696
|
-
});
|