@mdguggenbichler/slugbase-core 0.0.4 → 0.0.6

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.
Files changed (120) hide show
  1. package/frontend/index.tsx +3 -3
  2. package/frontend/public/favicon.svg +1 -0
  3. package/frontend/public/slugbase_icon_blue.svg +1 -0
  4. package/frontend/public/slugbase_icon_white.png +0 -0
  5. package/frontend/public/slugbase_icon_white.svg +1 -0
  6. package/frontend/src/App.tsx +179 -0
  7. package/frontend/src/api/client.ts +134 -0
  8. package/frontend/src/components/AppSidebar.tsx +214 -0
  9. package/frontend/src/components/EmptyState.tsx +33 -0
  10. package/frontend/src/components/Favicon.tsx +76 -0
  11. package/frontend/src/components/FilterChips.tsx +60 -0
  12. package/frontend/src/components/FolderIcon.tsx +207 -0
  13. package/frontend/src/components/GlobalSearch.tsx +275 -0
  14. package/frontend/src/components/Layout.tsx +60 -0
  15. package/frontend/src/components/PageHeader.tsx +31 -0
  16. package/frontend/src/components/ScopeSegmentedControl.tsx +42 -0
  17. package/frontend/src/components/SentryDebug.tsx +32 -0
  18. package/frontend/src/components/StatCard.tsx +66 -0
  19. package/frontend/src/components/TopBar.tsx +63 -0
  20. package/frontend/src/components/UserDropdown.tsx +86 -0
  21. package/frontend/src/components/admin/AdminAI.tsx +207 -0
  22. package/frontend/src/components/admin/AdminOIDCProviders.tsx +183 -0
  23. package/frontend/src/components/admin/AdminSettings.tsx +413 -0
  24. package/frontend/src/components/admin/AdminTeams.tsx +177 -0
  25. package/frontend/src/components/admin/AdminUsers.tsx +225 -0
  26. package/frontend/src/components/bookmarks/BookmarkCard.tsx +312 -0
  27. package/frontend/src/components/bookmarks/BookmarkListItem.tsx +159 -0
  28. package/frontend/src/components/bookmarks/BookmarkTableView.tsx +419 -0
  29. package/frontend/src/components/bookmarks/BulkActionModals.tsx +162 -0
  30. package/frontend/src/components/bookmarks/FilterChips.tsx +5 -0
  31. package/frontend/src/components/modals/BookmarkModal.tsx +493 -0
  32. package/frontend/src/components/modals/FolderModal.tsx +306 -0
  33. package/frontend/src/components/modals/ImportModal.tsx +232 -0
  34. package/frontend/src/components/modals/OIDCProviderModal.tsx +284 -0
  35. package/frontend/src/components/modals/SharingModal.tsx +96 -0
  36. package/frontend/src/components/modals/TagModal.tsx +101 -0
  37. package/frontend/src/components/modals/TeamAssignmentModal.tsx +354 -0
  38. package/frontend/src/components/modals/TeamModal.tsx +117 -0
  39. package/frontend/src/components/modals/UserModal.tsx +225 -0
  40. package/frontend/src/components/profile/CreateTokenModal.tsx +172 -0
  41. package/frontend/src/components/sharing/ShareResourceDialog.tsx +422 -0
  42. package/frontend/src/components/ui/Autocomplete.tsx +155 -0
  43. package/frontend/src/components/ui/Button.tsx +68 -0
  44. package/frontend/src/components/ui/ConfirmDialog.tsx +79 -0
  45. package/frontend/src/components/ui/FormFieldWrapper.tsx +36 -0
  46. package/frontend/src/components/ui/ModalFooterActions.tsx +49 -0
  47. package/frontend/src/components/ui/ModalSection.tsx +34 -0
  48. package/frontend/src/components/ui/PageLoadingSkeleton.tsx +24 -0
  49. package/frontend/src/components/ui/Select.tsx +61 -0
  50. package/frontend/src/components/ui/SharingField.tsx +298 -0
  51. package/frontend/src/components/ui/Toast.tsx +47 -0
  52. package/frontend/src/components/ui/Tooltip.tsx +21 -0
  53. package/frontend/src/components/ui/alert-dialog.tsx +139 -0
  54. package/frontend/src/components/ui/badge.tsx +36 -0
  55. package/frontend/src/components/ui/button-base.tsx +57 -0
  56. package/frontend/src/components/ui/card.tsx +76 -0
  57. package/frontend/src/components/ui/command.tsx +161 -0
  58. package/frontend/src/components/ui/dialog.tsx +120 -0
  59. package/frontend/src/components/ui/dropdown-menu.tsx +199 -0
  60. package/frontend/src/components/ui/input.tsx +22 -0
  61. package/frontend/src/components/ui/label.tsx +24 -0
  62. package/frontend/src/components/ui/popover.tsx +33 -0
  63. package/frontend/src/components/ui/progress.tsx +26 -0
  64. package/frontend/src/components/ui/scroll-area.tsx +48 -0
  65. package/frontend/src/components/ui/select-base.tsx +159 -0
  66. package/frontend/src/components/ui/separator.tsx +29 -0
  67. package/frontend/src/components/ui/sheet.tsx +140 -0
  68. package/frontend/src/components/ui/sidebar.tsx +783 -0
  69. package/frontend/src/components/ui/skeleton.tsx +15 -0
  70. package/frontend/src/components/ui/sonner.tsx +46 -0
  71. package/frontend/src/components/ui/switch.tsx +28 -0
  72. package/frontend/src/components/ui/table.tsx +120 -0
  73. package/frontend/src/components/ui/tooltip-base.tsx +30 -0
  74. package/frontend/src/config/api.ts +16 -0
  75. package/frontend/src/config/mode.ts +6 -0
  76. package/frontend/src/contexts/AppConfigContext.tsx +39 -0
  77. package/frontend/src/contexts/AuthContext.tsx +137 -0
  78. package/frontend/src/contexts/SearchCommandContext.tsx +28 -0
  79. package/frontend/src/hooks/use-mobile.tsx +19 -0
  80. package/frontend/src/hooks/useConfirmDialog.ts +63 -0
  81. package/frontend/src/hooks/useMarketingTheme.ts +47 -0
  82. package/frontend/src/i18n.ts +39 -0
  83. package/frontend/src/index.css +117 -0
  84. package/frontend/src/instrument.ts +20 -0
  85. package/frontend/src/lib/utils.ts +6 -0
  86. package/frontend/src/locales/de.json +899 -0
  87. package/frontend/src/locales/en.json +937 -0
  88. package/frontend/src/locales/es.json +884 -0
  89. package/frontend/src/locales/fr.json +550 -0
  90. package/frontend/src/locales/it.json +535 -0
  91. package/frontend/src/locales/ja.json +535 -0
  92. package/frontend/src/locales/nl.json +550 -0
  93. package/frontend/src/locales/pl.json +535 -0
  94. package/frontend/src/locales/pt.json +535 -0
  95. package/frontend/src/locales/ru.json +535 -0
  96. package/frontend/src/locales/zh.json +535 -0
  97. package/frontend/src/main.tsx +44 -0
  98. package/frontend/src/pages/Bookmarks.tsx +1004 -0
  99. package/frontend/src/pages/Dashboard.tsx +427 -0
  100. package/frontend/src/pages/Folders.tsx +578 -0
  101. package/frontend/src/pages/GoPreferences.tsx +134 -0
  102. package/frontend/src/pages/Login.tsx +196 -0
  103. package/frontend/src/pages/PasswordReset.tsx +242 -0
  104. package/frontend/src/pages/Profile.tsx +593 -0
  105. package/frontend/src/pages/SearchEngineGuide.tsx +135 -0
  106. package/frontend/src/pages/Setup.tsx +210 -0
  107. package/frontend/src/pages/Signup.tsx +199 -0
  108. package/frontend/src/pages/Tags.tsx +421 -0
  109. package/frontend/src/pages/VerifyEmail.tsx +254 -0
  110. package/frontend/src/pages/admin/AdminAIPage.tsx +5 -0
  111. package/frontend/src/pages/admin/AdminLayout.tsx +40 -0
  112. package/frontend/src/pages/admin/AdminMembersPage.tsx +5 -0
  113. package/frontend/src/pages/admin/AdminOIDCPage.tsx +5 -0
  114. package/frontend/src/pages/admin/AdminSettingsPage.tsx +5 -0
  115. package/frontend/src/pages/admin/AdminTeamsPage.tsx +5 -0
  116. package/frontend/src/utils/favicon.ts +36 -0
  117. package/frontend/src/utils/formatRelativeTime.ts +37 -0
  118. package/frontend/src/utils/safeHref.ts +31 -0
  119. package/frontend/src/vite-env.d.ts +10 -0
  120. package/package.json +9 -1
@@ -0,0 +1,155 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { X } from 'lucide-react';
3
+ import { Popover, PopoverAnchor, PopoverContent } from './popover';
4
+ import { Input } from './input';
5
+ import { Badge } from './badge';
6
+ import { ScrollArea } from './scroll-area';
7
+ import { cn } from '@/lib/utils';
8
+
9
+ interface AutocompleteOption {
10
+ id: string;
11
+ name: string;
12
+ }
13
+
14
+ interface AutocompleteProps {
15
+ value: AutocompleteOption[];
16
+ onChange: (value: AutocompleteOption[]) => void;
17
+ options: AutocompleteOption[];
18
+ placeholder?: string;
19
+ onCreateNew?: (name: string) => Promise<AutocompleteOption | null>;
20
+ className?: string;
21
+ }
22
+
23
+ export default function Autocomplete({
24
+ value,
25
+ onChange,
26
+ options,
27
+ placeholder = 'Type to search...',
28
+ onCreateNew,
29
+ className = '',
30
+ }: AutocompleteProps) {
31
+ const [inputValue, setInputValue] = useState('');
32
+ const [open, setOpen] = useState(false);
33
+ const [filteredOptions, setFilteredOptions] = useState<AutocompleteOption[]>([]);
34
+ const inputRef = useRef<HTMLInputElement>(null);
35
+
36
+ useEffect(() => {
37
+ if (inputValue.trim()) {
38
+ const filtered = options.filter(
39
+ (opt) =>
40
+ !value.find((v) => v.id === opt.id) &&
41
+ opt.name.toLowerCase().includes(inputValue.toLowerCase())
42
+ );
43
+ setFilteredOptions(filtered);
44
+ setOpen(
45
+ Boolean(
46
+ filtered.length > 0 ||
47
+ (onCreateNew !== undefined && inputValue.trim().length > 0)
48
+ )
49
+ );
50
+ } else {
51
+ setFilteredOptions([]);
52
+ setOpen(false);
53
+ }
54
+ }, [inputValue, options, value, onCreateNew]);
55
+
56
+ const handleSelect = (option: AutocompleteOption) => {
57
+ onChange([...value, option]);
58
+ setInputValue('');
59
+ setOpen(false);
60
+ inputRef.current?.focus();
61
+ };
62
+
63
+ const handleRemove = (id: string) => {
64
+ onChange(value.filter((v) => v.id !== id));
65
+ };
66
+
67
+ const handleCreateNew = async () => {
68
+ if (onCreateNew && inputValue.trim()) {
69
+ const newOption = await onCreateNew(inputValue.trim());
70
+ if (newOption) {
71
+ handleSelect(newOption);
72
+ }
73
+ }
74
+ };
75
+
76
+ const handleKeyDown = (e: React.KeyboardEvent) => {
77
+ if (e.key === 'Enter' && open && filteredOptions.length > 0) {
78
+ e.preventDefault();
79
+ handleSelect(filteredOptions[0]);
80
+ } else if (e.key === 'Enter' && onCreateNew && inputValue.trim()) {
81
+ e.preventDefault();
82
+ handleCreateNew();
83
+ } else if (e.key === 'Escape') {
84
+ setOpen(false);
85
+ }
86
+ };
87
+
88
+ return (
89
+ <div className={cn('relative', className)}>
90
+ {value.length > 0 && (
91
+ <div className="mb-2 flex flex-wrap gap-2">
92
+ {value.map((item) => (
93
+ <Badge
94
+ key={item.id}
95
+ variant="secondary"
96
+ className="pr-1 gap-1.5"
97
+ >
98
+ {item.name}
99
+ <button
100
+ type="button"
101
+ onClick={() => handleRemove(item.id)}
102
+ className="rounded-full hover:bg-secondary/80 p-0.5 transition-colors"
103
+ >
104
+ <X className="h-3.5 w-3.5" />
105
+ </button>
106
+ </Badge>
107
+ ))}
108
+ </div>
109
+ )}
110
+
111
+ <Popover open={open} onOpenChange={setOpen}>
112
+ <PopoverAnchor asChild>
113
+ <Input
114
+ ref={inputRef}
115
+ type="text"
116
+ value={inputValue}
117
+ onChange={(e) => setInputValue(e.target.value)}
118
+ onFocus={() => inputValue.trim() && setOpen(true)}
119
+ onKeyDown={handleKeyDown}
120
+ placeholder={placeholder}
121
+ className="w-full"
122
+ />
123
+ </PopoverAnchor>
124
+ <PopoverContent className="w-[var(--radix-popover-anchor-width)] p-0" align="start">
125
+ <ScrollArea className="max-h-60">
126
+ {filteredOptions.length === 0 && onCreateNew && inputValue.trim() ? (
127
+ <button
128
+ type="button"
129
+ onClick={handleCreateNew}
130
+ className="w-full text-left px-3 py-2 text-sm text-primary hover:bg-accent hover:text-accent-foreground"
131
+ >
132
+ Create &quot;{inputValue.trim()}&quot;
133
+ </button>
134
+ ) : filteredOptions.length === 0 ? (
135
+ <div className="px-3 py-2 text-sm text-muted-foreground">
136
+ No options found
137
+ </div>
138
+ ) : (
139
+ filteredOptions.map((option) => (
140
+ <button
141
+ key={option.id}
142
+ type="button"
143
+ onClick={() => handleSelect(option)}
144
+ className="w-full text-left px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
145
+ >
146
+ {option.name}
147
+ </button>
148
+ ))
149
+ )}
150
+ </ScrollArea>
151
+ </PopoverContent>
152
+ </Popover>
153
+ </div>
154
+ );
155
+ }
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import { LucideIcon, Loader2 } from 'lucide-react';
3
+ import { Button as ShadcnButton, buttonVariants } from './button-base';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const variantMap = {
7
+ primary: 'default',
8
+ secondary: 'secondary',
9
+ danger: 'destructive',
10
+ ghost: 'ghost',
11
+ outline: 'outline',
12
+ } as const;
13
+
14
+ const sizeMap = {
15
+ sm: 'sm',
16
+ md: 'default',
17
+ lg: 'lg',
18
+ } as const;
19
+
20
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
21
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'outline';
22
+ size?: 'sm' | 'md' | 'lg';
23
+ icon?: LucideIcon;
24
+ iconPosition?: 'left' | 'right';
25
+ iconClassName?: string;
26
+ loading?: boolean;
27
+ children?: React.ReactNode;
28
+ }
29
+
30
+ const defaultIconClass = 'h-4 w-4';
31
+
32
+ export default function Button({
33
+ variant = 'primary',
34
+ size = 'md',
35
+ icon: Icon,
36
+ iconPosition = 'left',
37
+ iconClassName = defaultIconClass,
38
+ loading = false,
39
+ children,
40
+ className = '',
41
+ disabled,
42
+ ...props
43
+ }: ButtonProps) {
44
+ const iconClass = iconClassName || defaultIconClass;
45
+ const content = loading ? (
46
+ <Loader2 className="h-4 w-4 animate-spin" />
47
+ ) : (
48
+ <>
49
+ {Icon && iconPosition === 'left' && <Icon className={iconClass} />}
50
+ {children && <span>{children}</span>}
51
+ {Icon && iconPosition === 'right' && <Icon className={iconClass} />}
52
+ </>
53
+ );
54
+
55
+ return (
56
+ <ShadcnButton
57
+ className={cn(className)}
58
+ variant={variantMap[variant]}
59
+ size={sizeMap[size]}
60
+ disabled={disabled ?? loading}
61
+ {...props}
62
+ >
63
+ {content}
64
+ </ShadcnButton>
65
+ );
66
+ }
67
+
68
+ export { buttonVariants };
@@ -0,0 +1,79 @@
1
+ import { AlertTriangle } from 'lucide-react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ } from './alert-dialog';
13
+
14
+ interface ConfirmDialogProps {
15
+ isOpen: boolean;
16
+ title: string;
17
+ message: string;
18
+ itemName?: string;
19
+ confirmText?: string;
20
+ cancelText?: string;
21
+ variant?: 'danger' | 'warning' | 'default';
22
+ onConfirm: () => void;
23
+ onCancel: () => void;
24
+ }
25
+
26
+ const variantBorderClasses = {
27
+ danger: 'border-destructive/50',
28
+ warning: 'border-yellow-500/50',
29
+ default: 'border-border',
30
+ };
31
+
32
+ export default function ConfirmDialog({
33
+ isOpen,
34
+ title,
35
+ message,
36
+ itemName,
37
+ confirmText,
38
+ cancelText,
39
+ variant = 'default',
40
+ onConfirm,
41
+ onCancel,
42
+ }: ConfirmDialogProps) {
43
+ const { t } = useTranslation();
44
+
45
+ const description = itemName
46
+ ? `${message} "${itemName}"`
47
+ : message;
48
+
49
+ return (
50
+ <AlertDialog open={isOpen} onOpenChange={(open) => !open && onCancel()}>
51
+ <AlertDialogContent className={`max-w-sm ${variantBorderClasses[variant]}`}>
52
+ <AlertDialogHeader>
53
+ <div className="flex items-start gap-4">
54
+ <div className="flex-shrink-0 text-destructive">
55
+ <AlertTriangle className="h-6 w-6" />
56
+ </div>
57
+ <div className="flex-1 min-w-0">
58
+ <AlertDialogTitle>{title}</AlertDialogTitle>
59
+ <AlertDialogDescription className="mt-2">
60
+ {description}
61
+ </AlertDialogDescription>
62
+ </div>
63
+ </div>
64
+ </AlertDialogHeader>
65
+ <AlertDialogFooter>
66
+ <AlertDialogCancel onClick={onCancel}>
67
+ {cancelText || t('common.cancel')}
68
+ </AlertDialogCancel>
69
+ <AlertDialogAction
70
+ onClick={onConfirm}
71
+ className={variant === 'danger' ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' : ''}
72
+ >
73
+ {confirmText || t('common.confirm')}
74
+ </AlertDialogAction>
75
+ </AlertDialogFooter>
76
+ </AlertDialogContent>
77
+ </AlertDialog>
78
+ );
79
+ }
@@ -0,0 +1,36 @@
1
+ import * as React from 'react';
2
+ import { Label } from './label';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ interface FormFieldWrapperProps {
6
+ label: string;
7
+ error?: string;
8
+ required?: boolean;
9
+ htmlFor?: string;
10
+ children: React.ReactNode;
11
+ className?: string;
12
+ }
13
+
14
+ export function FormFieldWrapper({
15
+ label,
16
+ error,
17
+ required,
18
+ htmlFor,
19
+ children,
20
+ className,
21
+ }: FormFieldWrapperProps) {
22
+ return (
23
+ <div className={cn('space-y-2', className)}>
24
+ <Label htmlFor={htmlFor} className="text-sm font-medium">
25
+ {label}
26
+ {required && <span className="text-destructive ml-0.5">*</span>}
27
+ </Label>
28
+ {children}
29
+ {error && (
30
+ <p className="text-sm text-destructive" role="alert">
31
+ {error}
32
+ </p>
33
+ )}
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,49 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import Button from './Button';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ interface ModalFooterActionsProps {
6
+ onCancel: () => void;
7
+ cancelLabel?: string;
8
+ submitLabel: string;
9
+ loading?: boolean;
10
+ submitDisabled?: boolean;
11
+ submitVariant?: 'primary' | 'danger';
12
+ formId?: string;
13
+ className?: string;
14
+ }
15
+
16
+ export function ModalFooterActions({
17
+ onCancel,
18
+ cancelLabel,
19
+ submitLabel,
20
+ loading = false,
21
+ submitDisabled = false,
22
+ submitVariant = 'primary',
23
+ formId,
24
+ className,
25
+ }: ModalFooterActionsProps) {
26
+ const { t } = useTranslation();
27
+
28
+ return (
29
+ <div
30
+ className={cn(
31
+ 'flex flex-row justify-between gap-2 sm:justify-end',
32
+ className
33
+ )}
34
+ >
35
+ <Button variant="secondary" onClick={onCancel} type="button">
36
+ {cancelLabel ?? t('common.cancel')}
37
+ </Button>
38
+ <Button
39
+ variant={submitVariant}
40
+ loading={loading}
41
+ disabled={submitDisabled || loading}
42
+ type="submit"
43
+ form={formId}
44
+ >
45
+ {submitLabel}
46
+ </Button>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,34 @@
1
+ import * as React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ interface ModalSectionProps {
5
+ title?: string;
6
+ description?: string;
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ }
10
+
11
+ export function ModalSection({
12
+ title,
13
+ description,
14
+ children,
15
+ className,
16
+ }: ModalSectionProps) {
17
+ return (
18
+ <div className={cn('space-y-3', className)}>
19
+ {(title || description) && (
20
+ <div>
21
+ {title && (
22
+ <h4 className="text-sm font-semibold leading-none tracking-tight">
23
+ {title}
24
+ </h4>
25
+ )}
26
+ {description && (
27
+ <p className="mt-1 text-xs text-muted-foreground">{description}</p>
28
+ )}
29
+ </div>
30
+ )}
31
+ {children}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,24 @@
1
+ import { Skeleton } from './skeleton';
2
+
3
+ interface PageLoadingSkeletonProps {
4
+ /** Number of content blocks to show (default: 4) */
5
+ lines?: number;
6
+ }
7
+
8
+ export function PageLoadingSkeleton({ lines = 4 }: PageLoadingSkeletonProps) {
9
+ return (
10
+ <div className="space-y-6">
11
+ <div className="space-y-2">
12
+ <Skeleton className="h-8 w-48" />
13
+ <Skeleton className="h-4 w-96" />
14
+ </div>
15
+ <div className=" rounded-lg border bg-card p-6">
16
+ <div className="space-y-4">
17
+ {Array.from({ length: lines }).map((_, i) => (
18
+ <Skeleton key={i} className="h-12 w-full" />
19
+ ))}
20
+ </div>
21
+ </div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,61 @@
1
+ import FolderIcon from '../FolderIcon';
2
+ import {
3
+ Select as ShadcnSelect,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ } from './select-base';
9
+
10
+ interface SelectOption {
11
+ value: string;
12
+ label: string;
13
+ disabled?: boolean;
14
+ icon?: string | null;
15
+ }
16
+
17
+ interface SelectProps {
18
+ value: string;
19
+ onChange: (value: string) => void;
20
+ options: SelectOption[];
21
+ placeholder?: string;
22
+ disabled?: boolean;
23
+ className?: string;
24
+ }
25
+
26
+ export default function Select({
27
+ value,
28
+ onChange,
29
+ options,
30
+ placeholder = 'Select...',
31
+ disabled = false,
32
+ className = '',
33
+ }: SelectProps) {
34
+ return (
35
+ <ShadcnSelect value={value} onValueChange={onChange} disabled={disabled}>
36
+ <SelectTrigger className={className}>
37
+ <SelectValue placeholder={placeholder} />
38
+ </SelectTrigger>
39
+ <SelectContent>
40
+ {options.length === 0 ? (
41
+ <div className="px-2 py-1.5 text-sm text-muted-foreground">No options</div>
42
+ ) : (
43
+ options.map((option) => (
44
+ <SelectItem
45
+ key={option.value}
46
+ value={option.value}
47
+ disabled={option.disabled}
48
+ >
49
+ <span className="flex items-center gap-2">
50
+ {option.icon != null && (
51
+ <FolderIcon iconName={option.icon} size={16} className="text-muted-foreground" />
52
+ )}
53
+ {option.label}
54
+ </span>
55
+ </SelectItem>
56
+ ))
57
+ )}
58
+ </SelectContent>
59
+ </ShadcnSelect>
60
+ );
61
+ }