@checkstack/ui 0.0.2

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 (68) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/bunfig.toml +2 -0
  3. package/package.json +40 -0
  4. package/src/components/Accordion.tsx +55 -0
  5. package/src/components/Alert.tsx +90 -0
  6. package/src/components/AmbientBackground.tsx +105 -0
  7. package/src/components/AnimatedCounter.tsx +54 -0
  8. package/src/components/BackLink.tsx +56 -0
  9. package/src/components/Badge.tsx +38 -0
  10. package/src/components/Button.tsx +55 -0
  11. package/src/components/Card.tsx +56 -0
  12. package/src/components/Checkbox.tsx +46 -0
  13. package/src/components/ColorPicker.tsx +69 -0
  14. package/src/components/CommandPalette.tsx +74 -0
  15. package/src/components/ConfirmationModal.tsx +134 -0
  16. package/src/components/DateRangeFilter.tsx +128 -0
  17. package/src/components/DateTimePicker.tsx +65 -0
  18. package/src/components/Dialog.tsx +134 -0
  19. package/src/components/DropdownMenu.tsx +96 -0
  20. package/src/components/DynamicForm/DynamicForm.tsx +126 -0
  21. package/src/components/DynamicForm/DynamicOptionsField.tsx +220 -0
  22. package/src/components/DynamicForm/FormField.tsx +690 -0
  23. package/src/components/DynamicForm/JsonField.tsx +98 -0
  24. package/src/components/DynamicForm/index.ts +11 -0
  25. package/src/components/DynamicForm/types.ts +95 -0
  26. package/src/components/DynamicForm/utils.ts +39 -0
  27. package/src/components/DynamicIcon.tsx +45 -0
  28. package/src/components/EditableText.tsx +141 -0
  29. package/src/components/EmptyState.tsx +32 -0
  30. package/src/components/HealthBadge.tsx +57 -0
  31. package/src/components/InfoBanner.tsx +97 -0
  32. package/src/components/Input.tsx +20 -0
  33. package/src/components/Label.tsx +17 -0
  34. package/src/components/LoadingSpinner.tsx +29 -0
  35. package/src/components/Markdown.tsx +206 -0
  36. package/src/components/NavItem.tsx +112 -0
  37. package/src/components/Page.tsx +58 -0
  38. package/src/components/PageLayout.tsx +83 -0
  39. package/src/components/PaginatedList.tsx +135 -0
  40. package/src/components/Pagination.tsx +195 -0
  41. package/src/components/PermissionDenied.tsx +31 -0
  42. package/src/components/PermissionGate.tsx +97 -0
  43. package/src/components/PluginConfigForm.tsx +91 -0
  44. package/src/components/SectionHeader.tsx +30 -0
  45. package/src/components/Select.tsx +157 -0
  46. package/src/components/StatusCard.tsx +78 -0
  47. package/src/components/StatusUpdateTimeline.tsx +222 -0
  48. package/src/components/StrategyConfigCard.tsx +333 -0
  49. package/src/components/SubscribeButton.tsx +96 -0
  50. package/src/components/Table.tsx +119 -0
  51. package/src/components/Tabs.tsx +141 -0
  52. package/src/components/TemplateEditor.test.ts +156 -0
  53. package/src/components/TemplateEditor.tsx +435 -0
  54. package/src/components/TerminalFeed.tsx +152 -0
  55. package/src/components/Textarea.tsx +22 -0
  56. package/src/components/ThemeProvider.tsx +76 -0
  57. package/src/components/Toast.tsx +118 -0
  58. package/src/components/ToastProvider.tsx +126 -0
  59. package/src/components/Toggle.tsx +47 -0
  60. package/src/components/Tooltip.tsx +20 -0
  61. package/src/components/UserMenu.tsx +79 -0
  62. package/src/hooks/usePagination.e2e.ts +275 -0
  63. package/src/hooks/usePagination.ts +231 -0
  64. package/src/index.ts +53 -0
  65. package/src/themes.css +204 -0
  66. package/src/utils/strip-markdown.ts +44 -0
  67. package/src/utils.ts +8 -0
  68. package/tsconfig.json +6 -0
@@ -0,0 +1,134 @@
1
+ import * as React from "react";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "../utils";
5
+
6
+ const Dialog = DialogPrimitive.Root;
7
+
8
+ const DialogTrigger = DialogPrimitive.Trigger;
9
+
10
+ const DialogPortal = DialogPrimitive.Portal;
11
+
12
+ const DialogClose = DialogPrimitive.Close;
13
+
14
+ const DialogOverlay = React.forwardRef<
15
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
16
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
17
+ >(({ className, ...props }, ref) => (
18
+ <DialogPrimitive.Overlay
19
+ ref={ref}
20
+ className={cn(
21
+ "fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
+ className
23
+ )}
24
+ {...props}
25
+ />
26
+ ));
27
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
28
+
29
+ const dialogContentVariants = cva(
30
+ "fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background text-foreground p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg max-h-[85vh] overflow-y-auto",
31
+ {
32
+ variants: {
33
+ size: {
34
+ sm: "max-w-sm",
35
+ default: "max-w-lg",
36
+ lg: "max-w-2xl",
37
+ xl: "max-w-4xl",
38
+ full: "max-w-[90vw]",
39
+ },
40
+ },
41
+ defaultVariants: {
42
+ size: "default",
43
+ },
44
+ }
45
+ );
46
+
47
+ interface DialogContentProps
48
+ extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
49
+ VariantProps<typeof dialogContentVariants> {}
50
+
51
+ const DialogContent = React.forwardRef<
52
+ React.ElementRef<typeof DialogPrimitive.Content>,
53
+ DialogContentProps
54
+ >(({ className, children, size, ...props }, ref) => (
55
+ <DialogPortal>
56
+ <DialogOverlay />
57
+ <DialogPrimitive.Content
58
+ ref={ref}
59
+ className={cn(dialogContentVariants({ size }), className)}
60
+ {...props}
61
+ >
62
+ {children}
63
+ </DialogPrimitive.Content>
64
+ </DialogPortal>
65
+ ));
66
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
67
+
68
+ const DialogHeader = ({
69
+ className,
70
+ ...props
71
+ }: React.HTMLAttributes<HTMLDivElement>) => (
72
+ <div
73
+ className={cn(
74
+ "flex flex-col space-y-1.5 text-center sm:text-left",
75
+ className
76
+ )}
77
+ {...props}
78
+ />
79
+ );
80
+ DialogHeader.displayName = "DialogHeader";
81
+
82
+ const DialogFooter = ({
83
+ className,
84
+ ...props
85
+ }: React.HTMLAttributes<HTMLDivElement>) => (
86
+ <div
87
+ className={cn(
88
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
89
+ className
90
+ )}
91
+ {...props}
92
+ />
93
+ );
94
+ DialogFooter.displayName = "DialogFooter";
95
+
96
+ const DialogTitle = React.forwardRef<
97
+ React.ElementRef<typeof DialogPrimitive.Title>,
98
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
99
+ >(({ className, ...props }, ref) => (
100
+ <DialogPrimitive.Title
101
+ ref={ref}
102
+ className={cn(
103
+ "text-lg font-semibold leading-none tracking-tight",
104
+ className
105
+ )}
106
+ {...props}
107
+ />
108
+ ));
109
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
110
+
111
+ const DialogDescription = React.forwardRef<
112
+ React.ElementRef<typeof DialogPrimitive.Description>,
113
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
114
+ >(({ className, ...props }, ref) => (
115
+ <DialogPrimitive.Description
116
+ ref={ref}
117
+ className={cn("text-sm text-muted-foreground", className)}
118
+ {...props}
119
+ />
120
+ ));
121
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
122
+
123
+ export {
124
+ Dialog,
125
+ DialogPortal,
126
+ DialogOverlay,
127
+ DialogClose,
128
+ DialogTrigger,
129
+ DialogContent,
130
+ DialogHeader,
131
+ DialogFooter,
132
+ DialogTitle,
133
+ DialogDescription,
134
+ };
@@ -0,0 +1,96 @@
1
+ import React, { useRef, useEffect } from "react";
2
+ import { cn } from "../utils";
3
+
4
+ export const DropdownMenu: React.FC<{ children: React.ReactNode }> = ({
5
+ children,
6
+ }) => {
7
+ return <div className="relative inline-block text-left">{children}</div>;
8
+ };
9
+
10
+ export const DropdownMenuTrigger: React.FC<{
11
+ children: React.ReactNode;
12
+ asChild?: boolean;
13
+ onClick?: () => void;
14
+ }> = ({ children, onClick }) => {
15
+ return (
16
+ <div onClick={onClick} className="cursor-pointer">
17
+ {children}
18
+ </div>
19
+ );
20
+ };
21
+
22
+ export const DropdownMenuContent: React.FC<{
23
+ children: React.ReactNode;
24
+ isOpen: boolean;
25
+ onClose: () => void;
26
+ className?: string;
27
+ }> = ({ children, isOpen, onClose, className }) => {
28
+ const contentRef = useRef<HTMLDivElement>(null);
29
+
30
+ useEffect(() => {
31
+ const handleClickOutside = (event: MouseEvent) => {
32
+ if (
33
+ contentRef.current &&
34
+ !contentRef.current.contains(event.target as Node)
35
+ ) {
36
+ onClose();
37
+ }
38
+ };
39
+
40
+ if (isOpen) {
41
+ document.addEventListener("mousedown", handleClickOutside);
42
+ }
43
+ return () => {
44
+ document.removeEventListener("mousedown", handleClickOutside);
45
+ };
46
+ }, [isOpen, onClose]);
47
+
48
+ if (!isOpen) return <React.Fragment />;
49
+
50
+ return (
51
+ <div
52
+ ref={contentRef}
53
+ className={cn(
54
+ "absolute right-0 mt-2 w-56 origin-top-right rounded-md bg-popover shadow-lg ring-1 ring-border focus:outline-none z-[100] animate-in fade-in zoom-in-95 duration-100",
55
+ className
56
+ )}
57
+ >
58
+ <div className="py-1" role="none">
59
+ {children}
60
+ </div>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ export const DropdownMenuItem: React.FC<{
66
+ children: React.ReactNode;
67
+ onClick?: () => void;
68
+ className?: string;
69
+ icon?: React.ReactNode;
70
+ }> = ({ children, onClick, className, icon }) => {
71
+ return (
72
+ <button
73
+ onClick={onClick}
74
+ className={cn(
75
+ "flex items-center w-full px-4 py-2 text-sm text-popover-foreground hover:bg-accent hover:text-accent-foreground transition-colors",
76
+ className
77
+ )}
78
+ role="menuitem"
79
+ >
80
+ {icon && <span className="mr-3 text-muted-foreground">{icon}</span>}
81
+ {children}
82
+ </button>
83
+ );
84
+ };
85
+
86
+ export const DropdownMenuSeparator: React.FC = () => (
87
+ <div className="my-1 h-px bg-border" />
88
+ );
89
+
90
+ export const DropdownMenuLabel: React.FC<{ children: React.ReactNode }> = ({
91
+ children,
92
+ }) => (
93
+ <div className="px-4 py-2 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
94
+ {children}
95
+ </div>
96
+ );
@@ -0,0 +1,126 @@
1
+ import React from "react";
2
+
3
+ import { EmptyState } from "../../index";
4
+
5
+ import type { DynamicFormProps, JsonSchemaProperty } from "./types";
6
+ import { extractDefaults } from "./utils";
7
+ import { FormField } from "./FormField";
8
+
9
+ /**
10
+ * Check if a value is considered "empty" for validation purposes.
11
+ */
12
+ function isValueEmpty(val: unknown, propSchema: JsonSchemaProperty): boolean {
13
+ if (val === undefined || val === null) return true;
14
+ if (typeof val === "string" && val.trim() === "") return true;
15
+ // For arrays, check if empty
16
+ if (Array.isArray(val) && val.length === 0) return true;
17
+ // For objects (nested schemas), recursively check required fields
18
+ if (propSchema.type === "object" && propSchema.properties) {
19
+ const objVal = val as Record<string, unknown>;
20
+ const requiredKeys = propSchema.required ?? [];
21
+ for (const key of requiredKeys) {
22
+ const nestedPropSchema = propSchema.properties[key];
23
+ if (nestedPropSchema && isValueEmpty(objVal[key], nestedPropSchema)) {
24
+ return true;
25
+ }
26
+ }
27
+ }
28
+ return false;
29
+ }
30
+
31
+ /**
32
+ * DynamicForm generates a form from a JSON Schema.
33
+ * Supports all standard JSON Schema types plus custom extensions for
34
+ * secrets, colors, templates, dynamic options, and discriminated unions.
35
+ */
36
+ export const DynamicForm: React.FC<DynamicFormProps> = ({
37
+ schema,
38
+ value,
39
+ onChange,
40
+ onValidChange,
41
+ optionsResolvers,
42
+ templateProperties,
43
+ }) => {
44
+ // Track previous validity to avoid redundant callbacks
45
+ const prevValidRef = React.useRef<boolean | undefined>(undefined);
46
+
47
+ // Initialize form with default values from schema
48
+ React.useEffect(() => {
49
+ if (!schema || !schema.properties) return;
50
+
51
+ const defaults = extractDefaults(schema);
52
+ const merged = { ...defaults, ...value };
53
+
54
+ // Only update if there are new defaults to apply
55
+ if (JSON.stringify(merged) !== JSON.stringify(value)) {
56
+ onChange(merged);
57
+ }
58
+ }, [schema]); // Only run when schema changes
59
+
60
+ // Compute validity and report changes
61
+ React.useEffect(() => {
62
+ if (!onValidChange || !schema || !schema.properties) return;
63
+
64
+ // Check all required fields (including hidden ones like connectionId)
65
+ const requiredKeys = schema.required ?? [];
66
+ let isValid = true;
67
+
68
+ for (const key of requiredKeys) {
69
+ const propSchema = schema.properties[key];
70
+ if (!propSchema) continue;
71
+
72
+ // Skip hidden fields - they are auto-populated
73
+ if (propSchema["x-hidden"]) continue;
74
+
75
+ if (isValueEmpty(value[key], propSchema)) {
76
+ isValid = false;
77
+ break;
78
+ }
79
+ }
80
+
81
+ // Only call onValidChange if validity actually changed
82
+ if (prevValidRef.current !== isValid) {
83
+ prevValidRef.current = isValid;
84
+ onValidChange(isValid);
85
+ }
86
+ }, [schema, value, onValidChange]);
87
+
88
+ if (
89
+ !schema ||
90
+ !schema.properties ||
91
+ Object.keys(schema.properties).length === 0
92
+ ) {
93
+ return (
94
+ <EmptyState
95
+ title="No Configuration Required"
96
+ description="This component doesn't require any configuration."
97
+ />
98
+ );
99
+ }
100
+
101
+ return (
102
+ <div className="space-y-6">
103
+ {Object.entries(schema.properties)
104
+ .filter(([, propSchema]) => !propSchema["x-hidden"])
105
+ .map(([key, propSchema]) => {
106
+ const isRequired = schema.required?.includes(key);
107
+ const label = key.charAt(0).toUpperCase() + key.slice(1);
108
+
109
+ return (
110
+ <FormField
111
+ key={key}
112
+ id={key}
113
+ label={label}
114
+ propSchema={propSchema}
115
+ value={value[key]}
116
+ isRequired={isRequired}
117
+ formValues={value}
118
+ optionsResolvers={optionsResolvers}
119
+ templateProperties={templateProperties}
120
+ onChange={(val) => onChange({ ...value, [key]: val })}
121
+ />
122
+ );
123
+ })}
124
+ </div>
125
+ );
126
+ };
@@ -0,0 +1,220 @@
1
+ import React from "react";
2
+ import { Loader2, ChevronDown } from "lucide-react";
3
+
4
+ import {
5
+ Input,
6
+ Label,
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "../../index";
13
+
14
+ import type { DynamicOptionsFieldProps, ResolverOption } from "./types";
15
+ import { getCleanDescription } from "./utils";
16
+
17
+ /**
18
+ * Field component for dynamically resolved options.
19
+ * Fetches options using the specified resolver and renders a Select.
20
+ * When searchable is true, shows a searchable dropdown with filter inside.
21
+ */
22
+ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
23
+ id,
24
+ label,
25
+ description,
26
+ value,
27
+ isRequired,
28
+ resolverName,
29
+ dependsOn,
30
+ searchable,
31
+ formValues,
32
+ optionsResolvers,
33
+ onChange,
34
+ }) => {
35
+ const [options, setOptions] = React.useState<ResolverOption[]>([]);
36
+ const [loading, setLoading] = React.useState(true);
37
+ const [error, setError] = React.useState<string | undefined>();
38
+ const [searchQuery, setSearchQuery] = React.useState("");
39
+ const [open, setOpen] = React.useState(false);
40
+
41
+ // Use ref to store formValues to avoid re-renders when unrelated fields change
42
+ const formValuesRef = React.useRef(formValues);
43
+ formValuesRef.current = formValues;
44
+
45
+ // Build dependency values string for useEffect dependency tracking
46
+ // Only includes the specific fields this resolver depends on
47
+ const dependencyValues = React.useMemo(() => {
48
+ if (!dependsOn || dependsOn.length === 0) return "";
49
+ return dependsOn.map((key) => JSON.stringify(formValues[key])).join("|");
50
+ }, [dependsOn, formValues]);
51
+
52
+ React.useEffect(() => {
53
+ const resolver = optionsResolvers[resolverName];
54
+ if (!resolver) {
55
+ setError(`Resolver "${resolverName}" not found`);
56
+ setLoading(false);
57
+ return;
58
+ }
59
+
60
+ let cancelled = false;
61
+ setLoading(true);
62
+ setError(undefined);
63
+
64
+ // Use ref to get current form values without adding to dependencies
65
+ resolver(formValuesRef.current)
66
+ .then((result) => {
67
+ if (!cancelled) {
68
+ setOptions(result);
69
+ setLoading(false);
70
+ }
71
+ })
72
+ .catch((error_) => {
73
+ if (!cancelled) {
74
+ setError(
75
+ error_ instanceof Error ? error_.message : "Failed to load options"
76
+ );
77
+ setLoading(false);
78
+ }
79
+ });
80
+
81
+ return () => {
82
+ cancelled = true;
83
+ };
84
+ // Only re-fetch when resolver changes or explicit dependencies change
85
+ }, [resolverName, optionsResolvers, dependencyValues]);
86
+
87
+ // Filter options based on search query
88
+ const filteredOptions = React.useMemo(() => {
89
+ if (!searchable || !searchQuery.trim()) return options;
90
+ const query = searchQuery.toLowerCase();
91
+ return options.filter((opt) => opt.label.toLowerCase().includes(query));
92
+ }, [options, searchQuery, searchable]);
93
+
94
+ // Get the selected option label
95
+ const selectedLabel = React.useMemo(() => {
96
+ const selected = options.find((opt) => opt.value === value);
97
+ return selected?.label;
98
+ }, [options, value]);
99
+
100
+ const cleanDesc = getCleanDescription(description);
101
+
102
+ // Render searchable dropdown with search inside
103
+ if (searchable && !loading && !error && options.length > 0) {
104
+ return (
105
+ <div className="space-y-2">
106
+ <div>
107
+ <Label htmlFor={id}>
108
+ {label} {isRequired && "*"}
109
+ </Label>
110
+ {cleanDesc && (
111
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
112
+ )}
113
+ </div>
114
+ <div className="relative">
115
+ <button
116
+ type="button"
117
+ onClick={() => setOpen(!open)}
118
+ className="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm text-left ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
119
+ >
120
+ <span className={selectedLabel ? "" : "text-muted-foreground"}>
121
+ {selectedLabel || `Select ${label}`}
122
+ </span>
123
+ <ChevronDown className="h-4 w-4 opacity-50" />
124
+ </button>
125
+ {open && (
126
+ <div className="absolute z-[100] mt-1 w-full rounded-md border border-border bg-popover shadow-lg">
127
+ <div className="p-2 border-b border-border">
128
+ <Input
129
+ type="text"
130
+ placeholder="Search..."
131
+ value={searchQuery}
132
+ onChange={(e) => setSearchQuery(e.target.value)}
133
+ className="h-8"
134
+ autoFocus
135
+ />
136
+ </div>
137
+ <div className="max-h-60 overflow-y-auto p-1">
138
+ {filteredOptions.length === 0 ? (
139
+ <div className="py-2 px-3 text-sm text-muted-foreground text-center">
140
+ No matching options
141
+ </div>
142
+ ) : (
143
+ filteredOptions.map((opt) => (
144
+ <button
145
+ key={opt.value}
146
+ type="button"
147
+ onClick={() => {
148
+ onChange(opt.value);
149
+ setOpen(false);
150
+ setSearchQuery("");
151
+ }}
152
+ className={`w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground ${
153
+ opt.value === value
154
+ ? "bg-accent text-accent-foreground"
155
+ : ""
156
+ }`}
157
+ >
158
+ {opt.label}
159
+ </button>
160
+ ))
161
+ )}
162
+ </div>
163
+ </div>
164
+ )}
165
+ </div>
166
+ </div>
167
+ );
168
+ }
169
+
170
+ // Regular dropdown
171
+ return (
172
+ <div className="space-y-2">
173
+ <div>
174
+ <Label htmlFor={id}>
175
+ {label} {isRequired && "*"}
176
+ </Label>
177
+ {cleanDesc && (
178
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
179
+ )}
180
+ </div>
181
+ <div className="relative">
182
+ {loading ? (
183
+ <div className="flex items-center gap-2 h-10 px-3 border rounded-md bg-muted/50">
184
+ <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
185
+ <span className="text-sm text-muted-foreground">
186
+ Loading options...
187
+ </span>
188
+ </div>
189
+ ) : error ? (
190
+ <div className="flex items-center h-10 px-3 border border-destructive rounded-md bg-destructive/10">
191
+ <span className="text-sm text-destructive">{error}</span>
192
+ </div>
193
+ ) : (
194
+ <Select
195
+ value={(value as string) || ""}
196
+ onValueChange={(val) => onChange(val)}
197
+ disabled={options.length === 0}
198
+ >
199
+ <SelectTrigger id={id}>
200
+ <SelectValue
201
+ placeholder={
202
+ options.length === 0
203
+ ? "No options available"
204
+ : `Select ${label}`
205
+ }
206
+ />
207
+ </SelectTrigger>
208
+ <SelectContent>
209
+ {options.map((opt) => (
210
+ <SelectItem key={opt.value} value={opt.value}>
211
+ {opt.label}
212
+ </SelectItem>
213
+ ))}
214
+ </SelectContent>
215
+ </Select>
216
+ )}
217
+ </div>
218
+ </div>
219
+ );
220
+ };