@ouraihub/aether-ui 0.1.1 → 0.1.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 (34) hide show
  1. package/package.json +2 -2
  2. package/src/chat/ChatComposer.svelte +47 -5
  3. package/src/chat/ChatPanel.css +380 -380
  4. package/src/chat/ChatPanel.svelte +48 -51
  5. package/src/chat/ChatPanelComposer.svelte +44 -25
  6. package/src/chat/ChatPanelHeader.svelte +1 -25
  7. package/src/chat/ChatPanelMode.svelte +3 -3
  8. package/src/chat/ChatPanelToolbar.svelte +14 -3
  9. package/src/chat/ContextUsageStatus.svelte +97 -0
  10. package/src/chat/ModelEffortPicker.svelte +326 -0
  11. package/src/chat/PickerEffort.svelte +71 -0
  12. package/src/chat/ReasoningControl.svelte +10 -112
  13. package/src/chat/ReasoningSlider.svelte +174 -0
  14. package/src/chat/SkillPicker.svelte +1 -0
  15. package/src/chat/UsageControl.svelte +12 -5
  16. package/src/chat/UsageDetails.svelte +19 -3
  17. package/src/chat/chat-attachments.svelte.ts +9 -5
  18. package/src/chat/chat-panel-models.svelte.ts +97 -0
  19. package/src/chat/chat-panel-props.ts +6 -0
  20. package/src/chat/context-status.ts +101 -0
  21. package/src/chat/icons/LICENSE-REMIX +186 -0
  22. package/src/chat/icons/LICENSE-SIMPLE-ICONS +30 -0
  23. package/src/chat/icons/PathIcon.svelte +8 -0
  24. package/src/chat/icons/effort-icons.ts +65 -0
  25. package/src/chat/icons/status-icons.ts +105 -0
  26. package/src/chat/index.ts +20 -0
  27. package/src/chat/model-effort.ts +177 -0
  28. package/src/chat/test-support/chat-fixture.ts +16 -0
  29. package/src/i18n/en-chat.d.ts +1 -0
  30. package/src/i18n/en-chat.js +201 -0
  31. package/src/i18n/en.ts +24 -174
  32. package/src/i18n/zh-CN-chat.d.ts +1 -0
  33. package/src/i18n/zh-CN-chat.js +190 -0
  34. package/src/i18n/zh-CN.ts +2 -158
@@ -0,0 +1,326 @@
1
+ <script lang="ts">
2
+ // Composer pill + upward popover (docs/31 §4.2). The host owns data and every event's effect.
3
+ import { Command, Popover } from 'bits-ui'
4
+ import CheckIcon from '@lucide/svelte/icons/check'
5
+ import ChevronDownIcon from '@lucide/svelte/icons/chevron-down'
6
+ import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
7
+ import KeyIcon from '@lucide/svelte/icons/key-round'
8
+ import PinIcon from '@lucide/svelte/icons/pin'
9
+ import RefreshIcon from '@lucide/svelte/icons/refresh-cw'
10
+ import SearchIcon from '@lucide/svelte/icons/search'
11
+ import { SvelteSet } from 'svelte/reactivity'
12
+ import { Button } from '#lib/components/ui/button/index.js'
13
+ import { Separator } from '#lib/components/ui/separator/index.js'
14
+ import { cn } from '#lib/utils.js'
15
+ import type { KitI18n } from '../i18n'
16
+ import type { ReasoningEffort } from '../ports'
17
+ import { REASONING_EFFORTS } from '../providers/reasoning'
18
+ import PickerEffort from './PickerEffort.svelte'
19
+ import {
20
+ effortBars,
21
+ effortTone,
22
+ groupModels,
23
+ groupSummary,
24
+ nextValue,
25
+ type PickerModel,
26
+ type PickerValue,
27
+ } from './model-effort'
28
+
29
+ let {
30
+ models,
31
+ value = null,
32
+ defaultValue = null,
33
+ disabled = false,
34
+ refreshing = false,
35
+ notice = '',
36
+ i18n,
37
+ open = $bindable(false),
38
+ onselect,
39
+ onsetdefault,
40
+ onrefresh,
41
+ onopensettings,
42
+ }: {
43
+ models: readonly PickerModel[]
44
+ value?: PickerValue | null
45
+ defaultValue?: PickerValue | null
46
+ disabled?: boolean
47
+ refreshing?: boolean
48
+ notice?: string
49
+ i18n: KitI18n
50
+ open?: boolean
51
+ onselect: (value: PickerValue) => void
52
+ onsetdefault?: ((value: PickerValue) => void) | undefined
53
+ onrefresh?: ((force: boolean) => void) | undefined
54
+ /** Opens host settings for the current model when it declares no reasoning levels. */
55
+ onopensettings?: (() => void) | undefined
56
+ } = $props()
57
+ const t = (key: string, params?: Record<string, string | number>) => i18n.t(key, params)
58
+ let query = $state('')
59
+ // Command tracks the highlighted item; arrows/Enter come from the primitive.
60
+ let highlighted = $state('')
61
+ let trigger = $state<HTMLButtonElement | null>(null)
62
+ // Groups opened while browsing, and groups folded while searching (searching opens all).
63
+ const expanded = new SvelteSet<string>()
64
+ const searchFolded = new SvelteSet<string>()
65
+ const searching = $derived(query.trim() !== '')
66
+ // Hosts may declare levels in any order; the rail always reads 关闭 → 最大.
67
+ const current = $derived.by(() => {
68
+ const found = models.find((model) => model.key === value?.key)
69
+ return found
70
+ ? {
71
+ ...found,
72
+ efforts: REASONING_EFFORTS.filter((level) => found.efforts.includes(level)),
73
+ }
74
+ : null
75
+ })
76
+ const effort: ReasoningEffort = $derived(
77
+ current && value && current.efforts.includes(value.effort) ? value.effort : 'default',
78
+ )
79
+ const effortLabel = $derived(t(`chat.effort.${effort}`))
80
+ const name = $derived(current?.name ?? t('chat.picker.choose'))
81
+ const bars = $derived(current ? effortBars(current, effort) : 0)
82
+ const tone = $derived(effortTone(effort))
83
+ const groups = $derived(groupModels(models, query))
84
+ const isOpen = (id: string) => (searching ? !searchFolded.has(id) : expanded.has(id))
85
+ const sameAsDefault = $derived(
86
+ value !== null && value.key === defaultValue?.key && value.effort === defaultValue.effort,
87
+ )
88
+ $effect(() => {
89
+ // A new current model reveals its group; other groups keep the user's folding.
90
+ if (current) expanded.add(current.providerId)
91
+ })
92
+ function openChanged(next: boolean) {
93
+ if (!next) return
94
+ query = ''
95
+ highlighted = value?.key ?? ''
96
+ searchFolded.clear()
97
+ onrefresh?.(false)
98
+ }
99
+ function toggle(id: string) {
100
+ const set = searching ? searchFolded : expanded
101
+ if (!set.delete(id)) set.add(id)
102
+ }
103
+ </script>
104
+
105
+ <Popover.Root bind:open onOpenChange={openChanged}>
106
+ <Popover.Trigger {disabled}>
107
+ {#snippet child({ props })}
108
+ <Button
109
+ {...props}
110
+ bind:ref={trigger}
111
+ variant="ghost"
112
+ size="sm"
113
+ class="min-w-0 max-w-70 shrink"
114
+ data-composer-model
115
+ aria-label={t('chat.picker.trigger', { name, effort: effortLabel })}
116
+ >
117
+ <span
118
+ data-composer-model-detail
119
+ class="text-kit-xs text-muted-foreground"
120
+ aria-hidden="true">{current?.provider.charAt(0).toUpperCase() ?? '·'}</span
121
+ >
122
+ <span class="min-w-0 truncate">{name}</span>
123
+ {#if current && effort !== 'default'}
124
+ <span class="inline-flex h-3 shrink-0 items-end gap-0.5" aria-hidden="true">
125
+ {#each ['h-1', 'h-1.5', 'h-2', 'h-3'] as height, bar (height)}
126
+ <span
127
+ class={cn(
128
+ 'w-0.5 rounded-full bg-border',
129
+ height,
130
+ bar < bars && tone === 'fast' && 'bg-info',
131
+ bar < bars && tone === 'regular' && 'bg-primary',
132
+ bar < bars && tone === 'deep' && 'bg-warning',
133
+ )}
134
+ ></span>
135
+ {/each}
136
+ </span>
137
+ <span data-composer-model-detail class="text-kit-xs text-muted-foreground"
138
+ >{effortLabel}</span
139
+ >
140
+ {/if}
141
+ {#if current?.hasKey}<KeyIcon
142
+ data-composer-model-detail
143
+ data-icon="inline-end"
144
+ aria-hidden="true"
145
+ />{/if}
146
+ <ChevronDownIcon data-icon="inline-end" aria-hidden="true" />
147
+ </Button>
148
+ {/snippet}
149
+ </Popover.Trigger>
150
+ <Popover.Portal>
151
+ <Popover.Content
152
+ class={cn(
153
+ 'z-[210] flex w-[min(360px,calc(100vw-16px))] flex-col overflow-hidden rounded-kit',
154
+ 'max-h-[min(calc(100dvh-64px),var(--bits-popover-content-available-height))]',
155
+ 'border bg-popover text-xs text-popover-foreground shadow-lg',
156
+ )}
157
+ side="top"
158
+ align="start"
159
+ sideOffset={4}
160
+ collisionPadding={8}
161
+ trapFocus={false}
162
+ preventScroll={false}
163
+ onEscapeKeydown={(event) => {
164
+ if (event.isComposing) event.preventDefault()
165
+ event.stopPropagation()
166
+ }}
167
+ onCloseAutoFocus={(event) => {
168
+ event.preventDefault()
169
+ trigger?.focus({ preventScroll: true })
170
+ }}
171
+ >
172
+ <Command.Root
173
+ bind:value={highlighted}
174
+ shouldFilter={false}
175
+ loop
176
+ label={t('chat.picker.choose')}
177
+ class="flex min-h-0 flex-col"
178
+ >
179
+ <div class="flex items-center gap-1.5 px-2 text-muted-foreground">
180
+ <SearchIcon class="size-3.5 shrink-0" aria-hidden="true" />
181
+ <Command.Input
182
+ bind:value={query}
183
+ placeholder={t('chat.picker.search')}
184
+ aria-label={t('chat.picker.searchLabel')}
185
+ class="h-8 min-w-0 flex-1 bg-transparent text-foreground outline-none"
186
+ />
187
+ </div>
188
+ <Separator />
189
+ <Command.List class="max-h-[clamp(48px,calc(100dvh-250px),248px)] overflow-y-auto p-1">
190
+ {#if models.length === 0 || (searching && groups.length === 0)}
191
+ <Command.Empty forceMount class="p-3 text-kit-xs text-muted-foreground">
192
+ {t(models.length === 0 ? 'chat.picker.empty' : 'chat.picker.noMatch')}
193
+ </Command.Empty>
194
+ {/if}
195
+ {#each groups as group (group.id)}
196
+ {@const shown = isOpen(group.id)}
197
+ <Command.Group value={`group\n${group.id}`}>
198
+ <Command.GroupHeading>
199
+ <Button
200
+ variant="ghost"
201
+ size="xs"
202
+ class="w-full justify-start font-normal"
203
+ aria-expanded={shown}
204
+ aria-label={t('chat.picker.group', {
205
+ provider: group.provider,
206
+ state: t(shown ? 'chat.picker.expanded' : 'chat.picker.collapsed'),
207
+ })}
208
+ onclick={() => toggle(group.id)}
209
+ >
210
+ <span class="text-muted-foreground">{group.provider.charAt(0).toUpperCase()}</span
211
+ >
212
+ <span class="min-w-0 truncate">{group.provider}</span>
213
+ <span class="ml-auto max-w-32 truncate text-muted-foreground"
214
+ >{groupSummary(group, i18n)}</span
215
+ >
216
+ {#if shown}<ChevronDownIcon data-icon="inline-end" aria-hidden="true" />{:else}
217
+ <ChevronRightIcon data-icon="inline-end" aria-hidden="true" />{/if}
218
+ </Button>
219
+ </Command.GroupHeading>
220
+ {#if shown}
221
+ <Command.GroupItems>
222
+ {#each group.models as model (model.key)}
223
+ {@const selected = model.key === value?.key}
224
+ <Command.Item
225
+ value={model.key}
226
+ {disabled}
227
+ aria-current={selected ? 'true' : undefined}
228
+ aria-label={t('chat.picker.select', {
229
+ name: model.name,
230
+ provider: model.provider,
231
+ })}
232
+ onSelect={() => !disabled && onselect(nextValue(model, value, query))}
233
+ class={cn(
234
+ 'flex cursor-pointer items-center gap-2 rounded-kit-sm py-1 pr-2 pl-6',
235
+ 'data-[selected]:bg-accent data-[selected]:text-accent-foreground',
236
+ 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
237
+ selected && 'bg-accent/60',
238
+ )}
239
+ >
240
+ <span class="flex min-w-0 flex-1 flex-col">
241
+ <span class="truncate">{model.name}</span>
242
+ <span
243
+ class="flex min-w-0 items-center gap-1 text-kit-xs text-muted-foreground"
244
+ >
245
+ {#if model.hasKey}<KeyIcon
246
+ class="size-3 shrink-0"
247
+ aria-hidden="true"
248
+ />{/if}
249
+ <span class="truncate">{model.detail}</span>
250
+ </span>
251
+ </span>
252
+ {#if selected}<CheckIcon class="size-3 shrink-0" aria-hidden="true" />{/if}
253
+ </Command.Item>
254
+ {/each}
255
+ </Command.GroupItems>
256
+ {/if}
257
+ </Command.Group>
258
+ {/each}
259
+ </Command.List>
260
+ </Command.Root>
261
+ {#if current && current.efforts.length > 0}
262
+ <Separator />
263
+ <PickerEffort
264
+ model={current}
265
+ {effort}
266
+ {disabled}
267
+ {i18n}
268
+ onselect={(next) => onselect({ key: current.key, effort: next })}
269
+ />
270
+ {:else if current}
271
+ <Separator />
272
+ <div class="flex items-center justify-between gap-2 px-2 py-1.5">
273
+ <p class="text-kit-xs text-muted-foreground">{t('chat.reasoningUnconfigured')}</p>
274
+ {#if onopensettings}
275
+ <Button
276
+ variant="outline"
277
+ size="xs"
278
+ onclick={() => {
279
+ open = false
280
+ onopensettings()
281
+ }}>{t('chat.openSettings')}</Button
282
+ >
283
+ {/if}
284
+ </div>
285
+ {/if}
286
+ {#if notice}
287
+ <p class="truncate px-2 pb-1 text-kit-xs text-muted-foreground" role="status">
288
+ {notice}
289
+ </p>
290
+ {/if}
291
+ <Separator />
292
+ <div class="flex items-center gap-0.5 p-1">
293
+ {#if onsetdefault}
294
+ <Button
295
+ variant="ghost"
296
+ size="xs"
297
+ disabled={disabled || value === null || sameAsDefault}
298
+ onclick={() => value && onsetdefault(value)}
299
+ >
300
+ <PinIcon data-icon="inline-start" aria-hidden="true" />{t('chat.picker.setDefault')}
301
+ </Button>
302
+ {/if}
303
+ <Button
304
+ variant="ghost"
305
+ size="xs"
306
+ disabled={disabled || !current?.efforts.includes('none')}
307
+ onclick={() => current && onselect({ key: current.key, effort: 'none' })}
308
+ >
309
+ <RefreshIcon data-icon="inline-start" aria-hidden="true" />{t('chat.picker.resetOff')}
310
+ </Button>
311
+ {#if onrefresh}
312
+ <Button
313
+ variant="outline"
314
+ size="xs"
315
+ class="ml-auto"
316
+ disabled={refreshing}
317
+ onclick={() => onrefresh(true)}
318
+ >
319
+ <RefreshIcon data-icon="inline-start" aria-hidden="true" />
320
+ {t(refreshing ? 'chat.picker.refreshing' : 'chat.picker.refresh')}
321
+ </Button>
322
+ {/if}
323
+ </div>
324
+ </Popover.Content>
325
+ </Popover.Portal>
326
+ </Popover.Root>
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ // Effort section of the model picker: rail, level toggles, reset and hint (docs/31 §4.2).
3
+ import { ToggleGroup } from 'bits-ui'
4
+ import { Button } from '#lib/components/ui/button/index.js'
5
+ import type { KitI18n } from '../i18n'
6
+ import type { ReasoningEffort } from '../ports'
7
+ import ReasoningSlider from './ReasoningSlider.svelte'
8
+ import type { PickerModel } from './model-effort'
9
+
10
+ let {
11
+ model,
12
+ effort,
13
+ disabled = false,
14
+ i18n,
15
+ onselect,
16
+ }: {
17
+ model: PickerModel
18
+ effort: ReasoningEffort
19
+ disabled?: boolean
20
+ i18n: KitI18n
21
+ onselect: (effort: ReasoningEffort) => void
22
+ } = $props()
23
+ const t = (key: string, params?: Record<string, string | number>) => i18n.t(key, params)
24
+ const label = $derived(t(`chat.effort.${effort}`))
25
+ </script>
26
+
27
+ <section class="flex flex-col gap-0.5 px-2 pt-2 pb-1" aria-label={t('chat.reasoningEffort')}>
28
+ <div class="flex items-center justify-between gap-2">
29
+ <p class="text-kit-xs text-muted-foreground">
30
+ {t('chat.picker.effortFor', { name: model.name })}
31
+ </p>
32
+ {#if effort !== 'default'}
33
+ <Button variant="link" size="xs" {disabled} onclick={() => onselect('default')}>
34
+ {t('chat.resetReasoning')}
35
+ </Button>
36
+ {/if}
37
+ </div>
38
+ {#if model.efforts.length > 1}
39
+ <ReasoningSlider
40
+ efforts={model.efforts}
41
+ value={effort}
42
+ defaultIndex={Math.floor(model.efforts.length / 2)}
43
+ {disabled}
44
+ label={t('chat.reasoningEffort')}
45
+ valueText={label}
46
+ onchange={onselect}
47
+ />
48
+ {/if}
49
+ <ToggleGroup.Root
50
+ type="single"
51
+ value={effort === 'default' ? '' : effort}
52
+ onValueChange={(next) => next && onselect(next as ReasoningEffort)}
53
+ {disabled}
54
+ aria-label={t('chat.reasoningEffort')}
55
+ class="flex"
56
+ >
57
+ {#each model.efforts as level (level)}
58
+ <ToggleGroup.Item
59
+ value={level}
60
+ aria-label={`${t('chat.reasoningEffort')} ${t(`chat.effort.${level}`)}`}
61
+ class="min-w-0 flex-1 rounded-kit-sm py-0.5 text-kit-xs whitespace-nowrap
62
+ text-muted-foreground hover:text-foreground focus-visible:outline-2
63
+ focus-visible:outline-ring disabled:cursor-not-allowed disabled:opacity-50
64
+ data-[state=on]:font-semibold data-[state=on]:text-foreground"
65
+ >
66
+ {t(`chat.effort.${level}`)}
67
+ </ToggleGroup.Item>
68
+ {/each}
69
+ </ToggleGroup.Root>
70
+ <p class="text-kit-xs text-muted-foreground">{t(`chat.effortHint.${effort}`)}</p>
71
+ </section>
@@ -7,6 +7,7 @@
7
7
  import type { KitI18n } from '../i18n'
8
8
  import type { ReasoningEffort } from '../ports'
9
9
  import { REASONING_EFFORTS } from '../providers/reasoning'
10
+ import ReasoningSlider from './ReasoningSlider.svelte'
10
11
 
11
12
  let {
12
13
  value = 'default',
@@ -41,10 +42,6 @@
41
42
  function choose(effort: ReasoningEffort) {
42
43
  if (!disabled && (effort === 'default' || allowedEfforts.includes(effort))) onchange(effort)
43
44
  }
44
- function commit(event: Event & { currentTarget: HTMLInputElement }) {
45
- const effort = allowedEfforts[Number(event.currentTarget.value)]
46
- if (effort) choose(effort)
47
- }
48
45
  </script>
49
46
 
50
47
  <Popover.Root bind:open>
@@ -105,45 +102,15 @@
105
102
  <div class="reasoning-scale" aria-hidden="true">
106
103
  <span>{t('chat.reasoningFaster')}</span><span>{t('chat.reasoningSmarter')}</span>
107
104
  </div>
108
- <div
109
- class="reasoning-slider"
110
- style:--reasoning-progress={`${(effortIndex / (allowedEfforts.length - 1)) * 100}%`}
111
- >
112
- <input
113
- type="range"
114
- min="0"
115
- max={allowedEfforts.length - 1}
116
- step="1"
117
- value={effortIndex}
118
- aria-label={t('chat.reasoningEffort')}
119
- aria-valuetext={effortLabel}
120
- {disabled}
121
- oninput={commit}
122
- onpointerup={(event) => {
123
- if (event.button === 0) commit(event)
124
- }}
125
- onkeyup={(event) => {
126
- if (
127
- !event.isComposing &&
128
- ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(
129
- event.key,
130
- )
131
- ) {
132
- commit(event)
133
- }
134
- }}
135
- />
136
- <div class="reasoning-ticks" aria-hidden="true">
137
- {#each allowedEfforts as effort, index (effort)}
138
- <span
139
- class="reasoning-tick"
140
- data-effort={effort}
141
- class:current={index === effortIndex}
142
- style:left={`${(index / (allowedEfforts.length - 1)) * 100}%`}
143
- ></span>
144
- {/each}
145
- </div>
146
- </div>
105
+ <ReasoningSlider
106
+ efforts={allowedEfforts}
107
+ value={effectiveEffort}
108
+ defaultIndex={effortIndex}
109
+ {disabled}
110
+ label={t('chat.reasoningEffort')}
111
+ valueText={effortLabel}
112
+ onchange={choose}
113
+ />
147
114
  {:else}
148
115
  <Field.FieldSet {disabled}>
149
116
  <Field.FieldLegend class="sr-only">{t('chat.reasoningEffort')}</Field.FieldLegend>
@@ -205,75 +172,6 @@
205
172
  :global(.reasoning-popover input) {
206
173
  accent-color: var(--color-primary);
207
174
  }
208
- .reasoning-slider {
209
- position: relative;
210
- height: 1.75rem;
211
- display: flex;
212
- align-items: center;
213
- }
214
- :global(.reasoning-slider input[type='range']) {
215
- appearance: none;
216
- width: 100%;
217
- height: 1.75rem;
218
- margin: 0;
219
- background: transparent;
220
- cursor: pointer;
221
- }
222
- :global(.reasoning-slider input::-webkit-slider-runnable-track) {
223
- height: 0.375rem;
224
- border-radius: 999px;
225
- background: linear-gradient(
226
- to right,
227
- var(--color-primary) var(--reasoning-progress),
228
- var(--color-muted) var(--reasoning-progress)
229
- );
230
- }
231
- :global(.reasoning-slider input::-moz-range-track) {
232
- height: 0.375rem;
233
- border-radius: 999px;
234
- background: linear-gradient(
235
- to right,
236
- var(--color-primary) var(--reasoning-progress),
237
- var(--color-muted) var(--reasoning-progress)
238
- );
239
- }
240
- :global(.reasoning-slider input::-webkit-slider-thumb) {
241
- appearance: none;
242
- width: 1.125rem;
243
- height: 1.125rem;
244
- margin-top: -0.375rem;
245
- border: 1px solid var(--color-border);
246
- border-radius: 50%;
247
- background: var(--color-popover);
248
- }
249
- :global(.reasoning-slider input::-moz-range-thumb) {
250
- width: 1.125rem;
251
- height: 1.125rem;
252
- box-sizing: border-box;
253
- border: 1px solid var(--color-border);
254
- border-radius: 50%;
255
- background: var(--color-popover);
256
- }
257
- .reasoning-ticks {
258
- position: absolute;
259
- inset: 50% 0.5625rem auto;
260
- pointer-events: none;
261
- }
262
- .reasoning-tick {
263
- position: absolute;
264
- width: 0.25rem;
265
- height: 0.25rem;
266
- border-radius: 50%;
267
- background: var(--color-popover);
268
- transform: translate(-50%, -50%);
269
- }
270
- .reasoning-tick.current {
271
- visibility: hidden;
272
- }
273
- :global(.reasoning-slider input:disabled) {
274
- cursor: not-allowed;
275
- opacity: 0.5;
276
- }
277
175
  :global(.reasoning-popover input:focus-visible) {
278
176
  outline: 2px solid var(--color-ring);
279
177
  outline-offset: 2px;