@timeax/form-palette 0.0.3 → 0.0.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/{src/schema/adapter.ts → dist/adapters.d.mts} +118 -43
- package/dist/adapters.d.ts +292 -0
- package/dist/adapters.js +13283 -0
- package/dist/adapters.js.map +1 -0
- package/dist/adapters.mjs +13269 -0
- package/dist/adapters.mjs.map +1 -0
- package/dist/index.d.mts +3744 -0
- package/dist/index.d.ts +3744 -0
- package/dist/index.js +43014 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +42965 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +22 -7
- package/.scaffold-cache.json +0 -537
- package/src/.scaffold-cache.json +0 -544
- package/src/adapters/axios.ts +0 -117
- package/src/adapters/index.ts +0 -91
- package/src/adapters/inertia.ts +0 -187
- package/src/core/adapter-registry.ts +0 -87
- package/src/core/bound/bind-host.ts +0 -14
- package/src/core/bound/observe-bound-field.ts +0 -172
- package/src/core/bound/wait-for-bound-field.ts +0 -57
- package/src/core/context.ts +0 -23
- package/src/core/core-provider.tsx +0 -818
- package/src/core/core-root.tsx +0 -72
- package/src/core/core-shell.tsx +0 -44
- package/src/core/errors/error-strip.tsx +0 -71
- package/src/core/errors/index.ts +0 -2
- package/src/core/errors/map-error-bag.ts +0 -51
- package/src/core/errors/map-zod.ts +0 -39
- package/src/core/hooks/use-button.ts +0 -220
- package/src/core/hooks/use-core-context.ts +0 -20
- package/src/core/hooks/use-core-utility.ts +0 -0
- package/src/core/hooks/use-core.ts +0 -13
- package/src/core/hooks/use-field.ts +0 -497
- package/src/core/hooks/use-optional-field.ts +0 -28
- package/src/core/index.ts +0 -0
- package/src/core/registry/binder-registry.ts +0 -82
- package/src/core/registry/field-registry.ts +0 -187
- package/src/core/test.tsx +0 -17
- package/src/global.d.ts +0 -14
- package/src/index.ts +0 -68
- package/src/input/index.ts +0 -4
- package/src/input/input-field.tsx +0 -854
- package/src/input/input-layout-graph.ts +0 -230
- package/src/input/input-props.ts +0 -190
- package/src/lib/get-global-countries.ts +0 -87
- package/src/lib/utils.ts +0 -6
- package/src/presets/index.ts +0 -0
- package/src/presets/shadcn-preset.ts +0 -0
- package/src/presets/shadcn-variants/checkbox.tsx +0 -849
- package/src/presets/shadcn-variants/chips.tsx +0 -756
- package/src/presets/shadcn-variants/color.tsx +0 -284
- package/src/presets/shadcn-variants/custom.tsx +0 -227
- package/src/presets/shadcn-variants/date.tsx +0 -796
- package/src/presets/shadcn-variants/file.tsx +0 -764
- package/src/presets/shadcn-variants/keyvalue.tsx +0 -556
- package/src/presets/shadcn-variants/multiselect.tsx +0 -1132
- package/src/presets/shadcn-variants/number.tsx +0 -176
- package/src/presets/shadcn-variants/password.tsx +0 -737
- package/src/presets/shadcn-variants/phone.tsx +0 -628
- package/src/presets/shadcn-variants/radio.tsx +0 -578
- package/src/presets/shadcn-variants/select.tsx +0 -956
- package/src/presets/shadcn-variants/slider.tsx +0 -622
- package/src/presets/shadcn-variants/text.tsx +0 -343
- package/src/presets/shadcn-variants/textarea.tsx +0 -66
- package/src/presets/shadcn-variants/toggle.tsx +0 -218
- package/src/presets/shadcn-variants/treeselect.tsx +0 -784
- package/src/presets/ui/badge.tsx +0 -46
- package/src/presets/ui/button.tsx +0 -60
- package/src/presets/ui/calendar.tsx +0 -214
- package/src/presets/ui/checkbox.tsx +0 -115
- package/src/presets/ui/custom.tsx +0 -0
- package/src/presets/ui/dialog.tsx +0 -141
- package/src/presets/ui/field.tsx +0 -246
- package/src/presets/ui/input-mask.tsx +0 -739
- package/src/presets/ui/input-otp.tsx +0 -77
- package/src/presets/ui/input.tsx +0 -1011
- package/src/presets/ui/label.tsx +0 -22
- package/src/presets/ui/number.tsx +0 -1370
- package/src/presets/ui/popover.tsx +0 -46
- package/src/presets/ui/radio-group.tsx +0 -43
- package/src/presets/ui/scroll-area.tsx +0 -56
- package/src/presets/ui/select.tsx +0 -190
- package/src/presets/ui/separator.tsx +0 -28
- package/src/presets/ui/slider.tsx +0 -61
- package/src/presets/ui/switch.tsx +0 -32
- package/src/presets/ui/textarea.tsx +0 -634
- package/src/presets/ui/time-dropdowns.tsx +0 -350
- package/src/schema/core.ts +0 -429
- package/src/schema/field-map.ts +0 -0
- package/src/schema/field.ts +0 -224
- package/src/schema/index.ts +0 -0
- package/src/schema/input-field.ts +0 -260
- package/src/schema/presets.ts +0 -0
- package/src/schema/variant.ts +0 -216
- package/src/variants/core/checkbox.tsx +0 -54
- package/src/variants/core/chips.tsx +0 -22
- package/src/variants/core/color.tsx +0 -16
- package/src/variants/core/custom.tsx +0 -18
- package/src/variants/core/date.tsx +0 -25
- package/src/variants/core/file.tsx +0 -9
- package/src/variants/core/keyvalue.tsx +0 -12
- package/src/variants/core/multiselect.tsx +0 -28
- package/src/variants/core/number.tsx +0 -115
- package/src/variants/core/password.tsx +0 -35
- package/src/variants/core/phone.tsx +0 -16
- package/src/variants/core/radio.tsx +0 -38
- package/src/variants/core/select.tsx +0 -15
- package/src/variants/core/slider.tsx +0 -55
- package/src/variants/core/text.tsx +0 -114
- package/src/variants/core/textarea.tsx +0 -22
- package/src/variants/core/toggle.tsx +0 -50
- package/src/variants/core/treeselect.tsx +0 -11
- package/src/variants/helpers/selection-summary.tsx +0 -236
- package/src/variants/index.ts +0 -75
- package/src/variants/registry.ts +0 -38
- package/src/variants/select-shared.ts +0 -0
- package/src/variants/shared.ts +0 -126
- package/tsconfig.json +0 -14
|
@@ -1,956 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import type { VariantBaseProps, ChangeDetail } from "@/variants/shared";
|
|
3
|
-
import { cn } from "@/lib/utils";
|
|
4
|
-
import {
|
|
5
|
-
Select,
|
|
6
|
-
SelectTrigger,
|
|
7
|
-
SelectContent,
|
|
8
|
-
SelectItem,
|
|
9
|
-
} from "@/presets/ui/select";
|
|
10
|
-
import { Input } from "@/presets/ui/input";
|
|
11
|
-
import { Search, X } from "lucide-react";
|
|
12
|
-
|
|
13
|
-
type SelectPrimitive = string | number;
|
|
14
|
-
|
|
15
|
-
type Size = "sm" | "md" | "lg";
|
|
16
|
-
type Density = "compact" | "comfortable" | "loose";
|
|
17
|
-
|
|
18
|
-
export type SelectOption =
|
|
19
|
-
| SelectPrimitive
|
|
20
|
-
| {
|
|
21
|
-
label?: React.ReactNode;
|
|
22
|
-
value?: SelectPrimitive;
|
|
23
|
-
description?: React.ReactNode;
|
|
24
|
-
disabled?: boolean;
|
|
25
|
-
[key: string]: any;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type NormalizedSelectItem = {
|
|
29
|
-
key: string;
|
|
30
|
-
value: SelectPrimitive;
|
|
31
|
-
labelNode: React.ReactNode;
|
|
32
|
-
labelText: string;
|
|
33
|
-
description?: React.ReactNode;
|
|
34
|
-
disabled?: boolean;
|
|
35
|
-
icon?: React.ReactNode;
|
|
36
|
-
raw: SelectOption;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Shadcn-based Select variant.
|
|
41
|
-
*/
|
|
42
|
-
export interface ShadcnSelectVariantProps
|
|
43
|
-
extends Pick<
|
|
44
|
-
VariantBaseProps<SelectPrimitive | undefined>,
|
|
45
|
-
"value" | "onValue" | "error" | "disabled" | "readOnly" | "size" | "density"
|
|
46
|
-
> {
|
|
47
|
-
/**
|
|
48
|
-
* Options for the select.
|
|
49
|
-
*
|
|
50
|
-
* You can pass:
|
|
51
|
-
* - primitives: ["ng", "gh", "ke"]
|
|
52
|
-
* - objects: [{ label, value, ...extra }]
|
|
53
|
-
*/
|
|
54
|
-
options?: SelectOption[];
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Automatically capitalise the first letter of the label
|
|
58
|
-
* (when the resolved label is a string).
|
|
59
|
-
*/
|
|
60
|
-
autoCap?: boolean;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* How to read the label from each option.
|
|
64
|
-
*
|
|
65
|
-
* - string → key on the option object
|
|
66
|
-
* - function → custom mapper
|
|
67
|
-
* - omitted → tries `label`, else String(value)
|
|
68
|
-
*/
|
|
69
|
-
optionLabel?: string | ((item: SelectOption) => React.ReactNode);
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* How to read the value from each option.
|
|
73
|
-
*
|
|
74
|
-
* - string → key on the option object
|
|
75
|
-
* - function → custom mapper
|
|
76
|
-
* - omitted → uses `value`, or `id`, or `key`, or index
|
|
77
|
-
*/
|
|
78
|
-
optionValue?: string | ((item: SelectOption) => SelectPrimitive);
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Optional description line under the label.
|
|
82
|
-
*/
|
|
83
|
-
optionDescription?: string | ((item: SelectOption) => React.ReactNode);
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* How to determine if an option is disabled.
|
|
87
|
-
*/
|
|
88
|
-
optionDisabled?: string | ((item: SelectOption) => boolean);
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* How to extract an icon for each option.
|
|
92
|
-
*
|
|
93
|
-
* - string → key on the option object (default "icon")
|
|
94
|
-
* - function → custom mapper
|
|
95
|
-
*/
|
|
96
|
-
optionIcon?: string | ((item: SelectOption) => React.ReactNode);
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* How to compute the React key for each option.
|
|
100
|
-
*/
|
|
101
|
-
optionKey?: string | ((item: SelectOption, index: number) => React.Key);
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Enable inline search inside the dropdown.
|
|
105
|
-
*/
|
|
106
|
-
searchable?: boolean;
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Placeholder for the search input.
|
|
110
|
-
*/
|
|
111
|
-
searchPlaceholder?: string;
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Label shown when there are no options available at all.
|
|
115
|
-
*
|
|
116
|
-
* If omitted, falls back to `emptySearchText` or a default message.
|
|
117
|
-
*/
|
|
118
|
-
emptyLabel?: React.ReactNode;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Text to show when search yields no results
|
|
122
|
-
* (but there *are* options in general).
|
|
123
|
-
*/
|
|
124
|
-
emptySearchText?: React.ReactNode;
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Show a small clear button in the trigger when a value is selected.
|
|
128
|
-
*/
|
|
129
|
-
clearable?: boolean;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Placeholder when nothing is selected.
|
|
133
|
-
*/
|
|
134
|
-
placeholder?: React.ReactNode;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Wrapper class for the whole variant.
|
|
138
|
-
*/
|
|
139
|
-
className?: string;
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Extra classes for the SelectTrigger.
|
|
143
|
-
*/
|
|
144
|
-
triggerClassName?: string;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Extra classes for the SelectContent popover.
|
|
148
|
-
*/
|
|
149
|
-
contentClassName?: string;
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Custom renderer for each option row.
|
|
153
|
-
*/
|
|
154
|
-
renderOption?: (ctx: {
|
|
155
|
-
item: NormalizedSelectItem;
|
|
156
|
-
selected: boolean;
|
|
157
|
-
index: number;
|
|
158
|
-
option: React.ReactNode; // prebuilt <SelectItem> you can wrap
|
|
159
|
-
}) => React.ReactNode;
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Custom renderer for the trigger value.
|
|
163
|
-
*/
|
|
164
|
-
renderValue?: (ctx: {
|
|
165
|
-
selectedItem: NormalizedSelectItem | null;
|
|
166
|
-
placeholder?: React.ReactNode;
|
|
167
|
-
}) => React.ReactNode;
|
|
168
|
-
|
|
169
|
-
// ─────────────────────────────────────────────
|
|
170
|
-
// Icons & controls (mirror text variant concepts)
|
|
171
|
-
// ─────────────────────────────────────────────
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* One or more icons displayed inside the trigger, on the left.
|
|
175
|
-
*
|
|
176
|
-
* If not provided and `icon` is set, that single icon
|
|
177
|
-
* is treated as `leadingIcons[0]`.
|
|
178
|
-
*/
|
|
179
|
-
leadingIcons?: React.ReactNode[];
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Icons displayed on the right side of the trigger,
|
|
183
|
-
* near the clear button / chevron area.
|
|
184
|
-
*/
|
|
185
|
-
trailingIcons?: React.ReactNode[];
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Convenience single-icon prop for the left side.
|
|
189
|
-
*/
|
|
190
|
-
icon?: React.ReactNode;
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Base gap between icons and text.
|
|
194
|
-
* Defaults to 4px-ish via `gap-1`.
|
|
195
|
-
*/
|
|
196
|
-
iconGap?: number;
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Extra spacing to apply between leading icons and the text.
|
|
200
|
-
*/
|
|
201
|
-
leadingIconSpacing?: number;
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Extra spacing to apply between trailing icons and the clear button.
|
|
205
|
-
*/
|
|
206
|
-
trailingIconSpacing?: number;
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Arbitrary React node rendered before the select (e.g. a button).
|
|
210
|
-
*/
|
|
211
|
-
leadingControl?: React.ReactNode;
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Arbitrary React node rendered after the select (e.g. a button).
|
|
215
|
-
*/
|
|
216
|
-
trailingControl?: React.ReactNode;
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Extra classes for the leading control wrapper.
|
|
220
|
-
*/
|
|
221
|
-
leadingControlClassName?: string;
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Extra classes for the trailing control wrapper.
|
|
225
|
-
*/
|
|
226
|
-
trailingControlClassName?: string;
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* If true and there are controls, the select trigger + controls share
|
|
230
|
-
* a single visual box (borders, radius, focus states).
|
|
231
|
-
*/
|
|
232
|
-
joinControls?: boolean;
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* When joinControls is true, whether the box styling extends over controls
|
|
236
|
-
* (true) or controls are visually separate (false).
|
|
237
|
-
*/
|
|
238
|
-
extendBoxToControls?: boolean;
|
|
239
|
-
|
|
240
|
-
// ─────────────────────────────────────────────
|
|
241
|
-
// Virtual-scroll-ish incremental rendering
|
|
242
|
-
// ─────────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Enable incremental rendering for large option lists.
|
|
246
|
-
*
|
|
247
|
-
* When true, only a page of options is rendered initially,
|
|
248
|
-
* and more are revealed as the user scrolls down.
|
|
249
|
-
*/
|
|
250
|
-
virtualScroll?: boolean;
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Number of options to render per "page" when virtualScroll is enabled.
|
|
254
|
-
* Default: 50.
|
|
255
|
-
*/
|
|
256
|
-
virtualScrollPageSize?: number;
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Distance from the bottom (in px) at which the next page loads.
|
|
260
|
-
* Default: 48px.
|
|
261
|
-
*/
|
|
262
|
-
virtualScrollThreshold?: number;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ─────────────────────────────────────────────
|
|
266
|
-
// Helpers
|
|
267
|
-
// ─────────────────────────────────────────────
|
|
268
|
-
|
|
269
|
-
function capitalizeFirst(label: string): string {
|
|
270
|
-
if (!label) return label;
|
|
271
|
-
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function normalizeOptions(
|
|
275
|
-
opts: readonly SelectOption[] | undefined,
|
|
276
|
-
config: Pick<
|
|
277
|
-
ShadcnSelectVariantProps,
|
|
278
|
-
| "autoCap"
|
|
279
|
-
| "optionLabel"
|
|
280
|
-
| "optionValue"
|
|
281
|
-
| "optionDescription"
|
|
282
|
-
| "optionDisabled"
|
|
283
|
-
| "optionKey"
|
|
284
|
-
| "optionIcon"
|
|
285
|
-
>
|
|
286
|
-
): NormalizedSelectItem[] {
|
|
287
|
-
if (!opts || !opts.length) return [];
|
|
288
|
-
|
|
289
|
-
return opts.map((raw, index) => {
|
|
290
|
-
const asObj: any =
|
|
291
|
-
typeof raw === "string" || typeof raw === "number"
|
|
292
|
-
? { label: String(raw), value: raw }
|
|
293
|
-
: raw;
|
|
294
|
-
|
|
295
|
-
const value: SelectPrimitive =
|
|
296
|
-
typeof config.optionValue === "function"
|
|
297
|
-
? config.optionValue(raw)
|
|
298
|
-
: typeof config.optionValue === "string"
|
|
299
|
-
? (asObj[config.optionValue] as SelectPrimitive)
|
|
300
|
-
: (asObj.value ??
|
|
301
|
-
asObj.id ??
|
|
302
|
-
asObj.key ??
|
|
303
|
-
String(index));
|
|
304
|
-
|
|
305
|
-
let labelNode: React.ReactNode =
|
|
306
|
-
typeof config.optionLabel === "function"
|
|
307
|
-
? config.optionLabel(raw)
|
|
308
|
-
: typeof config.optionLabel === "string"
|
|
309
|
-
? asObj[config.optionLabel] ?? asObj.label ?? String(value)
|
|
310
|
-
: asObj.label ?? String(value);
|
|
311
|
-
|
|
312
|
-
if (config.autoCap && typeof labelNode === "string") {
|
|
313
|
-
labelNode = capitalizeFirst(labelNode);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const labelText =
|
|
317
|
-
typeof labelNode === "string"
|
|
318
|
-
? labelNode
|
|
319
|
-
: typeof labelNode === "number"
|
|
320
|
-
? String(labelNode)
|
|
321
|
-
: asObj.labelText ?? String(value);
|
|
322
|
-
|
|
323
|
-
const description: React.ReactNode =
|
|
324
|
-
typeof config.optionDescription === "function"
|
|
325
|
-
? config.optionDescription(raw)
|
|
326
|
-
: typeof config.optionDescription === "string"
|
|
327
|
-
? asObj[config.optionDescription]
|
|
328
|
-
: asObj.description;
|
|
329
|
-
|
|
330
|
-
const disabled: boolean =
|
|
331
|
-
typeof config.optionDisabled === "function"
|
|
332
|
-
? config.optionDisabled(raw)
|
|
333
|
-
: typeof config.optionDisabled === "string"
|
|
334
|
-
? !!asObj[config.optionDisabled]
|
|
335
|
-
: !!asObj.disabled;
|
|
336
|
-
|
|
337
|
-
const icon: React.ReactNode =
|
|
338
|
-
typeof config.optionIcon === "function"
|
|
339
|
-
? config.optionIcon(raw)
|
|
340
|
-
: typeof config.optionIcon === "string"
|
|
341
|
-
? asObj[config.optionIcon]
|
|
342
|
-
: asObj.icon;
|
|
343
|
-
|
|
344
|
-
const key: React.Key =
|
|
345
|
-
typeof config.optionKey === "function"
|
|
346
|
-
? config.optionKey(raw, index)
|
|
347
|
-
: typeof config.optionKey === "string"
|
|
348
|
-
? asObj[config.optionKey] ?? value ?? index
|
|
349
|
-
: asObj.key ?? value ?? index;
|
|
350
|
-
|
|
351
|
-
return {
|
|
352
|
-
key: String(key),
|
|
353
|
-
value,
|
|
354
|
-
labelNode,
|
|
355
|
-
labelText,
|
|
356
|
-
description,
|
|
357
|
-
disabled,
|
|
358
|
-
icon,
|
|
359
|
-
raw,
|
|
360
|
-
};
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function triggerHeight(size?: Size) {
|
|
365
|
-
switch (size) {
|
|
366
|
-
case "sm":
|
|
367
|
-
return "h-8 text-xs";
|
|
368
|
-
case "lg":
|
|
369
|
-
return "h-11 text-base";
|
|
370
|
-
default:
|
|
371
|
-
return "h-9 text-sm";
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function triggerPadding(density?: Density) {
|
|
376
|
-
switch (density) {
|
|
377
|
-
case "compact":
|
|
378
|
-
return "py-1";
|
|
379
|
-
case "loose":
|
|
380
|
-
return "py-2";
|
|
381
|
-
case "comfortable":
|
|
382
|
-
default:
|
|
383
|
-
return "py-1.5";
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// ─────────────────────────────────────────────
|
|
388
|
-
// Component
|
|
389
|
-
// ─────────────────────────────────────────────
|
|
390
|
-
|
|
391
|
-
export const ShadcnSelectVariant = React.forwardRef<
|
|
392
|
-
HTMLButtonElement,
|
|
393
|
-
ShadcnSelectVariantProps
|
|
394
|
-
>(function ShadcnSelectVariant(props, _ref) {
|
|
395
|
-
const {
|
|
396
|
-
value,
|
|
397
|
-
onValue,
|
|
398
|
-
error,
|
|
399
|
-
disabled,
|
|
400
|
-
readOnly,
|
|
401
|
-
size,
|
|
402
|
-
density,
|
|
403
|
-
|
|
404
|
-
options,
|
|
405
|
-
|
|
406
|
-
autoCap,
|
|
407
|
-
optionLabel,
|
|
408
|
-
optionValue,
|
|
409
|
-
optionDescription,
|
|
410
|
-
optionDisabled,
|
|
411
|
-
optionIcon,
|
|
412
|
-
optionKey,
|
|
413
|
-
|
|
414
|
-
searchable,
|
|
415
|
-
searchPlaceholder,
|
|
416
|
-
|
|
417
|
-
emptyLabel,
|
|
418
|
-
emptySearchText,
|
|
419
|
-
|
|
420
|
-
clearable,
|
|
421
|
-
|
|
422
|
-
placeholder,
|
|
423
|
-
|
|
424
|
-
className,
|
|
425
|
-
triggerClassName,
|
|
426
|
-
contentClassName,
|
|
427
|
-
|
|
428
|
-
renderOption,
|
|
429
|
-
renderValue,
|
|
430
|
-
|
|
431
|
-
// Icons & controls
|
|
432
|
-
leadingIcons,
|
|
433
|
-
trailingIcons,
|
|
434
|
-
icon,
|
|
435
|
-
iconGap,
|
|
436
|
-
leadingIconSpacing,
|
|
437
|
-
trailingIconSpacing,
|
|
438
|
-
leadingControl,
|
|
439
|
-
trailingControl,
|
|
440
|
-
leadingControlClassName,
|
|
441
|
-
trailingControlClassName,
|
|
442
|
-
joinControls = true,
|
|
443
|
-
extendBoxToControls = true,
|
|
444
|
-
|
|
445
|
-
// Virtual scroll / incremental render
|
|
446
|
-
virtualScroll = false,
|
|
447
|
-
virtualScrollPageSize = 50,
|
|
448
|
-
virtualScrollThreshold = 48,
|
|
449
|
-
} = props;
|
|
450
|
-
|
|
451
|
-
const [open, setOpen] = React.useState(false);
|
|
452
|
-
const [query, setQuery] = React.useState("");
|
|
453
|
-
|
|
454
|
-
const items = React.useMemo(
|
|
455
|
-
() =>
|
|
456
|
-
normalizeOptions(options ?? [], {
|
|
457
|
-
autoCap,
|
|
458
|
-
optionLabel,
|
|
459
|
-
optionValue,
|
|
460
|
-
optionDescription,
|
|
461
|
-
optionDisabled,
|
|
462
|
-
optionKey,
|
|
463
|
-
optionIcon,
|
|
464
|
-
}),
|
|
465
|
-
[
|
|
466
|
-
options,
|
|
467
|
-
autoCap,
|
|
468
|
-
optionLabel,
|
|
469
|
-
optionValue,
|
|
470
|
-
optionDescription,
|
|
471
|
-
optionDisabled,
|
|
472
|
-
optionKey,
|
|
473
|
-
optionIcon,
|
|
474
|
-
]
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
const valueMap = React.useMemo(() => {
|
|
478
|
-
const map = new Map<string, SelectPrimitive>();
|
|
479
|
-
for (const item of items) {
|
|
480
|
-
map.set(String(item.value), item.value);
|
|
481
|
-
}
|
|
482
|
-
return map;
|
|
483
|
-
}, [items]);
|
|
484
|
-
|
|
485
|
-
const selectedItem =
|
|
486
|
-
value == null
|
|
487
|
-
? null
|
|
488
|
-
: items.find((it) => it.value === value) ?? null;
|
|
489
|
-
|
|
490
|
-
const filteredItems = React.useMemo(() => {
|
|
491
|
-
if (!query) return items;
|
|
492
|
-
const q = query.toLowerCase();
|
|
493
|
-
return items.filter((it) =>
|
|
494
|
-
it.labelText.toLowerCase().includes(q)
|
|
495
|
-
);
|
|
496
|
-
}, [items, query]);
|
|
497
|
-
|
|
498
|
-
// ─────────────────────────────────────────────
|
|
499
|
-
// Incremental render state
|
|
500
|
-
// ─────────────────────────────────────────────
|
|
501
|
-
|
|
502
|
-
const [visibleCount, setVisibleCount] = React.useState(() =>
|
|
503
|
-
virtualScroll
|
|
504
|
-
? Math.min(virtualScrollPageSize, filteredItems.length)
|
|
505
|
-
: filteredItems.length
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
const listRef = React.useRef<HTMLDivElement | null>(null);
|
|
509
|
-
|
|
510
|
-
// Reset visibleCount when list / filter / toggle changes
|
|
511
|
-
React.useEffect(() => {
|
|
512
|
-
if (!virtualScroll) {
|
|
513
|
-
setVisibleCount(filteredItems.length);
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
setVisibleCount(
|
|
518
|
-
Math.min(virtualScrollPageSize, filteredItems.length)
|
|
519
|
-
);
|
|
520
|
-
}, [virtualScroll, filteredItems.length, virtualScrollPageSize]);
|
|
521
|
-
|
|
522
|
-
const handleListScroll = React.useCallback(() => {
|
|
523
|
-
if (!virtualScroll) return;
|
|
524
|
-
const el = listRef.current;
|
|
525
|
-
if (!el) return;
|
|
526
|
-
|
|
527
|
-
const { scrollTop, scrollHeight, clientHeight } = el;
|
|
528
|
-
const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
|
|
529
|
-
|
|
530
|
-
if (distanceFromBottom <= virtualScrollThreshold) {
|
|
531
|
-
setVisibleCount((prev) => {
|
|
532
|
-
if (prev >= filteredItems.length) return prev;
|
|
533
|
-
const next = prev + virtualScrollPageSize;
|
|
534
|
-
return Math.min(next, filteredItems.length);
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
}, [virtualScroll, filteredItems.length, virtualScrollPageSize, virtualScrollThreshold]);
|
|
538
|
-
|
|
539
|
-
const renderedItems = React.useMemo(
|
|
540
|
-
() =>
|
|
541
|
-
virtualScroll
|
|
542
|
-
? filteredItems.slice(0, visibleCount)
|
|
543
|
-
: filteredItems,
|
|
544
|
-
[filteredItems, visibleCount, virtualScroll]
|
|
545
|
-
);
|
|
546
|
-
|
|
547
|
-
const handleChange = React.useCallback(
|
|
548
|
-
(rawKey: string) => {
|
|
549
|
-
if (!onValue) return;
|
|
550
|
-
|
|
551
|
-
const primitive =
|
|
552
|
-
valueMap.get(rawKey) ??
|
|
553
|
-
(rawKey as unknown as SelectPrimitive);
|
|
554
|
-
|
|
555
|
-
const item =
|
|
556
|
-
items.find(
|
|
557
|
-
(it) => String(it.value) === String(primitive)
|
|
558
|
-
) ?? null;
|
|
559
|
-
|
|
560
|
-
const detail: ChangeDetail = {
|
|
561
|
-
source: "variant",
|
|
562
|
-
raw: item?.raw ?? primitive,
|
|
563
|
-
nativeEvent: undefined,
|
|
564
|
-
meta: undefined,
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
onValue(primitive, detail);
|
|
568
|
-
},
|
|
569
|
-
[onValue, valueMap, items]
|
|
570
|
-
);
|
|
571
|
-
|
|
572
|
-
const currentKey =
|
|
573
|
-
selectedItem != null ? String(selectedItem.value) : "";
|
|
574
|
-
|
|
575
|
-
const heightCls = triggerHeight(size as Size | undefined);
|
|
576
|
-
const padCls = triggerPadding(density as Density | undefined);
|
|
577
|
-
|
|
578
|
-
const showClear = clearable && value != null;
|
|
579
|
-
|
|
580
|
-
// ─────────────────────────────────────────────
|
|
581
|
-
// Icons setup (similar to text variant)
|
|
582
|
-
// ─────────────────────────────────────────────
|
|
583
|
-
|
|
584
|
-
const resolvedLeadingIcons: React.ReactNode[] = (() => {
|
|
585
|
-
if (leadingIcons && leadingIcons.length) return leadingIcons;
|
|
586
|
-
if (icon) return [icon];
|
|
587
|
-
return [];
|
|
588
|
-
})();
|
|
589
|
-
|
|
590
|
-
const resolvedTrailingIcons: React.ReactNode[] = trailingIcons ?? [];
|
|
591
|
-
|
|
592
|
-
const baseIconGap = iconGap ?? 4;
|
|
593
|
-
const leadingGap = leadingIconSpacing ?? baseIconGap;
|
|
594
|
-
const trailingGap = trailingIconSpacing ?? baseIconGap;
|
|
595
|
-
|
|
596
|
-
const hasLeadingIcons = resolvedLeadingIcons.length > 0;
|
|
597
|
-
const hasTrailingIcons = resolvedTrailingIcons.length > 0;
|
|
598
|
-
|
|
599
|
-
const hasLeadingControl = !!leadingControl;
|
|
600
|
-
const hasTrailingControl = !!trailingControl;
|
|
601
|
-
const hasControls = hasLeadingControl || hasTrailingControl;
|
|
602
|
-
|
|
603
|
-
const triggerInner = renderValue ? (
|
|
604
|
-
renderValue({
|
|
605
|
-
selectedItem,
|
|
606
|
-
placeholder,
|
|
607
|
-
})
|
|
608
|
-
) : selectedItem ? (
|
|
609
|
-
<span className="truncate flex items-center gap-2">
|
|
610
|
-
{selectedItem.icon && (
|
|
611
|
-
<span className="shrink-0">
|
|
612
|
-
{selectedItem.icon}
|
|
613
|
-
</span>
|
|
614
|
-
)}
|
|
615
|
-
<span className="truncate">{selectedItem.labelNode}</span>
|
|
616
|
-
</span>
|
|
617
|
-
) : (
|
|
618
|
-
<span className="truncate text-muted-foreground">
|
|
619
|
-
{placeholder ?? "Select..."}
|
|
620
|
-
</span>
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
const baseBoxClasses = cn(
|
|
624
|
-
"border-input w-full min-w-0 rounded-md border bg-transparent shadow-xs",
|
|
625
|
-
"transition-[color,box-shadow] outline-none",
|
|
626
|
-
"focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]",
|
|
627
|
-
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
// Trigger content (inner layout: icons + label + clear + trailing icons)
|
|
631
|
-
const TriggerBody = (
|
|
632
|
-
<SelectTrigger
|
|
633
|
-
onPointerDown={(e) => {
|
|
634
|
-
if (e.target instanceof HTMLButtonElement) {
|
|
635
|
-
if (
|
|
636
|
-
e.target.getAttribute("data-slot") ==
|
|
637
|
-
"clear"
|
|
638
|
-
) {
|
|
639
|
-
e.preventDefault();
|
|
640
|
-
e.stopPropagation();
|
|
641
|
-
if (!onValue) return;
|
|
642
|
-
const detail: ChangeDetail = {
|
|
643
|
-
source: "variant",
|
|
644
|
-
raw: undefined,
|
|
645
|
-
nativeEvent: undefined,
|
|
646
|
-
meta: { action: "clear" },
|
|
647
|
-
};
|
|
648
|
-
onValue(undefined, detail);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}}
|
|
652
|
-
className={cn(
|
|
653
|
-
"w-full justify-between",
|
|
654
|
-
heightCls,
|
|
655
|
-
padCls,
|
|
656
|
-
hasControls && joinControls && extendBoxToControls
|
|
657
|
-
? "border-none shadow-none focus:ring-0 focus:outline-none"
|
|
658
|
-
: "",
|
|
659
|
-
triggerClassName
|
|
660
|
-
)}
|
|
661
|
-
>
|
|
662
|
-
<div className="flex w-full items-center justify-between gap-2">
|
|
663
|
-
{/* Left side: leading icons + label */}
|
|
664
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
665
|
-
{hasLeadingIcons && (
|
|
666
|
-
<span
|
|
667
|
-
className="flex items-center gap-1 shrink-0"
|
|
668
|
-
style={{ columnGap: leadingGap }}
|
|
669
|
-
data-slot="leading-icons"
|
|
670
|
-
>
|
|
671
|
-
{resolvedLeadingIcons.map((node, idx) => (
|
|
672
|
-
<span
|
|
673
|
-
key={idx}
|
|
674
|
-
className="flex items-center justify-center"
|
|
675
|
-
>
|
|
676
|
-
{node}
|
|
677
|
-
</span>
|
|
678
|
-
))}
|
|
679
|
-
</span>
|
|
680
|
-
)}
|
|
681
|
-
<div className="min-w-0 flex-1">
|
|
682
|
-
{triggerInner}
|
|
683
|
-
</div>
|
|
684
|
-
</div>
|
|
685
|
-
|
|
686
|
-
{/* Right side: clear button + trailing icons */}
|
|
687
|
-
<div className="flex items-center gap-1 shrink-0">
|
|
688
|
-
{showClear && (
|
|
689
|
-
<button
|
|
690
|
-
data-slot={"clear"}
|
|
691
|
-
type="button"
|
|
692
|
-
aria-label="Clear selection"
|
|
693
|
-
className="flex h-4 w-4 items-center justify-center rounded hover:bg-muted"
|
|
694
|
-
>
|
|
695
|
-
<X className="h-3 w-3 pointer-events-none" />
|
|
696
|
-
</button>
|
|
697
|
-
)}
|
|
698
|
-
|
|
699
|
-
{hasTrailingIcons && (
|
|
700
|
-
<span
|
|
701
|
-
className="flex items-center gap-1"
|
|
702
|
-
style={{ columnGap: trailingGap }}
|
|
703
|
-
data-slot="trailing-icons"
|
|
704
|
-
>
|
|
705
|
-
{resolvedTrailingIcons.map((node, idx) => (
|
|
706
|
-
<span
|
|
707
|
-
key={idx}
|
|
708
|
-
className="flex items-center justify-center"
|
|
709
|
-
>
|
|
710
|
-
{node}
|
|
711
|
-
</span>
|
|
712
|
-
))}
|
|
713
|
-
</span>
|
|
714
|
-
)}
|
|
715
|
-
</div>
|
|
716
|
-
</div>
|
|
717
|
-
</SelectTrigger>
|
|
718
|
-
);
|
|
719
|
-
|
|
720
|
-
const SelectWithTrigger = (
|
|
721
|
-
<Select
|
|
722
|
-
value={currentKey}
|
|
723
|
-
onValueChange={handleChange}
|
|
724
|
-
disabled={disabled || readOnly}
|
|
725
|
-
open={open}
|
|
726
|
-
onOpenChange={(nextOpen) => {
|
|
727
|
-
setOpen(nextOpen);
|
|
728
|
-
if (!nextOpen) setQuery("");
|
|
729
|
-
}}
|
|
730
|
-
>
|
|
731
|
-
{TriggerBody}
|
|
732
|
-
|
|
733
|
-
<SelectContent
|
|
734
|
-
className={cn("min-w-[8rem]", contentClassName)}
|
|
735
|
-
>
|
|
736
|
-
{searchable && (
|
|
737
|
-
<div className="p-1">
|
|
738
|
-
<Input
|
|
739
|
-
autoFocus
|
|
740
|
-
icon={<Search className="size-4" />}
|
|
741
|
-
value={query}
|
|
742
|
-
onChange={(e) =>
|
|
743
|
-
setQuery(e.target.value)
|
|
744
|
-
}
|
|
745
|
-
placeholder={
|
|
746
|
-
searchPlaceholder ?? "Search..."
|
|
747
|
-
}
|
|
748
|
-
size={size}
|
|
749
|
-
density={density}
|
|
750
|
-
/>
|
|
751
|
-
</div>
|
|
752
|
-
)}
|
|
753
|
-
|
|
754
|
-
{/* CASE 1: no options at all */}
|
|
755
|
-
{items.length === 0 ? (
|
|
756
|
-
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
|
757
|
-
{emptyLabel ??
|
|
758
|
-
emptySearchText ??
|
|
759
|
-
"No options available"}
|
|
760
|
-
</div>
|
|
761
|
-
) : /* CASE 2: have options, but search filters everything out */ filteredItems.length === 0 ? (
|
|
762
|
-
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
|
763
|
-
{emptySearchText ?? "No results found"}
|
|
764
|
-
</div>
|
|
765
|
-
) : (
|
|
766
|
-
// CASE 3: normal list, possibly virtually paged
|
|
767
|
-
<div
|
|
768
|
-
ref={listRef}
|
|
769
|
-
className="max-h-60 overflow-auto"
|
|
770
|
-
onScroll={handleListScroll}
|
|
771
|
-
>
|
|
772
|
-
{renderedItems.map((item, index) => {
|
|
773
|
-
const optionNode = (
|
|
774
|
-
<SelectItem
|
|
775
|
-
key={item.key}
|
|
776
|
-
value={String(item.value)}
|
|
777
|
-
disabled={item.disabled}
|
|
778
|
-
>
|
|
779
|
-
<div className="flex items-start gap-2">
|
|
780
|
-
{item.icon && (
|
|
781
|
-
<span className="mt-0.5 shrink-0">
|
|
782
|
-
{item.icon}
|
|
783
|
-
</span>
|
|
784
|
-
)}
|
|
785
|
-
<div className="flex flex-col">
|
|
786
|
-
<span>{item.labelNode}</span>
|
|
787
|
-
{item.description && (
|
|
788
|
-
<span className="text-xs text-muted-foreground">
|
|
789
|
-
{item.description}
|
|
790
|
-
</span>
|
|
791
|
-
)}
|
|
792
|
-
</div>
|
|
793
|
-
</div>
|
|
794
|
-
</SelectItem>
|
|
795
|
-
);
|
|
796
|
-
|
|
797
|
-
if (!renderOption) return optionNode;
|
|
798
|
-
|
|
799
|
-
return renderOption({
|
|
800
|
-
item,
|
|
801
|
-
selected:
|
|
802
|
-
selectedItem != null &&
|
|
803
|
-
String(selectedItem.value) ===
|
|
804
|
-
String(item.value),
|
|
805
|
-
index,
|
|
806
|
-
option: optionNode,
|
|
807
|
-
});
|
|
808
|
-
})}
|
|
809
|
-
|
|
810
|
-
{virtualScroll &&
|
|
811
|
-
renderedItems.length <
|
|
812
|
-
filteredItems.length && (
|
|
813
|
-
<div className="px-2 py-1 text-[10px] text-muted-foreground text-center">
|
|
814
|
-
Scroll to load more…
|
|
815
|
-
</div>
|
|
816
|
-
)}
|
|
817
|
-
</div>
|
|
818
|
-
)}
|
|
819
|
-
</SelectContent>
|
|
820
|
-
</Select>
|
|
821
|
-
);
|
|
822
|
-
|
|
823
|
-
// ─────────────────────────────────────────────
|
|
824
|
-
// Layout modes:
|
|
825
|
-
// - no controls
|
|
826
|
-
// - controls + joinControls
|
|
827
|
-
// - controls, separate boxes
|
|
828
|
-
// ─────────────────────────────────────────────
|
|
829
|
-
|
|
830
|
-
// CASE 1: no controls → just the select
|
|
831
|
-
if (!hasControls) {
|
|
832
|
-
return (
|
|
833
|
-
<div
|
|
834
|
-
data-slot="select-field"
|
|
835
|
-
className={cn(
|
|
836
|
-
"w-full",
|
|
837
|
-
disabled && "opacity-50 cursor-not-allowed",
|
|
838
|
-
className
|
|
839
|
-
)}
|
|
840
|
-
aria-disabled={disabled || undefined}
|
|
841
|
-
aria-invalid={error ? "true" : undefined}
|
|
842
|
-
>
|
|
843
|
-
{SelectWithTrigger}
|
|
844
|
-
</div>
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// CASE 2: controls + joinControls → share single box like text variant
|
|
849
|
-
if (joinControls) {
|
|
850
|
-
const groupClassName = cn(
|
|
851
|
-
"flex items-stretch w-full",
|
|
852
|
-
extendBoxToControls &&
|
|
853
|
-
cn(
|
|
854
|
-
"relative",
|
|
855
|
-
baseBoxClasses // ring via :focus-within
|
|
856
|
-
),
|
|
857
|
-
!extendBoxToControls &&
|
|
858
|
-
"relative border-none shadow-none bg-transparent",
|
|
859
|
-
className
|
|
860
|
-
);
|
|
861
|
-
|
|
862
|
-
return (
|
|
863
|
-
<div
|
|
864
|
-
data-slot="select-field"
|
|
865
|
-
className="w-full"
|
|
866
|
-
aria-disabled={disabled || undefined}
|
|
867
|
-
aria-invalid={error ? "true" : undefined}
|
|
868
|
-
>
|
|
869
|
-
<div
|
|
870
|
-
className={groupClassName}
|
|
871
|
-
data-slot="select-group"
|
|
872
|
-
data-disabled={disabled ? "true" : "false"}
|
|
873
|
-
>
|
|
874
|
-
{hasLeadingControl && (
|
|
875
|
-
<div
|
|
876
|
-
className={cn(
|
|
877
|
-
"flex items-center px-2",
|
|
878
|
-
leadingControlClassName
|
|
879
|
-
)}
|
|
880
|
-
data-slot="leading-control"
|
|
881
|
-
>
|
|
882
|
-
{leadingControl}
|
|
883
|
-
</div>
|
|
884
|
-
)}
|
|
885
|
-
|
|
886
|
-
<div
|
|
887
|
-
className={cn(
|
|
888
|
-
"flex-1 min-w-0 flex items-stretch"
|
|
889
|
-
)}
|
|
890
|
-
data-slot="select-region"
|
|
891
|
-
>
|
|
892
|
-
{SelectWithTrigger}
|
|
893
|
-
</div>
|
|
894
|
-
|
|
895
|
-
{hasTrailingControl && (
|
|
896
|
-
<div
|
|
897
|
-
className={cn(
|
|
898
|
-
"flex items-center px-2",
|
|
899
|
-
trailingControlClassName
|
|
900
|
-
)}
|
|
901
|
-
data-slot="trailing-control"
|
|
902
|
-
>
|
|
903
|
-
{trailingControl}
|
|
904
|
-
</div>
|
|
905
|
-
)}
|
|
906
|
-
</div>
|
|
907
|
-
</div>
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// CASE 3: controls present, but separate (no joined box)
|
|
912
|
-
return (
|
|
913
|
-
<div
|
|
914
|
-
data-slot="select-field"
|
|
915
|
-
className={cn(
|
|
916
|
-
"flex items-stretch w-full",
|
|
917
|
-
disabled && "opacity-50 cursor-not-allowed",
|
|
918
|
-
className
|
|
919
|
-
)}
|
|
920
|
-
aria-disabled={disabled || undefined}
|
|
921
|
-
aria-invalid={error ? "true" : undefined}
|
|
922
|
-
>
|
|
923
|
-
{hasLeadingControl && (
|
|
924
|
-
<div
|
|
925
|
-
className={cn(
|
|
926
|
-
"flex items-center mr-1",
|
|
927
|
-
leadingControlClassName
|
|
928
|
-
)}
|
|
929
|
-
data-slot="leading-control"
|
|
930
|
-
>
|
|
931
|
-
{leadingControl}
|
|
932
|
-
</div>
|
|
933
|
-
)}
|
|
934
|
-
|
|
935
|
-
<div className="flex-1 min-w-0">
|
|
936
|
-
{SelectWithTrigger}
|
|
937
|
-
</div>
|
|
938
|
-
|
|
939
|
-
{hasTrailingControl && (
|
|
940
|
-
<div
|
|
941
|
-
className={cn(
|
|
942
|
-
"flex items-center ml-1",
|
|
943
|
-
trailingControlClassName
|
|
944
|
-
)}
|
|
945
|
-
data-slot="trailing-control"
|
|
946
|
-
>
|
|
947
|
-
{trailingControl}
|
|
948
|
-
</div>
|
|
949
|
-
)}
|
|
950
|
-
</div>
|
|
951
|
-
);
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
ShadcnSelectVariant.displayName = "ShadcnSelectVariant";
|
|
955
|
-
|
|
956
|
-
export default ShadcnSelectVariant;
|