@sero-ai/ui 0.1.0 → 0.2.1

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 (62) hide show
  1. package/.turbo/turbo-typecheck.log +1 -1
  2. package/LICENSE +201 -0
  3. package/package.json +22 -18
  4. package/src/components/ai-elements/attachments.tsx +4 -4
  5. package/src/components/ai-elements/audio-player.tsx +1 -1
  6. package/src/components/ai-elements/chain-of-thought.tsx +2 -2
  7. package/src/components/ai-elements/confirmation.tsx +2 -2
  8. package/src/components/ai-elements/context.tsx +2 -2
  9. package/src/components/ai-elements/environment-variables.tsx +6 -6
  10. package/src/components/ai-elements/file-tree.tsx +3 -3
  11. package/src/components/ai-elements/inline-citation.tsx +2 -2
  12. package/src/components/ai-elements/jsx-preview.tsx +2 -2
  13. package/src/components/ai-elements/message.tsx +2 -2
  14. package/src/components/ai-elements/mic-selector.tsx +8 -8
  15. package/src/components/ai-elements/open-in-chat.tsx +2 -2
  16. package/src/components/ai-elements/package-info.tsx +4 -4
  17. package/src/components/ai-elements/plan.tsx +2 -2
  18. package/src/components/ai-elements/prompt-input-context.tsx +252 -0
  19. package/src/components/ai-elements/prompt-input-elements.tsx +371 -0
  20. package/src/components/ai-elements/prompt-input-textarea.tsx +117 -0
  21. package/src/components/ai-elements/prompt-input.tsx +59 -990
  22. package/src/components/ai-elements/queue.tsx +1 -1
  23. package/src/components/ai-elements/reasoning.tsx +3 -3
  24. package/src/components/ai-elements/schema-display.tsx +7 -7
  25. package/src/components/ai-elements/snippet.tsx +3 -3
  26. package/src/components/ai-elements/sources.tsx +2 -2
  27. package/src/components/ai-elements/terminal.tsx +5 -5
  28. package/src/components/ai-elements/test-results.tsx +8 -8
  29. package/src/components/ai-elements/transcription.tsx +2 -2
  30. package/src/components/ai-elements/voice-selector-accent.tsx +50 -0
  31. package/src/components/ai-elements/voice-selector.tsx +8 -177
  32. package/src/components/ai-elements/web-preview.tsx +5 -6
  33. package/src/components/model-selection/available-model-picker.tsx +231 -0
  34. package/src/components/model-selection/model-warning-list.tsx +33 -0
  35. package/src/components/model-selection/thinking-level-picker.tsx +55 -0
  36. package/src/components/ui/alert-dialog.tsx +1 -1
  37. package/src/components/ui/calendar.tsx +1 -1
  38. package/src/components/ui/carousel.tsx +1 -1
  39. package/src/components/ui/chart.tsx +3 -3
  40. package/src/components/ui/command.tsx +2 -2
  41. package/src/components/ui/dialog.tsx +1 -1
  42. package/src/components/ui/field.tsx +2 -2
  43. package/src/components/ui/form.tsx +2 -2
  44. package/src/components/ui/input-otp.tsx +2 -2
  45. package/src/components/ui/input.tsx +3 -1
  46. package/src/components/ui/menubar.tsx +1 -1
  47. package/src/components/ui/native-select.tsx +2 -0
  48. package/src/components/ui/navigation-menu.tsx +1 -1
  49. package/src/components/ui/plugin-safety-disclaimer.tsx +25 -0
  50. package/src/components/ui/progress.tsx +1 -1
  51. package/src/components/ui/resizable.tsx +1 -1
  52. package/src/components/ui/search-input.tsx +39 -0
  53. package/src/components/ui/slider.tsx +7 -2
  54. package/src/components/ui/textarea.tsx +3 -1
  55. package/src/components/ui/toggle-group.tsx +1 -1
  56. package/src/components/ui/tree.tsx +3 -3
  57. package/src/index.ts +128 -7
  58. package/src/styles/globals.css +91 -26
  59. package/src/styles/plugin.css +4 -0
  60. package/src/theme/apply-theme.ts +250 -0
  61. package/src/theme/index.ts +2 -0
  62. package/src/theme/types.ts +253 -0
@@ -0,0 +1,231 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import { Check, ChevronDown, Sparkles, X } from 'lucide-react';
3
+ import {
4
+ filterModelGroups,
5
+ findGroup,
6
+ findModel,
7
+ modelKey,
8
+ parseModelKey,
9
+ type SharedAvailableModelGroup,
10
+ type SharedModelInfo,
11
+ } from '@sero-ai/common';
12
+ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
13
+ import { SearchInput } from '../ui/search-input';
14
+ import { cn } from '../../lib/utils';
15
+
16
+ interface AvailableModelPickerProps<
17
+ TModel extends SharedModelInfo,
18
+ TGroup extends SharedAvailableModelGroup<TModel>,
19
+ > {
20
+ groups: TGroup[];
21
+ value: string;
22
+ onChange: (value: string) => void;
23
+ placeholder?: string;
24
+ searchPlaceholder?: string;
25
+ emptyLabel?: string;
26
+ noModelsLabel?: string;
27
+ allowClear?: boolean;
28
+ disabled?: boolean;
29
+ className?: string;
30
+ }
31
+
32
+ export function AvailableModelPicker<
33
+ TModel extends SharedModelInfo,
34
+ TGroup extends SharedAvailableModelGroup<TModel>,
35
+ >({
36
+ groups,
37
+ value,
38
+ onChange,
39
+ placeholder = 'Choose a model',
40
+ searchPlaceholder = 'Search models...',
41
+ emptyLabel = 'No matching models',
42
+ noModelsLabel = 'No models available',
43
+ allowClear = false,
44
+ disabled = false,
45
+ className,
46
+ }: AvailableModelPickerProps<TModel, TGroup>) {
47
+ const [open, setOpen] = useState(false);
48
+ const [query, setQuery] = useState('');
49
+ const inputRef = useRef<HTMLInputElement>(null);
50
+
51
+ const selected = useMemo(() => {
52
+ const parsed = parseModelKey(value);
53
+ if (!parsed) {
54
+ return value
55
+ ? { group: null, model: null, fallbackLabel: value }
56
+ : { group: null, model: null, fallbackLabel: null };
57
+ }
58
+
59
+ const group = findGroup(groups, parsed.provider, parsed.modelId) ?? null;
60
+ const model = findModel(groups, parsed.provider, parsed.modelId) ?? null;
61
+ return {
62
+ group,
63
+ model,
64
+ fallbackLabel: model ? null : value,
65
+ };
66
+ }, [groups, value]);
67
+
68
+ const filteredGroups = useMemo(
69
+ () => filterModelGroups(groups, query),
70
+ [groups, query],
71
+ );
72
+
73
+ const totalResults = useMemo(
74
+ () => filteredGroups.reduce((count, group) => count + group.models.length, 0),
75
+ [filteredGroups],
76
+ );
77
+
78
+ const handleOpenChange = useCallback((nextOpen: boolean) => {
79
+ setOpen(nextOpen);
80
+ if (!nextOpen) return;
81
+ setQuery('');
82
+ requestAnimationFrame(() => inputRef.current?.focus());
83
+ }, []);
84
+
85
+ const handleSelect = useCallback((provider: string, modelId: string) => {
86
+ onChange(modelKey(provider, modelId));
87
+ setOpen(false);
88
+ setQuery('');
89
+ }, [onChange]);
90
+
91
+ const handleClear = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
92
+ event.stopPropagation();
93
+ onChange('');
94
+ setOpen(false);
95
+ setQuery('');
96
+ }, [onChange]);
97
+
98
+ return (
99
+ <Popover open={open} onOpenChange={handleOpenChange}>
100
+ <PopoverTrigger asChild disabled={disabled}>
101
+ <div
102
+ role="button"
103
+ tabIndex={disabled ? -1 : 0}
104
+ className={cn(
105
+ 'flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-left text-sm transition-colors',
106
+ 'hover:bg-secondary/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
107
+ disabled && 'cursor-not-allowed opacity-50',
108
+ className,
109
+ )}
110
+ >
111
+ {selected.group && selected.model ? (
112
+ <>
113
+ <img
114
+ src={selected.group.logo}
115
+ alt={selected.group.displayName}
116
+ className="size-4 shrink-0 rounded-sm dark:invert"
117
+ />
118
+ <div className="min-w-0 flex-1">
119
+ <div className="flex items-center gap-1.5">
120
+ <span className="truncate font-medium text-foreground">{selected.model.name}</span>
121
+ {selected.model.reasoning ? (
122
+ <Sparkles className="size-3 shrink-0 text-amber-500/70" />
123
+ ) : null}
124
+ </div>
125
+ <div className="truncate text-[11px] text-muted-foreground">
126
+ {selected.group.displayName}
127
+ </div>
128
+ </div>
129
+ </>
130
+ ) : selected.fallbackLabel ? (
131
+ <span className="min-w-0 flex-1 truncate font-mono text-xs text-muted-foreground">
132
+ {selected.fallbackLabel}
133
+ </span>
134
+ ) : (
135
+ <span className="min-w-0 flex-1 truncate text-muted-foreground">
136
+ {placeholder}
137
+ </span>
138
+ )}
139
+
140
+ {allowClear && value ? (
141
+ <button
142
+ type="button"
143
+ onClick={handleClear}
144
+ className="rounded p-1 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
145
+ title="Clear selection"
146
+ >
147
+ <X className="size-3.5" />
148
+ </button>
149
+ ) : null}
150
+
151
+ <ChevronDown className="size-4 shrink-0 text-muted-foreground" />
152
+ </div>
153
+ </PopoverTrigger>
154
+
155
+ <PopoverContent
156
+ side="bottom"
157
+ align="start"
158
+ sideOffset={4}
159
+ className="w-[var(--radix-popover-trigger-width)] overflow-hidden rounded-xl border-border/60 bg-background p-0 shadow-xl"
160
+ onWheel={(event) => event.stopPropagation()}
161
+ >
162
+ <SearchInput
163
+ ref={inputRef}
164
+ value={query}
165
+ onChange={(event) => setQuery(event.target.value)}
166
+ placeholder={searchPlaceholder}
167
+ containerClassName="border-b border-border/40"
168
+ />
169
+
170
+ <div className="max-h-[280px] overflow-y-auto py-1">
171
+ {groups.length === 0 ? (
172
+ <div className="px-3 py-4 text-center text-xs text-muted-foreground">
173
+ {noModelsLabel}
174
+ </div>
175
+ ) : totalResults === 0 ? (
176
+ <div className="px-3 py-4 text-center text-xs text-muted-foreground">
177
+ {emptyLabel}
178
+ </div>
179
+ ) : (
180
+ filteredGroups.map((group, index) => (
181
+ <div key={group.provider}>
182
+ {index > 0 ? <div className="mx-3 border-t border-border/30" /> : null}
183
+ <div className="flex items-center gap-2 px-3 pb-1 pt-2">
184
+ <img
185
+ src={group.logo}
186
+ alt={group.displayName}
187
+ className="size-3.5 shrink-0 rounded-sm dark:invert"
188
+ />
189
+ <span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
190
+ {group.displayName}
191
+ </span>
192
+ </div>
193
+ <div className="px-1">
194
+ {group.models.map((model) => {
195
+ const nextValue = modelKey(model.provider, model.modelId);
196
+ const isSelected = nextValue === value;
197
+ return (
198
+ <button
199
+ key={nextValue}
200
+ type="button"
201
+ onClick={() => handleSelect(model.provider, model.modelId)}
202
+ className={cn(
203
+ 'flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left text-xs transition-colors',
204
+ isSelected
205
+ ? 'bg-secondary text-foreground'
206
+ : 'text-muted-foreground hover:bg-secondary/60 hover:text-foreground',
207
+ )}
208
+ >
209
+ <div className="flex size-4 shrink-0 items-center justify-center">
210
+ {isSelected ? (
211
+ <Check className="size-3.5 text-emerald-500" />
212
+ ) : (
213
+ <div className="size-1.5 rounded-full bg-border" />
214
+ )}
215
+ </div>
216
+ <span className="min-w-0 flex-1 truncate font-medium">{model.name}</span>
217
+ {model.reasoning ? (
218
+ <Sparkles className="size-3 shrink-0 text-amber-500/60" />
219
+ ) : null}
220
+ </button>
221
+ );
222
+ })}
223
+ </div>
224
+ </div>
225
+ ))
226
+ )}
227
+ </div>
228
+ </PopoverContent>
229
+ </Popover>
230
+ );
231
+ }
@@ -0,0 +1,33 @@
1
+ import { TriangleAlert } from 'lucide-react';
2
+ import { formatModelValidationWarning, type ModelValidationWarning } from '@sero-ai/common';
3
+ import { cn } from '../../lib/utils';
4
+
5
+ interface ModelWarningListProps {
6
+ warnings: ModelValidationWarning[];
7
+ className?: string;
8
+ }
9
+
10
+ export function ModelWarningList({ warnings, className }: ModelWarningListProps) {
11
+ if (warnings.length === 0) return null;
12
+
13
+ return (
14
+ <div className={cn('space-y-2', className)}>
15
+ {warnings.map((warning, index) => (
16
+ <div
17
+ key={`${warning.code}:${index}`}
18
+ className={cn(
19
+ 'rounded-lg border px-3 py-2 text-xs',
20
+ warning.severity === 'info'
21
+ ? 'border-primary/20 bg-primary/5 text-primary'
22
+ : 'border-amber-500/20 bg-amber-500/5 text-amber-700 dark:text-amber-300',
23
+ )}
24
+ >
25
+ <div className="flex items-start gap-2">
26
+ <TriangleAlert className="mt-0.5 size-3.5 shrink-0" />
27
+ <span>{formatModelValidationWarning(warning)}</span>
28
+ </div>
29
+ </div>
30
+ ))}
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ THINKING_LABELS,
3
+ THINKING_LEVELS,
4
+ type ThinkingLevel,
5
+ } from '@sero-ai/common';
6
+ import { cn } from '../../lib/utils';
7
+
8
+ interface ThinkingLevelPickerProps {
9
+ value: string;
10
+ onChange: (value: ThinkingLevel) => void;
11
+ availableLevels?: readonly string[];
12
+ disabled?: boolean;
13
+ showUnsupported?: boolean;
14
+ className?: string;
15
+ }
16
+
17
+ export function ThinkingLevelPicker({
18
+ value,
19
+ onChange,
20
+ availableLevels,
21
+ disabled = false,
22
+ showUnsupported = true,
23
+ className,
24
+ }: ThinkingLevelPickerProps) {
25
+ const allowed = new Set<string>(availableLevels ?? THINKING_LEVELS);
26
+ const levels = showUnsupported || !availableLevels
27
+ ? THINKING_LEVELS
28
+ : THINKING_LEVELS.filter((level) => allowed.has(level));
29
+
30
+ return (
31
+ <div className={cn('grid grid-cols-6 gap-1 rounded-lg border border-border/50 bg-muted/20 p-1', className)}>
32
+ {levels.map((level) => {
33
+ const isActive = value === level;
34
+ const isAvailable = allowed.has(level) || !availableLevels;
35
+ return (
36
+ <button
37
+ key={level}
38
+ type="button"
39
+ disabled={disabled || !isAvailable}
40
+ onClick={() => onChange(level)}
41
+ className={cn(
42
+ 'rounded-md px-2 py-1.5 text-[11px] font-medium transition-colors',
43
+ isActive
44
+ ? 'bg-background text-foreground shadow-sm'
45
+ : 'text-muted-foreground hover:bg-background/70 hover:text-foreground',
46
+ (!isAvailable || disabled) && 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground',
47
+ )}
48
+ >
49
+ {THINKING_LABELS[level]}
50
+ </button>
51
+ );
52
+ })}
53
+ </div>
54
+ );
55
+ }
@@ -58,7 +58,7 @@ function AlertDialogContent({
58
58
  data-slot="alert-dialog-content"
59
59
  data-size={size}
60
60
  className={cn(
61
- "bg-background 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 group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg",
61
+ "bg-background 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 group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg",
62
62
  className
63
63
  )}
64
64
  {...props}
@@ -102,7 +102,7 @@ function Calendar({
102
102
  defaultClassNames.week_number
103
103
  ),
104
104
  day: cn(
105
- "relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
105
+ "relative size-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
106
106
  props.showWeekNumber
107
107
  ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
108
108
  : "[&:first-child[data-selected=true]_button]:rounded-l-md",
@@ -31,7 +31,7 @@ type CarouselContextProps = {
31
31
  const CarouselContext = React.createContext<CarouselContextProps | null>(null)
32
32
 
33
33
  function useCarousel() {
34
- const context = React.useContext(CarouselContext)
34
+ const context = React.use(CarouselContext)
35
35
 
36
36
  if (!context) {
37
37
  throw new Error("useCarousel must be used within a <Carousel />")
@@ -23,7 +23,7 @@ type ChartContextProps = {
23
23
  const ChartContext = React.createContext<ChartContextProps | null>(null)
24
24
 
25
25
  function useChart() {
26
- const context = React.useContext(ChartContext)
26
+ const context = React.use(ChartContext)
27
27
 
28
28
  if (!context) {
29
29
  throw new Error("useChart must be used within a <ChartContainer />")
@@ -204,7 +204,7 @@ function ChartTooltipContent({
204
204
  className={cn(
205
205
  "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
206
206
  {
207
- "h-2.5 w-2.5": indicator === "dot",
207
+ "size-2.5": indicator === "dot",
208
208
  "w-1": indicator === "line",
209
209
  "w-0 border-[1.5px] border-dashed bg-transparent":
210
210
  indicator === "dashed",
@@ -292,7 +292,7 @@ function ChartLegendContent({
292
292
  <itemConfig.icon />
293
293
  ) : (
294
294
  <div
295
- className="h-2 w-2 shrink-0 rounded-[2px]"
295
+ className="size-2 shrink-0 rounded-[2px]"
296
296
  style={{
297
297
  backgroundColor: item.color,
298
298
  }}
@@ -21,7 +21,7 @@ function Command({
21
21
  <CommandPrimitive
22
22
  data-slot="command"
23
23
  className={cn(
24
- "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
24
+ "bg-popover text-popover-foreground flex size-full flex-col overflow-hidden rounded-md",
25
25
  className
26
26
  )}
27
27
  {...props}
@@ -73,7 +73,7 @@ function CommandInput({
73
73
  <CommandPrimitive.Input
74
74
  data-slot="command-input"
75
75
  className={cn(
76
- "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
76
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent pl-1.5 py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
77
77
  className
78
78
  )}
79
79
  {...props}
@@ -61,7 +61,7 @@ function DialogContent({
61
61
  <DialogPrimitive.Content
62
62
  data-slot="dialog-content"
63
63
  className={cn(
64
- "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
64
+ "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
65
65
  className
66
66
  )}
67
67
  {...props}
@@ -211,8 +211,8 @@ function FieldError({
211
211
  return (
212
212
  <ul className="ml-4 flex list-disc flex-col gap-1">
213
213
  {uniqueErrors.map(
214
- (error, index) =>
215
- error?.message && <li key={index}>{error.message}</li>
214
+ (error) =>
215
+ error?.message && <li key={error.message}>{error.message}</li>
216
216
  )}
217
217
  </ul>
218
218
  )
@@ -41,8 +41,8 @@ const FormField = <
41
41
  }
42
42
 
43
43
  const useFormField = () => {
44
- const fieldContext = React.useContext(FormFieldContext)
45
- const itemContext = React.useContext(FormItemContext)
44
+ const fieldContext = React.use(FormFieldContext)
45
+ const itemContext = React.use(FormItemContext)
46
46
  const { getFieldState } = useFormContext()
47
47
  const formState = useFormState({ name: fieldContext.name })
48
48
  const fieldState = getFieldState(fieldContext.name, formState)
@@ -43,7 +43,7 @@ function InputOTPSlot({
43
43
  }: React.ComponentProps<"div"> & {
44
44
  index: number
45
45
  }) {
46
- const inputOTPContext = React.useContext(OTPInputContext)
46
+ const inputOTPContext = React.use(OTPInputContext)
47
47
  const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
48
48
 
49
49
  return (
@@ -51,7 +51,7 @@ function InputOTPSlot({
51
51
  data-slot="input-otp-slot"
52
52
  data-active={isActive}
53
53
  className={cn(
54
- "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
54
+ "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex size-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
55
55
  className
56
56
  )}
57
57
  {...props}
@@ -2,9 +2,11 @@ import * as React from "react"
2
2
 
3
3
  import { cn } from "../../lib/utils"
4
4
 
5
- function Input({ className, type, ...props }: React.ComponentProps<"input">) {
5
+ function Input({ className, type, placeholder, "aria-label": ariaLabel, ...props }: React.ComponentProps<"input">) {
6
6
  return (
7
7
  <input
8
+ aria-label={ariaLabel ?? (typeof placeholder === "string" ? placeholder : undefined)}
9
+ placeholder={placeholder}
8
10
  type={type}
9
11
  data-slot="input"
10
12
  className={cn(
@@ -235,7 +235,7 @@ function MenubarSubTrigger({
235
235
  {...props}
236
236
  >
237
237
  {children}
238
- <ChevronRightIcon className="ml-auto h-4 w-4" />
238
+ <ChevronRightIcon className="ml-auto size-4" />
239
239
  </MenubarPrimitive.SubTrigger>
240
240
  )
241
241
  }
@@ -6,6 +6,7 @@ import { cn } from "../../lib/utils"
6
6
  function NativeSelect({
7
7
  className,
8
8
  size = "default",
9
+ "aria-label": ariaLabel,
9
10
  ...props
10
11
  }: Omit<React.ComponentProps<"select">, "size"> & { size?: "sm" | "default" }) {
11
12
  return (
@@ -14,6 +15,7 @@ function NativeSelect({
14
15
  data-slot="native-select-wrapper"
15
16
  >
16
17
  <select
18
+ aria-label={ariaLabel}
17
19
  data-slot="native-select"
18
20
  data-size={size}
19
21
  className={cn(
@@ -150,7 +150,7 @@ function NavigationMenuIndicator({
150
150
  )}
151
151
  {...props}
152
152
  >
153
- <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
153
+ <div className="bg-border relative top-[60%] size-2 rotate-45 rounded-tl-sm shadow-md" />
154
154
  </NavigationMenuPrimitive.Indicator>
155
155
  )
156
156
  }
@@ -0,0 +1,25 @@
1
+ import { ShieldAlert } from 'lucide-react';
2
+ import { cn } from '../../lib/utils';
3
+
4
+ export interface PluginSafetyDisclaimerProps {
5
+ className?: string;
6
+ }
7
+
8
+ export function PluginSafetyDisclaimer({ className }: PluginSafetyDisclaimerProps) {
9
+ return (
10
+ <div
11
+ role="note"
12
+ className={cn(
13
+ 'flex shrink-0 items-start gap-2 border-t border-status-warning-border bg-status-warning-faint px-4 py-2.5 text-[11px] leading-5 text-[var(--text-secondary)]',
14
+ className,
15
+ )}
16
+ >
17
+ <ShieldAlert className="mt-0.5 size-3.5 shrink-0 text-status-warning" />
18
+ <p>
19
+ <span className="font-medium text-status-warning">Heads up:</span>{' '}
20
+ Plugins run with full access to this workspace and can execute code on your device.
21
+ Only install plugins from sources you trust.
22
+ </p>
23
+ </div>
24
+ );
25
+ }
@@ -21,7 +21,7 @@ function Progress({
21
21
  >
22
22
  <ProgressPrimitive.Indicator
23
23
  data-slot="progress-indicator"
24
- className="bg-primary h-full w-full flex-1 transition-all"
24
+ className="bg-primary size-full flex-1 transition-all"
25
25
  style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
26
26
  />
27
27
  </ProgressPrimitive.Root>
@@ -13,7 +13,7 @@ function ResizablePanelGroup({
13
13
  <ResizablePrimitive.Group
14
14
  data-slot="resizable-panel-group"
15
15
  className={cn(
16
- "flex h-full w-full aria-[orientation=vertical]:flex-col",
16
+ "flex size-full aria-[orientation=vertical]:flex-col",
17
17
  className
18
18
  )}
19
19
  {...props}
@@ -0,0 +1,39 @@
1
+ import * as React from "react"
2
+ import { SearchIcon } from "lucide-react"
3
+
4
+ import { cn } from "../../lib/utils"
5
+
6
+ interface SearchInputProps extends React.ComponentProps<"input"> {
7
+ containerClassName?: string
8
+ endAdornment?: React.ReactNode
9
+ iconClassName?: string
10
+ ref?: React.Ref<HTMLInputElement>
11
+ }
12
+
13
+ function SearchInput({ containerClassName, className, endAdornment, iconClassName, placeholder, "aria-label": ariaLabel, ref, ...props }: SearchInputProps) {
14
+ return (
15
+ <div
16
+ data-slot="search-input"
17
+ className={cn("flex items-center gap-2 px-3", containerClassName)}
18
+ >
19
+ <SearchIcon
20
+ data-slot="search-input-icon"
21
+ className={cn("text-muted-foreground size-3.5 shrink-0", iconClassName)}
22
+ />
23
+ <input
24
+ aria-label={ariaLabel ?? (typeof placeholder === "string" ? placeholder : "Search")}
25
+ placeholder={placeholder}
26
+ ref={ref}
27
+ data-slot="search-input-control"
28
+ className={cn(
29
+ "placeholder:text-muted-foreground h-9 w-full min-w-0 bg-transparent pl-1.5 text-xs text-foreground outline-none disabled:cursor-not-allowed disabled:opacity-50",
30
+ className
31
+ )}
32
+ {...props}
33
+ />
34
+ {endAdornment}
35
+ </div>
36
+ )
37
+ }
38
+
39
+ export { SearchInput }
@@ -20,6 +20,11 @@ function Slider({
20
20
  : [min, max],
21
21
  [value, defaultValue, min, max]
22
22
  )
23
+ const thumbKeysRef = React.useRef<string[]>([])
24
+ while (thumbKeysRef.current.length < _values.length) {
25
+ thumbKeysRef.current.push(`thumb-${thumbKeysRef.current.length}`)
26
+ }
27
+ const thumbKeys = thumbKeysRef.current.slice(0, _values.length)
23
28
 
24
29
  return (
25
30
  <SliderPrimitive.Root
@@ -47,10 +52,10 @@ function Slider({
47
52
  )}
48
53
  />
49
54
  </SliderPrimitive.Track>
50
- {Array.from({ length: _values.length }, (_, index) => (
55
+ {_values.map((_, index) => (
51
56
  <SliderPrimitive.Thumb
52
57
  data-slot="slider-thumb"
53
- key={index}
58
+ key={thumbKeys[index]}
54
59
  className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
55
60
  />
56
61
  ))}
@@ -2,9 +2,11 @@ import * as React from "react"
2
2
 
3
3
  import { cn } from "../../lib/utils"
4
4
 
5
- function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
5
+ function Textarea({ className, placeholder, "aria-label": ariaLabel, ...props }: React.ComponentProps<"textarea">) {
6
6
  return (
7
7
  <textarea
8
+ aria-label={ariaLabel ?? (typeof placeholder === "string" ? placeholder : undefined)}
9
+ placeholder={placeholder}
8
10
  data-slot="textarea"
9
11
  className={cn(
10
12
  "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
@@ -56,7 +56,7 @@ function ToggleGroupItem({
56
56
  ...props
57
57
  }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
58
58
  VariantProps<typeof toggleVariants>) {
59
- const context = React.useContext(ToggleGroupContext)
59
+ const context = React.use(ToggleGroupContext)
60
60
 
61
61
  return (
62
62
  <ToggleGroupPrimitive.Item