@optilogic/core 1.2.2 → 1.2.3
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/dist/index.cjs +662 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +143 -5
- package/dist/index.d.ts +143 -5
- package/dist/index.js +658 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/combobox.tsx +340 -0
- package/src/components/data-grid/DataGrid.tsx +66 -24
- package/src/components/data-grid/components/HeaderCell.tsx +5 -1
- package/src/components/data-grid/hooks/useColumnResize.ts +30 -1
- package/src/components/data-grid/types.ts +5 -0
- package/src/components/multi-select.tsx +375 -0
- package/src/components/select.tsx +18 -2
- package/src/components/tabs.tsx +92 -28
- package/src/index.ts +16 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ChevronDown, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../utils/cn";
|
|
5
|
+
import { Checkbox } from "./checkbox";
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
7
|
+
import type { AutocompleteOption } from "./autocomplete";
|
|
8
|
+
|
|
9
|
+
export interface MultiSelectProps {
|
|
10
|
+
/** Array of options to display */
|
|
11
|
+
options: AutocompleteOption[];
|
|
12
|
+
/** Currently selected values */
|
|
13
|
+
value?: string[];
|
|
14
|
+
/** Callback when selection changes */
|
|
15
|
+
onChange?: (values: string[]) => void;
|
|
16
|
+
/** Placeholder text when no selection */
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
/** Placeholder for the search input */
|
|
19
|
+
searchPlaceholder?: string;
|
|
20
|
+
/** Text to show when no options match the search */
|
|
21
|
+
emptyText?: string;
|
|
22
|
+
/** Whether the multi-select is disabled */
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
/** Additional class name for the trigger */
|
|
25
|
+
className?: string;
|
|
26
|
+
/** Whether to show the clear-all button */
|
|
27
|
+
clearable?: boolean;
|
|
28
|
+
/** Max number of pills to display before showing "+N more" */
|
|
29
|
+
maxDisplayItems?: number;
|
|
30
|
+
/** Whether to show a "Select All" toggle */
|
|
31
|
+
showSelectAll?: boolean;
|
|
32
|
+
/** Custom label for the select all option */
|
|
33
|
+
selectAllLabel?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* MultiSelect component - a multi-select dropdown with checkboxes
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Checkbox selection for multiple options
|
|
41
|
+
* - Search filtering
|
|
42
|
+
* - Grouped options support
|
|
43
|
+
* - Descriptions per option
|
|
44
|
+
* - Tag pills with individual removal
|
|
45
|
+
* - "Select All" toggle
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* <MultiSelect
|
|
49
|
+
* options={[
|
|
50
|
+
* { value: 'react', label: 'React', description: 'A JS library' },
|
|
51
|
+
* { value: 'vue', label: 'Vue', group: 'Frameworks' },
|
|
52
|
+
* ]}
|
|
53
|
+
* value={selected}
|
|
54
|
+
* onChange={setSelected}
|
|
55
|
+
* placeholder="Select frameworks..."
|
|
56
|
+
* showSelectAll
|
|
57
|
+
* />
|
|
58
|
+
*/
|
|
59
|
+
export function MultiSelect({
|
|
60
|
+
options,
|
|
61
|
+
value,
|
|
62
|
+
onChange,
|
|
63
|
+
placeholder = "Select items...",
|
|
64
|
+
searchPlaceholder = "Search...",
|
|
65
|
+
emptyText = "No options found.",
|
|
66
|
+
disabled = false,
|
|
67
|
+
className,
|
|
68
|
+
clearable = true,
|
|
69
|
+
maxDisplayItems = 3,
|
|
70
|
+
showSelectAll = false,
|
|
71
|
+
selectAllLabel = "Select all",
|
|
72
|
+
}: MultiSelectProps) {
|
|
73
|
+
const [open, setOpen] = React.useState(false);
|
|
74
|
+
const [search, setSearch] = React.useState("");
|
|
75
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
76
|
+
|
|
77
|
+
const safeOptions = options ?? [];
|
|
78
|
+
const safeValue = value ?? [];
|
|
79
|
+
const selectedSet = React.useMemo(() => new Set(safeValue), [safeValue]);
|
|
80
|
+
|
|
81
|
+
const filteredOptions = React.useMemo(() => {
|
|
82
|
+
if (!search.trim()) return safeOptions;
|
|
83
|
+
const searchLower = search.toLowerCase();
|
|
84
|
+
return safeOptions.filter(
|
|
85
|
+
(opt) =>
|
|
86
|
+
opt.label.toLowerCase().includes(searchLower) ||
|
|
87
|
+
opt.description?.toLowerCase().includes(searchLower)
|
|
88
|
+
);
|
|
89
|
+
}, [safeOptions, search]);
|
|
90
|
+
|
|
91
|
+
const groupedOptions = React.useMemo(() => {
|
|
92
|
+
const groups: Record<string, AutocompleteOption[]> = {};
|
|
93
|
+
const ungrouped: AutocompleteOption[] = [];
|
|
94
|
+
|
|
95
|
+
filteredOptions.forEach((opt) => {
|
|
96
|
+
if (opt.group) {
|
|
97
|
+
if (!groups[opt.group]) groups[opt.group] = [];
|
|
98
|
+
groups[opt.group]!.push(opt);
|
|
99
|
+
} else {
|
|
100
|
+
ungrouped.push(opt);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return { groups, ungrouped };
|
|
105
|
+
}, [filteredOptions]);
|
|
106
|
+
|
|
107
|
+
const hasGroups = Object.keys(groupedOptions.groups).length > 0;
|
|
108
|
+
|
|
109
|
+
// Select All logic
|
|
110
|
+
const selectableFiltered = React.useMemo(
|
|
111
|
+
() => filteredOptions.filter((opt) => !opt.disabled),
|
|
112
|
+
[filteredOptions]
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const allFilteredSelected = React.useMemo(
|
|
116
|
+
() =>
|
|
117
|
+
selectableFiltered.length > 0 &&
|
|
118
|
+
selectableFiltered.every((opt) => selectedSet.has(opt.value)),
|
|
119
|
+
[selectableFiltered, selectedSet]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const someFilteredSelected = React.useMemo(
|
|
123
|
+
() =>
|
|
124
|
+
!allFilteredSelected &&
|
|
125
|
+
selectableFiltered.some((opt) => selectedSet.has(opt.value)),
|
|
126
|
+
[selectableFiltered, selectedSet, allFilteredSelected]
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const handleToggle = React.useCallback(
|
|
130
|
+
(optionValue: string) => {
|
|
131
|
+
const next = selectedSet.has(optionValue)
|
|
132
|
+
? safeValue.filter((v) => v !== optionValue)
|
|
133
|
+
: [...safeValue, optionValue];
|
|
134
|
+
onChange?.(next);
|
|
135
|
+
},
|
|
136
|
+
[onChange, safeValue, selectedSet]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const handleRemove = React.useCallback(
|
|
140
|
+
(optionValue: string, e: React.MouseEvent) => {
|
|
141
|
+
e.stopPropagation();
|
|
142
|
+
onChange?.(safeValue.filter((v) => v !== optionValue));
|
|
143
|
+
},
|
|
144
|
+
[onChange, safeValue]
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const handleClearAll = React.useCallback(
|
|
148
|
+
(e: React.MouseEvent) => {
|
|
149
|
+
e.stopPropagation();
|
|
150
|
+
onChange?.([]);
|
|
151
|
+
},
|
|
152
|
+
[onChange]
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const handleSelectAll = React.useCallback(() => {
|
|
156
|
+
if (allFilteredSelected) {
|
|
157
|
+
// Deselect all filtered
|
|
158
|
+
const filteredValues = new Set(selectableFiltered.map((o) => o.value));
|
|
159
|
+
onChange?.(safeValue.filter((v) => !filteredValues.has(v)));
|
|
160
|
+
} else {
|
|
161
|
+
// Select all filtered (merge with existing selections)
|
|
162
|
+
const existing = new Set(safeValue);
|
|
163
|
+
const next = [...safeValue];
|
|
164
|
+
for (const opt of selectableFiltered) {
|
|
165
|
+
if (!existing.has(opt.value)) {
|
|
166
|
+
next.push(opt.value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
onChange?.(next);
|
|
170
|
+
}
|
|
171
|
+
}, [allFilteredSelected, selectableFiltered, safeValue, onChange]);
|
|
172
|
+
|
|
173
|
+
React.useEffect(() => {
|
|
174
|
+
if (open) {
|
|
175
|
+
const timeout = setTimeout(() => inputRef.current?.focus(), 0);
|
|
176
|
+
return () => clearTimeout(timeout);
|
|
177
|
+
} else {
|
|
178
|
+
setSearch("");
|
|
179
|
+
}
|
|
180
|
+
}, [open]);
|
|
181
|
+
|
|
182
|
+
const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => {
|
|
183
|
+
if (e.key === "Escape") setOpen(false);
|
|
184
|
+
}, []);
|
|
185
|
+
|
|
186
|
+
const selectedLabels = React.useMemo(
|
|
187
|
+
() =>
|
|
188
|
+
safeValue
|
|
189
|
+
.map((v) => safeOptions.find((o) => o.value === v)?.label ?? v)
|
|
190
|
+
.slice(0, maxDisplayItems),
|
|
191
|
+
[safeValue, safeOptions, maxDisplayItems]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const overflow = safeValue.length - maxDisplayItems;
|
|
195
|
+
|
|
196
|
+
const isSearching = search.trim().length > 0;
|
|
197
|
+
|
|
198
|
+
const renderOption = (option: AutocompleteOption) => (
|
|
199
|
+
<button
|
|
200
|
+
key={option.value}
|
|
201
|
+
type="button"
|
|
202
|
+
disabled={option.disabled}
|
|
203
|
+
onClick={() => handleToggle(option.value)}
|
|
204
|
+
className={cn(
|
|
205
|
+
"relative flex w-full cursor-pointer select-none items-start gap-2 rounded-sm px-2 py-1.5 text-sm outline-none",
|
|
206
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
207
|
+
option.disabled && "pointer-events-none opacity-50"
|
|
208
|
+
)}
|
|
209
|
+
>
|
|
210
|
+
<Checkbox
|
|
211
|
+
checked={selectedSet.has(option.value)}
|
|
212
|
+
tabIndex={-1}
|
|
213
|
+
className="mt-0.5 pointer-events-none"
|
|
214
|
+
/>
|
|
215
|
+
<div className="flex-1 min-w-0">
|
|
216
|
+
<div className="truncate">{option.label}</div>
|
|
217
|
+
{option.description && (
|
|
218
|
+
<div className="text-xs text-muted-foreground truncate">
|
|
219
|
+
{option.description}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
</button>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
228
|
+
<PopoverTrigger asChild disabled={disabled}>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
role="combobox"
|
|
232
|
+
aria-expanded={open}
|
|
233
|
+
aria-haspopup="listbox"
|
|
234
|
+
disabled={disabled}
|
|
235
|
+
className={cn(
|
|
236
|
+
"flex min-h-9 w-full items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-sm ring-offset-background",
|
|
237
|
+
"hover:border-input-hover",
|
|
238
|
+
"focus:outline-none focus:ring-1 focus:ring-ring",
|
|
239
|
+
"disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-input",
|
|
240
|
+
className
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
<div className="flex flex-1 flex-wrap gap-1 items-center min-w-0">
|
|
244
|
+
{safeValue.length === 0 ? (
|
|
245
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
246
|
+
) : (
|
|
247
|
+
<>
|
|
248
|
+
{selectedLabels.map((label, i) => (
|
|
249
|
+
<span
|
|
250
|
+
key={safeValue[i]}
|
|
251
|
+
className="inline-flex items-center gap-0.5 rounded-sm bg-accent px-1.5 py-0.5 text-xs font-medium text-accent-foreground"
|
|
252
|
+
>
|
|
253
|
+
<span className="truncate max-w-[100px]">{label}</span>
|
|
254
|
+
<span
|
|
255
|
+
role="button"
|
|
256
|
+
tabIndex={-1}
|
|
257
|
+
onClick={(e) => handleRemove(safeValue[i]!, e)}
|
|
258
|
+
className="rounded-sm hover:bg-foreground/10 p-0.5"
|
|
259
|
+
>
|
|
260
|
+
<X className="h-3 w-3" />
|
|
261
|
+
</span>
|
|
262
|
+
</span>
|
|
263
|
+
))}
|
|
264
|
+
{overflow > 0 && (
|
|
265
|
+
<span className="text-xs text-muted-foreground">
|
|
266
|
+
+{overflow}
|
|
267
|
+
</span>
|
|
268
|
+
)}
|
|
269
|
+
</>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
273
|
+
{clearable && safeValue.length > 0 && (
|
|
274
|
+
<span
|
|
275
|
+
role="button"
|
|
276
|
+
tabIndex={-1}
|
|
277
|
+
onClick={handleClearAll}
|
|
278
|
+
className="rounded-sm hover:bg-muted p-0.5"
|
|
279
|
+
>
|
|
280
|
+
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
|
281
|
+
</span>
|
|
282
|
+
)}
|
|
283
|
+
<ChevronDown className="h-4 w-4 opacity-50" />
|
|
284
|
+
</div>
|
|
285
|
+
</button>
|
|
286
|
+
</PopoverTrigger>
|
|
287
|
+
<PopoverContent
|
|
288
|
+
className="w-[--radix-popover-trigger-width] p-0"
|
|
289
|
+
align="start"
|
|
290
|
+
sideOffset={4}
|
|
291
|
+
onKeyDown={handleKeyDown}
|
|
292
|
+
>
|
|
293
|
+
{/* Search input */}
|
|
294
|
+
<div className="flex items-center border-b border-border px-3">
|
|
295
|
+
<input
|
|
296
|
+
ref={inputRef}
|
|
297
|
+
type="text"
|
|
298
|
+
value={search}
|
|
299
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
300
|
+
placeholder={searchPlaceholder}
|
|
301
|
+
className="flex h-9 w-full bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground"
|
|
302
|
+
/>
|
|
303
|
+
{search && (
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
onClick={() => setSearch("")}
|
|
307
|
+
className="p-1 hover:bg-muted rounded-sm"
|
|
308
|
+
>
|
|
309
|
+
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
|
310
|
+
</button>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{/* Options list */}
|
|
315
|
+
<div className="max-h-[300px] overflow-y-auto p-1">
|
|
316
|
+
{/* Select All */}
|
|
317
|
+
{showSelectAll && selectableFiltered.length > 0 && (
|
|
318
|
+
<>
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={handleSelectAll}
|
|
322
|
+
className={cn(
|
|
323
|
+
"relative flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none",
|
|
324
|
+
"hover:bg-accent hover:text-accent-foreground"
|
|
325
|
+
)}
|
|
326
|
+
>
|
|
327
|
+
<Checkbox
|
|
328
|
+
checked={
|
|
329
|
+
allFilteredSelected
|
|
330
|
+
? true
|
|
331
|
+
: someFilteredSelected
|
|
332
|
+
? "indeterminate"
|
|
333
|
+
: false
|
|
334
|
+
}
|
|
335
|
+
tabIndex={-1}
|
|
336
|
+
className="pointer-events-none"
|
|
337
|
+
/>
|
|
338
|
+
<span className="font-medium">
|
|
339
|
+
{isSearching
|
|
340
|
+
? `${selectAllLabel} (filtered)`
|
|
341
|
+
: selectAllLabel}
|
|
342
|
+
</span>
|
|
343
|
+
</button>
|
|
344
|
+
<div className="-mx-1 my-1 h-px bg-border" />
|
|
345
|
+
</>
|
|
346
|
+
)}
|
|
347
|
+
|
|
348
|
+
{filteredOptions.length === 0 ? (
|
|
349
|
+
<div className="py-6 text-center text-sm text-muted-foreground">
|
|
350
|
+
{emptyText}
|
|
351
|
+
</div>
|
|
352
|
+
) : hasGroups ? (
|
|
353
|
+
<>
|
|
354
|
+
{groupedOptions.ungrouped.map(renderOption)}
|
|
355
|
+
{Object.entries(groupedOptions.groups).map(([group, opts]) => (
|
|
356
|
+
<div key={group}>
|
|
357
|
+
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
358
|
+
{group}
|
|
359
|
+
</div>
|
|
360
|
+
{opts.map(renderOption)}
|
|
361
|
+
</div>
|
|
362
|
+
))}
|
|
363
|
+
</>
|
|
364
|
+
) : (
|
|
365
|
+
filteredOptions.map(renderOption)
|
|
366
|
+
)}
|
|
367
|
+
</div>
|
|
368
|
+
</PopoverContent>
|
|
369
|
+
</Popover>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
MultiSelect.displayName = "MultiSelect";
|
|
374
|
+
|
|
375
|
+
export default MultiSelect;
|
|
@@ -119,12 +119,12 @@ const SelectItem = React.forwardRef<
|
|
|
119
119
|
<SelectPrimitive.Item
|
|
120
120
|
ref={ref}
|
|
121
121
|
className={cn(
|
|
122
|
-
"relative flex w-full cursor-default select-none items-
|
|
122
|
+
"relative flex w-full cursor-default select-none items-start rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
123
123
|
className
|
|
124
124
|
)}
|
|
125
125
|
{...props}
|
|
126
126
|
>
|
|
127
|
-
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
127
|
+
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center mt-0.5">
|
|
128
128
|
<SelectPrimitive.ItemIndicator>
|
|
129
129
|
<Check className="h-4 w-4" />
|
|
130
130
|
</SelectPrimitive.ItemIndicator>
|
|
@@ -134,6 +134,21 @@ const SelectItem = React.forwardRef<
|
|
|
134
134
|
));
|
|
135
135
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
|
136
136
|
|
|
137
|
+
export interface SelectItemDescriptionProps
|
|
138
|
+
extends React.HTMLAttributes<HTMLSpanElement> {}
|
|
139
|
+
|
|
140
|
+
const SelectItemDescription = React.forwardRef<
|
|
141
|
+
HTMLSpanElement,
|
|
142
|
+
SelectItemDescriptionProps
|
|
143
|
+
>(({ className, ...props }, ref) => (
|
|
144
|
+
<span
|
|
145
|
+
ref={ref}
|
|
146
|
+
className={cn("text-xs text-muted-foreground", className)}
|
|
147
|
+
{...props}
|
|
148
|
+
/>
|
|
149
|
+
));
|
|
150
|
+
SelectItemDescription.displayName = "SelectItemDescription";
|
|
151
|
+
|
|
137
152
|
const SelectSeparator = React.forwardRef<
|
|
138
153
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
139
154
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
@@ -154,6 +169,7 @@ export {
|
|
|
154
169
|
SelectContent,
|
|
155
170
|
SelectLabel,
|
|
156
171
|
SelectItem,
|
|
172
|
+
SelectItemDescription,
|
|
157
173
|
SelectSeparator,
|
|
158
174
|
SelectScrollUpButton,
|
|
159
175
|
SelectScrollDownButton,
|
package/src/components/tabs.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
4
|
|
|
4
5
|
import { cn } from "../utils/cn";
|
|
5
6
|
|
|
@@ -20,56 +21,113 @@ import { cn } from "../utils/cn";
|
|
|
20
21
|
*/
|
|
21
22
|
const Tabs = TabsPrimitive.Root;
|
|
22
23
|
|
|
24
|
+
const tabsListVariants = cva(
|
|
25
|
+
"inline-flex h-10 items-center justify-start bg-transparent",
|
|
26
|
+
{
|
|
27
|
+
variants: {
|
|
28
|
+
variant: {
|
|
29
|
+
default: "border-b border-border",
|
|
30
|
+
pill: "gap-1 rounded-lg bg-muted p-1",
|
|
31
|
+
unstyled: "",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: {
|
|
35
|
+
variant: "default",
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const tabsTriggerVariants = cva(
|
|
41
|
+
[
|
|
42
|
+
"inline-flex items-center justify-center whitespace-nowrap",
|
|
43
|
+
"px-4 py-2.5 text-sm font-medium",
|
|
44
|
+
"transition-colors",
|
|
45
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
46
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
47
|
+
],
|
|
48
|
+
{
|
|
49
|
+
variants: {
|
|
50
|
+
variant: {
|
|
51
|
+
default: [
|
|
52
|
+
"-mb-px",
|
|
53
|
+
"border-transparent text-muted-foreground",
|
|
54
|
+
"hover:text-foreground hover:border-muted-foreground/50",
|
|
55
|
+
"data-[state=active]:border-foreground data-[state=active]:text-foreground",
|
|
56
|
+
],
|
|
57
|
+
pill: [
|
|
58
|
+
"rounded-md",
|
|
59
|
+
"text-muted-foreground",
|
|
60
|
+
"hover:text-foreground",
|
|
61
|
+
"data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
62
|
+
],
|
|
63
|
+
unstyled: [
|
|
64
|
+
"text-muted-foreground",
|
|
65
|
+
"hover:text-foreground",
|
|
66
|
+
"data-[state=active]:text-foreground",
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
indicatorSize: {
|
|
70
|
+
sm: "border-b",
|
|
71
|
+
default: "border-b-2",
|
|
72
|
+
lg: "border-b-[3px]",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
compoundVariants: [
|
|
76
|
+
{ variant: "pill", indicatorSize: "sm", className: "border-b-0" },
|
|
77
|
+
{ variant: "pill", indicatorSize: "default", className: "border-b-0" },
|
|
78
|
+
{ variant: "pill", indicatorSize: "lg", className: "border-b-0" },
|
|
79
|
+
{ variant: "unstyled", indicatorSize: "sm", className: "border-b-0" },
|
|
80
|
+
{ variant: "unstyled", indicatorSize: "default", className: "border-b-0" },
|
|
81
|
+
{ variant: "unstyled", indicatorSize: "lg", className: "border-b-0" },
|
|
82
|
+
],
|
|
83
|
+
defaultVariants: {
|
|
84
|
+
variant: "default",
|
|
85
|
+
indicatorSize: "default",
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export interface TabsListProps
|
|
91
|
+
extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>,
|
|
92
|
+
VariantProps<typeof tabsListVariants> {}
|
|
93
|
+
|
|
23
94
|
/**
|
|
24
95
|
* TabsList
|
|
25
96
|
*
|
|
26
97
|
* Container for tab triggers. Provides the tab header bar.
|
|
98
|
+
* Supports variants: default, pill, unstyled
|
|
27
99
|
*/
|
|
28
100
|
const TabsList = React.forwardRef<
|
|
29
101
|
React.ElementRef<typeof TabsPrimitive.List>,
|
|
30
|
-
|
|
31
|
-
>(({ className, ...props }, ref) => (
|
|
102
|
+
TabsListProps
|
|
103
|
+
>(({ className, variant, ...props }, ref) => (
|
|
32
104
|
<TabsPrimitive.List
|
|
33
105
|
ref={ref}
|
|
34
|
-
className={cn(
|
|
35
|
-
"inline-flex h-10 items-center justify-start",
|
|
36
|
-
"border-b border-border",
|
|
37
|
-
"bg-transparent",
|
|
38
|
-
className
|
|
39
|
-
)}
|
|
106
|
+
className={cn(tabsListVariants({ variant }), className)}
|
|
40
107
|
{...props}
|
|
41
108
|
/>
|
|
42
109
|
));
|
|
43
110
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
44
111
|
|
|
112
|
+
export interface TabsTriggerProps
|
|
113
|
+
extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>,
|
|
114
|
+
VariantProps<typeof tabsTriggerVariants> {}
|
|
115
|
+
|
|
45
116
|
/**
|
|
46
117
|
* TabsTrigger
|
|
47
118
|
*
|
|
48
119
|
* Individual tab button that activates its associated content.
|
|
120
|
+
* Supports variants: default, pill, unstyled
|
|
121
|
+
* Supports indicatorSize: sm, default, lg (only applies to default variant)
|
|
49
122
|
*/
|
|
50
123
|
const TabsTrigger = React.forwardRef<
|
|
51
124
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
52
|
-
|
|
53
|
-
>(({ className, ...props }, ref) => (
|
|
125
|
+
TabsTriggerProps
|
|
126
|
+
>(({ className, variant, indicatorSize, ...props }, ref) => (
|
|
54
127
|
<TabsPrimitive.Trigger
|
|
55
128
|
ref={ref}
|
|
56
129
|
className={cn(
|
|
57
|
-
|
|
58
|
-
"inline-flex items-center justify-center whitespace-nowrap",
|
|
59
|
-
"px-4 py-2.5 text-sm font-medium",
|
|
60
|
-
"transition-colors",
|
|
61
|
-
// Border-bottom indicator style
|
|
62
|
-
"border-b-2 -mb-px",
|
|
63
|
-
// Default state
|
|
64
|
-
"border-transparent text-muted-foreground",
|
|
65
|
-
// Hover state
|
|
66
|
-
"hover:text-foreground hover:border-muted-foreground/50",
|
|
67
|
-
// Active/selected state
|
|
68
|
-
"data-[state=active]:border-foreground data-[state=active]:text-foreground",
|
|
69
|
-
// Focus styles
|
|
70
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
71
|
-
// Disabled styles
|
|
72
|
-
"disabled:pointer-events-none disabled:opacity-50",
|
|
130
|
+
tabsTriggerVariants({ variant, indicatorSize }),
|
|
73
131
|
className
|
|
74
132
|
)}
|
|
75
133
|
{...props}
|
|
@@ -90,7 +148,6 @@ const TabsContent = React.forwardRef<
|
|
|
90
148
|
ref={ref}
|
|
91
149
|
className={cn(
|
|
92
150
|
"mt-2",
|
|
93
|
-
// Focus styles
|
|
94
151
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
95
152
|
className
|
|
96
153
|
)}
|
|
@@ -99,4 +156,11 @@ const TabsContent = React.forwardRef<
|
|
|
99
156
|
));
|
|
100
157
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
101
158
|
|
|
102
|
-
export {
|
|
159
|
+
export {
|
|
160
|
+
Tabs,
|
|
161
|
+
TabsList,
|
|
162
|
+
TabsTrigger,
|
|
163
|
+
TabsContent,
|
|
164
|
+
tabsListVariants,
|
|
165
|
+
tabsTriggerVariants,
|
|
166
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -22,10 +22,12 @@ export {
|
|
|
22
22
|
SelectContent,
|
|
23
23
|
SelectLabel,
|
|
24
24
|
SelectItem,
|
|
25
|
+
SelectItemDescription,
|
|
25
26
|
SelectSeparator,
|
|
26
27
|
SelectScrollUpButton,
|
|
27
28
|
SelectScrollDownButton,
|
|
28
29
|
type SelectTriggerProps,
|
|
30
|
+
type SelectItemDescriptionProps,
|
|
29
31
|
} from "./components/select";
|
|
30
32
|
|
|
31
33
|
export {
|
|
@@ -33,6 +35,10 @@ export {
|
|
|
33
35
|
TabsList,
|
|
34
36
|
TabsTrigger,
|
|
35
37
|
TabsContent,
|
|
38
|
+
tabsListVariants,
|
|
39
|
+
tabsTriggerVariants,
|
|
40
|
+
type TabsListProps,
|
|
41
|
+
type TabsTriggerProps,
|
|
36
42
|
} from "./components/tabs";
|
|
37
43
|
|
|
38
44
|
export {
|
|
@@ -287,6 +293,16 @@ export {
|
|
|
287
293
|
type AutocompleteOption,
|
|
288
294
|
} from "./components/autocomplete";
|
|
289
295
|
|
|
296
|
+
export {
|
|
297
|
+
MultiSelect,
|
|
298
|
+
type MultiSelectProps,
|
|
299
|
+
} from "./components/multi-select";
|
|
300
|
+
|
|
301
|
+
export {
|
|
302
|
+
Combobox,
|
|
303
|
+
type ComboboxProps,
|
|
304
|
+
} from "./components/combobox";
|
|
305
|
+
|
|
290
306
|
// Utility components
|
|
291
307
|
export {
|
|
292
308
|
IconButton,
|