refactor(components): unify button styling using buttonClass utility across multiple components

This commit is contained in:
Richard R 2026-05-14 12:06:21 -06:00
parent 205d8f37e4
commit c3fb3af3d3
18 changed files with 156 additions and 78 deletions

View file

@ -9,6 +9,7 @@ import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext
import { showPrivacyModal } from '@/components/PrivacyModal';
import { GithubIcon } from '@/components/icons/Icons';
import { LoadingSpinner } from '@/components/Spinner';
import { buttonClass } from '@/components/formPrimitives';
function SessionExpiredLoader({ setSessionExpired }: { setSessionExpired: (v: boolean) => void }) {
const searchParams = useSearchParams();
@ -196,10 +197,7 @@ function SignInContent() {
type="submit"
disabled={isAnyLoading}
onClick={handleSignIn}
className="w-full rounded-lg bg-accent py-2 text-sm font-medium text-background
hover:bg-secondary-accent focus:outline-none focus:ring-2 focus:ring-accent
focus:ring-offset-2 disabled:opacity-50 transform transition-transform
duration-200 hover:scale-[1.02]"
className={buttonClass({ variant: 'primary', size: 'md', className: 'w-full hover:scale-[1.02]' })}
>
{loadingEmail ? <LoadingSpinner className="w-4 h-4 mx-auto" /> : 'Connect'}
</Button>
@ -210,11 +208,11 @@ function SignInContent() {
type="button"
disabled={isAnyLoading}
onClick={handleGithubSignIn}
className="w-full rounded-lg bg-background py-2 text-sm font-medium text-foreground
hover:bg-offbase focus:outline-none focus:ring-2 focus:ring-accent
focus:ring-offset-2 disabled:opacity-50 border border-offbase
transform transition-transform duration-200 hover:scale-[1.02]
flex items-center justify-center gap-2"
className={buttonClass({
variant: 'outline',
size: 'md',
className: 'w-full hover:scale-[1.02] flex items-center justify-center gap-2',
})}
>
{loadingGithub ? (
<LoadingSpinner className="w-4 h-4" />
@ -233,10 +231,7 @@ function SignInContent() {
type="button"
disabled={isAnyLoading}
onClick={handleAnonymousContinue}
className="w-full rounded-lg bg-background py-2 text-sm font-medium text-foreground
hover:bg-offbase focus:outline-none focus:ring-2 focus:ring-accent
focus:ring-offset-2 disabled:opacity-50 border border-offbase
transform transition-transform duration-200 hover:scale-[1.02]"
className={buttonClass({ variant: 'outline', size: 'md', className: 'w-full hover:scale-[1.02]' })}
>
{loadingAnonymous ? <LoadingSpinner className="w-4 h-4 mx-auto" /> : 'Continue anonymously'}
</Button>

View file

@ -8,6 +8,7 @@ import { getAuthClient } from '@/lib/client/auth-client';
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { showPrivacyModal } from '@/components/PrivacyModal';
import { LoadingSpinner } from '@/components/Spinner';
import { buttonClass } from '@/components/formPrimitives';
import toast from 'react-hot-toast';
export default function SignUpPage() {
@ -151,7 +152,11 @@ export default function SignUpPage() {
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted hover:text-foreground"
className={buttonClass({
variant: 'ghost',
size: 'icon',
className: 'absolute right-2 top-1/2 -translate-y-1/2 h-7 w-7 text-muted',
})}
>
{showPassword ? '👁️' : '👁️‍🗨️'}
</button>
@ -213,10 +218,7 @@ export default function SignUpPage() {
type="submit"
disabled={loading}
onClick={handleSignUp}
className="w-full rounded-lg bg-accent py-2 text-sm font-medium text-background
hover:bg-secondary-accent focus:outline-none focus:ring-2 focus:ring-accent
focus:ring-offset-2 disabled:opacity-50 transform transition-transform
duration-200 hover:scale-[1.02]"
className={buttonClass({ variant: 'primary', size: 'md', className: 'w-full hover:scale-[1.02]' })}
>
{loading ? <LoadingSpinner className="w-4 h-4 mx-auto" /> : 'Create Account'}
</Button>

View file

@ -2,6 +2,7 @@
import { useState } from 'react';
import { CopyIcon, CheckIcon } from '@/components/icons/Icons';
import { buttonClass } from '@/components/formPrimitives';
export function CodeBlock({ children }: { children: string }) {
const [copied, setCopied] = useState(false);
@ -20,9 +21,11 @@ export function CodeBlock({ children }: { children: string }) {
</pre>
<button
onClick={handleCopy}
className="absolute top-2 right-2 p-1.5 rounded-md bg-base hover:bg-offbase hover:text-accent
transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100
hover:scale-[1.05]"
className={buttonClass({
variant: 'secondary',
size: 'icon',
className: 'absolute top-2 right-2 h-7 w-7 opacity-0 group-hover:opacity-100 focus:opacity-100 hover:scale-[1.05]',
})}
title="Copy to clipboard"
>
{copied ? <CheckIcon className="w-4 h-4" /> : <CopyIcon className="w-4 h-4" />}

View file

@ -1,5 +1,6 @@
import { Fragment, KeyboardEvent } from 'react';
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
import { buttonClass } from '@/components/formPrimitives';
interface ConfirmDialogProps {
isOpen: boolean;
@ -75,23 +76,22 @@ export function ConfirmDialog({
<div className="mt-6 flex justify-end space-x-3">
<button
type="button"
className="inline-flex justify-center rounded-lg bg-background px-3 py-1.5 text-sm
font-medium text-foreground hover:bg-offbase focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-accent"
className={buttonClass({
variant: 'outline',
size: 'sm',
className: 'hover:scale-[1.04]',
})}
onClick={onClose}
>
{cancelText}
</button>
<button
type="button"
className={`inline-flex justify-center rounded-lg px-3 py-1.5 text-sm text-wrap
font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
transform transition-transform duration-200 ease-in-out hover:scale-[1.04]
${isDangerous
? 'bg-accent text-background hover:bg-secondary-accent focus-visible:ring-accent'
: 'bg-accent text-background hover:bg-secondary-accent focus-visible:ring-accent'
}`}
className={buttonClass({
variant: isDangerous ? 'danger' : 'primary',
size: 'sm',
className: 'text-wrap hover:scale-[1.04]',
})}
onClick={onConfirm}
>
{confirmText}

View file

@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import Link from 'next/link';
import { getConsentState, setConsentState } from '@/lib/client/analytics';
import { Transition } from '@headlessui/react';
import { buttonClass } from '@/components/formPrimitives';
export function CookieConsentBanner() {
const [show, setShow] = useState(false);
@ -56,13 +57,21 @@ export function CookieConsentBanner() {
<div className="flex flex-col gap-3 min-w-fit sm:flex-row">
<button
onClick={handleDecline}
className="whitespace-nowrap rounded-lg px-4 py-2.5 text-sm font-medium text-foreground hover:bg-offbase focus:outline-none focus-visible:ring-2 focus-visible:ring-accent transition-colors"
className={buttonClass({
variant: 'ghost',
size: 'md',
className: 'whitespace-nowrap',
})}
>
Decline Non-Essential
</button>
<button
onClick={handleAccept}
className="whitespace-nowrap rounded-lg bg-accent px-6 py-2.5 text-sm font-bold text-background hover:bg-secondary-accent shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-accent transition-transform hover:scale-[1.02]"
className={buttonClass({
variant: 'primary',
size: 'md',
className: 'whitespace-nowrap font-bold shadow-sm',
})}
>
Accept All
</button>

View file

@ -1,3 +1,5 @@
import { buttonClass } from '@/components/formPrimitives';
interface ProgressCardProps {
progress: number;
estimatedTimeRemaining?: string;
@ -52,7 +54,11 @@ export function ProgressCard({
</div>
<button
type="button"
className="shrink-0 inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium text-foreground hover:text-accent hover:bg-background/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent transform transition-transform duration-200 ease-in-out hover:scale-[1.04]"
className={buttonClass({
variant: 'ghost',
size: 'xs',
className: 'shrink-0 hover:bg-background/50 hover:scale-[1.04]',
})}
onClick={(e) => onCancel(e)}
>
<span>{cancelText}</span>

View file

@ -55,10 +55,7 @@ import { AdminFeaturesPanel } from '@/components/admin/AdminFeaturesPanel';
import { useSharedProviders } from '@/hooks/useSharedProviders';
import { useLibraryDocumentsQuery } from '@/hooks/useLibraryDocumentsQuery';
import {
btnDanger,
btnOutline,
btnPrimary,
btnSecondary,
buttonClass,
inputClass,
listboxButtonClass,
listboxOptionClass,
@ -509,10 +506,6 @@ export function SettingsModal({ className = '' }: { className?: string }) {
const fieldLabelClass = 'block text-[11px] font-semibold uppercase tracking-wide text-muted';
const sectionShellClass = 'space-y-2 pb-3 border-b border-offbase px-0.5';
const sectionHeadingClass = 'text-sm font-semibold text-foreground';
const accountBtnBase = 'inline-flex items-center justify-center rounded-lg text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 transform transition-transform duration-200 ease-in-out';
const accountBtnPrimary = `${accountBtnBase} bg-accent text-background hover:bg-secondary-accent hover:scale-[1.04]`;
const accountBtnOutline = `${accountBtnBase} bg-background border border-offbase text-foreground hover:bg-offbase hover:text-accent hover:scale-[1.04]`;
const accountBtnDanger = `${accountBtnBase} bg-red-600 text-white border border-red-700 hover:bg-red-700 hover:scale-[1.02]`;
const effectiveProviderType = resolveEffectiveProviderType({
providerRef: selectedProviderRef,
providerType: localProviderType,
@ -881,7 +874,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
<div className="pt-4 flex justify-end gap-2">
<Button
type="button"
className={`${btnSecondary} px-4 py-2`}
className={buttonClass({ variant: 'secondary', size: 'md' })}
onClick={async () => {
const defaults = resolveProviderDefaults({
providerRef: runtimeConfig.defaultTtsProvider,
@ -901,7 +894,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
<Button
data-testid="settings-save-button"
type="button"
className={`${btnPrimary} px-4 py-2`}
className={buttonClass({ variant: 'primary', size: 'md' })}
disabled={!canSubmit}
onClick={async () => {
const defaults = resolveProviderDefaults({
@ -1167,7 +1160,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
<Button
onClick={handleImportLibrary}
disabled={isBusy}
className={`${btnOutline} px-4 py-2 disabled:opacity-50`}
className={buttonClass({ variant: 'outline', size: 'md' })}
>
{isImportingLibrary ? `Importing... ${Math.round(progress)}%` : 'Import from library'}
</Button>
@ -1179,14 +1172,14 @@ export function SettingsModal({ className = '' }: { className?: string }) {
<Button
onClick={handleRefresh}
disabled={isBusy}
className={`${btnOutline} px-4 py-2 disabled:opacity-50`}
className={buttonClass({ variant: 'outline', size: 'md' })}
>
Refresh
</Button>
<Button
onClick={handleClearCache}
disabled={isBusy}
className={`${btnOutline} px-4 py-2 disabled:opacity-50`}
className={buttonClass({ variant: 'outline', size: 'md' })}
>
Clear cache
</Button>
@ -1194,7 +1187,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
<Button
onClick={() => setShowDeleteDocsConfirm(true)}
disabled={isBusy}
className={`${btnDanger} px-4 py-2`}
className={buttonClass({ variant: 'danger', size: 'md' })}
>
Delete all data
</Button>
@ -1288,7 +1281,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
<>
<Button
onClick={handleSignOut}
className={`${accountBtnOutline} px-4 py-2`}
className={buttonClass({ variant: 'outline', size: 'md', className: 'hover:scale-[1.04]' })}
>
Disconnect account
</Button>
@ -1297,7 +1290,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
<label className="block text-sm font-medium text-red-500 mb-2">Danger Zone</label>
<Button
onClick={() => setShowDeleteAccountConfirm(true)}
className={`${accountBtnDanger} px-4 py-2`}
className={buttonClass({ variant: 'danger', size: 'md' })}
>
Delete Account
</Button>
@ -1315,17 +1308,17 @@ export function SettingsModal({ className = '' }: { className?: string }) {
</p>
<div className="flex flex-wrap gap-2">
<Link href="/signin">
<Button className={`${accountBtnOutline} px-4 py-2`}>
<Button className={buttonClass({ variant: 'outline', size: 'md', className: 'hover:scale-[1.04]' })}>
Connect
</Button>
</Link>
<Link href="/signup">
<Button className={`${accountBtnPrimary} px-4 py-2`}>
<Button className={buttonClass({ variant: 'primary', size: 'md', className: 'hover:scale-[1.04]' })}>
Create account
</Button>
</Link>
<Link href="/?redirect=false">
<Button className={`${accountBtnOutline} px-4 py-2`}>
<Button className={buttonClass({ variant: 'outline', size: 'md', className: 'hover:scale-[1.04]' })}>
Back to landing page
</Button>
</Link>

View file

@ -9,8 +9,7 @@ import {
Badge,
Section,
ToggleRow,
btnPrimary,
btnSecondary,
buttonClass,
inputClass,
listboxButtonClass,
listboxOptionClass,
@ -340,14 +339,14 @@ export function AdminFeaturesPanel() {
<Button
onClick={discardAll}
disabled={dirty.size === 0 || saving}
className={`${btnSecondary} px-4 py-1.5`}
className={buttonClass({ variant: 'secondary', size: 'sm' })}
>
Discard
</Button>
<Button
onClick={saveAll}
disabled={dirty.size === 0 || saving}
className={`${btnPrimary} px-4 py-1.5`}
className={buttonClass({ variant: 'primary', size: 'sm' })}
>
{saving ? 'Saving…' : dirty.size > 0 ? `Save (${dirty.size})` : 'Save'}
</Button>

View file

@ -13,10 +13,7 @@ import {
Field,
Section,
ToggleRow,
btnDanger,
btnOutline,
btnPrimary,
btnSecondary,
buttonClass,
inputClass,
listboxButtonClass,
listboxOptionClass,
@ -261,7 +258,7 @@ export function AdminProvidersPanel() {
{!editingId ? (
<Button
onClick={startCreate}
className={`${btnPrimary} h-7 w-7 p-0 inline-flex items-center justify-center`}
className={buttonClass({ variant: 'primary', size: 'icon', className: 'h-7 w-7' })}
aria-label="Add provider"
title="Add provider"
>
@ -486,13 +483,13 @@ export function AdminProvidersPanel() {
/>
<div className="pt-1 flex justify-end gap-2">
<Button onClick={cancelEdit} className={`${btnSecondary} px-4 py-1.5`}>
<Button onClick={cancelEdit} className={buttonClass({ variant: 'secondary', size: 'sm' })}>
Cancel
</Button>
<Button
onClick={submit}
disabled={submitDisabled}
className={`${btnPrimary} px-4 py-1.5`}
className={buttonClass({ variant: 'primary', size: 'sm' })}
>
{saveMutation.isPending ? 'Saving…' : isEditingExisting ? 'Save changes' : 'Create'}
</Button>
@ -526,14 +523,14 @@ export function AdminProvidersPanel() {
<div className="shrink-0 flex gap-1.5">
<Button
onClick={() => startEdit(p)}
className={`${btnOutline} px-2.5 py-1 text-xs`}
className={buttonClass({ variant: 'outline', size: 'xs', className: 'h-7' })}
disabled={!!editingId || deleteMutation.isPending}
>
Edit
</Button>
<Button
onClick={() => remove(p.id)}
className={`${btnDanger} px-2.5 py-1 text-xs`}
className={buttonClass({ variant: 'danger', size: 'xs', className: 'h-7' })}
disabled={!!editingId || deleteMutation.isPending}
>
Delete

View file

@ -4,8 +4,10 @@ export {
Field,
Section,
ToggleRow,
buttonClass,
btnBase,
btnDanger,
btnGhost,
btnOutline,
btnPrimary,
btnSecondary,

View file

@ -7,6 +7,7 @@ import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext
import { useAuthSession } from '@/hooks/useAuthSession';
import { getAuthClient } from '@/lib/client/auth-client';
import { LoadingSpinner } from '@/components/Spinner';
import { buttonClass } from '@/components/formPrimitives';
function sleep(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms));
@ -266,7 +267,7 @@ export function AuthLoader({ children }: { children: ReactNode }) {
setBootstrapError(null);
setRetryNonce((v) => v + 1);
}}
className="rounded-md bg-accent px-3 py-1.5 text-sm font-medium text-background hover:bg-secondary-accent focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2"
className={buttonClass({ variant: 'primary', size: 'sm' })}
>
Retry
</button>

View file

@ -14,6 +14,7 @@ import { CreateFolderDialog } from '@/components/doclist/CreateFolderDialog';
import { DocumentListSkeleton } from '@/components/doclist/DocumentListSkeleton';
import { Button } from '@headlessui/react';
import { DocumentUploader } from '@/components/documents/DocumentUploader';
import { buttonClass } from '@/components/formPrimitives';
type DocumentToDelete = {
id: string;
@ -331,7 +332,11 @@ export function DocumentList() {
<p className="text-sm text-foreground">Drag files on top of each other to make folders</p>
<Button
onClick={() => setShowHint(false)}
className="p-1 rounded-md hover:bg-base hover:text-accent transition-colors"
className={buttonClass({
variant: 'ghost',
size: 'icon',
className: 'h-7 w-7 hover:bg-base',
})}
aria-label="Dismiss hint"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View file

@ -7,6 +7,7 @@ import { DocumentListDocument } from '@/types/documents';
import { DocumentPreview } from '@/components/doclist/DocumentPreview';
import { useAuthSession } from '@/hooks/useAuthSession';
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
import { buttonClass } from '@/components/formPrimitives';
interface DocumentListItemProps {
doc: DocumentListDocument;
@ -116,7 +117,11 @@ export function DocumentListItem({
{showDeleteButton && (
<Button
onClick={() => onDelete(doc)}
className="ml-1 p-1.5 text-muted hover:text-accent rounded-md hover:bg-offbase transition-colors"
className={buttonClass({
variant: 'ghost',
size: 'icon',
className: 'ml-1 h-7 w-7 text-muted hover:bg-offbase',
})}
aria-label="Delete document"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -155,7 +160,11 @@ export function DocumentListItem({
{showDeleteButton && (
<Button
onClick={() => onDelete(doc)}
className="ml-1 p-1.5 text-muted hover:text-accent rounded-md hover:bg-offbase transition-colors"
className={buttonClass({
variant: 'ghost',
size: 'icon',
className: 'ml-1 h-7 w-7 text-muted hover:bg-offbase',
})}
aria-label="Delete document"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View file

@ -3,6 +3,7 @@
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
import { Fragment, useEffect, useState } from 'react';
import { BaseDocument } from '@/types/documents';
import { buttonClass } from '@/components/formPrimitives';
interface DocumentSelectionModalProps {
isOpen: boolean;
@ -207,14 +208,14 @@ export function DocumentSelectionModal({
<div className="mt-4 flex justify-end gap-3 flex-shrink-0">
<button
type="button"
className="inline-flex justify-center rounded-lg bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-offbase focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2"
className={buttonClass({ variant: 'outline', size: 'md' })}
onClick={onClose}
>
Cancel
</button>
<button
type="button"
className="inline-flex justify-center rounded-lg bg-accent px-4 py-2 text-sm font-medium text-background hover:bg-secondary-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
className={buttonClass({ variant: 'primary', size: 'md' })}
onClick={handleConfirmClick}
disabled={isLoading || selectedCount === 0 || isProcessing}
>

View file

@ -1,3 +1,5 @@
import { buttonClass } from '@/components/formPrimitives';
export function ZoomControl({
value,
onIncrease,
@ -17,7 +19,11 @@ export function ZoomControl({
type="button"
onClick={onDecrease}
disabled={value <= min}
className="px-1 text-sm leading-none text-foreground hover:text-accent disabled:opacity-50 disabled:cursor-not-allowed transform transition-transform duration-200 ease-in-out hover:scale-[1.09]"
className={buttonClass({
variant: 'ghost',
size: 'icon',
className: 'h-6 w-6 text-sm leading-none hover:scale-[1.09]',
})}
aria-label="Zoom out"
>
@ -27,7 +33,11 @@ export function ZoomControl({
type="button"
onClick={onIncrease}
disabled={value >= max}
className="px-1 text-sm leading-none text-foreground hover:text-accent disabled:opacity-50 disabled:cursor-not-allowed transform transition-transform duration-200 ease-in-out hover:scale-[1.09]"
className={buttonClass({
variant: 'ghost',
size: 'icon',
className: 'h-6 w-6 text-sm leading-none hover:scale-[1.09]',
})}
aria-label="Zoom in"
>

View file

@ -13,6 +13,38 @@ export const btnPrimary = `${btnBase} bg-accent text-background hover:bg-seconda
export const btnSecondary = `${btnBase} bg-base text-foreground border border-offbase hover:bg-offbase hover:scale-[1.03]`;
export const btnOutline = `${btnBase} bg-background border border-offbase text-foreground hover:bg-base hover:text-accent hover:scale-[1.02]`;
export const btnDanger = `${btnBase} bg-red-600 text-white border border-red-700 hover:bg-red-700 hover:scale-[1.02]`;
export const btnGhost = `${btnBase} bg-transparent text-foreground hover:bg-base hover:text-accent`;
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'danger' | 'ghost';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'icon';
const BUTTON_VARIANT_CLASS: Record<ButtonVariant, string> = {
primary: btnPrimary,
secondary: btnSecondary,
outline: btnOutline,
danger: btnDanger,
ghost: btnGhost,
};
const BUTTON_SIZE_CLASS: Record<ButtonSize, string> = {
xs: 'h-6 px-2 text-xs rounded-md',
sm: 'h-7 px-2.5 text-xs rounded-md',
md: 'h-8 px-3 text-sm rounded-md',
lg: 'h-10 px-4 text-base rounded-lg',
icon: 'h-8 w-8 rounded-md p-0',
};
export function buttonClass({
variant = 'secondary',
size = 'md',
className = '',
}: {
variant?: ButtonVariant;
size?: ButtonSize;
className?: string;
} = {}) {
return [BUTTON_VARIANT_CLASS[variant], BUTTON_SIZE_CLASS[size], className].filter(Boolean).join(' ');
}
// Inputs use `bg-base` so they remain visible regardless of whether the
// surrounding container is `bg-background` (Card) or `bg-base` (Section).

View file

@ -4,6 +4,7 @@ import { Fragment, type ReactNode } from 'react';
import { Transition } from '@headlessui/react';
import { XCircleIcon } from '@/components/icons/Icons';
import { useReaderSidebarBounds } from '@/hooks/useReaderSidebarBounds';
import { buttonClass } from '@/components/formPrimitives';
interface ReaderSidebarShellProps {
isOpen: boolean;
@ -81,7 +82,11 @@ export function ReaderSidebarShell({
onClick={onClose}
aria-label="Close"
title="Close"
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border border-offbase bg-base text-muted hover:bg-offbase hover:text-accent transition-colors"
className={buttonClass({
variant: 'secondary',
size: 'icon',
className: 'h-8 w-8 text-muted',
})}
>
<XCircleIcon className="w-4 h-4" />
</button>

View file

@ -8,6 +8,7 @@ import toast from 'react-hot-toast';
import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext';
import { RefreshIcon, InfoIcon } from '@/components/icons/Icons';
import { buttonClass } from '@/components/formPrimitives';
import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell';
import { compareSegmentLocators, locatorGroupKey, locatorIdentityKey } from '@/lib/shared/tts-locator';
import { buildSegmentKey, buildSegmentKeyPrefix } from '@/lib/shared/tts-segment-plan';
@ -590,7 +591,11 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
aria-label="Clear segments cache"
title="Clear cache for listed segments"
disabled={isClearingSegments}
className="inline-flex items-center justify-center h-8 px-2 rounded-lg border border-offbase bg-base text-xs text-muted hover:bg-offbase hover:text-accent disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className={buttonClass({
variant: 'secondary',
size: 'xs',
className: 'h-8 px-2 text-muted',
})}
>
{isClearingSegments ? 'Clearing…' : 'Clear'}
</button>
@ -599,7 +604,11 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
onClick={handleRefresh}
aria-label="Refresh segments"
title="Refresh"
className="inline-flex items-center justify-center w-8 h-8 rounded-lg border border-offbase bg-base text-muted hover:bg-offbase hover:text-accent transition-colors"
className={buttonClass({
variant: 'secondary',
size: 'icon',
className: 'h-8 w-8 text-muted',
})}
>
<RefreshIcon className="w-3.5 h-3.5" />
</button>