@sero-ai/ui 0.4.2 → 0.6.0

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 (91) hide show
  1. package/.turbo/turbo-build.log +636 -628
  2. package/.turbo/turbo-test.log +12 -11
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/CHANGELOG.md +20 -2
  5. package/dist/components/ai-elements/conversation-virtual.cjs +115 -0
  6. package/dist/components/ai-elements/conversation-virtual.d.cts +30 -0
  7. package/dist/components/ai-elements/conversation-virtual.d.ts +30 -0
  8. package/dist/components/ai-elements/conversation-virtual.js +115 -0
  9. package/dist/components/model-selection/available-model-picker.cjs +21 -77
  10. package/dist/components/model-selection/available-model-picker.js +22 -78
  11. package/dist/components/model-selection/model-picker-body.cjs +106 -0
  12. package/dist/components/model-selection/model-picker-body.d.cts +41 -0
  13. package/dist/components/model-selection/model-picker-body.d.ts +41 -0
  14. package/dist/components/model-selection/model-picker-body.js +106 -0
  15. package/dist/components/model-selection/thinking-level-picker.cjs +7 -3
  16. package/dist/components/model-selection/thinking-level-picker.js +7 -3
  17. package/dist/components/model-selection/thinking-picker.cjs +1 -1
  18. package/dist/components/model-selection/thinking-picker.js +1 -1
  19. package/dist/components/ui/calendar.cjs +1 -1
  20. package/dist/components/ui/calendar.js +1 -1
  21. package/dist/components/ui/combobox.cjs +1 -1
  22. package/dist/components/ui/combobox.js +1 -1
  23. package/dist/components/ui/form.d.cts +2 -2
  24. package/dist/components/ui/form.d.ts +2 -2
  25. package/dist/components/ui/input-group.cjs +1 -1
  26. package/dist/components/ui/input-group.js +1 -1
  27. package/dist/components/ui/input-otp.cjs +1 -1
  28. package/dist/components/ui/input-otp.js +1 -1
  29. package/dist/components/ui/input.cjs +3 -3
  30. package/dist/components/ui/input.js +3 -3
  31. package/dist/components/ui/native-select.cjs +2 -2
  32. package/dist/components/ui/native-select.js +2 -2
  33. package/dist/components/ui/select.cjs +1 -1
  34. package/dist/components/ui/select.js +1 -1
  35. package/dist/components/ui/slider.cjs +6 -0
  36. package/dist/components/ui/slider.d.cts +1 -1
  37. package/dist/components/ui/slider.d.ts +1 -1
  38. package/dist/components/ui/slider.js +6 -0
  39. package/dist/components/ui/spinner.d.cts +2 -1
  40. package/dist/components/ui/spinner.d.ts +2 -1
  41. package/dist/components/ui/textarea.cjs +1 -1
  42. package/dist/components/ui/textarea.js +1 -1
  43. package/dist/index.cjs +0 -55
  44. package/dist/index.d.cts +2 -66
  45. package/dist/index.d.ts +2 -66
  46. package/dist/index.js +0 -55
  47. package/dist/reference.cjs +56 -83
  48. package/dist/reference.js +56 -83
  49. package/dist/styles/ai-elements.css +2 -0
  50. package/dist/styles/context-editor.css +2 -0
  51. package/dist/styles/globals.css +32 -0
  52. package/dist/styles/model-selection.css +2 -0
  53. package/dist/styles/plugin.css +4 -1
  54. package/dist/theme/apply-theme.cjs +65 -12
  55. package/dist/theme/apply-theme.js +55 -2
  56. package/dist/theme/index.d.cts +1 -1
  57. package/dist/theme/index.d.ts +1 -1
  58. package/dist/theme/types.cjs +15 -2
  59. package/dist/theme/types.d.cts +21 -1
  60. package/dist/theme/types.d.ts +21 -1
  61. package/dist/theme/types.js +14 -1
  62. package/package.json +44 -30
  63. package/scripts/smoke-dist.mjs +106 -4
  64. package/src/components/ai-elements/conversation-virtual.test.tsx +126 -0
  65. package/src/components/ai-elements/conversation-virtual.tsx +162 -0
  66. package/src/components/model-selection/available-model-picker.tsx +26 -98
  67. package/src/components/model-selection/model-picker-body.test.tsx +152 -0
  68. package/src/components/model-selection/model-picker-body.tsx +188 -0
  69. package/src/components/model-selection/thinking-level-picker.tsx +9 -3
  70. package/src/components/model-selection/thinking-picker.test.tsx +41 -0
  71. package/src/components/model-selection/thinking-picker.tsx +1 -1
  72. package/src/components/ui/calendar.test.tsx +43 -0
  73. package/src/components/ui/calendar.tsx +1 -1
  74. package/src/components/ui/combobox.tsx +1 -1
  75. package/src/components/ui/input-group.tsx +1 -1
  76. package/src/components/ui/input-otp.tsx +1 -1
  77. package/src/components/ui/input.tsx +3 -3
  78. package/src/components/ui/native-select.tsx +2 -2
  79. package/src/components/ui/select.tsx +1 -1
  80. package/src/components/ui/slider.tsx +6 -0
  81. package/src/components/ui/spinner.tsx +1 -1
  82. package/src/components/ui/textarea.tsx +1 -1
  83. package/src/index.ts +4 -63
  84. package/src/styles/ai-elements.css +2 -0
  85. package/src/styles/context-editor.css +2 -0
  86. package/src/styles/globals.css +32 -0
  87. package/src/styles/model-selection.css +2 -0
  88. package/src/styles/plugin.css +4 -1
  89. package/src/theme/apply-theme.test.ts +126 -0
  90. package/src/theme/apply-theme.ts +66 -3
  91. package/src/theme/types.ts +33 -0
@@ -0,0 +1,152 @@
1
+ import { act } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { renderToStaticMarkup } from 'react-dom/server';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { ModelPickerBody, ProviderLogo } from './model-picker-body';
6
+
7
+ const GROUPS = [
8
+ {
9
+ provider: 'openai',
10
+ displayName: 'OpenAI',
11
+ logo: 'https://models.dev/logos/openai.svg',
12
+ models: [
13
+ { provider: 'openai', modelId: 'gpt-5', name: 'GPT-5', reasoning: true },
14
+ { provider: 'openai', modelId: 'gpt-5-mini', name: 'GPT-5 Mini', reasoning: false },
15
+ ],
16
+ },
17
+ {
18
+ provider: 'anthropic',
19
+ displayName: 'Anthropic',
20
+ logo: '',
21
+ models: [
22
+ { provider: 'anthropic', modelId: 'claude-opus-5', name: 'Claude Opus 5', reasoning: true },
23
+ ],
24
+ },
25
+ ];
26
+
27
+ describe('ProviderLogo', () => {
28
+ it('renders nothing when the provider has no logo', () => {
29
+ const html = renderToStaticMarkup(<ProviderLogo logo="" displayName="Anthropic" />);
30
+
31
+ // An empty src renders a broken image, which looks like a fault.
32
+ expect(html).toBe('');
33
+ });
34
+
35
+ it('hides itself when the logo fails to load', () => {
36
+ // Logos are remote models.dev URLs. A phone on the local network may
37
+ // have no route to the internet, and must not show broken images.
38
+ const host = document.createElement('div');
39
+ document.body.appendChild(host);
40
+ const root = createRoot(host);
41
+
42
+ act(() => {
43
+ root.render(<ProviderLogo logo="https://models.dev/logos/openai.svg" displayName="OpenAI" />);
44
+ });
45
+ const img = host.querySelector('img');
46
+ expect(img).not.toBeNull();
47
+
48
+ act(() => {
49
+ img?.dispatchEvent(new Event('error', { bubbles: false }));
50
+ });
51
+ expect(host.querySelector('img')).toBeNull();
52
+
53
+ act(() => root.unmount());
54
+ host.remove();
55
+ });
56
+ });
57
+
58
+ describe('ModelPickerBody', () => {
59
+ it('lists every provider group and its models', () => {
60
+ const html = renderToStaticMarkup(
61
+ <ModelPickerBody groups={GROUPS} value="openai/gpt-5" onChange={vi.fn()} />,
62
+ );
63
+
64
+ expect(html).toContain('OpenAI');
65
+ expect(html).toContain('Anthropic');
66
+ expect(html).toContain('GPT-5 Mini');
67
+ expect(html).toContain('Claude Opus 5');
68
+ });
69
+
70
+ it('shows no logos when the caller turns them off', () => {
71
+ const withLogos = renderToStaticMarkup(
72
+ <ModelPickerBody groups={GROUPS} value="" onChange={vi.fn()} />,
73
+ );
74
+ const withoutLogos = renderToStaticMarkup(
75
+ <ModelPickerBody groups={GROUPS} value="" onChange={vi.fn()} showProviderLogos={false} />,
76
+ );
77
+
78
+ expect(withLogos).toContain('<img');
79
+ expect(withoutLogos).not.toContain('<img');
80
+ });
81
+
82
+ it('says so when the host has no models at all', () => {
83
+ const html = renderToStaticMarkup(
84
+ <ModelPickerBody
85
+ groups={[]}
86
+ value=""
87
+ onChange={vi.fn()}
88
+ noModelsLabel="No models available"
89
+ />,
90
+ );
91
+
92
+ expect(html).toContain('No models available');
93
+ });
94
+
95
+ it('hands back the selected model as provider/modelId', () => {
96
+ const onChange = vi.fn();
97
+ const host = document.createElement('div');
98
+ document.body.appendChild(host);
99
+ const root = createRoot(host);
100
+
101
+ act(() => {
102
+ root.render(
103
+ <ModelPickerBody
104
+ groups={GROUPS}
105
+ value="openai/gpt-5"
106
+ onChange={onChange}
107
+ autoFocusSearch={false}
108
+ />,
109
+ );
110
+ });
111
+
112
+ const button = [...host.querySelectorAll('button')].find(
113
+ (candidate) => candidate.textContent?.includes('Claude Opus 5'),
114
+ );
115
+ act(() => button?.click());
116
+
117
+ expect(onChange).toHaveBeenCalledWith('anthropic/claude-opus-5');
118
+
119
+ act(() => root.unmount());
120
+ host.remove();
121
+ });
122
+
123
+ it('filters the list by the search query', () => {
124
+ const host = document.createElement('div');
125
+ document.body.appendChild(host);
126
+ const root = createRoot(host);
127
+
128
+ act(() => {
129
+ root.render(
130
+ <ModelPickerBody groups={GROUPS} value="" onChange={vi.fn()} autoFocusSearch={false} />,
131
+ );
132
+ });
133
+
134
+ const input = host.querySelector('input');
135
+ expect(input).not.toBeNull();
136
+ act(() => {
137
+ // React tracks the value, so set it through the native setter.
138
+ const setter = Object.getOwnPropertyDescriptor(
139
+ window.HTMLInputElement.prototype,
140
+ 'value',
141
+ )?.set;
142
+ setter?.call(input, 'opus');
143
+ input?.dispatchEvent(new Event('input', { bubbles: true }));
144
+ });
145
+
146
+ expect(host.textContent).toContain('Claude Opus 5');
147
+ expect(host.textContent).not.toContain('GPT-5 Mini');
148
+
149
+ act(() => root.unmount());
150
+ host.remove();
151
+ });
152
+ });
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The model list itself: a search field, provider groups, and model rows.
3
+ *
4
+ * This holds no popover and no trigger, so a caller decides how it is
5
+ * shown. `AvailableModelPicker` wraps it in a full-width settings field.
6
+ * A chat composer wraps it in a compact chip and adds a thinking control
7
+ * below it, inside the same popover.
8
+ */
9
+
10
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
11
+ import { Check, Sparkles } from 'lucide-react';
12
+ import {
13
+ filterModelGroups,
14
+ modelKey,
15
+ type SharedAvailableModelGroup,
16
+ type SharedModelInfo,
17
+ } from '@sero-ai/common';
18
+ import { SearchInput } from '../ui/search-input';
19
+ import { cn } from '../../lib/utils';
20
+
21
+ /**
22
+ * A provider logo.
23
+ *
24
+ * `logo` is a remote URL, so it is empty for some providers and it fails
25
+ * on a machine with no internet. Either way we show nothing rather than
26
+ * a broken image.
27
+ */
28
+ export function ProviderLogo({
29
+ logo,
30
+ displayName,
31
+ className,
32
+ }: {
33
+ logo: string;
34
+ displayName: string;
35
+ className?: string;
36
+ }) {
37
+ const [failed, setFailed] = useState(false);
38
+
39
+ // A new provider deserves a fresh attempt.
40
+ useEffect(() => setFailed(false), [logo]);
41
+
42
+ if (!logo || failed) return null;
43
+
44
+ return (
45
+ <img
46
+ src={logo}
47
+ alt={displayName}
48
+ onError={() => setFailed(true)}
49
+ className={cn('size-3.5 shrink-0 rounded-sm dark:invert', className)}
50
+ />
51
+ );
52
+ }
53
+
54
+ interface ModelPickerBodyProps<
55
+ TModel extends SharedModelInfo,
56
+ TGroup extends SharedAvailableModelGroup<TModel>,
57
+ > {
58
+ groups: TGroup[];
59
+ /** The selected model as `provider/modelId`, or '' for none. */
60
+ value: string;
61
+ /** Called with the new `provider/modelId`. */
62
+ onChange: (value: string) => void;
63
+ searchPlaceholder?: string;
64
+ emptyLabel?: string;
65
+ noModelsLabel?: string;
66
+ /**
67
+ * Focus the search field on mount. Turn this off on a phone: the
68
+ * on-screen keyboard would cover the list the user came to read.
69
+ */
70
+ autoFocusSearch?: boolean;
71
+ /** Hide provider logos where the network may not reach them. */
72
+ showProviderLogos?: boolean;
73
+ /** Shown at the right of the search field, such as a settings button. */
74
+ searchEndAdornment?: ReactNode;
75
+ className?: string;
76
+ /** Applied to the scrolling list, so a caller sets its height. */
77
+ listClassName?: string;
78
+ }
79
+
80
+ export function ModelPickerBody<
81
+ TModel extends SharedModelInfo,
82
+ TGroup extends SharedAvailableModelGroup<TModel>,
83
+ >({
84
+ groups,
85
+ value,
86
+ onChange,
87
+ searchPlaceholder = 'Search models...',
88
+ emptyLabel = 'No matching models',
89
+ noModelsLabel = 'No models available',
90
+ autoFocusSearch = true,
91
+ showProviderLogos = true,
92
+ searchEndAdornment,
93
+ className,
94
+ listClassName,
95
+ }: ModelPickerBodyProps<TModel, TGroup>) {
96
+ const [query, setQuery] = useState('');
97
+ const inputRef = useRef<HTMLInputElement>(null);
98
+
99
+ // The body mounts when its popover opens, so focusing on mount is
100
+ // enough; the query starts empty for the same reason.
101
+ useEffect(() => {
102
+ if (!autoFocusSearch) return;
103
+ const frame = requestAnimationFrame(() => inputRef.current?.focus());
104
+ return () => cancelAnimationFrame(frame);
105
+ }, [autoFocusSearch]);
106
+
107
+ const filteredGroups = useMemo(() => filterModelGroups(groups, query), [groups, query]);
108
+
109
+ const totalResults = useMemo(
110
+ () => filteredGroups.reduce((count, group) => count + group.models.length, 0),
111
+ [filteredGroups],
112
+ );
113
+
114
+ const handleSelect = useCallback(
115
+ (provider: string, modelId: string) => {
116
+ onChange(modelKey(provider, modelId));
117
+ setQuery('');
118
+ },
119
+ [onChange],
120
+ );
121
+
122
+ return (
123
+ <div className={className}>
124
+ <SearchInput
125
+ ref={inputRef}
126
+ value={query}
127
+ onChange={(event) => setQuery(event.target.value)}
128
+ placeholder={searchPlaceholder}
129
+ data-slot="model-filter"
130
+ containerClassName="border-b border-border/40"
131
+ endAdornment={searchEndAdornment}
132
+ />
133
+
134
+ <div className={cn('max-h-[280px] overflow-y-auto py-1', listClassName)}>
135
+ {groups.length === 0 ? (
136
+ <div className="px-3 py-4 text-center text-sm text-muted-foreground">{noModelsLabel}</div>
137
+ ) : totalResults === 0 ? (
138
+ <div className="px-3 py-4 text-center text-sm text-muted-foreground">{emptyLabel}</div>
139
+ ) : (
140
+ filteredGroups.map((group, index) => (
141
+ <div key={group.provider}>
142
+ {index > 0 ? <div className="mx-3 border-t border-border/30" /> : null}
143
+ <div className="flex items-center gap-2 px-3 pb-1 pt-2">
144
+ {showProviderLogos ? (
145
+ <ProviderLogo logo={group.logo} displayName={group.displayName} />
146
+ ) : null}
147
+ <span className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
148
+ {group.displayName}
149
+ </span>
150
+ </div>
151
+ <div className="px-1">
152
+ {group.models.map((model) => {
153
+ const nextValue = modelKey(model.provider, model.modelId);
154
+ const isSelected = nextValue === value;
155
+ return (
156
+ <button
157
+ key={nextValue}
158
+ type="button"
159
+ onClick={() => handleSelect(model.provider, model.modelId)}
160
+ className={cn(
161
+ 'flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-left text-sm transition-colors',
162
+ isSelected
163
+ ? 'bg-secondary text-foreground'
164
+ : 'text-muted-foreground hover:bg-secondary/60 hover:text-foreground',
165
+ )}
166
+ >
167
+ <div className="flex size-4 shrink-0 items-center justify-center">
168
+ {isSelected ? (
169
+ <Check className="size-3.5 text-emerald-500" />
170
+ ) : (
171
+ <div className="size-1.5 rounded-full bg-border" />
172
+ )}
173
+ </div>
174
+ <span className="min-w-0 flex-1 truncate font-medium">{model.name}</span>
175
+ {model.reasoning ? (
176
+ <Sparkles className="size-3 shrink-0 text-amber-500/60" />
177
+ ) : null}
178
+ </button>
179
+ );
180
+ })}
181
+ </div>
182
+ </div>
183
+ ))
184
+ )}
185
+ </div>
186
+ </div>
187
+ );
188
+ }
@@ -5,6 +5,12 @@ import {
5
5
  } from '@sero-ai/common';
6
6
  import { cn } from '../../lib/utils';
7
7
 
8
+ // Compact label so all seven segments stay on a single line without wrapping.
9
+ const COMPACT_LABELS: Record<ThinkingLevel, string> = {
10
+ ...THINKING_LABELS,
11
+ xhigh: 'X-High',
12
+ };
13
+
8
14
  interface ThinkingLevelPickerProps {
9
15
  value: string;
10
16
  onChange: (value: ThinkingLevel) => void;
@@ -28,7 +34,7 @@ export function ThinkingLevelPicker({
28
34
  : THINKING_LEVELS.filter((level) => allowed.has(level));
29
35
 
30
36
  return (
31
- <div className={cn('grid grid-cols-6 gap-1 rounded-lg border border-border/50 bg-muted/20 p-1', className)}>
37
+ <div className={cn('grid grid-cols-7 gap-1 rounded-lg border border-border/50 bg-muted/20 p-1', className)}>
32
38
  {levels.map((level) => {
33
39
  const isActive = value === level;
34
40
  const isAvailable = allowed.has(level) || !availableLevels;
@@ -39,14 +45,14 @@ export function ThinkingLevelPicker({
39
45
  disabled={disabled || !isAvailable}
40
46
  onClick={() => onChange(level)}
41
47
  className={cn(
42
- 'rounded-md px-2 py-1.5 text-sm font-medium transition-colors',
48
+ 'whitespace-nowrap rounded-md px-2 py-1.5 text-sm font-medium transition-colors',
43
49
  isActive
44
50
  ? 'bg-background text-foreground shadow-sm'
45
51
  : 'text-muted-foreground hover:bg-background/70 hover:text-foreground',
46
52
  (!isAvailable || disabled) && 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground',
47
53
  )}
48
54
  >
49
- {THINKING_LABELS[level]}
55
+ {COMPACT_LABELS[level]}
50
56
  </button>
51
57
  );
52
58
  })}
@@ -0,0 +1,41 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { ThinkingPicker } from './thinking-picker';
4
+
5
+ describe('ThinkingPicker', () => {
6
+ it('hides unsupported thinking levels', () => {
7
+ const html = renderToStaticMarkup(
8
+ <ThinkingPicker
9
+ available={['off', 'low', 'high']}
10
+ current="low"
11
+ disabled={false}
12
+ onSelect={vi.fn()}
13
+ />,
14
+ );
15
+
16
+ expect(html).toContain('Off');
17
+ expect(html).toContain('Low');
18
+ expect(html).toContain('High');
19
+ expect(html).not.toContain('X-High');
20
+ expect(html).not.toContain('Max');
21
+ });
22
+
23
+ it('shows only Off when the model has no thinking support', () => {
24
+ const html = renderToStaticMarkup(
25
+ <ThinkingPicker
26
+ available={[]}
27
+ current="off"
28
+ disabled
29
+ onSelect={vi.fn()}
30
+ />,
31
+ );
32
+
33
+ expect(html).toContain('Off');
34
+ expect(html).not.toContain('Min');
35
+ expect(html).not.toContain('Low');
36
+ expect(html).not.toContain('Med');
37
+ expect(html).not.toContain('High');
38
+ expect(html).not.toContain('X-High');
39
+ expect(html).not.toContain('Max');
40
+ });
41
+ });
@@ -26,7 +26,7 @@ export function ThinkingPicker({
26
26
  className
27
27
  }: ThinkingPickerProps) {
28
28
  const levels = disabled
29
- ? THINKING_LEVELS
29
+ ? THINKING_LEVELS.filter((level) => level === 'off')
30
30
  : THINKING_LEVELS.filter((level) => level === 'off' || available.includes(level));
31
31
  const activeIndex = Math.max(
32
32
  0,
@@ -0,0 +1,43 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Calendar } from './calendar';
4
+
5
+ /**
6
+ * Nothing in this repo renders the calendar, but plugin authors get it
7
+ * from the package. react-day-picker v10 removed class-name slots that
8
+ * v9 accepted, and a removed slot is silently ignored at runtime, so a
9
+ * render is the only thing that catches it.
10
+ */
11
+ describe('Calendar', () => {
12
+ it('renders a month grid', () => {
13
+ const html = renderToStaticMarkup(
14
+ <Calendar month={new Date(2026, 0, 1)} />,
15
+ );
16
+
17
+ expect(html).toContain('January');
18
+ // Every week of January 2026 plus the outside days.
19
+ expect(html).toContain('>1<');
20
+ expect(html).toContain('>31<');
21
+ });
22
+
23
+ it('keeps the styling on the grid slot v10 renamed', () => {
24
+ const html = renderToStaticMarkup(
25
+ <Calendar month={new Date(2026, 0, 1)} />,
26
+ );
27
+
28
+ expect(html).toContain('border-collapse');
29
+ });
30
+
31
+ it('renders the dropdown caption layout', () => {
32
+ const html = renderToStaticMarkup(
33
+ <Calendar
34
+ month={new Date(2026, 0, 1)}
35
+ captionLayout="dropdown"
36
+ startMonth={new Date(2025, 0)}
37
+ endMonth={new Date(2027, 11)}
38
+ />,
39
+ );
40
+
41
+ expect(html).toContain('<select');
42
+ });
43
+ });
@@ -86,7 +86,7 @@ function Calendar({
86
86
  : "rounded-md pl-2 pr-1 flex items-center gap-1 text-base h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
87
87
  defaultClassNames.caption_label
88
88
  ),
89
- table: "w-full border-collapse",
89
+ month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
90
90
  weekdays: cn("flex", defaultClassNames.weekdays),
91
91
  weekday: cn(
92
92
  "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
@@ -235,7 +235,7 @@ function ComboboxChips({
235
235
  <ComboboxPrimitive.Chips
236
236
  data-slot="combobox-chips"
237
237
  className={cn(
238
- "dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border bg-transparent bg-clip-padding px-2.5 py-1.5 text-base shadow-xs transition-[color,box-shadow] focus-within:ring-[3px] has-aria-invalid:ring-[3px] has-data-[slot=combobox-chip]:px-1.5",
238
+ "dark:bg-input/30 border-input focus-within:border-ring has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border bg-transparent bg-clip-padding px-2.5 py-1.5 text-base shadow-xs transition-[color,box-shadow] has-data-[slot=combobox-chip]:px-1.5",
239
239
  className
240
240
  )}
241
241
  {...props}
@@ -22,7 +22,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
22
22
  "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
23
23
 
24
24
  // Focus state.
25
- "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
25
+ "has-[[data-slot=input-group-control]:focus-visible]:border-ring",
26
26
 
27
27
  // Error state.
28
28
  "has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
@@ -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 size-9 items-center justify-center border-y border-r text-base 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 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-base shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10",
55
55
  className
56
56
  )}
57
57
  {...props}
@@ -10,9 +10,9 @@ function Input({ className, type, placeholder, "aria-label": ariaLabel, ...props
10
10
  type={type}
11
11
  data-slot="input"
12
12
  className={cn(
13
- "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-base file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-base",
14
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
15
- "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
13
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-base file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
14
+ "focus-visible:border-ring",
15
+ "aria-invalid:border-destructive",
16
16
  className
17
17
  )}
18
18
  {...props}
@@ -20,8 +20,8 @@ function NativeSelect({
20
20
  data-size={size}
21
21
  className={cn(
22
22
  "border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1",
23
- "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
24
- "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
23
+ "focus-visible:border-ring",
24
+ "aria-invalid:border-destructive",
25
25
  className
26
26
  )}
27
27
  {...props}
@@ -38,7 +38,7 @@ function SelectTrigger({
38
38
  data-slot="select-trigger"
39
39
  data-size={size}
40
40
  className={cn(
41
- "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]: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 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-base whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
41
+ "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-base whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
42
42
  className
43
43
  )}
44
44
  {...props}
@@ -9,6 +9,9 @@ function Slider({
9
9
  value,
10
10
  min = 0,
11
11
  max = 100,
12
+ id,
13
+ "aria-label": ariaLabel,
14
+ "aria-labelledby": ariaLabelledBy,
12
15
  ...props
13
16
  }: React.ComponentProps<typeof SliderPrimitive.Root>) {
14
17
  const _values = React.useMemo(
@@ -56,6 +59,9 @@ function Slider({
56
59
  <SliderPrimitive.Thumb
57
60
  data-slot="slider-thumb"
58
61
  key={thumbKeys[index]}
62
+ id={id && index === 0 ? id : undefined}
63
+ aria-label={ariaLabel}
64
+ aria-labelledby={ariaLabelledBy}
59
65
  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"
60
66
  />
61
67
  ))}
@@ -2,7 +2,7 @@ import { Loader2Icon } from "lucide-react"
2
2
 
3
3
  import { cn } from "../../lib/utils"
4
4
 
5
- function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
5
+ function Spinner({ className, ...props }: React.ComponentProps<typeof Loader2Icon>) {
6
6
  return (
7
7
  <Loader2Icon
8
8
  role="status"
@@ -9,7 +9,7 @@ function Textarea({ className, placeholder, "aria-label": ariaLabel, ...props }:
9
9
  placeholder={placeholder}
10
10
  data-slot="textarea"
11
11
  className={cn(
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-base",
12
+ "border-input placeholder:text-muted-foreground focus-visible:border-ring 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 disabled:cursor-not-allowed disabled:opacity-50",
13
13
  className
14
14
  )}
15
15
  {...props}