refactor(ui): centralize standard select controls

This commit is contained in:
Richard R 2026-06-06 11:05:36 -06:00
parent 112dbc0da9
commit 8071ae1a61
7 changed files with 235 additions and 325 deletions

View file

@ -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({
<div className="space-y-1.5">
<label className="text-[11px] uppercase tracking-wider font-medium text-soft">Format</label>
{chapters.length === 0 ? (
<Listbox
<Select
value={format}
onChange={(newFormat) => setFormat(newFormat)}
options={['m4b', 'mp3'] as const}
disabled={chapters.length > 0 || settingsLocked}
>
<div className="relative">
<SharedListboxButton className="bg-surface">
<span className="block truncate text-sm font-medium">{format.toUpperCase()}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-4 w-4 text-soft" />
</span>
</SharedListboxButton>
<Transition
as={Fragment}
leave="transition ease-standard duration-fast"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<SharedListboxOptions className="absolute left-0 mt-1 w-full">
<SharedListboxOption
value="m4b"
inset="none"
itemClassName="py-2"
>
{({ selected }) => (
<span className={`block truncate text-sm ${selected ? 'font-medium' : 'font-normal'}`}>
M4B
</span>
)}
</SharedListboxOption>
<SharedListboxOption
value="mp3"
inset="none"
itemClassName="py-2"
>
{({ selected }) => (
<span className={`block truncate text-sm ${selected ? 'font-medium' : 'font-normal'}`}>
MP3
</span>
)}
</SharedListboxOption>
</SharedListboxOptions>
</Transition>
</div>
</Listbox>
renderValue={(option) => (
<span className="text-sm font-medium">{option.toUpperCase()}</span>
)}
renderOption={(option, { selected }) => (
<span className={`block truncate text-sm ${selected ? 'font-medium' : 'font-normal'}`}>
{option.toUpperCase()}
</span>
)}
buttonClassName="bg-surface"
chevronClassName="h-4 w-4 text-soft"
optionInset="none"
optionItemClassName="py-2"
showCheckmark={false}
/>
) : (
<div className="text-sm font-medium text-foreground py-1.5 pl-3">{format.toUpperCase()}</div>
)}

View file

@ -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.
</p>
) : (
<Listbox
<Select
value={selectedProviderOption!}
options={ttsProviders}
getOptionKey={(provider) => provider.id}
renderValue={(provider) => provider.name}
renderOption={(provider, { selected }) => (
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{provider.name}
</span>
)}
onChange={(provider) => {
const defaults = resolveProviderDefaults({
providerRef: provider.id,
@ -675,46 +677,7 @@ export function SettingsModal({
}
setCustomModelInput('');
}}
>
<SharedListboxButton>
<span className="block truncate">
{selectedProviderOption?.name || 'Select Provider'}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-soft" />
</span>
</SharedListboxButton>
<Transition
as={Fragment}
leave="transition ease-standard duration-fast"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<SharedListboxOptions
anchor="bottom start"
>
{ttsProviders.map((provider) => (
<SharedListboxOption
key={provider.id}
value={provider}
>
{({ selected }) => (
<>
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{provider.name}
</span>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-accent">
<CheckIcon className="h-5 w-5" />
</span>
)}
</>
)}
</SharedListboxOption>
))}
</SharedListboxOptions>
</Transition>
</Listbox>
/>
)}
</div>
{restrictUserApiKeys && (
@ -768,8 +731,30 @@ export function SettingsModal({
</p>
)}
<div className="flex flex-col gap-2">
<Listbox
value={ttsModels.find(m => m.id === selectedModelId) || ttsModels[0]}
<Select
value={selectedModel}
options={ttsModels}
getOptionKey={(model) => model.id}
renderValue={(model) => (
<span className="block">
<span className="block truncate">{model.name}</span>
{selectedModelVersion ? (
<span className="block truncate text-xs text-soft">
{selectedModelVersion}
</span>
) : null}
</span>
)}
renderOption={(model, { selected }) => (
<span className={`block ${selected ? 'font-medium' : 'font-normal'}`}>
<span className="block truncate">{model.name}</span>
{model.id.includes(':') ? (
<span className="block truncate text-xs text-soft">
{model.id.slice(model.id.indexOf(':'))}
</span>
) : null}
</span>
)}
onChange={(model) => {
if (model.id === 'custom') {
setModelValue(customModelInput);
@ -778,62 +763,7 @@ export function SettingsModal({
setCustomModelInput('');
}
}}
>
<SharedListboxButton>
{selectedModel ? (
<span className="block">
<span className="block truncate">
{selectedModel.name}
</span>
{selectedModelVersion && (
<span className="block truncate text-xs text-soft">
{selectedModelVersion}
</span>
)}
</span>
) : (
<span className="block truncate">Select Model</span>
)}
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-soft" />
</span>
</SharedListboxButton>
<Transition
as={Fragment}
leave="transition ease-standard duration-fast"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<SharedListboxOptions
anchor="bottom start"
>
{ttsModels.map((model) => (
<SharedListboxOption
key={model.id}
value={model}
>
{({ selected }) => (
<>
<span className={`block ${selected ? 'font-medium' : 'font-normal'}`}>
<span className="block truncate">{model.name}</span>
{model.id.includes(':') && (
<span className="block truncate text-xs text-soft">
{model.id.slice(model.id.indexOf(':'))}
</span>
)}
</span>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-accent">
<CheckIcon className="h-5 w-5" />
</span>
)}
</>
)}
</SharedListboxOption>
))}
</SharedListboxOptions>
</Transition>
</Listbox>
/>
{supportsCustom && selectedModelId === 'custom' && (
<Input

View file

@ -1,18 +1,14 @@
'use client';
import { Fragment, useEffect, useMemo, useState } from 'react';
import { Listbox, Transition } from '@headlessui/react';
import { useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
import { ChevronUpDownIcon, CheckIcon } from '@/components/icons/Icons';
import {
Badge,
Section,
ToggleRow,
inputClass,
SharedListboxButton,
SharedListboxOption,
SharedListboxOptions,
Select,
Button,
} from '@/components/ui';
import { type TtsProviderId } from '@/lib/shared/tts-provider-catalog';
@ -204,42 +200,19 @@ export function AdminFeaturesPanel() {
<div className="shrink-0">{renderSource('defaultTtsProvider')}</div>
</div>
{providerOptions.length > 0 ? (
<Listbox value={selectedProviderOption} onChange={handleProviderChange}>
<SharedListboxButton>
<span className="block truncate">{selectedProviderOption?.name ?? 'Select provider'}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-4 w-4 text-muted" />
<Select
value={selectedProviderOption}
onChange={handleProviderChange}
options={providerOptions}
getOptionKey={(option) => option.id}
renderValue={(option) => option.name}
renderOption={(option, { selected }) => (
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{option.name}
</span>
</SharedListboxButton>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<SharedListboxOptions anchor="bottom start">
{providerOptions.map((opt) => (
<SharedListboxOption
key={opt.id}
value={opt}
>
{({ selected }) => (
<>
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{opt.name}
</span>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-accent">
<CheckIcon className="h-5 w-5" />
</span>
)}
</>
)}
</SharedListboxOption>
))}
</SharedListboxOptions>
</Transition>
</Listbox>
)}
chevronClassName="h-4 w-4 text-muted"
/>
) : (
<div className="px-0.5 py-2 text-sm text-muted">
No shared providers yet. Add one first.

View file

@ -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() {
/>
</Field>
<Field label="Provider type">
<Listbox
<Select
value={selectedProviderType}
options={PROVIDER_TYPE_OPTIONS}
getOptionKey={(option) => option.value}
renderValue={(option) => option.label}
renderOption={(option, { selected }) => (
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{option.label}
</span>
)}
chevronClassName="h-4 w-4 text-muted"
onChange={(opt) => {
const nextModel = providerDefaultModel(opt.value);
setForm({
@ -423,49 +430,29 @@ export function AdminProvidersPanel() {
});
setCustomModelInput('');
}}
>
<SharedListboxButton>
<span className="block truncate">{selectedProviderType.label}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-4 w-4 text-muted" />
</span>
</SharedListboxButton>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<SharedListboxOptions anchor="bottom start">
{PROVIDER_TYPE_OPTIONS.map((opt) => (
<SharedListboxOption
key={opt.value}
value={opt}
>
{({ selected }) => (
<>
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{opt.label}
</span>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-accent">
<CheckIcon className="h-5 w-5" />
</span>
)}
</>
)}
</SharedListboxOption>
))}
</SharedListboxOptions>
</Transition>
</Listbox>
/>
</Field>
<Field label="Default model" hint="Pre-selected for users picking this provider.">
<div className="space-y-2">
<Listbox
value={selectedModelId}
onChange={(modelId: string) => {
if (modelId === 'custom') {
<Select
value={selectedModelDefinition}
options={modelDefinitions}
getOptionKey={(model) => model.id}
renderValue={(model) => model.name}
renderOption={(model, { selected }) => (
<span className={`block ${selected ? 'font-medium' : 'font-normal'}`}>
<span className="block truncate">{model.name}</span>
{model.id.includes(':') ? (
<span className="block truncate text-xs text-muted">
{model.id.slice(model.id.indexOf(':'))}
</span>
) : null}
</span>
)}
placeholder="Select model"
chevronClassName="h-4 w-4 text-muted"
onChange={(model) => {
if (model.id === 'custom') {
const nextModel = customModelInput.trim();
setForm({
...form,
@ -476,54 +463,12 @@ export function AdminProvidersPanel() {
}
setForm({
...form,
defaultModel: modelId,
defaultInstructions: modelSupportsInstructions(modelId) ? form.defaultInstructions : '',
defaultModel: model.id,
defaultInstructions: modelSupportsInstructions(model.id) ? form.defaultInstructions : '',
});
setCustomModelInput('');
}}
>
<SharedListboxButton>
<span className="block truncate">
{selectedModelDefinition?.name ?? 'Select model'}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-4 w-4 text-muted" />
</span>
</SharedListboxButton>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<SharedListboxOptions anchor="bottom start">
{modelDefinitions.map((model) => (
<SharedListboxOption
key={model.id}
value={model.id}
>
{({ selected }) => (
<>
<span className={`block ${selected ? 'font-medium' : 'font-normal'}`}>
<span className="block truncate">{model.name}</span>
{model.id.includes(':') && (
<span className="block truncate text-xs text-muted">
{model.id.slice(model.id.indexOf(':'))}
</span>
)}
</span>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-accent">
<CheckIcon className="h-5 w-5" />
</span>
)}
</>
)}
</SharedListboxOption>
))}
</SharedListboxOptions>
</Transition>
</Listbox>
/>
{supportsCustomModel && selectedModelId === 'custom' && (
<Input
type="text"

View file

@ -16,7 +16,15 @@ import {
clampSegmentPreloadSentenceLookahead,
clampTtsSegmentMaxBlockLength,
} from '@/types/config';
import { IconButton, RangeInput, Section, ToggleRow, CheckItem, SegmentedControl } from '@/components/ui';
import {
IconButton,
RangeInput,
Section,
ToggleRow,
CheckItem,
SegmentedControl,
Select,
} from '@/components/ui';
import { RefreshIcon, SparkleIcon } from '@/components/icons/Icons';
import type { ParsedPdfBlockKind, PdfParseStatus } from '@/types/parsed-pdf';
import { isForceReparseDisabled } from '@/lib/client/pdf/force-reparse';
@ -148,6 +156,8 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, language, dete
voice,
documentLanguage: resolvedLanguage,
});
const selectedLanguage = DOCUMENT_LANGUAGE_OPTIONS.find((option) => 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"
>
<label className="block space-y-1.5">
<div className="space-y-1.5">
<span className="block text-[11px] font-semibold uppercase tracking-wide text-muted">
Document language
</span>
<select
value={language}
onChange={(event) => onLanguageChange(event.target.value)}
className="w-full rounded-md border border-offbase bg-surface-solid px-3 py-2 text-sm text-foreground"
>
{DOCUMENT_LANGUAGE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
<Select
value={selectedLanguage}
onChange={(option) => onLanguageChange(option.value)}
options={DOCUMENT_LANGUAGE_OPTIONS}
/>
</div>
{language === 'auto' && detectedLanguage ? (
<p className="text-xs text-soft">
Detected from document metadata: {getLanguageDisplayName(detectedLanguage)}

View file

@ -1,9 +1,9 @@
'use client';
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react';
import type { ComponentProps } from 'react';
import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react';
import { Fragment, type ComponentProps, type Key, type ReactNode } from 'react';
import { cn } from './cn';
import { CheckIcon, ChevronRightIcon } from '@/components/icons/Icons';
import { CheckIcon, ChevronUpDownIcon } from '@/components/icons/Icons';
const listboxButtonClass =
'relative w-full cursor-pointer rounded-md bg-surface-sunken border border-line py-1.5 pl-2.5 pr-9 text-left text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent-line hover:bg-accent-wash transition-colors duration-fast ease-standard';
@ -104,39 +104,92 @@ export function SharedListboxOption({
);
}
export function Select({
export type SelectOption = {
value: string;
label: string;
};
export function Select<T = SelectOption>({
value,
onChange,
options,
getOptionKey,
renderValue,
renderOption,
placeholder = 'Select',
disabled,
buttonClassName,
optionsClassName,
optionInset = 'check',
optionItemClassName,
showCheckmark = true,
chevronClassName = 'h-5 w-5 text-soft',
}: {
value: string;
onChange: (value: string) => void;
options: Array<{ value: string; label: string }>;
value: T | undefined;
onChange: (value: T) => void;
options: readonly T[];
getOptionKey?: (option: T) => Key;
renderValue?: (option: T) => ReactNode;
renderOption?: (option: T, state: { selected: boolean }) => ReactNode;
placeholder?: ReactNode;
disabled?: boolean;
buttonClassName?: string;
optionsClassName?: string;
optionInset?: 'check' | 'none';
optionItemClassName?: string;
showCheckmark?: boolean;
chevronClassName?: string;
}) {
const activeOption = options.find((option) => option.value === value) ?? options[0];
const defaultKey = (option: T): Key => {
if (typeof option === 'string' || typeof option === 'number') return option;
const record = option as Record<string, unknown>;
return String(record.value ?? record.id ?? record.label);
};
const defaultRender = (option: T): ReactNode => {
if (typeof option === 'string' || typeof option === 'number') return String(option);
const record = option as Record<string, unknown>;
return String(record.label ?? record.name ?? record.value ?? record.id ?? '');
};
const optionKey = getOptionKey ?? defaultKey;
const valueRenderer = renderValue ?? defaultRender;
const optionRenderer = renderOption ?? ((option: T) => valueRenderer(option));
return (
<Listbox value={value} onChange={onChange}>
<SharedListboxButton>
<span>{activeOption?.label ?? 'Select'}</span>
<span className="pointer-events-none absolute inset-y-0 right-2 flex items-center text-soft">
<ChevronRightIcon className="h-4 w-4 rotate-90" aria-hidden="true" />
<Listbox value={value} onChange={onChange} disabled={disabled}>
<SharedListboxButton className={buttonClassName}>
<span className="block truncate">{value === undefined ? placeholder : valueRenderer(value)}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className={chevronClassName} aria-hidden="true" />
</span>
</SharedListboxButton>
<SharedListboxOptions anchor="bottom">
{options.map((option) => (
<SharedListboxOption key={option.value} value={option.value}>
{({ selected }) => (
<>
<span className="absolute left-2 flex items-center text-accent">
{selected ? <CheckIcon className="h-4 w-4" aria-hidden="true" /> : null}
</span>
<span>{option.label}</span>
</>
)}
</SharedListboxOption>
))}
</SharedListboxOptions>
<Transition
as={Fragment}
leave="transition ease-standard duration-fast"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<SharedListboxOptions anchor="bottom start" className={optionsClassName}>
{options.map((option) => (
<SharedListboxOption
key={optionKey(option)}
value={option}
inset={optionInset}
itemClassName={optionItemClassName}
>
{({ selected }) => (
<>
{optionRenderer(option, { selected })}
{showCheckmark && selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-accent">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</SharedListboxOption>
))}
</SharedListboxOptions>
</Transition>
</Listbox>
);
}

View file

@ -0,0 +1,31 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { describe, expect, test } from 'vitest';
const STANDARD_SELECT_CONSUMERS = [
'src/components/SettingsModal.tsx',
'src/components/documents/DocumentSettings.tsx',
'src/components/admin/AdminFeaturesPanel.tsx',
'src/components/admin/AdminProvidersPanel.tsx',
'src/components/AudiobookExportModal.tsx',
];
describe('shared Select consumers', () => {
test('standard settings dropdowns use the high-level UI primitive', () => {
for (const relativePath of STANDARD_SELECT_CONSUMERS) {
const source = readFileSync(resolve(process.cwd(), relativePath), 'utf8');
expect(source, relativePath).toContain('<Select');
expect(source, relativePath).not.toContain('<Listbox');
expect(source, relativePath).not.toContain('<SharedListboxButton');
expect(source, relativePath).not.toContain('<SharedListboxOptions');
expect(source, relativePath).not.toContain('<SharedListboxOption');
}
});
test('the shared Select owns standard dropdown chrome', () => {
const source = readFileSync(resolve(process.cwd(), 'src/components/ui/select.tsx'), 'utf8');
expect(source).toContain('ChevronUpDownIcon');
expect(source).toContain('CheckIcon');
expect(source).toContain('<Transition');
});
});