diff --git a/src/components/AudiobookExportModal.tsx b/src/components/AudiobookExportModal.tsx index 71ceeca..ca378e7 100644 --- a/src/components/AudiobookExportModal.tsx +++ b/src/components/AudiobookExportModal.tsx @@ -1,7 +1,7 @@ 'use client'; import { Fragment, useState, useRef, useCallback, useEffect, useMemo } from 'react'; -import { Transition, Listbox, ListboxButton, ListboxOptions, ListboxOption, Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'; +import { Transition, Listbox, Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'; import { useTimeEstimation } from '@/hooks/useTimeEstimation'; import { ProgressPopup } from '@/components/ProgressPopup'; import { ProgressCard } from '@/components/ProgressCard'; @@ -13,7 +13,7 @@ import { VoicesControlBase } from '@/components/player/VoicesControlBase'; import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell'; import { resolveTtsProviderModelPolicy } from '@/lib/shared/tts-provider-policy'; import type { TTSAudiobookChapter, TTSAudiobookFormat } from '@/types/tts'; -import { Button, Card, IconButton, MenuItemClass, cn, listboxButtonClass, listboxOptionClass, listboxPanelClass, menuPanelClass, rangeInputClass } from '@/components/ui'; +import { Button, Card, IconButton, MenuItemClass, SharedListboxButton, SharedListboxOption, SharedListboxOptions, cn, menuPanelClass, rangeInputClass } from '@/components/ui'; import { getAudiobookStatus, deleteAudiobookChapter, @@ -561,40 +561,42 @@ export function AudiobookExportModal({ disabled={chapters.length > 0 || settingsLocked} >
- + {format.toUpperCase()} - + - - + cn(listboxOptionClass(active, selected, 'none'), 'py-2')} + inset="none" + itemClassName="py-2" > {({ selected }) => ( M4B )} - - + cn(listboxOptionClass(active, selected, 'none'), 'py-2')} + inset="none" + itemClassName="py-2" > {({ selected }) => ( MP3 )} - - + +
diff --git a/src/components/auth/UserMenu.tsx b/src/components/auth/UserMenu.tsx index ea437a7..c459eb1 100644 --- a/src/components/auth/UserMenu.tsx +++ b/src/components/auth/UserMenu.tsx @@ -57,16 +57,12 @@ export function UserMenu({ return (
- - - Connect - + + Connect {enableUserSignups && ( - - - Create account - + + Create account )}
diff --git a/src/components/doclist/window/FinderToolbar.tsx b/src/components/doclist/window/FinderToolbar.tsx index 811090a..e403754 100644 --- a/src/components/doclist/window/FinderToolbar.tsx +++ b/src/components/doclist/window/FinderToolbar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'; +import { Listbox } from '@headlessui/react'; import type { IconSize, SortBy, SortDirection, ViewMode } from '@/types/documents'; import { IconsViewIcon, @@ -10,7 +10,7 @@ import { HamburgerIcon, } from './finderIcons'; import { ChevronUpDownIcon } from '@/components/icons/Icons'; -import { SearchField, Toolbar, ToolbarButton, ToolbarGroup, ToolbarSegment, listboxCompactOptionClass, listboxCompactOptionsClass, toolbarButtonStyles } from '@/components/ui'; +import { SearchField, SharedListboxButton, SharedListboxOption, SharedListboxOptions, Toolbar, ToolbarButton, ToolbarGroup, ToolbarSegment } from '@/components/ui'; import type { ReactNode } from 'react'; interface FinderToolbarProps { @@ -145,23 +145,21 @@ export function FinderToolbar({ {directionLabel} - + {currentSort.label} - - + + {SORT_OPTIONS.map((opt) => ( - listboxCompactOptionClass(active, selected)} + tone="compact" > {opt.label} - + ))} - + )} diff --git a/src/components/player/VoicesControlBase.tsx b/src/components/player/VoicesControlBase.tsx index 75aaf57..b415744 100644 --- a/src/components/player/VoicesControlBase.tsx +++ b/src/components/player/VoicesControlBase.tsx @@ -2,16 +2,13 @@ import { Listbox, - ListboxButton, - ListboxOption, - ListboxOptions, } from '@headlessui/react'; import { ChevronUpDownIcon, AudioWaveIcon, CheckIcon } from '@/components/icons/Icons'; import { useEffect, useMemo, useState } from 'react'; import { buildKokoroVoiceString, parseKokoroVoiceNames } from '@/lib/shared/kokoro'; import { type TtsProviderType } from '@/lib/shared/tts-provider-catalog'; import { resolveTtsProviderModelPolicy } from '@/lib/shared/tts-provider-policy'; -import { cn, listboxButtonClass, listboxOptionClass, listboxPanelClass, popoverTriggerClass } from '@/components/ui'; +import { SharedListboxButton, SharedListboxOption, SharedListboxOptions, cn, popoverTriggerClass } from '@/components/ui'; export function VoicesControlBase({ availableVoices, @@ -35,8 +32,9 @@ export function VoicesControlBase({ : 'bottom-full right-0 mb-1'; const buttonClass = variant === 'field' - ? cn(listboxButtonClass, 'bg-surface pr-10') + ? 'bg-surface pr-10' : cn(popoverTriggerClass, 'space-x-0.5 px-1.5 py-0.5 text-xs sm:space-x-1 sm:px-2 sm:py-1 sm:text-sm'); + const buttonTone = variant === 'field' ? 'default' : 'unstyled'; const iconClass = variant === 'field' ? 'h-3.5 w-3.5 shrink-0' @@ -122,7 +120,7 @@ export function VoicesControlBase({ } }} > - + {variant === 'field' ? ( <> @@ -140,13 +138,14 @@ export function VoicesControlBase({ )} - - + + {availableVoices.map((voiceId) => ( - cn(listboxOptionClass(active, selected, 'none'), 'flex items-center gap-2 py-1 sm:py-2')} + inset="none" + itemClassName="flex items-center gap-2 py-1 sm:py-2" > {({ selected }) => ( <> @@ -158,13 +157,13 @@ export function VoicesControlBase({ {voiceId} )} - + ))} - + ) : ( - + {variant === 'field' ? ( <> @@ -182,18 +181,19 @@ export function VoicesControlBase({ )} - - + + {availableVoices.map((voiceId) => ( - cn(listboxOptionClass(active, selected, 'none'), 'py-1 sm:py-2')} + inset="none" + itemClassName="py-1 sm:py-2" > {voiceId} - + ))} - + )} diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 1fca5df..fb9e8ec 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -1,12 +1,16 @@ 'use client'; import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'; +import type { ComponentProps } from 'react'; import { cn } from './cn'; export { segmentedButtonClass, segmentedGroupClass } from './segmented-control'; export 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'; +export const listboxToolbarButtonClass = + 'inline-flex items-center rounded-md border border-line bg-surface px-2 py-1 text-xs text-foreground hover:border-accent-line hover:bg-accent-wash hover:text-accent transition-colors duration-fast ease-standard'; + export const listboxPanelClass = 'z-50 max-h-60 overflow-y-auto overscroll-contain rounded-md bg-surface p-1 shadow-elev-2 ring-1 ring-line focus:outline-none'; @@ -33,6 +37,68 @@ export const listboxCompactOptionClass = (active: boolean, selected = false) => : 'text-foreground', ); +export function SharedListboxButton({ + tone = 'default', + className, + children, + ...props +}: ComponentProps & { + tone?: 'default' | 'toolbar' | 'unstyled'; +}) { + const baseClass = tone === 'toolbar' + ? listboxToolbarButtonClass + : tone === 'unstyled' + ? '' + : listboxButtonClass; + return ( + + {children} + + ); +} + +export function SharedListboxOptions({ + tone = 'default', + className, + children, + ...props +}: ComponentProps & { + tone?: 'default' | 'compact'; +}) { + const baseClass = tone === 'compact' ? listboxCompactOptionsClass : listboxOptionsClass; + return ( + + {children} + + ); +} + +export function SharedListboxOption({ + tone = 'default', + inset = 'check', + itemClassName, + children, + ...props +}: Omit, 'className'> & { + tone?: 'default' | 'compact'; + inset?: 'check' | 'none'; + itemClassName?: string; +}) { + return ( + cn( + tone === 'compact' + ? listboxCompactOptionClass(active, selected) + : listboxOptionClass(active, selected, inset), + itemClassName, + )} + {...props} + > + {children} + + ); +} + export function Select({ value, onChange, @@ -46,22 +112,22 @@ export function Select({ return ( - + {activeOption?.label ?? 'Select'} v - - + + {options.map((option) => ( - listboxOptionClass(active, selected)}> + {({ selected }) => ( <> {selected ? '*' : ''} {option.label} )} - + ))} - + ); }