mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 18:17:44 +02:00
feat: add search and keyboard navigation to model selector
- Add search input when 5+ models available with filtering - Add keyboard navigation (Arrow keys, Tab, Enter, Escape) - Add scrollable model list with thin scrollbar styling - Fix highlight jumping: use -1 to indicate no selection until user interacts - Fix scroll position reset on dropdown open - Remove dropdown animations for instant open/close - Fix focus management: prevent Radix auto-focus return to trigger - Add onSelectionComplete callback for textarea focus after selection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
* Combined Agent and Model selector with vertical layout
|
||||
* Shows Agent icon + Model name, with vertical list for model selection
|
||||
*/
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ChevronDown, Loader2, Check } from 'lucide-react'
|
||||
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
||||
import { ChevronDown, Loader2, Check, Search } from 'lucide-react'
|
||||
import type { AgentCheckResult } from '../../../shared/electron-api'
|
||||
import type { SessionModelState, ModelId } from '../../../shared/types'
|
||||
|
||||
@@ -21,6 +21,9 @@ const AGENT_ICONS: Record<string, string> = {
|
||||
// Icons that need dark mode inversion (monochrome black icons)
|
||||
const INVERT_IN_DARK = new Set(['codex'])
|
||||
|
||||
// Minimum number of models to show search input
|
||||
const MIN_MODELS_FOR_SEARCH = 5
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
@@ -40,6 +43,7 @@ interface AgentModelSelectorProps {
|
||||
disabled?: boolean
|
||||
isSwitching?: boolean
|
||||
isInitializing?: boolean
|
||||
onSelectionComplete?: () => void
|
||||
}
|
||||
|
||||
export function AgentModelSelector({
|
||||
@@ -49,12 +53,16 @@ export function AgentModelSelector({
|
||||
onModelChange,
|
||||
disabled = false,
|
||||
isSwitching = false,
|
||||
isInitializing = false
|
||||
isInitializing = false,
|
||||
onSelectionComplete
|
||||
}: AgentModelSelectorProps): React.JSX.Element {
|
||||
const [agents, setAgents] = useState<AgentCheckResult[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0)
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadAgents()
|
||||
@@ -82,19 +90,113 @@ export function AgentModelSelector({
|
||||
)
|
||||
const displayName = currentModel?.name || currentAgentName
|
||||
|
||||
function handleAgentSelect(agentId: string): void {
|
||||
if (agentId !== currentAgentId) {
|
||||
onAgentChange(agentId)
|
||||
}
|
||||
setOpen(false)
|
||||
}
|
||||
// Show search when there are many models
|
||||
const showSearch = (modelState?.availableModels.length ?? 0) >= MIN_MODELS_FOR_SEARCH
|
||||
|
||||
function handleModelSelect(modelId: ModelId): void {
|
||||
if (modelId !== modelState?.currentModelId) {
|
||||
onModelChange(modelId)
|
||||
// Filter models based on search query
|
||||
const filteredModels = useMemo(() => {
|
||||
if (!modelState?.availableModels) return []
|
||||
if (!searchQuery.trim()) return modelState.availableModels
|
||||
const query = searchQuery.toLowerCase()
|
||||
return modelState.availableModels.filter((m) => m.name.toLowerCase().includes(query))
|
||||
}, [modelState?.availableModels, searchQuery])
|
||||
|
||||
// Reset search, highlight, and scroll position when dropdown opens
|
||||
// Use -1 to indicate no item is highlighted until user interacts
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSearchQuery('')
|
||||
setHighlightedIndex(-1)
|
||||
// Reset scroll position after DOM updates
|
||||
requestAnimationFrame(() => {
|
||||
if (listRef.current) {
|
||||
listRef.current.scrollTop = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
setOpen(false)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
// Reset highlight when filtered results change
|
||||
useEffect(() => {
|
||||
setHighlightedIndex(-1)
|
||||
}, [filteredModels.length])
|
||||
|
||||
const handleAgentSelect = useCallback(
|
||||
(agentId: string): void => {
|
||||
if (agentId !== currentAgentId) {
|
||||
onAgentChange(agentId)
|
||||
}
|
||||
setOpen(false)
|
||||
onSelectionComplete?.()
|
||||
},
|
||||
[currentAgentId, onAgentChange, onSelectionComplete]
|
||||
)
|
||||
|
||||
const handleModelSelect = useCallback(
|
||||
(modelId: ModelId): void => {
|
||||
if (modelId !== modelState?.currentModelId) {
|
||||
onModelChange(modelId)
|
||||
}
|
||||
setOpen(false)
|
||||
onSelectionComplete?.()
|
||||
},
|
||||
[modelState?.currentModelId, onModelChange, onSelectionComplete]
|
||||
)
|
||||
|
||||
// Keyboard navigation handler for search input
|
||||
const handleSearchKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const modelCount = filteredModels.length
|
||||
if (modelCount === 0) return
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
// If nothing highlighted, start from first item; otherwise move down
|
||||
setHighlightedIndex((prev) => (prev < 0 ? 0 : (prev + 1) % modelCount))
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
// If nothing highlighted, start from last item; otherwise move up
|
||||
setHighlightedIndex((prev) =>
|
||||
prev < 0 ? modelCount - 1 : (prev - 1 + modelCount) % modelCount
|
||||
)
|
||||
break
|
||||
case 'Tab':
|
||||
e.preventDefault()
|
||||
if (e.shiftKey) {
|
||||
setHighlightedIndex((prev) =>
|
||||
prev < 0 ? modelCount - 1 : (prev - 1 + modelCount) % modelCount
|
||||
)
|
||||
} else {
|
||||
setHighlightedIndex((prev) => (prev < 0 ? 0 : (prev + 1) % modelCount))
|
||||
}
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
// Only select if there's a valid highlighted item
|
||||
if (highlightedIndex >= 0 && filteredModels[highlightedIndex]) {
|
||||
handleModelSelect(filteredModels[highlightedIndex].modelId)
|
||||
}
|
||||
break
|
||||
case 'Escape':
|
||||
e.preventDefault()
|
||||
setOpen(false)
|
||||
break
|
||||
}
|
||||
},
|
||||
[filteredModels, highlightedIndex, handleModelSelect, setOpen]
|
||||
)
|
||||
|
||||
// Scroll highlighted item into view (only when user actively highlights an item)
|
||||
useEffect(() => {
|
||||
if (!listRef.current || highlightedIndex < 0) return
|
||||
const items = listRef.current.querySelectorAll('[data-model-item]')
|
||||
const highlightedItem = items[highlightedIndex] as HTMLElement | undefined
|
||||
if (highlightedItem) {
|
||||
highlightedItem.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
}, [highlightedIndex])
|
||||
|
||||
// Show skeleton during initialization
|
||||
if (isInitializing) {
|
||||
@@ -121,6 +223,7 @@ export function AgentModelSelector({
|
||||
'flex items-center gap-1.5 text-xs text-muted-foreground transition-colors px-2 py-1 rounded-md',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
'outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
(disabled || loading || isSwitching) && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
@@ -144,103 +247,177 @@ export function AgentModelSelector({
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Select agent and model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent side="top" align="start" className="min-w-[220px] p-1.5">
|
||||
{/* Current Agent Header */}
|
||||
<div className="flex items-center justify-between gap-3 px-2 py-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{currentAgentIcon && (
|
||||
<img
|
||||
src={currentAgentIcon}
|
||||
alt={currentAgentName}
|
||||
className={cn('h-3.5 w-3.5', currentAgentNeedsInvert && 'dark:invert')}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">{currentAgentName}</span>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="min-w-[220px] p-0 data-[state=open]:animate-none data-[state=closed]:animate-none"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
{/* Top section with padding: header + search */}
|
||||
<div className="px-1.5 pt-1.5">
|
||||
{/* Current Agent Header */}
|
||||
<div className="flex items-center justify-between gap-3 px-2 py-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{currentAgentIcon && (
|
||||
<img
|
||||
src={currentAgentIcon}
|
||||
alt={currentAgentName}
|
||||
className={cn('h-3.5 w-3.5', currentAgentNeedsInvert && 'dark:invert')}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">{currentAgentName}</span>
|
||||
</div>
|
||||
<span className="text-xs text-green-600/70">Active</span>
|
||||
</div>
|
||||
<span className="text-xs text-green-600/70">Active</span>
|
||||
|
||||
{/* Search input for many models */}
|
||||
{showSearch && (
|
||||
<div className="px-2 py-1.5">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground/50" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search models..."
|
||||
className="w-full h-7 pl-7 pr-2 text-xs bg-muted/50 border-none rounded-md outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Current Agent's Models */}
|
||||
{/* Full-width scrollable section - scrollbar flush to edge */}
|
||||
{modelState && modelState.availableModels.length > 0 ? (
|
||||
modelState.availableModels.map((model) => {
|
||||
const isSelectedModel = model.modelId === modelState.currentModelId
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={model.modelId}
|
||||
onClick={() => handleModelSelect(model.modelId)}
|
||||
className="flex items-center justify-between gap-6 pl-6 py-1.5"
|
||||
>
|
||||
<span className="text-sm">{model.name}</span>
|
||||
{isSelectedModel && <Check className="h-3.5 w-3.5 text-primary/70" />}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})
|
||||
<div ref={listRef} className="max-h-[200px] overflow-y-auto scrollbar-thin">
|
||||
{filteredModels.length > 0 ? (
|
||||
filteredModels.map((model, index) => {
|
||||
const isSelectedModel = model.modelId === modelState.currentModelId
|
||||
const isHighlighted = showSearch && index === highlightedIndex
|
||||
|
||||
// When search is active, use plain div to avoid Radix's roving focus
|
||||
// which steals focus from the search input on hover
|
||||
if (showSearch) {
|
||||
return (
|
||||
<div
|
||||
key={model.modelId}
|
||||
data-model-item
|
||||
role="option"
|
||||
aria-selected={isSelectedModel}
|
||||
onClick={() => handleModelSelect(model.modelId)}
|
||||
onMouseEnter={() => setHighlightedIndex(index)}
|
||||
className={cn(
|
||||
'flex items-center gap-6 pl-6 pr-2 py-1.5 cursor-pointer text-sm rounded-sm',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
isHighlighted && 'bg-accent text-accent-foreground'
|
||||
)}
|
||||
>
|
||||
<span className="flex-1">{model.name}</span>
|
||||
<span className="w-3.5 flex-shrink-0">
|
||||
{isSelectedModel && <Check className="h-3.5 w-3.5 text-primary/70" />}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Standard DropdownMenuItem when no search (preserves full accessibility)
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={model.modelId}
|
||||
data-model-item
|
||||
onClick={() => handleModelSelect(model.modelId)}
|
||||
className="flex items-center gap-6 pl-6 pr-2 py-1.5"
|
||||
>
|
||||
<span className="flex-1 text-sm">{model.name}</span>
|
||||
<span className="w-3.5 flex-shrink-0">
|
||||
{isSelectedModel && <Check className="h-3.5 w-3.5 text-primary/70" />}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="pl-6 pr-2 py-2 text-xs text-muted-foreground/70">
|
||||
No models match “{searchQuery}”
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="pl-6 pr-2 py-2 text-xs text-muted-foreground/70">No models available</div>
|
||||
<div className="px-1.5">
|
||||
<div className="pl-6 pr-2 py-2 text-xs text-muted-foreground/70">
|
||||
No models available
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Separator if there are other agents */}
|
||||
{otherAgents.length > 0 && <DropdownMenuSeparator />}
|
||||
{/* Bottom section: separator + other agents */}
|
||||
{otherAgents.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator className="mx-1.5" />
|
||||
<div className="px-1.5 pb-1.5">
|
||||
{otherAgents.map((agent) => {
|
||||
const icon = AGENT_ICONS[agent.id]
|
||||
const needsInvert = INVERT_IN_DARK.has(agent.id)
|
||||
const isInstalled = agent.installed
|
||||
|
||||
{/* Other Agents */}
|
||||
{otherAgents.map((agent) => {
|
||||
const icon = AGENT_ICONS[agent.id]
|
||||
const needsInvert = INVERT_IN_DARK.has(agent.id)
|
||||
const isInstalled = agent.installed
|
||||
|
||||
if (!isInstalled) {
|
||||
// Not installed - show disabled state
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="flex items-center justify-between gap-3 px-2 py-1.5 opacity-40"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{icon && (
|
||||
<img
|
||||
src={icon}
|
||||
alt={agent.name}
|
||||
className={cn('h-3.5 w-3.5', needsInvert && 'dark:invert')}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">{agent.name}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">Setup required</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Installed - show Switch button
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="flex items-center justify-between gap-3 px-2 py-1.5 hover:bg-accent/50 rounded-sm cursor-pointer transition-colors"
|
||||
onClick={() => handleAgentSelect(agent.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
handleAgentSelect(agent.id)
|
||||
if (!isInstalled) {
|
||||
// Not installed - show disabled state
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="flex items-center justify-between gap-3 px-2 py-1.5 opacity-40"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{icon && (
|
||||
<img
|
||||
src={icon}
|
||||
alt={agent.name}
|
||||
className={cn('h-3.5 w-3.5', needsInvert && 'dark:invert')}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">{agent.name}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">Setup required</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{icon && (
|
||||
<img
|
||||
src={icon}
|
||||
alt={agent.name}
|
||||
className={cn('h-3.5 w-3.5', needsInvert && 'dark:invert')}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">{agent.name}</span>
|
||||
</div>
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
||||
Switch
|
||||
</span>
|
||||
|
||||
// Installed - show Switch button
|
||||
return (
|
||||
<div
|
||||
key={agent.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="flex items-center justify-between gap-3 px-2 py-1.5 hover:bg-accent/50 rounded-sm cursor-pointer transition-colors"
|
||||
onClick={() => handleAgentSelect(agent.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
handleAgentSelect(agent.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{icon && (
|
||||
<img
|
||||
src={icon}
|
||||
alt={agent.name}
|
||||
className={cn('h-3.5 w-3.5', needsInvert && 'dark:invert')}
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground">{agent.name}</span>
|
||||
</div>
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
||||
Switch
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
||||
@@ -43,6 +43,8 @@ interface MessageInputProps {
|
||||
|
||||
const MAX_IMAGE_SIZE = 10 * 1024 * 1024 // 10MB
|
||||
const SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']
|
||||
// Minimal delay for focusing textarea after dropdown closes
|
||||
const DROPDOWN_CLOSE_DELAY_MS = 0
|
||||
|
||||
// Warning banner component for missing directory
|
||||
function DirectoryWarningBanner({
|
||||
@@ -285,6 +287,14 @@ export function MessageInput({
|
||||
textareaRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
// Focus textarea after selector completion (model/agent/mode selection)
|
||||
const handleSelectorComplete = useCallback(() => {
|
||||
// Delay to allow dropdown close animation and Radix auto-focus to complete
|
||||
setTimeout(() => {
|
||||
textareaRef.current?.focus()
|
||||
}, DROPDOWN_CLOSE_DELAY_MS)
|
||||
}, [])
|
||||
|
||||
// Handle submit
|
||||
const handleSubmit = useCallback(() => {
|
||||
const trimmed = value.trim()
|
||||
@@ -431,6 +441,7 @@ export function MessageInput({
|
||||
disabled={isProcessing}
|
||||
isSwitching={isSwitchingAgent}
|
||||
isInitializing={isInitializing}
|
||||
onSelectionComplete={handleSelectorComplete}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -441,6 +452,7 @@ export function MessageInput({
|
||||
onModeChange={onModeChange}
|
||||
disabled={isProcessing}
|
||||
isInitializing={isInitializing}
|
||||
onSelectionComplete={handleSelectorComplete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -20,13 +20,15 @@ interface ModeSelectorProps {
|
||||
onModeChange: (modeId: SessionModeId) => void
|
||||
disabled?: boolean
|
||||
isInitializing?: boolean
|
||||
onSelectionComplete?: () => void
|
||||
}
|
||||
|
||||
export function ModeSelector({
|
||||
modeState,
|
||||
onModeChange,
|
||||
disabled = false,
|
||||
isInitializing = false
|
||||
isInitializing = false,
|
||||
onSelectionComplete
|
||||
}: ModeSelectorProps): React.JSX.Element | null {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
@@ -53,6 +55,7 @@ export function ModeSelector({
|
||||
onModeChange(modeId)
|
||||
}
|
||||
setOpen(false)
|
||||
onSelectionComplete?.()
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -67,6 +70,7 @@ export function ModeSelector({
|
||||
'flex items-center gap-1.5 text-xs text-muted-foreground transition-colors px-2 py-1 rounded-md',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
'outline-none focus-visible:ring-1 focus-visible:ring-ring',
|
||||
disabled && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
@@ -80,7 +84,8 @@ export function ModeSelector({
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="min-w-[160px] max-h-[300px] overflow-y-auto p-1.5"
|
||||
className="min-w-[160px] max-h-[300px] overflow-y-auto p-1.5 data-[state=open]:animate-none data-[state=closed]:animate-none"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
{modeState.availableModes.map((mode) => {
|
||||
const isSelected = mode.id === modeState.currentModeId
|
||||
|
||||
@@ -41,6 +41,28 @@ body {
|
||||
background-color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* Thin scrollbar variant for compact UI elements */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: oklch(0.7 0 0 / 20%);
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background-color: oklch(0.7 0 0 / 35%);
|
||||
}
|
||||
|
||||
.dark .scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background-color: oklch(1 0 0 / 15%);
|
||||
}
|
||||
|
||||
.dark .scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background-color: oklch(1 0 0 / 25%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
Reference in New Issue
Block a user