feat: 8 AI providers, rich text editor, agent notifications, UI contrast & font settings
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s
- Add DeepSeek, OpenRouter, Mistral, Z.AI, LM Studio as AI providers with editable model names via Combobox in admin settings - Fix OpenRouter broken by normalizeProvider bug in config.ts - Convert agent-created notes from Markdown to HTML (TipTap rich text) - Add Notification model + in-app notifications for agent results - Agent notification click opens the created note directly - Add note count display on notebook and inbox headers - Fix checklist toggle in card view (persist state via localCheckItems) - Add checklist creation option in tabs/list view (dropdown on + button) - Fix image description ENOENT error with HTTP fallback - Improve UI contrast across all themes (input, border, checkbox visibility) - Add font family setting (Inter vs System Default) in Appearance settings - Fix CSS font-sans variable conflict (removed dead Geist references) - Update README with new features and 8 providers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Note } from '@/lib/types'
|
||||
import { getAllNotes, searchNotes, enableNoteHistory } from '@/app/actions/notes'
|
||||
import { getAllNotes, searchNotes, enableNoteHistory, getNoteById } from '@/app/actions/notes'
|
||||
import { NoteInput } from '@/components/note-input'
|
||||
import { NotesMainSection, type NotesViewMode } from '@/components/notes-main-section'
|
||||
import { NotesViewToggle } from '@/components/notes-view-toggle'
|
||||
@@ -165,6 +165,33 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
|
||||
useReminderCheck(notes)
|
||||
|
||||
// Handle ?openNote=ID — open a note from a notification click
|
||||
useEffect(() => {
|
||||
const openNoteId = searchParams.get('openNote')
|
||||
if (!openNoteId) return
|
||||
|
||||
const openNote = async () => {
|
||||
// Try to find the note in current state first
|
||||
const existing = notes.find(n => n.id === openNoteId)
|
||||
if (existing) {
|
||||
setEditingNote({ note: existing, readOnly: false })
|
||||
} else {
|
||||
// Fetch from server
|
||||
const fetched = await getNoteById(openNoteId)
|
||||
if (fetched) {
|
||||
setEditingNote({ note: fetched, readOnly: false })
|
||||
}
|
||||
}
|
||||
// Clean URL — remove openNote param
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.delete('openNote')
|
||||
router.replace(params.toString() ? `/?${params.toString()}` : '/', { scroll: false })
|
||||
}
|
||||
|
||||
openNote()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchParams])
|
||||
|
||||
// Listen for global label deletion and immediately update local state
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
@@ -321,6 +348,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
})()}
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white tracking-tight">{currentNotebook.name}</h1>
|
||||
<span className="text-sm font-medium text-muted-foreground mt-2">({notes.length})</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<NotesViewToggle mode={notesViewMode} onModeChange={setNotesViewMode} />
|
||||
@@ -351,6 +379,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
<FileText className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white tracking-tight">{t('notes.title')}</h1>
|
||||
<span className="text-sm font-medium text-muted-foreground mt-2">{notes.length} {notes.length === 1 ? (t('notes.note') || 'note') : (t('notes.notes') || 'notes')}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<NotesViewToggle mode={notesViewMode} onModeChange={setNotesViewMode} />
|
||||
|
||||
@@ -218,6 +218,13 @@ export const NoteCard = memo(function NoteCard({
|
||||
|
||||
// Local color state so color persists after transition ends
|
||||
const [localColor, setLocalColor] = useState(note.color)
|
||||
// Local checkItems state so checklist toggles persist after transition ends
|
||||
const [localCheckItems, setLocalCheckItems] = useState(note.checkItems)
|
||||
|
||||
// Sync local state when parent data refreshes
|
||||
useEffect(() => {
|
||||
setLocalCheckItems(note.checkItems)
|
||||
}, [note.checkItems])
|
||||
|
||||
const colorClasses = NOTE_COLORS[(localColor || optimisticNote.color) as NoteColor] || NOTE_COLORS.default
|
||||
|
||||
@@ -373,14 +380,15 @@ export const NoteCard = memo(function NoteCard({
|
||||
}
|
||||
|
||||
const handleCheckItem = async (checkItemId: string) => {
|
||||
if (note.type === 'checklist' && Array.isArray(note.checkItems)) {
|
||||
const updatedItems = note.checkItems.map(item =>
|
||||
if (note.type === 'checklist') {
|
||||
const currentItems = localCheckItems || note.checkItems || []
|
||||
const updatedItems = currentItems.map(item =>
|
||||
item.id === checkItemId ? { ...item, checked: !item.checked } : item
|
||||
)
|
||||
setLocalCheckItems(updatedItems) // instant visual update, survives transition
|
||||
startTransition(async () => {
|
||||
addOptimisticNote({ checkItems: updatedItems })
|
||||
await updateNote(note.id, { checkItems: updatedItems })
|
||||
// No router.refresh() — optimistic update is sufficient and avoids grid rebuild
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -621,7 +629,7 @@ export const NoteCard = memo(function NoteCard({
|
||||
{/* Content */}
|
||||
{optimisticNote.type === 'checklist' ? (
|
||||
<NoteChecklist
|
||||
items={optimisticNote.checkItems || []}
|
||||
items={localCheckItems || optimisticNote.checkItems || []}
|
||||
onToggleItem={handleCheckItem}
|
||||
/>
|
||||
) : optimisticNote.type === 'richtext' ? (
|
||||
|
||||
@@ -497,8 +497,14 @@ export function NoteInlineEditor({
|
||||
value={noteType}
|
||||
onChange={(newType) => {
|
||||
setNoteType(newType)
|
||||
if (newType !== 'markdown') setShowMarkdownPreview(false)
|
||||
onChange?.(note.id, { isMarkdown: newType === 'markdown' })
|
||||
if (newType === 'markdown') setShowMarkdownPreview(true)
|
||||
else setShowMarkdownPreview(false)
|
||||
// Persist both type and isMarkdown immediately
|
||||
saveInline(note.id, {
|
||||
type: newType,
|
||||
isMarkdown: newType === 'markdown',
|
||||
}).catch(() => {})
|
||||
onChange?.(note.id, { type: newType, isMarkdown: newType === 'markdown' })
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
useSortable,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { Note, NOTE_COLORS, NoteColor } from '@/lib/types'
|
||||
import { Note, NOTE_COLORS, NoteColor, NoteType } from '@/lib/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { NoteInlineEditor } from '@/components/note-inline-editor'
|
||||
import { useLanguage } from '@/lib/i18n'
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
@@ -737,11 +738,13 @@ export function NotesTabsView({
|
||||
const selected = items.find((n) => n.id === selectedId) ?? null
|
||||
const colorKey = selected ? getColorKey(selected) : 'default'
|
||||
|
||||
const handleCreateNote = () => {
|
||||
const handleCreateNote = (noteType: NoteType = 'richtext') => {
|
||||
startCreating(async () => {
|
||||
try {
|
||||
const newNote = await createNote({
|
||||
content: '',
|
||||
content: noteType === 'checklist' ? '' : '',
|
||||
type: noteType,
|
||||
checkItems: noteType === 'checklist' ? [{ id: Date.now().toString(), text: '', checked: false }] : undefined,
|
||||
title: undefined,
|
||||
notebookId: currentNotebookId || undefined,
|
||||
skipRevalidation: true
|
||||
@@ -843,19 +846,32 @@ export function NotesTabsView({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* New note button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-muted-foreground/70 hover:bg-primary/8 hover:text-primary"
|
||||
onClick={handleCreateNote}
|
||||
disabled={isCreating}
|
||||
title={t('notes.newNote')}
|
||||
>
|
||||
{isCreating
|
||||
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
: <Plus className="h-4 w-4" />}
|
||||
</Button>
|
||||
{/* New note button — dropdown to choose type */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-muted-foreground/70 hover:bg-primary/8 hover:text-primary"
|
||||
disabled={isCreating}
|
||||
title={t('notes.newNote')}
|
||||
>
|
||||
{isCreating
|
||||
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
: <Plus className="h-4 w-4" />}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-[160px]">
|
||||
<DropdownMenuItem onClick={() => handleCreateNote('richtext')}>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
{t('notes.newNote') || 'Note'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleCreateNote('checklist')}>
|
||||
<ListChecks className="h-4 w-4 mr-2" />
|
||||
{t('notes.newChecklist') || 'Checklist'}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2 } from 'lucide-react'
|
||||
import { Bell, Check, X, Clock, AlertCircle, CheckCircle2, Circle, Share2, Bot, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import { getPendingShareRequests, respondToShareRequest, getNotesWithReminders, toggleReminderDone } from '@/app/actions/notes'
|
||||
import { getUnreadNotifications, markNotificationRead, markAllNotificationsRead, type AppNotification } from '@/app/actions/notifications'
|
||||
import { toast } from 'sonner'
|
||||
import { useNoteRefreshOptional } from '@/context/NoteRefreshContext'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -47,19 +49,23 @@ interface ReminderNote {
|
||||
export function NotificationPanel() {
|
||||
const { triggerRefresh } = useNoteRefreshOptional()
|
||||
const { t } = useLanguage()
|
||||
const router = useRouter()
|
||||
const [requests, setRequests] = useState<ShareRequest[]>([])
|
||||
const [reminders, setReminders] = useState<ReminderNote[]>([])
|
||||
const [appNotifications, setAppNotifications] = useState<AppNotification[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
const [shareData, reminderData] = await Promise.all([
|
||||
const [shareData, reminderData, notifData] = await Promise.all([
|
||||
getPendingShareRequests(),
|
||||
getNotesWithReminders(),
|
||||
getUnreadNotifications(),
|
||||
])
|
||||
setRequests(shareData as any)
|
||||
setReminders((reminderData as any) || [])
|
||||
setAppNotifications(notifData || [])
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load notifications:', error)
|
||||
}
|
||||
@@ -81,7 +87,7 @@ export function NotificationPanel() {
|
||||
const overdueReminders = activeReminders.filter(r => new Date(r.reminder!) < now)
|
||||
const upcomingReminders = activeReminders.filter(r => new Date(r.reminder!) >= now)
|
||||
|
||||
const pendingCount = requests.length + overdueReminders.length
|
||||
const pendingCount = requests.length + overdueReminders.length + appNotifications.length
|
||||
|
||||
const handleAccept = async (shareId: string) => {
|
||||
try {
|
||||
@@ -121,7 +127,17 @@ export function NotificationPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
const hasContent = requests.length > 0 || activeReminders.length > 0
|
||||
const handleMarkNotifRead = async (notifId: string) => {
|
||||
await markNotificationRead(notifId)
|
||||
setAppNotifications(prev => prev.filter(n => n.id !== notifId))
|
||||
}
|
||||
|
||||
const handleMarkAllRead = async () => {
|
||||
await markAllNotificationsRead()
|
||||
setAppNotifications([])
|
||||
}
|
||||
|
||||
const hasContent = requests.length > 0 || activeReminders.length > 0 || appNotifications.length > 0
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
@@ -149,11 +165,22 @@ export function NotificationPanel() {
|
||||
<Bell className="h-4 w-4 text-primary dark:text-primary-foreground" />
|
||||
<span className="font-semibold text-sm">{t('notification.notifications')}</span>
|
||||
</div>
|
||||
{pendingCount > 0 && (
|
||||
<Badge className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-md">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{appNotifications.length > 0 && (
|
||||
<button
|
||||
onClick={handleMarkAllRead}
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={t('notification.markAllRead') || 'Mark all read'}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{pendingCount > 0 && (
|
||||
<Badge className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-md">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,6 +195,65 @@ export function NotificationPanel() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{/* App notifications (agents, system) */}
|
||||
{appNotifications.map((notif) => (
|
||||
<div
|
||||
key={notif.id}
|
||||
className="p-3 border-b last:border-0 hover:bg-accent/50 transition-colors duration-150 cursor-pointer"
|
||||
onClick={() => {
|
||||
if (notif.actionUrl) {
|
||||
handleMarkNotifRead(notif.id)
|
||||
setOpen(false)
|
||||
router.push(notif.actionUrl)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={cn(
|
||||
"mt-0.5 flex-none rounded-full p-1",
|
||||
notif.type === 'agent_success' && 'bg-green-100 dark:bg-green-900/30 text-green-600',
|
||||
notif.type === 'agent_failure' && 'bg-red-100 dark:bg-red-900/30 text-red-600',
|
||||
notif.type === 'system' && 'bg-blue-100 dark:bg-blue-900/30 text-blue-600',
|
||||
)}>
|
||||
{notif.type.startsWith('agent') ? (
|
||||
<Bot className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<AlertCircle className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 mb-0.5">
|
||||
<span className={cn(
|
||||
"text-[10px] font-semibold uppercase tracking-wider",
|
||||
notif.type === 'agent_success' && 'text-green-600 dark:text-green-400',
|
||||
notif.type === 'agent_failure' && 'text-red-600 dark:text-red-400',
|
||||
notif.type === 'system' && 'text-blue-600 dark:text-blue-400',
|
||||
)}>
|
||||
{notif.type === 'agent_success' && (t('notification.agentSuccess') || 'Agent completed')}
|
||||
{notif.type === 'agent_failure' && (t('notification.agentFailed') || 'Agent failed')}
|
||||
{notif.type === 'system' && 'System'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium truncate">{notif.title}</p>
|
||||
{notif.message && (
|
||||
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">{notif.message}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1 mt-1 text-xs text-muted-foreground">
|
||||
<Clock className="w-3 h-3" />
|
||||
{formatDistanceToNow(new Date(notif.createdAt), { addSuffix: true })}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleMarkNotifRead(notif.id) }}
|
||||
className="mt-0.5 text-muted-foreground/40 hover:text-foreground transition-colors"
|
||||
title={t('notification.dismiss') || 'Dismiss'}
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Overdue reminders */}
|
||||
{overdueReminders.map((note) => (
|
||||
<div
|
||||
|
||||
@@ -5,9 +5,10 @@ import { useEffect } from 'react'
|
||||
interface ThemeInitializerProps {
|
||||
theme?: string
|
||||
fontSize?: string
|
||||
fontFamily?: string
|
||||
}
|
||||
|
||||
export function ThemeInitializer({ theme, fontSize }: ThemeInitializerProps) {
|
||||
export function ThemeInitializer({ theme, fontSize, fontFamily }: ThemeInitializerProps) {
|
||||
useEffect(() => {
|
||||
|
||||
// Helper to apply theme
|
||||
@@ -77,7 +78,20 @@ export function ThemeInitializer({ theme, fontSize }: ThemeInitializerProps) {
|
||||
}
|
||||
|
||||
applyFontSize(fontSize)
|
||||
}, [theme, fontSize])
|
||||
|
||||
// Apply font family
|
||||
const localFontFamily = localStorage.getItem('font-family')
|
||||
const effectiveFontFamily = localFontFamily || fontFamily || 'inter'
|
||||
const root = document.documentElement
|
||||
if (effectiveFontFamily === 'system') {
|
||||
root.classList.add('font-system')
|
||||
} else {
|
||||
root.classList.remove('font-system')
|
||||
}
|
||||
if (!localFontFamily && fontFamily) {
|
||||
localStorage.setItem('font-family', fontFamily)
|
||||
}
|
||||
}, [theme, fontSize, fontFamily])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ function Checkbox({
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"peer border-input dark:bg-input/50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -23,6 +23,8 @@ interface ComboboxProps {
|
||||
emptyMessage?: string
|
||||
disabled?: boolean
|
||||
className?: string
|
||||
/** When true, the search text can be submitted as a custom value not in the options list */
|
||||
allowCustomValue?: boolean
|
||||
}
|
||||
|
||||
export function Combobox({
|
||||
@@ -34,6 +36,7 @@ export function Combobox({
|
||||
emptyMessage = 'No results found.',
|
||||
disabled = false,
|
||||
className,
|
||||
allowCustomValue = false,
|
||||
}: ComboboxProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [search, setSearch] = React.useState('')
|
||||
@@ -56,6 +59,14 @@ export function Combobox({
|
||||
setSearch('')
|
||||
}
|
||||
|
||||
const handleConfirmCustom = () => {
|
||||
if (allowCustomValue && search.trim()) {
|
||||
onChange(search.trim())
|
||||
setOpen(false)
|
||||
setSearch('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={(v) => { setOpen(v); if (!v) setSearch('') }}>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -85,14 +96,26 @@ export function Combobox({
|
||||
placeholder={searchPlaceholder}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleConfirmCustom() } }}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-60 overflow-y-auto p-1">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
allowCustomValue && search.trim() ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConfirmCustom}
|
||||
className="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground transition-colors"
|
||||
>
|
||||
<span className="text-muted-foreground mr-2">+</span>
|
||||
<span>Use "<strong>{search.trim()}</strong>"</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="py-6 text-center text-sm text-muted-foreground">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
filtered.map((option) => {
|
||||
const isSelected = option.value === value
|
||||
|
||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"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-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/50 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-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
|
||||
Reference in New Issue
Block a user