@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trycompai/design-system",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Design system for Comp AI - shadcn-style components with Tailwind CSS",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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
- return (
75
- <SelectPrimitive.Portal>
76
- <SelectPrimitive.Positioner
77
- side={side}
78
- sideOffset={sideOffset}
79
- align={align}
80
- alignOffset={alignOffset}
81
- alignItemWithTrigger={alignItemWithTrigger}
82
- className="isolate z-50"
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
- <SelectPrimitive.Popup
85
- data-slot="select-content"
86
- 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"
87
- {...props}
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
- setOpen(false); // Close modal on selection
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 = filteredOrganizations.length === 0 && !isEmpty;
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
- {organizations.map((org) => (
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
- <ComboboxPrimitive.Popup
393
- data-slot="organization-selector-content"
394
- 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-hidden 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)"
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
- filteredOrganizations.map((org) => (
547
+ sortedOrganizations.map((org) => (
425
548
  <ComboboxPrimitive.Item
426
549
  key={org.id}
427
550
  data-slot="organization-selector-item"