@trycompai/design-system 1.0.33 → 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/README.md
CHANGED
|
@@ -101,7 +101,7 @@ The design system includes:
|
|
|
101
101
|
|
|
102
102
|
## Theme Switching
|
|
103
103
|
|
|
104
|
-
The design system uses CSS class-based theming. Dark mode is enabled by adding the `.dark` class to the `<html>` element.
|
|
104
|
+
The design system uses CSS class-based theming. Light mode is the default. Dark mode is enabled by adding the `.dark` class to the `<html>` element.
|
|
105
105
|
|
|
106
106
|
### Theme Components
|
|
107
107
|
|
|
@@ -175,7 +175,7 @@ export default function RootLayout({ children }) {
|
|
|
175
175
|
return (
|
|
176
176
|
<html lang="en" suppressHydrationWarning>
|
|
177
177
|
<body>
|
|
178
|
-
<ThemeProvider attribute="class" defaultTheme="
|
|
178
|
+
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
|
|
179
179
|
{children}
|
|
180
180
|
</ThemeProvider>
|
|
181
181
|
</body>
|
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
|
// ============================================================================
|
|
@@ -16,10 +23,22 @@ export interface Organization {
|
|
|
16
23
|
name: string;
|
|
17
24
|
/** Optional icon or avatar to display */
|
|
18
25
|
icon?: React.ReactNode;
|
|
26
|
+
/** Optional logo URL to display */
|
|
27
|
+
logoUrl?: string;
|
|
28
|
+
/** Optional created at timestamp used for sorting */
|
|
29
|
+
createdAt?: string | number | Date;
|
|
19
30
|
/** Optional brand color for the indicator dot (e.g., "#10b981" or "bg-green-500") */
|
|
20
31
|
color?: string;
|
|
21
32
|
}
|
|
22
33
|
|
|
34
|
+
type OrganizationSortKey = 'name' | 'createdAt';
|
|
35
|
+
type OrganizationSortDirection = 'asc' | 'desc';
|
|
36
|
+
|
|
37
|
+
type OrganizationSort = {
|
|
38
|
+
key: OrganizationSortKey;
|
|
39
|
+
direction: OrganizationSortDirection;
|
|
40
|
+
};
|
|
41
|
+
|
|
23
42
|
export interface OrganizationSelectorProps {
|
|
24
43
|
/** List of organizations to display */
|
|
25
44
|
organizations: Organization[];
|
|
@@ -59,6 +78,12 @@ export interface OrganizationSelectorProps {
|
|
|
59
78
|
createLabel?: string;
|
|
60
79
|
/** Callback when create action is selected */
|
|
61
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;
|
|
62
87
|
}
|
|
63
88
|
|
|
64
89
|
// ============================================================================
|
|
@@ -100,14 +125,77 @@ function EmptyState({ icon, text }: { icon?: React.ReactNode; text: string }) {
|
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
function OrganizationItemContent({ org, maxWidth }: { org: Organization; maxWidth?: string }) {
|
|
128
|
+
const initials = org.name
|
|
129
|
+
.trim()
|
|
130
|
+
.split(/\s+/)
|
|
131
|
+
.slice(0, 2)
|
|
132
|
+
.map((part) => part[0]?.toUpperCase())
|
|
133
|
+
.join('');
|
|
134
|
+
|
|
135
|
+
const leading = org.icon ? (
|
|
136
|
+
<span className="shrink-0">{org.icon}</span>
|
|
137
|
+
) : org.logoUrl ? (
|
|
138
|
+
<img
|
|
139
|
+
src={org.logoUrl}
|
|
140
|
+
alt={`${org.name} logo`}
|
|
141
|
+
width={20}
|
|
142
|
+
height={20}
|
|
143
|
+
className="size-5 rounded-sm object-contain shrink-0"
|
|
144
|
+
loading="lazy"
|
|
145
|
+
/>
|
|
146
|
+
) : (
|
|
147
|
+
<span
|
|
148
|
+
className="size-5 rounded-full bg-muted text-muted-foreground flex items-center justify-center text-[10px] font-medium shrink-0"
|
|
149
|
+
aria-hidden
|
|
150
|
+
>
|
|
151
|
+
{initials || '?'}
|
|
152
|
+
</span>
|
|
153
|
+
);
|
|
154
|
+
|
|
103
155
|
return (
|
|
104
156
|
<>
|
|
105
|
-
{
|
|
106
|
-
<span className="truncate" style={maxWidth ? { maxWidth } : undefined}>
|
|
157
|
+
{leading}
|
|
158
|
+
<span className="truncate" style={maxWidth ? { maxWidth } : undefined}>
|
|
159
|
+
{org.name}
|
|
160
|
+
</span>
|
|
107
161
|
</>
|
|
108
162
|
);
|
|
109
163
|
}
|
|
110
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
|
+
|
|
111
199
|
// ============================================================================
|
|
112
200
|
// Main Component
|
|
113
201
|
// ============================================================================
|
|
@@ -132,10 +220,14 @@ function OrganizationSelector({
|
|
|
132
220
|
footer,
|
|
133
221
|
createLabel = 'Create organization',
|
|
134
222
|
onCreate,
|
|
223
|
+
closeOnSelect = false,
|
|
224
|
+
defaultSort = { key: 'name', direction: 'asc' },
|
|
225
|
+
enableSorting = true,
|
|
135
226
|
}: OrganizationSelectorProps) {
|
|
136
227
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? '');
|
|
137
228
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
138
229
|
const [open, setOpen] = React.useState(false);
|
|
230
|
+
const [sort, setSort] = React.useState<OrganizationSort>(defaultSort);
|
|
139
231
|
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
140
232
|
|
|
141
233
|
const selectedValue = value ?? internalValue;
|
|
@@ -171,6 +263,24 @@ function OrganizationSelector({
|
|
|
171
263
|
);
|
|
172
264
|
}, [organizations, searchQuery]);
|
|
173
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
|
+
|
|
174
284
|
const handleValueChange = React.useCallback(
|
|
175
285
|
(newValue: string | null) => {
|
|
176
286
|
const orgId = newValue ?? '';
|
|
@@ -179,9 +289,11 @@ function OrganizationSelector({
|
|
|
179
289
|
}
|
|
180
290
|
onValueChange?.(orgId);
|
|
181
291
|
setSearchQuery(''); // Clear search on selection
|
|
182
|
-
|
|
292
|
+
if (closeOnSelect) {
|
|
293
|
+
setOpen(false); // Close modal on selection
|
|
294
|
+
}
|
|
183
295
|
},
|
|
184
|
-
[value, onValueChange]
|
|
296
|
+
[value, onValueChange, closeOnSelect]
|
|
185
297
|
);
|
|
186
298
|
|
|
187
299
|
const triggerSizeClass = size === 'sm' ? 'h-7' : 'h-8';
|
|
@@ -189,7 +301,7 @@ function OrganizationSelector({
|
|
|
189
301
|
// Determine content state
|
|
190
302
|
const hasError = Boolean(error);
|
|
191
303
|
const isEmpty = organizations.length === 0;
|
|
192
|
-
const hasNoResults =
|
|
304
|
+
const hasNoResults = sortedOrganizations.length === 0 && !isEmpty;
|
|
193
305
|
|
|
194
306
|
const triggerButton = (
|
|
195
307
|
<button
|
|
@@ -248,6 +360,27 @@ function OrganizationSelector({
|
|
|
248
360
|
return 0;
|
|
249
361
|
}}
|
|
250
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
|
+
|
|
251
384
|
{/* Search Input */}
|
|
252
385
|
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
|
253
386
|
<div
|
|
@@ -286,7 +419,7 @@ function OrganizationSelector({
|
|
|
286
419
|
<CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm">
|
|
287
420
|
{emptySearchText}
|
|
288
421
|
</CommandPrimitive.Empty>
|
|
289
|
-
{
|
|
422
|
+
{sortedOrganizations.map((org) => (
|
|
290
423
|
<CommandPrimitive.Item
|
|
291
424
|
key={org.id}
|
|
292
425
|
data-slot="command-item"
|
|
@@ -358,10 +491,31 @@ function OrganizationSelector({
|
|
|
358
491
|
align="start"
|
|
359
492
|
className="isolate z-50"
|
|
360
493
|
>
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
|
|
365
519
|
{/* Search Input */}
|
|
366
520
|
<div className="border-b border-border p-2">
|
|
367
521
|
<div className="bg-muted/50 flex items-center gap-2 rounded-md px-2.5">
|
|
@@ -390,7 +544,7 @@ function OrganizationSelector({
|
|
|
390
544
|
) : hasNoResults ? (
|
|
391
545
|
<EmptyState text={emptySearchText} />
|
|
392
546
|
) : (
|
|
393
|
-
|
|
547
|
+
sortedOrganizations.map((org) => (
|
|
394
548
|
<ComboboxPrimitive.Item
|
|
395
549
|
key={org.id}
|
|
396
550
|
data-slot="organization-selector-item"
|