diff --git a/src/components/AudiobookExportModal.tsx b/src/components/AudiobookExportModal.tsx
index b88efda..254ac1f 100644
--- a/src/components/AudiobookExportModal.tsx
+++ b/src/components/AudiobookExportModal.tsx
@@ -1,11 +1,11 @@
'use client';
import { Fragment, useState, useRef, useCallback, useEffect, useMemo } from 'react';
-import { Transition, Listbox, Menu, MenuButton } from '@headlessui/react';
+import { Transition, Menu, MenuButton } from '@headlessui/react';
import { useTimeEstimation } from '@/hooks/useTimeEstimation';
import { ProgressPopup } from '@/components/ProgressPopup';
import { ProgressCard } from '@/components/ProgressCard';
-import { DownloadIcon, CheckCircleIcon, XCircleIcon, ClockIcon, ChevronUpDownIcon, RefreshIcon, DotsVerticalIcon } from '@/components/icons/Icons';
+import { DownloadIcon, CheckCircleIcon, XCircleIcon, ClockIcon, RefreshIcon, DotsVerticalIcon } from '@/components/icons/Icons';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { useConfig } from '@/contexts/ConfigContext';
import { useTTS } from '@/contexts/TTSContext';
@@ -14,7 +14,7 @@ import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell';
import { resolveTtsProviderModelPolicy } from '@/lib/shared/tts-provider-policy';
import { getTtsLanguageCompatibilityWarnings, resolveTtsLanguage } from '@/lib/shared/language';
import type { TTSAudiobookChapter, TTSAudiobookFormat } from '@/types/tts';
-import { Button, Card, IconButton, MenuActionItem, MenuItemsSurface, RangeInput, SharedListboxButton, SharedListboxOption, SharedListboxOptions } from '@/components/ui';
+import { Button, Card, IconButton, MenuActionItem, MenuItemsSurface, RangeInput, Select } from '@/components/ui';
import {
getAudiobookStatus,
deleteAudiobookChapter,
@@ -565,51 +565,25 @@ export function AudiobookExportModal({
{chapters.length === 0 ? (
-
setFormat(newFormat)}
+ options={['m4b', 'mp3'] as const}
disabled={chapters.length > 0 || settingsLocked}
- >
-
-
- {format.toUpperCase()}
-
-
-
-
-
-
-
- {({ selected }) => (
-
- M4B
-
- )}
-
-
- {({ selected }) => (
-
- MP3
-
- )}
-
-
-
-
-
+ renderValue={(option) => (
+
{option.toUpperCase()}
+ )}
+ renderOption={(option, { selected }) => (
+
+ {option.toUpperCase()}
+
+ )}
+ buttonClassName="bg-surface"
+ chevronClassName="h-4 w-4 text-soft"
+ optionInset="none"
+ optionItemClassName="py-2"
+ showCheckmark={false}
+ />
) : (
{format.toUpperCase()}
)}
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx
index a5ad457..c15bec2 100644
--- a/src/components/SettingsModal.tsx
+++ b/src/components/SettingsModal.tsx
@@ -1,14 +1,10 @@
'use client';
import { Fragment, useState, useEffect, useCallback, useMemo } from 'react';
-import {
- Transition,
- Listbox,
-} from '@headlessui/react';
import Link from 'next/link';
import { useTheme } from '@/contexts/ThemeContext';
import { useConfig } from '@/contexts/ConfigContext';
-import { ChevronUpDownIcon, CheckIcon, SettingsIcon, KeyIcon, PaletteIcon, DocumentIcon, UserIcon, DownloadIcon, ChevronRightIcon } from '@/components/icons/Icons';
+import { CheckIcon, SettingsIcon, KeyIcon, PaletteIcon, DocumentIcon, UserIcon, DownloadIcon, ChevronRightIcon } from '@/components/icons/Icons';
import { useDocuments } from '@/contexts/DocumentContext';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { ProgressPopup } from '@/components/ProgressPopup';
@@ -52,10 +48,8 @@ import {
Input,
ModalFrame,
ModalTitle,
+ Select,
inputClass,
- SharedListboxButton,
- SharedListboxOption,
- SharedListboxOptions,
} from '@/components/ui';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
@@ -654,8 +648,16 @@ export function SettingsModal({
User API keys are restricted and no shared provider is configured. Ask an admin to add one.
) : (
-
provider.id}
+ renderValue={(provider) => provider.name}
+ renderOption={(provider, { selected }) => (
+
+ {provider.name}
+
+ )}
onChange={(provider) => {
const defaults = resolveProviderDefaults({
providerRef: provider.id,
@@ -675,46 +677,7 @@ export function SettingsModal({
}
setCustomModelInput('');
}}
- >
-
-
- {selectedProviderOption?.name || 'Select Provider'}
-
-
-
-
-
-
-
- {ttsProviders.map((provider) => (
-
- {({ selected }) => (
- <>
-
- {provider.name}
-
- {selected && (
-
-
-
- )}
- >
- )}
-
- ))}
-
-
-
+ />
)}
{restrictUserApiKeys && (
@@ -768,8 +731,30 @@ export function SettingsModal({
)}
- m.id === selectedModelId) || ttsModels[0]}
+
+ />
{supportsCustom && selectedModelId === 'custom' && (
{renderSource('defaultTtsProvider')}
{providerOptions.length > 0 ? (
-
-
- {selectedProviderOption?.name ?? 'Select provider'}
-
-
+
-
-
- {providerOptions.map((opt) => (
-
- {({ selected }) => (
- <>
-
- {opt.name}
-
- {selected && (
-
-
-
- )}
- >
- )}
-
- ))}
-
-
-
+ )}
+ chevronClassName="h-4 w-4 text-muted"
+ />
) : (
No shared providers yet. Add one first.
diff --git a/src/components/admin/AdminProvidersPanel.tsx b/src/components/admin/AdminProvidersPanel.tsx
index 55dfc0f..f549790 100644
--- a/src/components/admin/AdminProvidersPanel.tsx
+++ b/src/components/admin/AdminProvidersPanel.tsx
@@ -1,11 +1,11 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
-import { Listbox, Menu, MenuButton, Transition } from '@headlessui/react';
+import { Menu, MenuButton, Transition } from '@headlessui/react';
import { Fragment } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
-import { ChevronUpDownIcon, CheckIcon, DotsHorizontalIcon, PlusIcon } from '@/components/icons/Icons';
+import { DotsHorizontalIcon, PlusIcon } from '@/components/icons/Icons';
import { providerSupportsCustomModel, resolveProviderModels, type TtsModelDefinition, type TtsProviderId } from '@/lib/shared/tts-provider-catalog';
import { defaultBaseUrlForProviderType, defaultModelForProviderType, resolveTtsProviderModelPolicy } from '@/lib/shared/tts-provider-policy';
import {
@@ -14,9 +14,7 @@ import {
Section,
ToggleRow,
inputClass,
- SharedListboxButton,
- SharedListboxOption,
- SharedListboxOptions,
+ Select,
Button,
IconButton,
Input,
@@ -410,8 +408,17 @@ export function AdminProvidersPanel() {
/>
- option.value}
+ renderValue={(option) => option.label}
+ renderOption={(option, { selected }) => (
+
+ {option.label}
+
+ )}
+ chevronClassName="h-4 w-4 text-muted"
onChange={(opt) => {
const nextModel = providerDefaultModel(opt.value);
setForm({
@@ -423,49 +430,29 @@ export function AdminProvidersPanel() {
});
setCustomModelInput('');
}}
- >
-
- {selectedProviderType.label}
-
-
-
-
-
-
- {PROVIDER_TYPE_OPTIONS.map((opt) => (
-
- {({ selected }) => (
- <>
-
- {opt.label}
-
- {selected && (
-
-
-
- )}
- >
- )}
-
- ))}
-
-
-
+ />
-
{
- if (modelId === 'custom') {
+
+ />
{supportsCustomModel && selectedModelId === 'custom' && (
option.value === language)
+ ?? DOCUMENT_LANGUAGE_OPTIONS[0];
const selectedView = viewTypeTextMapping.find(v => v.id === viewType) || viewTypeTextMapping[0];
const isPdfMode = !epub && !html && !!pdf;
const [localPreloadDepth, setLocalPreloadDepth] = useState(segmentPreloadDepthPages);
@@ -183,22 +193,16 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, language, dete
subtitle="Controls sentence splitting and synchronized word alignment."
variant="flat"
>
-