@trycompai/design-system 1.0.34 → 1.0.35
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,14 @@ 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);
|
|
170
231
|
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
171
232
|
|
|
172
233
|
const selectedValue = value ?? internalValue;
|
|
@@ -202,6 +263,24 @@ function OrganizationSelector({
|
|
|
202
263
|
);
|
|
203
264
|
}, [organizations, searchQuery]);
|
|
204
265
|
|
|
266
|
+
const sortedOrganizations = React.useMemo(() => {
|
|
267
|
+
const sorted = [...filteredOrganizations];
|
|
268
|
+
sorted.sort((a, b) => {
|
|
269
|
+
if (sort.key === 'name') {
|
|
270
|
+
const comparison = a.name.localeCompare(b.name);
|
|
271
|
+
return sort.direction === 'asc' ? comparison : -comparison;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const aTime = getCreatedAtValue(a.createdAt);
|
|
275
|
+
const bTime = getCreatedAtValue(b.createdAt);
|
|
276
|
+
if (aTime === null && bTime === null) return 0;
|
|
277
|
+
if (aTime === null) return 1;
|
|
278
|
+
if (bTime === null) return -1;
|
|
279
|
+
return sort.direction === 'asc' ? aTime - bTime : bTime - aTime;
|
|
280
|
+
});
|
|
281
|
+
return sorted;
|
|
282
|
+
}, [filteredOrganizations, sort]);
|
|
283
|
+
|
|
205
284
|
const handleValueChange = React.useCallback(
|
|
206
285
|
(newValue: string | null) => {
|
|
207
286
|
const orgId = newValue ?? '';
|
|
@@ -210,9 +289,11 @@ function OrganizationSelector({
|
|
|
210
289
|
}
|
|
211
290
|
onValueChange?.(orgId);
|
|
212
291
|
setSearchQuery(''); // Clear search on selection
|
|
213
|
-
|
|
292
|
+
if (closeOnSelect) {
|
|
293
|
+
setOpen(false); // Close modal on selection
|
|
294
|
+
}
|
|
214
295
|
},
|
|
215
|
-
[value, onValueChange]
|
|
296
|
+
[value, onValueChange, closeOnSelect]
|
|
216
297
|
);
|
|
217
298
|
|
|
218
299
|
const triggerSizeClass = size === 'sm' ? 'h-7' : 'h-8';
|
|
@@ -220,7 +301,7 @@ function OrganizationSelector({
|
|
|
220
301
|
// Determine content state
|
|
221
302
|
const hasError = Boolean(error);
|
|
222
303
|
const isEmpty = organizations.length === 0;
|
|
223
|
-
const hasNoResults =
|
|
304
|
+
const hasNoResults = sortedOrganizations.length === 0 && !isEmpty;
|
|
224
305
|
|
|
225
306
|
const triggerButton = (
|
|
226
307
|
<button
|
|
@@ -279,6 +360,27 @@ function OrganizationSelector({
|
|
|
279
360
|
return 0;
|
|
280
361
|
}}
|
|
281
362
|
>
|
|
363
|
+
{/* Sort Menu */}
|
|
364
|
+
{enableSorting && (
|
|
365
|
+
<div data-slot="organization-selector-sort" className="p-1 pb-0">
|
|
366
|
+
<Select
|
|
367
|
+
value={`${sort.key}:${sort.direction}`}
|
|
368
|
+
onValueChange={(value) => value && setSort(parseSortValue(value))}
|
|
369
|
+
>
|
|
370
|
+
<SelectTrigger size="sm">
|
|
371
|
+
<span>{getSortLabel(sort)}</span>
|
|
372
|
+
</SelectTrigger>
|
|
373
|
+
<SelectContent align="start">
|
|
374
|
+
{sortOptions.map((option) => (
|
|
375
|
+
<SelectItem key={option.value} value={option.value}>
|
|
376
|
+
{option.label}
|
|
377
|
+
</SelectItem>
|
|
378
|
+
))}
|
|
379
|
+
</SelectContent>
|
|
380
|
+
</Select>
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
|
|
282
384
|
{/* Search Input */}
|
|
283
385
|
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
|
284
386
|
<div
|
|
@@ -317,7 +419,7 @@ function OrganizationSelector({
|
|
|
317
419
|
<CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm">
|
|
318
420
|
{emptySearchText}
|
|
319
421
|
</CommandPrimitive.Empty>
|
|
320
|
-
{
|
|
422
|
+
{sortedOrganizations.map((org) => (
|
|
321
423
|
<CommandPrimitive.Item
|
|
322
424
|
key={org.id}
|
|
323
425
|
data-slot="command-item"
|
|
@@ -389,10 +491,31 @@ function OrganizationSelector({
|
|
|
389
491
|
align="start"
|
|
390
492
|
className="isolate z-50"
|
|
391
493
|
>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
494
|
+
<ComboboxPrimitive.Popup
|
|
495
|
+
data-slot="organization-selector-content"
|
|
496
|
+
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)"
|
|
497
|
+
>
|
|
498
|
+
{/* Sort Menu */}
|
|
499
|
+
{enableSorting && (
|
|
500
|
+
<div data-slot="organization-selector-sort" className="border-b border-border p-2">
|
|
501
|
+
<Select
|
|
502
|
+
value={`${sort.key}:${sort.direction}`}
|
|
503
|
+
onValueChange={(value) => value && setSort(parseSortValue(value))}
|
|
504
|
+
>
|
|
505
|
+
<SelectTrigger size="sm">
|
|
506
|
+
<span>{getSortLabel(sort)}</span>
|
|
507
|
+
</SelectTrigger>
|
|
508
|
+
<SelectContent align="start" portal={false} position="absolute">
|
|
509
|
+
{sortOptions.map((option) => (
|
|
510
|
+
<SelectItem key={option.value} value={option.value}>
|
|
511
|
+
{option.label}
|
|
512
|
+
</SelectItem>
|
|
513
|
+
))}
|
|
514
|
+
</SelectContent>
|
|
515
|
+
</Select>
|
|
516
|
+
</div>
|
|
517
|
+
)}
|
|
518
|
+
|
|
396
519
|
{/* Search Input */}
|
|
397
520
|
<div className="border-b border-border p-2">
|
|
398
521
|
<div className="bg-muted/50 flex items-center gap-2 rounded-md px-2.5">
|
|
@@ -421,7 +544,7 @@ function OrganizationSelector({
|
|
|
421
544
|
) : hasNoResults ? (
|
|
422
545
|
<EmptyState text={emptySearchText} />
|
|
423
546
|
) : (
|
|
424
|
-
|
|
547
|
+
sortedOrganizations.map((org) => (
|
|
425
548
|
<ComboboxPrimitive.Item
|
|
426
549
|
key={org.id}
|
|
427
550
|
data-slot="organization-selector-item"
|