refactor(components): unify button styling using buttonClass utility across multiple components
This commit is contained in:
parent
205d8f37e4
commit
c3fb3af3d3
18 changed files with 156 additions and 78 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ export {
|
|||
Field,
|
||||
Section,
|
||||
ToggleRow,
|
||||
buttonClass,
|
||||
btnBase,
|
||||
btnDanger,
|
||||
btnGhost,
|
||||
btnOutline,
|
||||
btnPrimary,
|
||||
btnSecondary,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
+
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue