@trycompai/design-system 1.0.34 → 1.0.36
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/package.json
CHANGED
|
@@ -65,34 +65,41 @@ function SelectContent({
|
|
|
65
65
|
align = 'center',
|
|
66
66
|
alignOffset = 0,
|
|
67
67
|
alignItemWithTrigger = true,
|
|
68
|
+
portal = true,
|
|
69
|
+
position = 'fixed',
|
|
68
70
|
...props
|
|
69
71
|
}: Omit<SelectPrimitive.Popup.Props, 'className'> &
|
|
70
72
|
Pick<
|
|
71
73
|
SelectPrimitive.Positioner.Props,
|
|
72
74
|
'align' | 'alignOffset' | 'side' | 'sideOffset' | 'alignItemWithTrigger'
|
|
73
|
-
>) {
|
|
74
|
-
|
|
75
|
-
<SelectPrimitive.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
> & { portal?: boolean; position?: 'fixed' | 'absolute' }) {
|
|
76
|
+
const content = (
|
|
77
|
+
<SelectPrimitive.Positioner
|
|
78
|
+
side={side}
|
|
79
|
+
sideOffset={sideOffset}
|
|
80
|
+
align={align}
|
|
81
|
+
alignOffset={alignOffset}
|
|
82
|
+
alignItemWithTrigger={alignItemWithTrigger}
|
|
83
|
+
className="isolate z-50"
|
|
84
|
+
style={{ position }}
|
|
85
|
+
>
|
|
86
|
+
<SelectPrimitive.Popup
|
|
87
|
+
data-slot="select-content"
|
|
88
|
+
className="bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-(--anchor-width) max-w-80 rounded-lg p-1 shadow-md ring-1 duration-100 relative isolate z-50 max-h-(--available-height) origin-(--transform-origin) overflow-x-hidden overflow-y-auto"
|
|
89
|
+
{...props}
|
|
83
90
|
>
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<SelectScrollUpButton />
|
|
90
|
-
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
|
91
|
-
<SelectScrollDownButton />
|
|
92
|
-
</SelectPrimitive.Popup>
|
|
93
|
-
</SelectPrimitive.Positioner>
|
|
94
|
-
</SelectPrimitive.Portal>
|
|
91
|
+
<SelectScrollUpButton />
|
|
92
|
+
<SelectPrimitive.List>{children}</SelectPrimitive.List>
|
|
93
|
+
<SelectScrollDownButton />
|
|
94
|
+
</SelectPrimitive.Popup>
|
|
95
|
+
</SelectPrimitive.Positioner>
|
|
95
96
|
);
|
|
97
|
+
|
|
98
|
+
if (!portal) {
|
|
99
|
+
return content;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return <SelectPrimitive.Portal>{content}</SelectPrimitive.Portal>;
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
function SelectLabel({ ...props }: Omit<SelectPrimitive.GroupLabel.Props, 'className'>) {
|
|
@@ -5,6 +5,13 @@ import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
|
|
|
5
5
|
import { Combobox as ComboboxPrimitive } from '@base-ui/react';
|
|
6
6
|
import { Checkmark, ChevronDown, Search } from '@carbon/icons-react';
|
|
7
7
|
import * as React from 'react';
|
|
8
|
+
import {
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectTrigger,
|
|
13
|
+
SelectValue,
|
|
14
|
+
} from '../molecules/select';
|
|
8
15
|
// ============================================================================
|
|
9
16
|
// Types
|
|
10
17
|
// ============================================================================
|
|
@@ -18,10 +25,20 @@ export interface Organization {
|
|
|
18
25
|
icon?: React.ReactNode;
|
|
19
26
|
/** Optional logo URL to display */
|
|
20
27
|
logoUrl?: string;
|
|
28
|
+
/** Optional created at timestamp used for sorting */
|
|
29
|
+
createdAt?: string | number | Date;
|
|
21
30
|
/** Optional brand color for the indicator dot (e.g., "#10b981" or "bg-green-500") */
|
|
22
31
|
color?: string;
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
type OrganizationSortKey = 'name' | 'createdAt';
|
|
35
|
+
type OrganizationSortDirection = 'asc' | 'desc';
|
|
36
|
+
|
|
37
|
+
type OrganizationSort = {
|
|
38
|
+
key: OrganizationSortKey;
|
|
39
|
+
direction: OrganizationSortDirection;
|
|
40
|
+
};
|
|
41
|
+
|
|
25
42
|
export interface OrganizationSelectorProps {
|
|
26
43
|
/** List of organizations to display */
|
|
27
44
|
organizations: Organization[];
|
|
@@ -61,6 +78,12 @@ export interface OrganizationSelectorProps {
|
|
|
61
78
|
createLabel?: string;
|
|
62
79
|
/** Callback when create action is selected */
|
|
63
80
|
onCreate?: () => void;
|
|
81
|
+
/** Whether to close the selector after selecting an organization */
|
|
82
|
+
closeOnSelect?: boolean;
|
|
83
|
+
/** Default sort mode for the organization list */
|
|
84
|
+
defaultSort?: OrganizationSort;
|
|
85
|
+
/** Whether to show the sort control */
|
|
86
|
+
enableSorting?: boolean;
|
|
64
87
|
}
|
|
65
88
|
|
|
66
89
|
// ============================================================================
|
|
@@ -139,6 +162,40 @@ function OrganizationItemContent({ org, maxWidth }: { org: Organization; maxWidt
|
|
|
139
162
|
);
|
|
140
163
|
}
|
|
141
164
|
|
|
165
|
+
function getSortLabel(sort: OrganizationSort) {
|
|
166
|
+
if (sort.key === 'name') {
|
|
167
|
+
return sort.direction === 'asc' ? 'Alphabetical (A–Z)' : 'Alphabetical (Z–A)';
|
|
168
|
+
}
|
|
169
|
+
return sort.direction === 'asc' ? 'Created (oldest)' : 'Created (newest)';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const sortOptions: { value: string; label: string }[] = [
|
|
173
|
+
{ value: 'name:asc', label: 'Alphabetical (A–Z)' },
|
|
174
|
+
{ value: 'name:desc', label: 'Alphabetical (Z–A)' },
|
|
175
|
+
{ value: 'createdAt:desc', label: 'Created (newest)' },
|
|
176
|
+
{ value: 'createdAt:asc', label: 'Created (oldest)' },
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
function parseSortValue(value: string): OrganizationSort {
|
|
180
|
+
const [key, direction] = value.split(':');
|
|
181
|
+
if (key === 'createdAt' && (direction === 'asc' || direction === 'desc')) {
|
|
182
|
+
return { key, direction };
|
|
183
|
+
}
|
|
184
|
+
if (key === 'name' && (direction === 'asc' || direction === 'desc')) {
|
|
185
|
+
return { key, direction };
|
|
186
|
+
}
|
|
187
|
+
return { key: 'name', direction: 'asc' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getCreatedAtValue(value?: string | number | Date) {
|
|
191
|
+
if (!value) return null;
|
|
192
|
+
if (value instanceof Date) {
|
|
193
|
+
return Number.isNaN(value.getTime()) ? null : value.getTime();
|
|
194
|
+
}
|
|
195
|
+
const parsed = new Date(value).getTime();
|
|
196
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
197
|
+
}
|
|
198
|
+
|
|
142
199
|
// ============================================================================
|
|
143
200
|
// Main Component
|
|
144
201
|
// ============================================================================
|
|
@@ -163,10 +220,16 @@ function OrganizationSelector({
|
|
|
163
220
|
footer,
|
|
164
221
|
createLabel = 'Create organization',
|
|
165
222
|
onCreate,
|
|
223
|
+
closeOnSelect = false,
|
|
224
|
+
defaultSort = { key: 'name', direction: 'asc' },
|
|
225
|
+
enableSorting = true,
|
|
166
226
|
}: OrganizationSelectorProps) {
|
|
167
227
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? '');
|
|
168
228
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
169
229
|
const [open, setOpen] = React.useState(false);
|
|
230
|
+
const [sort, setSort] = React.useState<OrganizationSort>(defaultSort);
|
|
231
|
+
const [dropdownOpen, setDropdownOpen] = React.useState(false);
|
|
232
|
+
const [sortOpen, setSortOpen] = React.useState(false);
|
|
170
233
|
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
171
234
|
|
|
172
235
|
const selectedValue = value ?? internalValue;
|
|
@@ -202,6 +265,24 @@ function OrganizationSelector({
|
|
|
202
265
|
);
|
|
203
266
|
}, [organizations, searchQuery]);
|
|
204
267
|
|
|
268
|
+
const sortedOrganizations = React.useMemo(() => {
|
|
269
|
+
const sorted = [...filteredOrganizations];
|
|
270
|
+
sorted.sort((a, b) => {
|
|
271
|
+
if (sort.key === 'name') {
|
|
272
|
+
const comparison = a.name.localeCompare(b.name);
|
|
273
|
+
return sort.direction === 'asc' ? comparison : -comparison;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const aTime = getCreatedAtValue(a.createdAt);
|
|
277
|
+
const bTime = getCreatedAtValue(b.createdAt);
|
|
278
|
+
if (aTime === null && bTime === null) return 0;
|
|
279
|
+
if (aTime === null) return 1;
|
|
280
|
+
if (bTime === null) return -1;
|
|
281
|
+
return sort.direction === 'asc' ? aTime - bTime : bTime - aTime;
|
|
282
|
+
});
|
|
283
|
+
return sorted;
|
|
284
|
+
}, [filteredOrganizations, sort]);
|
|
285
|
+
|
|
205
286
|
const handleValueChange = React.useCallback(
|
|
206
287
|
(newValue: string | null) => {
|
|
207
288
|
const orgId = newValue ?? '';
|
|
@@ -210,9 +291,12 @@ function OrganizationSelector({
|
|
|
210
291
|
}
|
|
211
292
|
onValueChange?.(orgId);
|
|
212
293
|
setSearchQuery(''); // Clear search on selection
|
|
213
|
-
|
|
294
|
+
if (closeOnSelect) {
|
|
295
|
+
setOpen(false); // Close modal on selection
|
|
296
|
+
setDropdownOpen(false); // Close dropdown on selection
|
|
297
|
+
}
|
|
214
298
|
},
|
|
215
|
-
[value, onValueChange]
|
|
299
|
+
[value, onValueChange, closeOnSelect]
|
|
216
300
|
);
|
|
217
301
|
|
|
218
302
|
const triggerSizeClass = size === 'sm' ? 'h-7' : 'h-8';
|
|
@@ -220,7 +304,7 @@ function OrganizationSelector({
|
|
|
220
304
|
// Determine content state
|
|
221
305
|
const hasError = Boolean(error);
|
|
222
306
|
const isEmpty = organizations.length === 0;
|
|
223
|
-
const hasNoResults =
|
|
307
|
+
const hasNoResults = sortedOrganizations.length === 0 && !isEmpty;
|
|
224
308
|
|
|
225
309
|
const triggerButton = (
|
|
226
310
|
<button
|
|
@@ -279,8 +363,29 @@ function OrganizationSelector({
|
|
|
279
363
|
return 0;
|
|
280
364
|
}}
|
|
281
365
|
>
|
|
366
|
+
{/* Sort Menu */}
|
|
367
|
+
{enableSorting && (
|
|
368
|
+
<div data-slot="organization-selector-sort" className="border-b border-border p-2">
|
|
369
|
+
<Select
|
|
370
|
+
value={`${sort.key}:${sort.direction}`}
|
|
371
|
+
onValueChange={(value) => value && setSort(parseSortValue(value))}
|
|
372
|
+
>
|
|
373
|
+
<SelectTrigger size="sm">
|
|
374
|
+
<span>{getSortLabel(sort)}</span>
|
|
375
|
+
</SelectTrigger>
|
|
376
|
+
<SelectContent align="start">
|
|
377
|
+
{sortOptions.map((option) => (
|
|
378
|
+
<SelectItem key={option.value} value={option.value}>
|
|
379
|
+
{option.label}
|
|
380
|
+
</SelectItem>
|
|
381
|
+
))}
|
|
382
|
+
</SelectContent>
|
|
383
|
+
</Select>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
|
|
282
387
|
{/* Search Input */}
|
|
283
|
-
<div data-slot="command-input-wrapper" className="
|
|
388
|
+
<div data-slot="command-input-wrapper" className="border-b border-border p-2">
|
|
284
389
|
<div
|
|
285
390
|
data-slot="input-group"
|
|
286
391
|
role="group"
|
|
@@ -304,7 +409,7 @@ function OrganizationSelector({
|
|
|
304
409
|
{/* Organization List */}
|
|
305
410
|
<CommandPrimitive.List
|
|
306
411
|
data-slot="command-list"
|
|
307
|
-
className="no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto"
|
|
412
|
+
className="no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto pt-2"
|
|
308
413
|
>
|
|
309
414
|
{loading ? (
|
|
310
415
|
<EmptyState icon={<LoadingSpinner />} text={loadingText} />
|
|
@@ -317,7 +422,7 @@ function OrganizationSelector({
|
|
|
317
422
|
<CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm">
|
|
318
423
|
{emptySearchText}
|
|
319
424
|
</CommandPrimitive.Empty>
|
|
320
|
-
{
|
|
425
|
+
{sortedOrganizations.map((org) => (
|
|
321
426
|
<CommandPrimitive.Item
|
|
322
427
|
key={org.id}
|
|
323
428
|
data-slot="command-item"
|
|
@@ -366,7 +471,15 @@ function OrganizationSelector({
|
|
|
366
471
|
|
|
367
472
|
// Dropdown mode (default): inline combobox
|
|
368
473
|
return (
|
|
369
|
-
<ComboboxPrimitive.Root
|
|
474
|
+
<ComboboxPrimitive.Root
|
|
475
|
+
value={selectedValue}
|
|
476
|
+
onValueChange={handleValueChange}
|
|
477
|
+
open={dropdownOpen}
|
|
478
|
+
onOpenChange={(nextOpen) => {
|
|
479
|
+
if (sortOpen && !nextOpen) return;
|
|
480
|
+
setDropdownOpen(nextOpen);
|
|
481
|
+
}}
|
|
482
|
+
>
|
|
370
483
|
<ComboboxPrimitive.Trigger
|
|
371
484
|
ref={triggerRef}
|
|
372
485
|
data-slot="organization-selector-trigger"
|
|
@@ -389,10 +502,32 @@ function OrganizationSelector({
|
|
|
389
502
|
align="start"
|
|
390
503
|
className="isolate z-50"
|
|
391
504
|
>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
505
|
+
<ComboboxPrimitive.Popup
|
|
506
|
+
data-slot="organization-selector-content"
|
|
507
|
+
className="bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 max-h-80 min-w-56 overflow-visible rounded-lg shadow-md ring-1 duration-100 group/org-selector relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin)"
|
|
508
|
+
>
|
|
509
|
+
{/* Sort Menu */}
|
|
510
|
+
{enableSorting && (
|
|
511
|
+
<div data-slot="organization-selector-sort" className="border-b border-border p-2">
|
|
512
|
+
<Select
|
|
513
|
+
value={`${sort.key}:${sort.direction}`}
|
|
514
|
+
onValueChange={(value) => value && setSort(parseSortValue(value))}
|
|
515
|
+
onOpenChange={setSortOpen}
|
|
516
|
+
>
|
|
517
|
+
<SelectTrigger size="sm">
|
|
518
|
+
<span>{getSortLabel(sort)}</span>
|
|
519
|
+
</SelectTrigger>
|
|
520
|
+
<SelectContent align="start">
|
|
521
|
+
{sortOptions.map((option) => (
|
|
522
|
+
<SelectItem key={option.value} value={option.value}>
|
|
523
|
+
{option.label}
|
|
524
|
+
</SelectItem>
|
|
525
|
+
))}
|
|
526
|
+
</SelectContent>
|
|
527
|
+
</Select>
|
|
528
|
+
</div>
|
|
529
|
+
)}
|
|
530
|
+
|
|
396
531
|
{/* Search Input */}
|
|
397
532
|
<div className="border-b border-border p-2">
|
|
398
533
|
<div className="bg-muted/50 flex items-center gap-2 rounded-md px-2.5">
|
|
@@ -410,7 +545,7 @@ function OrganizationSelector({
|
|
|
410
545
|
{/* Organization List */}
|
|
411
546
|
<ComboboxPrimitive.List
|
|
412
547
|
data-slot="organization-selector-list"
|
|
413
|
-
className="no-scrollbar max-h-64 scroll-py-1 overflow-y-auto
|
|
548
|
+
className="no-scrollbar max-h-64 scroll-py-1 overflow-y-auto px-1 pb-1 pt-2 overscroll-contain"
|
|
414
549
|
>
|
|
415
550
|
{loading ? (
|
|
416
551
|
<EmptyState icon={<LoadingSpinner />} text={loadingText} />
|
|
@@ -421,7 +556,7 @@ function OrganizationSelector({
|
|
|
421
556
|
) : hasNoResults ? (
|
|
422
557
|
<EmptyState text={emptySearchText} />
|
|
423
558
|
) : (
|
|
424
|
-
|
|
559
|
+
sortedOrganizations.map((org) => (
|
|
425
560
|
<ComboboxPrimitive.Item
|
|
426
561
|
key={org.id}
|
|
427
562
|
data-slot="organization-selector-item"
|