phase 3: depth and rhythm polish

This commit is contained in:
Richard R 2026-05-31 20:48:18 -06:00
parent 745c846a6e
commit c78b134d4d
46 changed files with 440 additions and 440 deletions

View file

@ -173,9 +173,9 @@ export default function EPUBPage() {
<p className="text-danger mb-4">{error}</p> <p className="text-danger mb-4">{error}</p>
<Link <Link
href="/app" href="/app"
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition duration-base ease-standard hover:text-accent" className="inline-flex items-center px-3 py-1 bg-surface text-foreground rounded-lg hover:bg-accent-wash transition duration-base ease-standard hover:text-accent"
> >
<svg className="w-4 h-4 mr-2 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 mr-2 text-soft" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg> </svg>
Back to Documents Back to Documents
@ -190,7 +190,7 @@ export default function EPUBPage() {
left={ left={
<Link <Link
href="/app" href="/app"
className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition duration-base ease-standard hover:text-accent" className="inline-flex items-center py-1 px-2 rounded-md border border-line bg-surface text-foreground text-xs hover:bg-accent-wash transition duration-base ease-standard hover:text-accent"
aria-label="Back to documents" aria-label="Back to documents"
> >
<svg className="w-3 h-3 mr-2" fill="currentColor" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3 h-3 mr-2" fill="currentColor" stroke="currentColor" viewBox="0 0 24 24">
@ -242,7 +242,7 @@ export default function EPUBPage() {
/> />
)} )}
{isAtLimit ? ( {isAtLimit ? (
<div className="sticky bottom-0 z-30 w-full border-t border-offbase bg-base" data-app-ttsbar> <div className="sticky bottom-0 z-30 w-full border-t border-line-soft bg-surface" data-app-ttsbar>
<div className="px-2 md:px-3 pt-1 pb-[max(0.375rem,env(safe-area-inset-bottom))] flex items-center justify-center gap-1 min-h-10"> <div className="px-2 md:px-3 pt-1 pb-[max(0.375rem,env(safe-area-inset-bottom))] flex items-center justify-center gap-1 min-h-10">
<RateLimitPauseButton /> <RateLimitPauseButton />
<RateLimitBanner /> <RateLimitBanner />

View file

@ -160,7 +160,7 @@ export default function HTMLPage() {
<p className="text-danger mb-4">{error}</p> <p className="text-danger mb-4">{error}</p>
<Link <Link
href="/app" href="/app"
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition duration-base ease-standard hover:text-accent" className="inline-flex items-center px-3 py-1 bg-surface text-foreground rounded-lg hover:bg-accent-wash transition duration-base ease-standard hover:text-accent"
> >
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
@ -177,7 +177,7 @@ export default function HTMLPage() {
left={ left={
<Link <Link
href="/app" href="/app"
className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition duration-base ease-standard hover:text-accent" className="inline-flex items-center py-1 px-2 rounded-md border border-line bg-surface text-foreground text-xs hover:bg-accent-wash transition duration-base ease-standard hover:text-accent"
aria-label="Back to documents" aria-label="Back to documents"
> >
<svg className="w-3 h-3 mr-2" fill="currentColor" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3 h-3 mr-2" fill="currentColor" stroke="currentColor" viewBox="0 0 24 24">
@ -233,7 +233,7 @@ export default function HTMLPage() {
/> />
)} )}
{isAtLimit ? ( {isAtLimit ? (
<div className="sticky bottom-0 z-30 w-full border-t border-offbase bg-base" data-app-ttsbar> <div className="sticky bottom-0 z-30 w-full border-t border-line-soft bg-surface" data-app-ttsbar>
<div className="px-2 md:px-3 pt-1 pb-[max(0.375rem,env(safe-area-inset-bottom))] flex items-center justify-center gap-1 min-h-10"> <div className="px-2 md:px-3 pt-1 pb-[max(0.375rem,env(safe-area-inset-bottom))] flex items-center justify-center gap-1 min-h-10">
<RateLimitPauseButton /> <RateLimitPauseButton />
<RateLimitBanner /> <RateLimitBanner />

View file

@ -255,9 +255,9 @@ export default function PDFViewerPage() {
<Link <Link
href="/app" href="/app"
onClick={handleBackToDocuments} onClick={handleBackToDocuments}
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition duration-base ease-standard hover:text-accent" className="inline-flex items-center px-3 py-1 bg-surface text-foreground rounded-lg hover:bg-accent-wash transition duration-base ease-standard hover:text-accent"
> >
<svg className="w-4 h-4 mr-2 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 mr-2 text-soft" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg> </svg>
Back to Documents Back to Documents
@ -308,34 +308,34 @@ export default function PDFViewerPage() {
: (isMerging ? 'Stage: merge' : 'Stage: infer')); : (isMerging ? 'Stage: merge' : 'Stage: infer'));
return ( return (
<div className="h-full w-full bg-base"> <div className="h-full w-full bg-surface">
<div className={`mx-auto flex h-full items-center px-4 py-6 transition duration-slow ease-standard ${showDetailedParseLoader ? 'max-w-lg' : 'max-w-md'}`}> <div className={`mx-auto flex h-full items-center px-4 py-6 transition duration-slow ease-standard ${showDetailedParseLoader ? 'max-w-lg' : 'max-w-md'}`}>
{showDetailedParseLoader ? ( {showDetailedParseLoader ? (
<div className="w-full rounded-xl border border-offbase bg-surface-sunken shadow-sm overflow-hidden"> <div className="w-full rounded-lg border border-line bg-surface-sunken shadow-elev-1 overflow-hidden">
<div className="h-1 bg-[linear-gradient(90deg,var(--accent),transparent_80%)]" /> <div className="h-1 bg-[linear-gradient(90deg,var(--accent),transparent_80%)]" />
<div className="p-3.5 sm:p-4"> <div className="p-3.5 sm:p-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="inline-flex items-center gap-2 rounded-md border border-offbase bg-surface-solid px-2.5 py-1"> <div className="inline-flex items-center gap-2 rounded-md border border-line bg-surface-solid px-2.5 py-1">
<LoadingSpinner className="h-3.5 w-3.5 text-accent" /> <LoadingSpinner className="h-3.5 w-3.5 text-accent" />
<span className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted">PDF Layout Parse</span> <span className="text-[10px] font-semibold uppercase tracking-[0.08em] text-soft">PDF Layout Parse</span>
</div> </div>
<p className="text-sm font-semibold text-foreground">{statusText}</p> <p className="text-sm font-semibold text-foreground">{statusText}</p>
</div> </div>
<div className="mt-3 rounded-lg border border-offbase bg-surface-solid p-2.5"> <div className="mt-3 rounded-lg border border-line bg-surface-solid p-2.5">
<div className="mb-1.5 flex items-end justify-between gap-2"> <div className="mb-1.5 flex items-end justify-between gap-2">
<p className="text-[11px] font-semibold text-foreground"> <p className="text-[11px] font-semibold text-foreground">
{hasMeasuredProgress ? `Page ${pagesParsed} / ${totalPages}` : 'Awaiting first page'} {hasMeasuredProgress ? `Page ${pagesParsed} / ${totalPages}` : 'Awaiting first page'}
</p> </p>
<p className="text-[10px] text-muted">{stageLabel}</p> <p className="text-[10px] text-soft">{stageLabel}</p>
</div> </div>
<div className="h-2 w-full rounded-full bg-offbase overflow-hidden"> <div className="h-2 w-full rounded-full bg-surface-sunken overflow-hidden">
<div <div
className="h-full bg-accent transition duration-slow ease-standard" className="h-full bg-accent transition duration-slow ease-standard"
style={{ width: `${hasMeasuredProgress ? progressPercent : 6}%` }} style={{ width: `${hasMeasuredProgress ? progressPercent : 6}%` }}
/> />
</div> </div>
<p className="mt-1.5 text-[10px] text-muted"> <p className="mt-1.5 text-[10px] text-soft">
{hasMeasuredProgress ? `${Math.round(progressPercent)}% complete` : statusSubText} {hasMeasuredProgress ? `${Math.round(progressPercent)}% complete` : statusSubText}
</p> </p>
</div> </div>
@ -345,7 +345,7 @@ export default function PDFViewerPage() {
<button <button
type="button" type="button"
onClick={requestForceReparse} onClick={requestForceReparse}
className="inline-flex items-center rounded-md border border-offbase bg-base px-3 py-1.5 text-xs font-medium text-foreground hover:text-accent transition-colors" className="inline-flex items-center rounded-md border border-line bg-surface px-3 py-1.5 text-xs font-medium text-foreground hover:text-accent transition-colors"
> >
Retry Parse Retry Parse
</button> </button>
@ -354,14 +354,14 @@ export default function PDFViewerPage() {
</div> </div>
</div> </div>
) : ( ) : (
<div className="w-full rounded-xl border border-offbase bg-surface-sunken p-4 shadow-sm transition duration-slow ease-standard overflow-hidden"> <div className="w-full rounded-lg border border-line bg-surface-sunken p-4 shadow-elev-1 transition duration-slow ease-standard overflow-hidden">
<div className="h-0.5 -mx-4 -mt-4 mb-3 bg-[linear-gradient(90deg,var(--accent),transparent_75%)]" /> <div className="h-0.5 -mx-4 -mt-4 mb-3 bg-[linear-gradient(90deg,var(--accent),transparent_75%)]" />
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-semibold text-foreground">{compactLabel}</p> <p className="text-sm font-semibold text-foreground">{compactLabel}</p>
<p className="mt-1 text-xs text-muted">{compactSubLabel}</p> <p className="mt-1 text-xs text-soft">{compactSubLabel}</p>
</div> </div>
<span className="inline-flex items-center justify-center rounded-md border border-offbase bg-base p-1.5"> <span className="inline-flex items-center justify-center rounded-md border border-line bg-surface p-1.5">
<LoadingSpinner className="h-3.5 w-3.5 text-accent" /> <LoadingSpinner className="h-3.5 w-3.5 text-accent" />
</span> </span>
</div> </div>
@ -384,7 +384,7 @@ export default function PDFViewerPage() {
<Link <Link
href="/app" href="/app"
onClick={handleBackToDocuments} onClick={handleBackToDocuments}
className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition duration-base ease-standard hover:text-accent" className="inline-flex items-center py-1 px-2 rounded-md border border-line bg-surface text-foreground text-xs hover:bg-accent-wash transition duration-base ease-standard hover:text-accent"
aria-label="Back to documents" aria-label="Back to documents"
> >
<svg className="w-3 h-3 mr-2" fill="currentColor" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3 h-3 mr-2" fill="currentColor" stroke="currentColor" viewBox="0 0 24 24">
@ -440,7 +440,7 @@ export default function PDFViewerPage() {
/> />
)} )}
{isAtLimit ? ( {isAtLimit ? (
<div className="sticky bottom-0 z-30 w-full border-t border-offbase bg-base" data-app-ttsbar> <div className="sticky bottom-0 z-30 w-full border-t border-line-soft bg-surface" data-app-ttsbar>
<div className="px-2 md:px-3 pt-1 pb-[max(0.375rem,env(safe-area-inset-bottom))] flex items-center justify-center gap-1 min-h-10"> <div className="px-2 md:px-3 pt-1 pb-[max(0.375rem,env(safe-area-inset-bottom))] flex items-center justify-center gap-1 min-h-10">
<RateLimitPauseButton /> <RateLimitPauseButton />
<RateLimitBanner /> <RateLimitBanner />

View file

@ -120,11 +120,11 @@ function SignInContent() {
<SessionExpiredLoader setSessionExpired={setSessionExpired} /> <SessionExpiredLoader setSessionExpired={setSessionExpired} />
</Suspense> </Suspense>
<div className="w-full max-w-md bg-base rounded-2xl shadow-xl p-6"> <div className="w-full max-w-md bg-surface rounded-lg shadow-elev-3 p-6">
<h1 className="text-xl font-semibold text-foreground"> <h1 className="text-xl font-semibold text-foreground">
{sessionExpired ? 'Session Expired' : 'Connect Account'} {sessionExpired ? 'Session Expired' : 'Connect Account'}
</h1> </h1>
<p className="text-sm text-muted mt-1"> <p className="text-sm text-soft mt-1">
{sessionExpired {sessionExpired
? 'Please sign in again to continue' ? 'Please sign in again to continue'
: 'Connect an email account to sync your data across devices'} : 'Connect an email account to sync your data across devices'}
@ -154,8 +154,8 @@ function SignInContent() {
value={email} value={email}
onChange={(e) => { setEmail(e.target.value); setError(null); }} onChange={(e) => { setEmail(e.target.value); setError(null); }}
placeholder="me@example.com" placeholder="me@example.com"
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-elev-1
focus:outline-none focus:ring-2 focus:ring-accent" focus:outline-none focus:ring-2 focus:ring-accent-line"
/> />
</div> </div>
@ -167,8 +167,8 @@ function SignInContent() {
value={password} value={password}
onChange={(e) => { setPassword(e.target.value); setError(null); }} onChange={(e) => { setPassword(e.target.value); setError(null); }}
placeholder="Password" placeholder="Password"
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-elev-1
focus:outline-none focus:ring-2 focus:ring-accent" focus:outline-none focus:ring-2 focus:ring-accent-line"
/> />
</div> </div>
@ -178,7 +178,7 @@ function SignInContent() {
type="checkbox" type="checkbox"
checked={rememberMe} checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)} onChange={(e) => setRememberMe(e.target.checked)}
className="rounded border-muted text-accent focus:ring-accent" className="rounded border-muted text-accent focus:ring-accent-line"
/> />
<span className="text-sm text-foreground">Remember me</span> <span className="text-sm text-foreground">Remember me</span>
</label> </label>
@ -230,16 +230,16 @@ function SignInContent() {
</div> </div>
{/* Footer */} {/* Footer */}
<div className="mt-6 pt-4 border-t border-offbase text-center space-y-2"> <div className="mt-6 pt-4 border-t border-line-soft text-center space-y-2">
{enableUserSignups && ( {enableUserSignups && (
<p className="text-xs text-muted"> <p className="text-xs text-soft">
Don&apos;t have an account?{' '} Don&apos;t have an account?{' '}
<Link href="/signup" className="underline hover:text-foreground"> <Link href="/signup" className="underline hover:text-foreground">
Sign up Sign up
</Link> </Link>
</p> </p>
)} )}
<p className="text-xs text-muted"> <p className="text-xs text-soft">
By signing in, you agree to our{' '} By signing in, you agree to our{' '}
<button <button
onClick={() => showPrivacyModal()} onClick={() => showPrivacyModal()}

View file

@ -105,13 +105,13 @@ export default function SignUpPage() {
if (!enableUserSignups) { if (!enableUserSignups) {
return ( return (
<div className="min-h-screen flex items-center justify-center p-4 bg-background"> <div className="min-h-screen flex items-center justify-center p-4 bg-background">
<div className="w-full max-w-md bg-base rounded-2xl shadow-xl p-6"> <div className="w-full max-w-md bg-surface rounded-lg shadow-elev-3 p-6">
<h1 className="text-xl font-semibold text-foreground">Sign-ups unavailable</h1> <h1 className="text-xl font-semibold text-foreground">Sign-ups unavailable</h1>
<p className="text-sm text-muted mt-1"> <p className="text-sm text-soft mt-1">
New account sign-ups are currently disabled by the site administrator. New account sign-ups are currently disabled by the site administrator.
</p> </p>
<div className="mt-6 pt-4 border-t border-offbase text-center"> <div className="mt-6 pt-4 border-t border-line-soft text-center">
<p className="text-xs text-muted"> <p className="text-xs text-soft">
Already have an account?{' '} Already have an account?{' '}
<Link href="/signin" className="underline hover:text-foreground"> <Link href="/signin" className="underline hover:text-foreground">
Sign in Sign in
@ -129,9 +129,9 @@ export default function SignUpPage() {
return ( return (
<div className="min-h-screen flex items-center justify-center p-4 bg-background"> <div className="min-h-screen flex items-center justify-center p-4 bg-background">
<div className="w-full max-w-md bg-base rounded-2xl shadow-xl p-6"> <div className="w-full max-w-md bg-surface rounded-lg shadow-elev-3 p-6">
<h1 className="text-xl font-semibold text-foreground">Sign Up</h1> <h1 className="text-xl font-semibold text-foreground">Sign Up</h1>
<p className="text-sm text-muted mt-1">Create your account to get started</p> <p className="text-sm text-soft mt-1">Create your account to get started</p>
{error && ( {error && (
<div className="mt-4 p-3 bg-danger-wash border border-danger rounded-lg"> <div className="mt-4 p-3 bg-danger-wash border border-danger rounded-lg">
@ -148,8 +148,8 @@ export default function SignUpPage() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="me@example.com" placeholder="me@example.com"
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-elev-1
focus:outline-none focus:ring-2 focus:ring-accent" focus:outline-none focus:ring-2 focus:ring-accent-line"
/> />
</div> </div>
@ -162,8 +162,8 @@ export default function SignUpPage() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="Password" placeholder="Password"
className="w-full rounded-lg bg-background py-2 px-3 pr-10 text-foreground shadow-sm className="w-full rounded-lg bg-background py-2 px-3 pr-10 text-foreground shadow-elev-1
focus:outline-none focus:ring-2 focus:ring-accent" focus:outline-none focus:ring-2 focus:ring-accent-line"
/> />
<button <button
type="button" type="button"
@ -171,7 +171,7 @@ export default function SignUpPage() {
className={buttonClass({ className={buttonClass({
variant: 'ghost', variant: 'ghost',
size: 'icon', size: 'icon',
className: 'absolute right-2 top-1/2 -translate-y-1/2 h-7 w-7 text-muted', className: 'absolute right-2 top-1/2 -translate-y-1/2 h-7 w-7 text-soft',
})} })}
> >
{showPassword ? '👁️' : '👁️‍🗨️'} {showPassword ? '👁️' : '👁️‍🗨️'}
@ -185,7 +185,7 @@ export default function SignUpPage() {
{[0, 1, 2, 3, 4].map((i) => ( {[0, 1, 2, 3, 4].map((i) => (
<div <div
key={i} key={i}
className={`h-1 flex-1 rounded ${i < strength ? strengthColors[strength - 1] : 'bg-offbase' className={`h-1 flex-1 rounded ${i < strength ? strengthColors[strength - 1] : 'bg-surface-sunken'
}`} }`}
/> />
))} ))}
@ -193,7 +193,7 @@ export default function SignUpPage() {
<p className={`text-xs ${strength >= 3 ? 'text-accent' : 'text-danger'}`}> <p className={`text-xs ${strength >= 3 ? 'text-accent' : 'text-danger'}`}>
{strengthLabels[strength - 1] || 'Very Weak'} {strengthLabels[strength - 1] || 'Very Weak'}
</p> </p>
<div className="text-xs space-y-0.5 text-muted"> <div className="text-xs space-y-0.5 text-soft">
{Object.entries(checks).map(([key, passed]) => ( {Object.entries(checks).map(([key, passed]) => (
<div key={key} className={`flex items-center gap-1 ${passed ? 'text-accent' : ''}`}> <div key={key} className={`flex items-center gap-1 ${passed ? 'text-accent' : ''}`}>
<span>{passed ? '✓' : '○'}</span> <span>{passed ? '✓' : '○'}</span>
@ -219,8 +219,8 @@ export default function SignUpPage() {
value={passwordConfirmation} value={passwordConfirmation}
onChange={(e) => setPasswordConfirmation(e.target.value)} onChange={(e) => setPasswordConfirmation(e.target.value)}
placeholder="Confirm Password" placeholder="Confirm Password"
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-elev-1
focus:outline-none focus:ring-2 focus:ring-accent" focus:outline-none focus:ring-2 focus:ring-accent-line"
/> />
{passwordConfirmation && password && ( {passwordConfirmation && password && (
<p className={`text-xs mt-1 ${password === passwordConfirmation ? 'text-accent' : 'text-danger'}`}> <p className={`text-xs mt-1 ${password === passwordConfirmation ? 'text-accent' : 'text-danger'}`}>
@ -241,14 +241,14 @@ export default function SignUpPage() {
</div> </div>
{/* Footer */} {/* Footer */}
<div className="mt-6 pt-4 border-t border-offbase text-center space-y-2"> <div className="mt-6 pt-4 border-t border-line-soft text-center space-y-2">
<p className="text-xs text-muted"> <p className="text-xs text-soft">
Already have an account?{' '} Already have an account?{' '}
<Link href="/signin" className="underline hover:text-foreground"> <Link href="/signin" className="underline hover:text-foreground">
Sign in Sign in
</Link> </Link>
</p> </p>
<p className="text-xs text-muted"> <p className="text-xs text-soft">
By creating an account, you agree to our{' '} By creating an account, you agree to our{' '}
<button <button
onClick={() => showPrivacyModal()} onClick={() => showPrivacyModal()}

View file

@ -476,12 +476,12 @@ export function AudiobookExportModal({
<> <>
<div className="space-y-4"> <div className="space-y-4">
{!isGenerating && ( {!isGenerating && (
<div className="w-full rounded-xl border border-offbase bg-background"> <div className="w-full rounded-lg border border-line bg-background">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-offbase bg-base rounded-t-xl"> <div className="flex items-center justify-between px-4 py-3 border-b border-line-soft bg-surface rounded-t-xl">
<h4 className="text-sm font-medium text-foreground tracking-tight">Generation settings</h4> <h4 className="text-sm font-medium text-foreground tracking-tight">Generation settings</h4>
{settingsLocked && ( {settingsLocked && (
<span className="inline-flex items-center gap-1 rounded-md bg-offbase px-2 py-0.5 text-[11px] font-medium text-muted uppercase tracking-wider"> <span className="inline-flex items-center gap-1 rounded-md bg-surface-sunken px-2 py-0.5 text-[11px] font-medium text-soft uppercase tracking-wider">
<svg className="h-3 w-3" viewBox="0 0 16 16" fill="currentColor"><path fillRule="evenodd" d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z" clipRule="evenodd" /></svg> <svg className="h-3 w-3" viewBox="0 0 16 16" fill="currentColor"><path fillRule="evenodd" d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z" clipRule="evenodd" /></svg>
Locked Locked
</span> </span>
@ -492,7 +492,7 @@ export function AudiobookExportModal({
{isLegacyAudiobookMissingSettings && ( {isLegacyAudiobookMissingSettings && (
<div className="mb-4 rounded-lg border border-accent-line bg-accent-wash p-3 text-xs text-foreground"> <div className="mb-4 rounded-lg border border-accent-line bg-accent-wash p-3 text-xs text-foreground">
<div className="font-medium">Saved generation settings not found</div> <div className="font-medium">Saved generation settings not found</div>
<div className="mt-1 text-muted"> <div className="mt-1 text-soft">
This audiobook was likely created before v1 metadata was introduced, so OpenReader can&apos;t know This audiobook was likely created before v1 metadata was introduced, so OpenReader can&apos;t know
which voice/speeds/format were used. Consider resetting this audiobook to regenerate it with which voice/speeds/format were used. Consider resetting this audiobook to regenerate it with
v1 metadata (so settings are saved for resumes across devices). v1 metadata (so settings are saved for resumes across devices).
@ -503,18 +503,18 @@ export function AudiobookExportModal({
{settingsLocked && savedSettings ? ( {settingsLocked && savedSettings ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="rounded-lg bg-base p-3"> <div className="rounded-lg bg-surface p-3">
<div className="text-[11px] uppercase tracking-wider text-muted mb-1">Voice</div> <div className="text-[11px] uppercase tracking-wider text-soft mb-1">Voice</div>
<div className="text-sm font-medium text-foreground truncate">{savedSettings.voice}</div> <div className="text-sm font-medium text-foreground truncate">{savedSettings.voice}</div>
</div> </div>
<div className="rounded-lg bg-base p-3"> <div className="rounded-lg bg-surface p-3">
<div className="text-[11px] uppercase tracking-wider text-muted mb-1">Format</div> <div className="text-[11px] uppercase tracking-wider text-soft mb-1">Format</div>
<div className="text-sm font-medium text-foreground">{savedSettings.format.toUpperCase()}</div> <div className="text-sm font-medium text-foreground">{savedSettings.format.toUpperCase()}</div>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div className="rounded-lg bg-base p-3"> <div className="rounded-lg bg-surface p-3">
<div className="text-[11px] uppercase tracking-wider text-muted mb-1">Native speed</div> <div className="text-[11px] uppercase tracking-wider text-soft mb-1">Native speed</div>
<div className="text-sm font-medium text-foreground"> <div className="text-sm font-medium text-foreground">
{resolveTtsProviderModelPolicy({ {resolveTtsProviderModelPolicy({
providerRef: savedSettings.providerRef, providerRef: savedSettings.providerRef,
@ -525,12 +525,12 @@ export function AudiobookExportModal({
: 'Not supported'} : 'Not supported'}
</div> </div>
</div> </div>
<div className="rounded-lg bg-base p-3"> <div className="rounded-lg bg-surface p-3">
<div className="text-[11px] uppercase tracking-wider text-muted mb-1">Post speed</div> <div className="text-[11px] uppercase tracking-wider text-soft mb-1">Post speed</div>
<div className="text-sm font-medium text-foreground">{formatSpeed(savedSettings.postSpeed)}x</div> <div className="text-sm font-medium text-foreground">{formatSpeed(savedSettings.postSpeed)}x</div>
</div> </div>
</div> </div>
<p className="text-xs text-muted"> <p className="text-xs text-soft">
Reset the audiobook to change generation settings. Reset the audiobook to change generation settings.
</p> </p>
</div> </div>
@ -539,7 +539,7 @@ export function AudiobookExportModal({
{/* Voice & Format row */} {/* Voice & Format row */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[11px] uppercase tracking-wider font-medium text-muted">Voice</label> <label className="text-[11px] uppercase tracking-wider font-medium text-soft">Voice</label>
<VoicesControlBase <VoicesControlBase
availableVoices={availableVoices} availableVoices={availableVoices}
voice={audiobookVoice} voice={audiobookVoice}
@ -552,7 +552,7 @@ export function AudiobookExportModal({
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[11px] uppercase tracking-wider font-medium text-muted">Format</label> <label className="text-[11px] uppercase tracking-wider font-medium text-soft">Format</label>
{chapters.length === 0 ? ( {chapters.length === 0 ? (
<Listbox <Listbox
value={format} value={format}
@ -560,10 +560,10 @@ export function AudiobookExportModal({
disabled={chapters.length > 0 || settingsLocked} disabled={chapters.length > 0 || settingsLocked}
> >
<div className="relative"> <div className="relative">
<ListboxButton className="relative cursor-pointer rounded-lg bg-base py-1.5 pl-3 pr-10 text-left text-foreground focus:outline-none focus:ring-2 focus:ring-accent transform transition-transform duration-base ease-standard hover:text-accent w-full"> <ListboxButton className="relative cursor-pointer rounded-lg bg-surface py-1.5 pl-3 pr-10 text-left text-foreground focus:outline-none focus:ring-2 focus:ring-accent-line transform transition-transform duration-base ease-standard hover:text-accent w-full">
<span className="block truncate text-sm font-medium">{format.toUpperCase()}</span> <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"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-4 w-4 text-muted" /> <ChevronUpDownIcon className="h-4 w-4 text-soft" />
</span> </span>
</ListboxButton> </ListboxButton>
<Transition <Transition
@ -572,11 +572,11 @@ export function AudiobookExportModal({
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<ListboxOptions className="absolute left-0 mt-1 max-h-60 w-full overflow-auto rounded-lg bg-base py-1 shadow-lg ring-1 ring-black/5 focus:outline-none z-10"> <ListboxOptions className="absolute left-0 mt-1 max-h-60 w-full overflow-auto rounded-lg bg-surface py-1 shadow-elev-2 ring-1 ring-line-soft focus:outline-none z-10">
<ListboxOption <ListboxOption
value="m4b" value="m4b"
className={({ active }) => className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-4 ${active ? 'bg-offbase text-accent' : 'text-foreground'}` `relative cursor-pointer select-none py-2 pl-3 pr-4 ${active ? 'bg-surface-sunken text-accent' : 'text-foreground'}`
} }
> >
{({ selected }) => ( {({ selected }) => (
@ -588,7 +588,7 @@ export function AudiobookExportModal({
<ListboxOption <ListboxOption
value="mp3" value="mp3"
className={({ active }) => className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-4 ${active ? 'bg-offbase text-accent' : 'text-foreground'}` `relative cursor-pointer select-none py-2 pl-3 pr-4 ${active ? 'bg-surface-sunken text-accent' : 'text-foreground'}`
} }
> >
{({ selected }) => ( {({ selected }) => (
@ -608,9 +608,9 @@ export function AudiobookExportModal({
</div> </div>
{/* Speed controls */} {/* Speed controls */}
<div className="rounded-lg bg-base p-3 space-y-3"> <div className="rounded-lg bg-surface p-3 space-y-3">
{!nativeSpeedSupported && ( {!nativeSpeedSupported && (
<div className="rounded-md border border-offbase bg-background px-2 py-1.5 text-[11px] text-muted"> <div className="rounded-md border border-line bg-background px-2 py-1.5 text-[11px] text-soft">
Native model speed is not available for this model. Native model speed is not available for this model.
</div> </div>
)} )}
@ -619,7 +619,7 @@ export function AudiobookExportModal({
<> <>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-[11px] uppercase tracking-wider font-medium text-muted">Native model speed</label> <label className="text-[11px] uppercase tracking-wider font-medium text-soft">Native model speed</label>
<span className="text-xs font-medium text-accent tabular-nums">{formatSpeed(nativeSpeed)}x</span> <span className="text-xs font-medium text-accent tabular-nums">{formatSpeed(nativeSpeed)}x</span>
</div> </div>
<input <input
@ -629,21 +629,21 @@ export function AudiobookExportModal({
step="0.1" step="0.1"
value={nativeSpeed} value={nativeSpeed}
onChange={(e) => setNativeSpeed(parseFloat(e.target.value))} onChange={(e) => setNativeSpeed(parseFloat(e.target.value))}
className="w-full h-1.5 bg-offbase rounded-full appearance-none cursor-pointer [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:-mt-[5px] [&::-webkit-slider-thumb]:shadow-sm [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-full [&::-moz-range-track]:h-1.5 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent [&::-moz-range-thumb]:border-0" className="w-full h-1.5 bg-surface-sunken rounded-full appearance-none cursor-pointer [&::-webkit-slider-runnable-track]:bg-surface-sunken [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:-mt-[5px] [&::-webkit-slider-thumb]:shadow-elev-1 [&::-moz-range-track]:bg-surface-sunken [&::-moz-range-track]:rounded-full [&::-moz-range-track]:h-1.5 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent [&::-moz-range-thumb]:border-0"
/> />
<div className="flex justify-between text-[10px] text-muted"> <div className="flex justify-between text-[10px] text-soft">
<span>0.5x</span> <span>0.5x</span>
<span>3x</span> <span>3x</span>
</div> </div>
</div> </div>
<div className="border-t border-offbase" /> <div className="border-t border-line-soft" />
</> </>
)} )}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-[11px] uppercase tracking-wider font-medium text-muted">Post-generation speed</label> <label className="text-[11px] uppercase tracking-wider font-medium text-soft">Post-generation speed</label>
<span className="text-xs font-medium text-accent tabular-nums">{formatSpeed(postSpeed)}x</span> <span className="text-xs font-medium text-accent tabular-nums">{formatSpeed(postSpeed)}x</span>
</div> </div>
<input <input
@ -653,9 +653,9 @@ export function AudiobookExportModal({
step="0.1" step="0.1"
value={postSpeed} value={postSpeed}
onChange={(e) => setPostSpeed(parseFloat(e.target.value))} onChange={(e) => setPostSpeed(parseFloat(e.target.value))}
className="w-full h-1.5 bg-offbase rounded-full appearance-none cursor-pointer [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:-mt-[5px] [&::-webkit-slider-thumb]:shadow-sm [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-full [&::-moz-range-track]:h-1.5 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent [&::-moz-range-thumb]:border-0" className="w-full h-1.5 bg-surface-sunken rounded-full appearance-none cursor-pointer [&::-webkit-slider-runnable-track]:bg-surface-sunken [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-webkit-slider-thumb]:-mt-[5px] [&::-webkit-slider-thumb]:shadow-elev-1 [&::-moz-range-track]:bg-surface-sunken [&::-moz-range-track]:rounded-full [&::-moz-range-track]:h-1.5 [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent [&::-moz-range-thumb]:border-0"
/> />
<div className="flex justify-between text-[10px] text-muted"> <div className="flex justify-between text-[10px] text-soft">
<span>0.5x</span> <span>0.5x</span>
<span>3x</span> <span>3x</span>
</div> </div>
@ -708,7 +708,7 @@ export function AudiobookExportModal({
</div> </div>
)} )}
{showRegenerateHint && ( {showRegenerateHint && (
<div className="flex items-start justify-between bg-offbase border border-offbase rounded-md px-3 py-2 text-xs sm:text-sm"> <div className="flex items-start justify-between bg-surface-sunken border border-line rounded-md px-3 py-2 text-xs sm:text-sm">
<p className="text-xs sm:text-sm text-foreground"> <p className="text-xs sm:text-sm text-foreground">
TTS audio for this chapter may be cached TTS audio for this chapter may be cached
<br /> <br />
@ -716,7 +716,7 @@ export function AudiobookExportModal({
</p> </p>
<Button <Button
onClick={() => setShowRegenerateHint(false)} onClick={() => setShowRegenerateHint(false)}
className="ml-3 p-1 rounded-md hover:bg-base hover:text-accent transition-colors" className="ml-3 p-1 rounded-md hover:bg-surface hover:text-accent transition-colors"
aria-label="Dismiss regenerate hint" aria-label="Dismiss regenerate hint"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -746,12 +746,12 @@ export function AudiobookExportModal({
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-foreground">Chapters</h4> <h4 className="text-sm font-medium text-foreground">Chapters</h4>
{isRefreshingChapters && <ClockIcon className="h-4 w-4 text-muted animate-spin" />} {isRefreshingChapters && <ClockIcon className="h-4 w-4 text-soft animate-spin" />}
</div> </div>
{displayChapters.map((chapter) => ( {displayChapters.map((chapter) => (
<div <div
key={chapter.index} key={chapter.index}
className={`flex items-center justify-between px-2 sm:px-3 py-1 sm:py-1.5 rounded-lg bg-offbase ${(regeneratingChapter === chapter.index || chapter.status === 'generating') ? 'prism-outline' : ''}`} className={`flex items-center justify-between px-2 sm:px-3 py-1 sm:py-1.5 rounded-lg bg-surface-sunken ${(regeneratingChapter === chapter.index || chapter.status === 'generating') ? 'prism-outline' : ''}`}
> >
<div className="flex items-center space-x-3 flex-1"> <div className="flex items-center space-x-3 flex-1">
{chapter.status === 'completed' ? ( {chapter.status === 'completed' ? (
@ -760,20 +760,20 @@ export function AudiobookExportModal({
<Button <Button
onClick={() => handleRegenerateChapter(chapter)} onClick={() => handleRegenerateChapter(chapter)}
disabled={regeneratingChapter !== null || chapter.status === 'generating' || isGenerating} disabled={regeneratingChapter !== null || chapter.status === 'generating' || isGenerating}
className="inline-flex items-center justify-center rounded-full bg-offbase text-accent hover:bg-accent-wash p-1.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent transform transition-transform duration-base ease-standard disabled:opacity-50 disabled:cursor-not-allowed" className="inline-flex items-center justify-center rounded-full bg-surface-sunken text-accent hover:bg-accent-wash p-1.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent transform transition-transform duration-base ease-standard disabled:opacity-50 disabled:cursor-not-allowed"
title={chapter.status === 'generating' ? 'Generating...' : 'Regenerate this chapter'} title={chapter.status === 'generating' ? 'Generating...' : 'Regenerate this chapter'}
> >
<RefreshIcon className={`h-4 w-4 ${regeneratingChapter === chapter.index || chapter.status === 'generating' ? 'animate-spin' : ''}`} /> <RefreshIcon className={`h-4 w-4 ${regeneratingChapter === chapter.index || chapter.status === 'generating' ? 'animate-spin' : ''}`} />
</Button> </Button>
) : ( ) : (
<ClockIcon className="h-5 w-5 text-muted" /> <ClockIcon className="h-5 w-5 text-soft" />
)} )}
<div className="flex flex-row flex-wrap items-center gap-1"> <div className="flex flex-row flex-wrap items-center gap-1">
<p className="text-sm font-medium text-foreground"> <p className="text-sm font-medium text-foreground">
{chapter.title} {chapter.title}
</p> </p>
<p></p> <p></p>
<p className="text-xs text-muted mt-0.5"> <p className="text-xs text-soft mt-0.5">
{chapter.status !== 'completed' && <span className="text-warning">Missing </span>}{formatDuration(chapter.duration)} {chapter.status !== 'completed' && <span className="text-warning">Missing </span>}{formatDuration(chapter.duration)}
</p> </p>
</div> </div>
@ -782,7 +782,7 @@ export function AudiobookExportModal({
{((onRegenerateChapter && !isGenerating) || chapter.status === 'completed') && ( {((onRegenerateChapter && !isGenerating) || chapter.status === 'completed') && (
<Menu as="div" className="relative inline-block text-left"> <Menu as="div" className="relative inline-block text-left">
<MenuButton <MenuButton
className="inline-flex items-center justify-center rounded-md p-1.5 hover:bg-background focus:outline-none focus-visible:ring-2 focus-visible:ring-accent text-muted hover:text-foreground transform transition-transform duration-base ease-standard" className="inline-flex items-center justify-center rounded-md p-1.5 hover:bg-background focus:outline-none focus-visible:ring-2 focus-visible:ring-accent text-soft hover:text-foreground transform transition-transform duration-base ease-standard"
title="Chapter actions" title="Chapter actions"
> >
<DotsVerticalIcon className="h-5 w-5" /> <DotsVerticalIcon className="h-5 w-5" />
@ -799,7 +799,7 @@ export function AudiobookExportModal({
<MenuItems <MenuItems
anchor={{ to: 'bottom end', gap: '8px', padding: '12px' }} anchor={{ to: 'bottom end', gap: '8px', padding: '12px' }}
portal portal
className="w-44 rounded-md bg-background shadow-lg ring-1 ring-black/5 focus:outline-none z-[70] p-1 origin-top-right" className="w-44 rounded-md bg-background shadow-elev-2 ring-1 ring-line-soft focus:outline-none z-[70] p-1 origin-top-right"
> >
{chapter.status === 'completed' && ( {chapter.status === 'completed' && (
<> <>
@ -807,7 +807,7 @@ export function AudiobookExportModal({
{({ active }) => ( {({ active }) => (
<button <button
onClick={() => setPendingDeleteChapter(chapter)} onClick={() => setPendingDeleteChapter(chapter)}
className={`${active ? 'bg-offbase' : ''} text-danger group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`} className={`${active ? 'bg-surface-sunken' : ''} text-danger group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`}
title="Delete this chapter" title="Delete this chapter"
> >
<XCircleIcon className="h-4 w-4" /> <XCircleIcon className="h-4 w-4" />
@ -819,7 +819,7 @@ export function AudiobookExportModal({
{({ active }) => ( {({ active }) => (
<button <button
onClick={() => handleDownloadChapter(chapter)} onClick={() => handleDownloadChapter(chapter)}
className={`${active ? 'bg-offbase text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`} className={`${active ? 'bg-surface-sunken text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`}
> >
<DownloadIcon className="h-4 w-4" /> <DownloadIcon className="h-4 w-4" />
<span>Download</span> <span>Download</span>
@ -833,7 +833,7 @@ export function AudiobookExportModal({
{({ active }) => ( {({ active }) => (
<button <button
onClick={handleCancel} onClick={handleCancel}
className={`${active ? 'bg-offbase text-danger' : 'text-danger'} group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`} className={`${active ? 'bg-surface-sunken text-danger' : 'text-danger'} group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`}
title="Cancel this chapter regeneration" title="Cancel this chapter regeneration"
> >
<XCircleIcon className="h-4 w-4" /> <XCircleIcon className="h-4 w-4" />
@ -848,7 +848,7 @@ export function AudiobookExportModal({
<button <button
onClick={() => handleRegenerateChapter(chapter)} onClick={() => handleRegenerateChapter(chapter)}
disabled={disabled} disabled={disabled}
className={`${active ? 'bg-offbase text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded px-2 py-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed`} className={`${active ? 'bg-surface-sunken text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded px-2 py-2 text-sm disabled:opacity-50 disabled:cursor-not-allowed`}
title="Regenerate this chapter" title="Regenerate this chapter"
> >
<RefreshIcon className={`h-4 w-4 ${regeneratingChapter === chapter.index ? 'animate-spin' : ''}`} /> <RefreshIcon className={`h-4 w-4 ${regeneratingChapter === chapter.index ? 'animate-spin' : ''}`} />
@ -868,7 +868,7 @@ export function AudiobookExportModal({
</div> </div>
{bookId && !isGenerating && ( {bookId && !isGenerating && (
<div className="pt-4 border-t border-offbase"> <div className="pt-4 border-t border-line-soft">
<Button <Button
onClick={handleDownloadComplete} onClick={handleDownloadComplete}
disabled={isCombining} disabled={isCombining}
@ -888,7 +888,7 @@ export function AudiobookExportModal({
{chapters.length === 0 && !isGenerating && !isLoadingExisting && ( {chapters.length === 0 && !isGenerating && !isLoadingExisting && (
<div className="text-center"> <div className="text-center">
<p className="text-sm text-muted"> <p className="text-sm text-soft">
Audiobook settings are fixed after generation. Chapters will appear here as they are ready. Audiobook settings are fixed after generation. Chapters will appear here as they are ready.
</p> </p>
</div> </div>
@ -938,43 +938,43 @@ export function AudiobookExportModal({
function AudiobookSettingsSkeleton() { function AudiobookSettingsSkeleton() {
return ( return (
<div className="space-y-4 animate-pulse" aria-label="Loading audiobook settings" aria-busy="true"> <div className="space-y-4 animate-pulse" aria-label="Loading audiobook settings" aria-busy="true">
<div className="w-full rounded-xl border border-offbase bg-background overflow-hidden"> <div className="w-full rounded-lg border border-line bg-background overflow-hidden">
<div className="flex items-center justify-between px-4 py-3 border-b border-offbase bg-base"> <div className="flex items-center justify-between px-4 py-3 border-b border-line-soft bg-surface">
<div className="h-4 w-40 rounded bg-offbase" /> <div className="h-4 w-40 rounded bg-surface-sunken" />
<div className="h-5 w-14 rounded bg-offbase" /> <div className="h-5 w-14 rounded bg-surface-sunken" />
</div> </div>
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="h-3 w-16 rounded bg-offbase" /> <div className="h-3 w-16 rounded bg-surface-sunken" />
<div className="h-9 w-full rounded-md bg-offbase" /> <div className="h-9 w-full rounded-md bg-surface-sunken" />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="h-3 w-16 rounded bg-offbase" /> <div className="h-3 w-16 rounded bg-surface-sunken" />
<div className="h-9 w-full rounded-md bg-offbase" /> <div className="h-9 w-full rounded-md bg-surface-sunken" />
</div> </div>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="h-3 w-24 rounded bg-offbase" /> <div className="h-3 w-24 rounded bg-surface-sunken" />
<div className="h-2 w-full rounded bg-offbase" /> <div className="h-2 w-full rounded bg-surface-sunken" />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="h-3 w-20 rounded bg-offbase" /> <div className="h-3 w-20 rounded bg-surface-sunken" />
<div className="h-2 w-full rounded bg-offbase" /> <div className="h-2 w-full rounded bg-surface-sunken" />
</div> </div>
</div> </div>
<div className="h-9 w-full rounded-md bg-offbase" /> <div className="h-9 w-full rounded-md bg-surface-sunken" />
</div> </div>
</div> </div>
<div className="w-full rounded-xl border border-offbase bg-background overflow-hidden"> <div className="w-full rounded-lg border border-line bg-background overflow-hidden">
<div className="px-4 py-3 border-b border-offbase bg-base"> <div className="px-4 py-3 border-b border-line-soft bg-surface">
<div className="h-4 w-28 rounded bg-offbase" /> <div className="h-4 w-28 rounded bg-surface-sunken" />
</div> </div>
<div className="p-4 space-y-2"> <div className="p-4 space-y-2">
{Array.from({ length: 4 }).map((_, index) => ( {Array.from({ length: 4 }).map((_, index) => (
<div key={index} className="h-16 rounded-lg border border-offbase bg-base" /> <div key={index} className="h-16 rounded-lg border border-line bg-surface" />
))} ))}
</div> </div>
</div> </div>

View file

@ -16,7 +16,7 @@ export function CodeBlock({ children }: { children: string }) {
return ( return (
<div className="relative group my-2"> <div className="relative group my-2">
<pre className="bg-background p-3 rounded-md overflow-x-auto text-xs text-left <pre className="bg-background p-3 rounded-md overflow-x-auto text-xs text-left
font-mono text-foreground border border-offbase"> font-mono text-foreground border border-line">
<code>{children}</code> <code>{children}</code>
</pre> </pre>
<button <button

View file

@ -91,7 +91,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
<PopoverPanel <PopoverPanel
anchor="bottom start" anchor="bottom start"
transition transition
className="z-[60] mt-2 w-56 rounded-xl shadow-xl border border-offbase bg-background p-3 space-y-3 className="z-[60] mt-2 w-56 rounded-lg shadow-elev-3 border border-line bg-background p-3 space-y-3
transition duration-fast ease-standard data-[closed]:opacity-0 data-[closed]:scale-95" transition duration-fast ease-standard data-[closed]:opacity-0 data-[closed]:scale-95"
> >
{/* Label */} {/* Label */}
@ -107,7 +107,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
className="p-1" className="p-1"
aria-label="Open system color picker" aria-label="Open system color picker"
> >
<PaletteIcon className="w-4 h-4 text-muted transform transition-transform duration-base ease-standard hover:text-accent" /> <PaletteIcon className="w-4 h-4 text-soft transform transition-transform duration-base ease-standard hover:text-accent" />
</button> </button>
<input <input
ref={nativeRef} ref={nativeRef}
@ -166,7 +166,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
}} }}
spellCheck={false} spellCheck={false}
maxLength={7} maxLength={7}
className="flex-1 rounded-lg px-2 py-1 text-xs font-mono border border-offbase bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-accent" className="flex-1 rounded-lg px-2 py-1 text-xs font-mono border border-line bg-background text-foreground focus:outline-none focus:ring-1 focus:ring-accent-line"
/> />
</div> </div>
</PopoverPanel> </PopoverPanel>

View file

@ -62,7 +62,7 @@ export function ConfirmDialog({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<DialogPanel role='dialog' className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition"> <DialogPanel role='dialog' className="w-full max-w-md transform rounded-lg bg-surface p-6 text-left align-middle shadow-elev-3 transition">
<DialogTitle <DialogTitle
as="h3" as="h3"
className="text-lg font-semibold leading-6 text-foreground" className="text-lg font-semibold leading-6 text-foreground"

View file

@ -43,7 +43,7 @@ export function CookieConsentBanner() {
leaveTo="translate-y-full opacity-0" leaveTo="translate-y-full opacity-0"
className="fixed bottom-0 left-0 right-0 z-[60] px-4 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-6 md:pt-6 md:pb-[max(1.5rem,env(safe-area-inset-bottom))]" className="fixed bottom-0 left-0 right-0 z-[60] px-4 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))] md:px-6 md:pt-6 md:pb-[max(1.5rem,env(safe-area-inset-bottom))]"
> >
<div className="mx-auto max-w-5xl rounded-xl border border-offbase bg-base p-5 shadow-2xl md:flex md:items-center md:justify-between md:gap-8"> <div className="mx-auto max-w-5xl rounded-lg border border-line bg-surface p-5 shadow-elev-3 md:flex md:items-center md:justify-between md:gap-8">
<div className="mb-4 md:mb-0"> <div className="mb-4 md:mb-0">
<h3 className="mb-2 text-lg font-bold"> <h3 className="mb-2 text-lg font-bold">
🍪 We use cookies 🍪 We use cookies
@ -70,7 +70,7 @@ export function CookieConsentBanner() {
className={buttonClass({ className={buttonClass({
variant: 'primary', variant: 'primary',
size: 'md', size: 'md',
className: 'whitespace-nowrap font-bold shadow-sm', className: 'whitespace-nowrap font-bold shadow-elev-1',
})} })}
> >
Accept All Accept All

View file

@ -10,7 +10,7 @@ export function Header({
right?: ReactNode; right?: ReactNode;
}) { }) {
return ( return (
<div className="sticky top-0 z-40 w-full border-b border-offbase bg-base" data-app-header> <div className="sticky top-0 z-40 w-full border-b border-line-soft bg-surface" data-app-header>
<div className="px-2 sm:px-3 py-1 flex items-center justify-between gap-2 min-h-10"> <div className="px-2 sm:px-3 py-1 flex items-center justify-between gap-2 min-h-10">
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex items-center gap-2 min-w-0 flex-1">
{left} {left}

View file

@ -20,8 +20,8 @@ interface PrivacyModalProps {
function PrivacyModalBody({ origin }: { origin: string }) { function PrivacyModalBody({ origin }: { origin: string }) {
return ( return (
<div className="mt-4 space-y-4 text-sm text-soft"> <div className="mt-4 space-y-4 text-sm text-soft">
<div className="rounded-lg border border-offbase bg-surface-sunken p-3"> <div className="rounded-lg border border-line bg-surface-sunken p-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted">Service Operator</div> <div className="text-xs font-semibold uppercase tracking-wide text-soft">Service Operator</div>
<div className="mt-1"> <div className="mt-1">
This instance is hosted at <span className="font-bold">{origin || 'this server'}</span>. This instance is hosted at <span className="font-bold">{origin || 'this server'}</span>.
</div> </div>
@ -102,7 +102,7 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<DialogPanel data-testid="privacy-modal" className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition"> <DialogPanel data-testid="privacy-modal" className="w-full max-w-md transform rounded-lg bg-surface p-6 text-left align-middle shadow-elev-3 transition">
<DialogTitle <DialogTitle
as="h3" as="h3"
className="text-lg font-semibold leading-6 text-foreground" className="text-lg font-semibold leading-6 text-foreground"
@ -113,7 +113,7 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
<PrivacyModalBody origin={origin} /> <PrivacyModalBody origin={origin} />
<div className="mt-6 space-y-4"> <div className="mt-6 space-y-4">
<div className="flex items-start gap-3 rounded-lg border border-offbase p-3 bg-surface-sunken"> <div className="flex items-start gap-3 rounded-lg border border-line p-3 bg-surface-sunken">
<div className="flex h-6 items-center"> <div className="flex h-6 items-center">
<input <input
data-testid="privacy-agree-checkbox" data-testid="privacy-agree-checkbox"
@ -121,7 +121,7 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
type="checkbox" type="checkbox"
checked={agreed} checked={agreed}
onChange={(e) => setAgreed(e.target.checked)} onChange={(e) => setAgreed(e.target.checked)}
className="h-4 w-4 rounded border-line text-accent focus:ring-accent bg-base" className="h-4 w-4 rounded border-line text-accent focus:ring-accent-line bg-surface"
/> />
</div> </div>
<div className="text-sm leading-6"> <div className="text-sm leading-6">
@ -216,7 +216,7 @@ export function showPrivacyModal(): void {
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition"> <DialogPanel className="w-full max-w-md transform rounded-lg bg-surface p-6 text-left align-middle shadow-elev-3 transition">
<DialogTitle <DialogTitle
as="h3" as="h3"
className="text-lg font-semibold leading-6 text-foreground" className="text-lg font-semibold leading-6 text-foreground"

View file

@ -32,7 +32,7 @@ export function ProgressCard({
const operationLabel = getOperationLabel(); const operationLabel = getOperationLabel();
return ( return (
<div className="bg-offbase rounded-lg p-3 space-y-2"> <div className="bg-surface-sunken rounded-lg p-3 space-y-2">
{/* Header with operation type and cancel button */} {/* Header with operation type and cancel button */}
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0 space-y-1"> <div className="flex-1 min-w-0 space-y-1">
@ -57,7 +57,7 @@ export function ProgressCard({
className={buttonClass({ className={buttonClass({
variant: 'ghost', variant: 'ghost',
size: 'xs', size: 'xs',
className: 'shrink-0 hover:bg-surface-sunken', className: 'shrink-0 hover:bg-accent-wash',
})} })}
onClick={(e) => onCancel(e)} onClick={(e) => onCancel(e)}
> >
@ -74,7 +74,7 @@ export function ProgressCard({
</div> </div>
{/* Stats row */} {/* Stats row */}
<div className="flex items-center gap-2 text-xs text-muted"> <div className="flex items-center gap-2 text-xs text-soft">
{completedChapters !== undefined && ( {completedChapters !== undefined && (
<> <>
<span className="font-medium">{completedChapters} chapters</span> <span className="font-medium">{completedChapters} chapters</span>

View file

@ -42,7 +42,7 @@ export function ProgressPopup({
<div className="fixed inset-x-0 top-2 z-[60] pointer-events-none px-4"> <div className="fixed inset-x-0 top-2 z-[60] pointer-events-none px-4">
<div className="w-full max-w-md mx-auto"> <div className="w-full max-w-md mx-auto">
<div <div
className={`pointer-events-auto shadow-xl ${ className={`pointer-events-auto shadow-elev-3 ${
onClick ? 'cursor-pointer' : '' onClick ? 'cursor-pointer' : ''
}`} }`}
onClick={onClick} onClick={onClick}

View file

@ -142,7 +142,7 @@ export function SettingsTrigger({
return ( return (
<Button <Button
onClick={onOpen} onClick={onOpen}
className={`inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase hover:text-accent transition-transform transition-colors duration-base ease-standard ${className}`} className={`inline-flex items-center py-1 px-2 rounded-md border border-line bg-surface text-foreground text-xs hover:bg-accent-wash hover:text-accent transition-transform transition-colors duration-base ease-standard ${className}`}
aria-label="Settings" aria-label="Settings"
tabIndex={0} tabIndex={0}
> >
@ -483,8 +483,8 @@ export function SettingsModal({
setActiveSection(visibleSections[0]?.id ?? 'theme'); setActiveSection(visibleSections[0]?.id ?? 'theme');
}, [activeSection, visibleSections]); }, [activeSection, visibleSections]);
const fieldLabelClass = 'block text-[11px] font-semibold uppercase tracking-wide text-muted'; const fieldLabelClass = 'block text-[11px] font-semibold uppercase tracking-wide text-soft';
const sectionShellClass = 'space-y-2 pb-3 border-b border-offbase px-0.5'; const sectionShellClass = 'space-y-2 pb-3 border-b border-line-soft px-0.5';
const sectionHeadingClass = 'text-sm font-semibold text-foreground'; const sectionHeadingClass = 'text-sm font-semibold text-foreground';
const effectiveProviderType = resolveEffectiveProviderType({ const effectiveProviderType = resolveEffectiveProviderType({
providerRef: selectedProviderRef, providerRef: selectedProviderRef,
@ -540,16 +540,16 @@ export function SettingsModal({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<DialogPanel data-testid="settings-modal" className="relative w-full max-w-4xl transform rounded-xl bg-base text-left align-middle shadow-xl transition overflow-hidden border border-offbase"> <DialogPanel data-testid="settings-modal" className="relative w-full max-w-4xl transform rounded-lg bg-surface text-left align-middle shadow-elev-3 transition overflow-hidden border border-line">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 py-3 border-b border-offbase"> <div className="flex items-center justify-between px-5 py-3 border-b border-line-soft">
<div className="flex items-baseline gap-4"> <div className="flex items-baseline gap-4">
<DialogTitle as="h3" className="text-lg font-semibold leading-6 text-foreground"> <DialogTitle as="h3" className="text-lg font-semibold leading-6 text-foreground">
Settings Settings
</DialogTitle> </DialogTitle>
<Button <Button
onClick={() => setIsChangelogOpen(true)} onClick={() => setIsChangelogOpen(true)}
className="text-sm font-medium leading-6 text-muted hover:text-accent transition-colors" className="text-sm font-medium leading-6 text-soft hover:text-accent transition-colors"
> >
{displayVersion ? `v${displayVersion} · Changelog` : 'Changelog'} {displayVersion ? `v${displayVersion} · Changelog` : 'Changelog'}
</Button> </Button>
@ -557,7 +557,7 @@ export function SettingsModal({
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
onClick={() => showPrivacyModal()} onClick={() => showPrivacyModal()}
className="text-sm font-medium text-muted hover:text-accent transition-colors" className="text-sm font-medium text-soft hover:text-accent transition-colors"
> >
Privacy Privacy
</Button> </Button>
@ -573,7 +573,7 @@ export function SettingsModal({
) : ( ) : (
<> <>
{/* Mobile: 2x2 grid nav */} {/* Mobile: 2x2 grid nav */}
<div className="grid grid-cols-2 gap-1 sm:hidden border-b border-offbase bg-background p-2"> <div className="grid grid-cols-2 gap-1 sm:hidden border-b border-line-soft bg-background p-2">
{visibleSections.map((section) => { {visibleSections.map((section) => {
const Icon = section.icon; const Icon = section.icon;
return ( return (
@ -583,7 +583,7 @@ export function SettingsModal({
className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-medium transition-colors ${ className={`flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-lg text-xs font-medium transition-colors ${
activeSection === section.id activeSection === section.id
? 'bg-accent text-background' ? 'bg-accent text-background'
: 'text-foreground hover:bg-offbase hover:text-accent' : 'text-foreground hover:bg-accent-wash hover:text-accent'
}`} }`}
> >
<Icon className="w-3.5 h-3.5" /> <Icon className="w-3.5 h-3.5" />
@ -595,7 +595,7 @@ export function SettingsModal({
<div className="flex flex-row h-[490px]"> <div className="flex flex-row h-[490px]">
{/* Desktop: vertical sidebar */} {/* Desktop: vertical sidebar */}
<nav className="hidden sm:block w-44 shrink-0 border-r border-offbase bg-background p-2"> <nav className="hidden sm:block w-44 shrink-0 border-r border-line-soft bg-background p-2">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{visibleSections.map((section) => { {visibleSections.map((section) => {
const Icon = section.icon; const Icon = section.icon;
@ -607,7 +607,7 @@ export function SettingsModal({
className={`w-full flex items-center gap-2.5 text-left px-2.5 py-1.5 rounded-md text-sm font-medium transition-colors whitespace-nowrap ${ className={`w-full flex items-center gap-2.5 text-left px-2.5 py-1.5 rounded-md text-sm font-medium transition-colors whitespace-nowrap ${
active active
? 'bg-accent text-background' ? 'bg-accent text-background'
: 'text-foreground hover:bg-base hover:text-accent' : 'text-foreground hover:bg-surface hover:text-accent'
}`} }`}
> >
<Icon className="w-4 h-4 shrink-0" /> <Icon className="w-4 h-4 shrink-0" />
@ -661,7 +661,7 @@ export function SettingsModal({
{selectedProviderOption?.name || 'Select Provider'} {selectedProviderOption?.name || 'Select Provider'}
</span> </span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-muted" /> <ChevronUpDownIcon className="h-5 w-5 text-soft" />
</span> </span>
</ListboxButton> </ListboxButton>
<Transition <Transition
@ -700,7 +700,7 @@ export function SettingsModal({
)} )}
</div> </div>
{restrictUserApiKeys && ( {restrictUserApiKeys && (
<p className="text-xs text-muted"> <p className="text-xs text-soft">
This instance restricts user API keys. TTS runs through admin-configured shared providers only. This instance restricts user API keys. TTS runs through admin-configured shared providers only.
</p> </p>
)} )}
@ -737,7 +737,7 @@ export function SettingsModal({
</div> </div>
)} )}
{isSharedSelected && ( {isSharedSelected && (
<p className="text-xs text-muted"> <p className="text-xs text-soft">
This is a shared provider configured by an admin. API key and base URL are managed server-side. This is a shared provider configured by an admin. API key and base URL are managed server-side.
</p> </p>
)} )}
@ -745,7 +745,7 @@ export function SettingsModal({
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className={fieldLabelClass}>TTS Model</label> <label className={fieldLabelClass}>TTS Model</label>
{!showAllProviderModels && ( {!showAllProviderModels && (
<p className="text-xs text-muted"> <p className="text-xs text-soft">
This instance restricts model selection to each provider&apos;s default model. This instance restricts model selection to each provider&apos;s default model.
</p> </p>
)} )}
@ -768,7 +768,7 @@ export function SettingsModal({
{selectedModel.name} {selectedModel.name}
</span> </span>
{selectedModelVersion && ( {selectedModelVersion && (
<span className="block truncate text-xs text-muted"> <span className="block truncate text-xs text-soft">
{selectedModelVersion} {selectedModelVersion}
</span> </span>
)} )}
@ -777,7 +777,7 @@ export function SettingsModal({
<span className="block truncate">Select Model</span> <span className="block truncate">Select Model</span>
)} )}
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-muted" /> <ChevronUpDownIcon className="h-5 w-5 text-soft" />
</span> </span>
</ListboxButton> </ListboxButton>
<Transition <Transition
@ -801,7 +801,7 @@ export function SettingsModal({
<span className={`block ${selected ? 'font-medium' : 'font-normal'}`}> <span className={`block ${selected ? 'font-medium' : 'font-normal'}`}>
<span className="block truncate">{model.name}</span> <span className="block truncate">{model.name}</span>
{model.id.includes(':') && ( {model.id.includes(':') && (
<span className="block truncate text-xs text-muted"> <span className="block truncate text-xs text-soft">
{model.id.slice(model.id.indexOf(':'))} {model.id.slice(model.id.indexOf(':'))}
</span> </span>
)} )}
@ -911,7 +911,7 @@ export function SettingsModal({
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 w-full text-left transition duration-base ease-standard transform border className={`flex items-center gap-2 rounded-lg px-2 py-1.5 w-full text-left transition duration-base ease-standard transform border
${isActive ${isActive
? 'border-accent' ? 'border-accent'
: 'border-offbase hover:border-muted' : 'border-line hover:border-accent-line'
}`} }`}
style={{ backgroundColor: colors.base }} style={{ backgroundColor: colors.base }}
> >
@ -927,8 +927,8 @@ export function SettingsModal({
{systemTheme.name} {systemTheme.name}
</span> </span>
<div className="flex gap-1 ml-auto"> <div className="flex gap-1 ml-auto">
<div className="w-4 h-4 rounded-full border border-offbase" style={{ backgroundColor: colors.background }} /> <div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
<div className="w-4 h-4 rounded-full border border-offbase" style={{ backgroundColor: colors.offbase }} /> <div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
@ -940,7 +940,7 @@ export function SettingsModal({
{/* Custom theme */} {/* Custom theme */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="block text-xs font-medium text-muted uppercase tracking-wide">Custom</label> <label className="block text-xs font-medium text-soft uppercase tracking-wide">Custom</label>
{(() => { {(() => {
const colors = getThemeColors('custom'); const colors = getThemeColors('custom');
const isActive = theme === 'custom'; const isActive = theme === 'custom';
@ -955,7 +955,7 @@ export function SettingsModal({
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 flex-1 text-left transition duration-base ease-standard transform border className={`flex items-center gap-2 rounded-lg px-2 py-1.5 flex-1 text-left transition duration-base ease-standard transform border
${isActive ${isActive
? 'border-accent' ? 'border-accent'
: 'border-offbase hover:border-muted' : 'border-line hover:border-accent-line'
}`} }`}
style={{ backgroundColor: colors.base }} style={{ backgroundColor: colors.base }}
> >
@ -971,8 +971,8 @@ export function SettingsModal({
Custom Custom
</span> </span>
<div className="flex gap-1 ml-auto"> <div className="flex gap-1 ml-auto">
<div className="w-4 h-4 rounded-full border border-offbase" style={{ backgroundColor: colors.background }} /> <div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
<div className="w-4 h-4 rounded-full border border-offbase" style={{ backgroundColor: colors.offbase }} /> <div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
@ -980,7 +980,7 @@ export function SettingsModal({
</button> </button>
<button <button
onClick={() => setIsCustomExpanded(!isCustomExpanded)} onClick={() => setIsCustomExpanded(!isCustomExpanded)}
className="shrink-0 p-1.5 rounded-lg border border-offbase hover:border-muted transition-colors" className="shrink-0 p-1.5 rounded-lg border border-line hover:border-accent-line transition-colors"
style={{ color: colors.muted, backgroundColor: colors.base }} style={{ color: colors.muted, backgroundColor: colors.base }}
aria-label={isCustomExpanded ? 'Collapse color picker' : 'Expand color picker'} aria-label={isCustomExpanded ? 'Collapse color picker' : 'Expand color picker'}
> >
@ -1043,7 +1043,7 @@ export function SettingsModal({
{/* Light themes */} {/* Light themes */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="block text-xs font-medium text-muted uppercase tracking-wide">Light</label> <label className="block text-xs font-medium text-soft uppercase tracking-wide">Light</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
{lightThemes.map((t) => { {lightThemes.map((t) => {
const colors = getThemeColors(t.id); const colors = getThemeColors(t.id);
@ -1055,7 +1055,7 @@ export function SettingsModal({
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition duration-base ease-standard transform border className={`flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition duration-base ease-standard transform border
${isActive ${isActive
? 'border-accent' ? 'border-accent'
: 'border-offbase hover:border-muted' : 'border-line hover:border-accent-line'
}`} }`}
style={{ backgroundColor: colors.base }} style={{ backgroundColor: colors.base }}
> >
@ -1071,8 +1071,8 @@ export function SettingsModal({
{t.name} {t.name}
</span> </span>
<div className="flex gap-1 ml-auto"> <div className="flex gap-1 ml-auto">
<div className="w-4 h-4 rounded-full border border-offbase" style={{ backgroundColor: colors.background }} /> <div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
<div className="w-4 h-4 rounded-full border border-offbase" style={{ backgroundColor: colors.offbase }} /> <div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
@ -1085,7 +1085,7 @@ export function SettingsModal({
{/* Dark themes */} {/* Dark themes */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="block text-xs font-medium text-muted uppercase tracking-wide">Dark</label> <label className="block text-xs font-medium text-soft uppercase tracking-wide">Dark</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5">
{darkThemes.map((t) => { {darkThemes.map((t) => {
const colors = getThemeColors(t.id); const colors = getThemeColors(t.id);
@ -1097,7 +1097,7 @@ export function SettingsModal({
className={`flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition duration-base ease-standard transform border className={`flex items-center gap-2 rounded-lg px-2 py-1.5 text-left transition duration-base ease-standard transform border
${isActive ${isActive
? 'border-accent' ? 'border-accent'
: 'border-offbase hover:border-muted' : 'border-line hover:border-accent-line'
}`} }`}
style={{ backgroundColor: colors.base }} style={{ backgroundColor: colors.base }}
> >
@ -1113,8 +1113,8 @@ export function SettingsModal({
{t.name} {t.name}
</span> </span>
<div className="flex gap-1 ml-auto"> <div className="flex gap-1 ml-auto">
<div className="w-4 h-4 rounded-full border border-offbase" style={{ backgroundColor: colors.background }} /> <div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.background }} />
<div className="w-4 h-4 rounded-full border border-offbase" style={{ backgroundColor: colors.offbase }} /> <div className="w-4 h-4 rounded-full border border-line" style={{ backgroundColor: colors.offbase }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.accent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.secondaryAccent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} /> <div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
@ -1199,10 +1199,10 @@ export function SettingsModal({
{activeSection === 'account' && ( {activeSection === 'account' && (
<div className="space-y-2"> <div className="space-y-2">
{/* Session info */} {/* Session info */}
<div className="rounded-lg bg-background border border-offbase p-4 space-y-2"> <div className="rounded-lg bg-background border border-line p-4 space-y-2">
<h4 className="text-sm font-medium text-foreground">Current Session</h4> <h4 className="text-sm font-medium text-foreground">Current Session</h4>
<div className="text-sm space-y-1"> <div className="text-sm space-y-1">
<p className="text-muted">Logged in as:</p> <p className="text-soft">Logged in as:</p>
{session?.user ? ( {session?.user ? (
<> <>
<p className="font-medium text-foreground"> <p className="font-medium text-foreground">
@ -1211,7 +1211,7 @@ export function SettingsModal({
: (session.user.name || session.user.email || 'Account')} : (session.user.name || session.user.email || 'Account')}
</p> </p>
{!session.user.isAnonymous && ( {!session.user.isAnonymous && (
<p className="text-xs text-muted font-mono">{session.user.email}</p> <p className="text-xs text-soft font-mono">{session.user.email}</p>
)} )}
{session.user.isAnonymous && ( {session.user.isAnonymous && (
<p className="text-xs text-accent mt-1">Anonymous session</p> <p className="text-xs text-accent mt-1">Anonymous session</p>
@ -1229,14 +1229,14 @@ export function SettingsModal({
onClick={() => { onClick={() => {
window.open('/api/user/export', '_blank'); window.open('/api/user/export', '_blank');
}} }}
className="w-full rounded-lg border border-offbase bg-background p-4 flex items-center gap-4 hover:bg-offbase transition-colors text-left group" className="w-full rounded-lg border border-line bg-background p-4 flex items-center gap-4 hover:bg-accent-wash transition-colors text-left group"
> >
<div className="flex-shrink-0 w-10 h-10 rounded-lg bg-offbase flex items-center justify-center group-hover:bg-background transition-colors"> <div className="flex-shrink-0 w-10 h-10 rounded-lg bg-surface-sunken flex items-center justify-center group-hover:bg-background transition-colors">
<DownloadIcon className="w-5 h-5 text-accent" /> <DownloadIcon className="w-5 h-5 text-accent" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">Export My Data</p> <p className="text-sm font-medium text-foreground">Export My Data</p>
<p className="text-xs text-muted">Download all your data as a ZIP file</p> <p className="text-xs text-soft">Download all your data as a ZIP file</p>
</div> </div>
</button> </button>
)} )}
@ -1252,7 +1252,7 @@ export function SettingsModal({
Disconnect account Disconnect account
</Button> </Button>
<div className="pt-4 mt-4 border-t border-offbase"> <div className="pt-4 mt-4 border-t border-line-soft">
<label className="block text-sm font-medium text-danger mb-2">Danger Zone</label> <label className="block text-sm font-medium text-danger mb-2">Danger Zone</label>
<Button <Button
onClick={() => setShowDeleteAccountConfirm(true)} onClick={() => setShowDeleteAccountConfirm(true)}
@ -1260,14 +1260,14 @@ export function SettingsModal({
> >
Delete Account Delete Account
</Button> </Button>
<p className="text-xs text-muted mt-2"> <p className="text-xs text-soft mt-2">
Permanently deletes your account and all data. Permanently deletes your account and all data.
</p> </p>
</div> </div>
</> </>
) : ( ) : (
<div className="pt-2 border-t border-offbase"> <div className="pt-2 border-t border-line-soft">
<p className="text-sm text-muted mb-3"> <p className="text-sm text-soft mb-3">
{session?.user?.isAnonymous {session?.user?.isAnonymous
? (runtimeConfig.enableUserSignups ? (runtimeConfig.enableUserSignups
? 'You are using an anonymous session. Sign up to save your progress permanently, your current data is automatically transferred.' ? 'You are using an anonymous session. Sign up to save your progress permanently, your current data is automatically transferred.'
@ -1435,11 +1435,11 @@ function SettingsChangelogPanel({
}, [expanded, manifest, manifestUrl, bodies]); }, [expanded, manifest, manifestUrl, bodies]);
return ( return (
<div className="h-[490px] flex flex-col bg-base"> <div className="h-[490px] flex flex-col bg-surface">
<div className="flex items-center gap-3 px-4 py-3 border-b border-offbase bg-background"> <div className="flex items-center gap-3 px-4 py-3 border-b border-line-soft bg-background">
<Button <Button
onClick={onClose} onClick={onClose}
className="inline-flex items-center justify-center rounded-md text-muted hover:text-accent hover:bg-base transition duration-base ease-standard transform" className="inline-flex items-center justify-center rounded-md text-soft hover:text-accent hover:bg-surface transition duration-base ease-standard transform"
aria-label="Back to settings" aria-label="Back to settings"
title="Back" title="Back"
> >
@ -1449,7 +1449,7 @@ function SettingsChangelogPanel({
</Button> </Button>
<div className="min-w-0"> <div className="min-w-0">
<h4 className="text-sm font-semibold text-foreground">Changelog</h4> <h4 className="text-sm font-semibold text-foreground">Changelog</h4>
<p className="text-xs text-muted truncate"> <p className="text-xs text-soft truncate">
{normalizedAppVersion {normalizedAppVersion
? `Current version: v${normalizedAppVersion}` ? `Current version: v${normalizedAppVersion}`
: 'Release history from GitHub'} : 'Release history from GitHub'}
@ -1459,15 +1459,15 @@ function SettingsChangelogPanel({
<div className="flex-1 overflow-y-auto px-3 pb-3"> <div className="flex-1 overflow-y-auto px-3 pb-3">
{loading && ( {loading && (
<div className="py-3 text-sm text-muted"> <div className="py-3 text-sm text-soft">
Loading changelog Loading changelog
</div> </div>
)} )}
{!loading && error && ( {!loading && error && (
<div className="py-3 space-y-2 border-b border-offbase"> <div className="py-3 space-y-2 border-b border-line-soft">
<p className="text-sm text-foreground">Could not load changelog right now.</p> <p className="text-sm text-foreground">Could not load changelog right now.</p>
<p className="text-xs text-muted break-words">{error}</p> <p className="text-xs text-soft break-words">{error}</p>
<a <a
href="https://github.com/richardr1126/openreader/releases" href="https://github.com/richardr1126/openreader/releases"
target="_blank" target="_blank"
@ -1480,7 +1480,7 @@ function SettingsChangelogPanel({
)} )}
{!loading && !error && manifest.length === 0 && ( {!loading && !error && manifest.length === 0 && (
<div className="py-3 text-sm text-muted"> <div className="py-3 text-sm text-soft">
No releases found. No releases found.
</div> </div>
)} )}
@ -1493,35 +1493,35 @@ function SettingsChangelogPanel({
const normalizedName = normalizeVersion(entry.name || ''); const normalizedName = normalizeVersion(entry.name || '');
const showName = Boolean(entry.name) && normalizedName !== normalizedTag; const showName = Boolean(entry.name) && normalizedName !== normalizedTag;
return ( return (
<div key={entry.tag_name} className="border-b border-offbase"> <div key={entry.tag_name} className="border-b border-line-soft">
<button <button
type="button" type="button"
onClick={() => setExpanded((prev) => ({ ...prev, [entry.tag_name]: !isExpanded }))} onClick={() => setExpanded((prev) => ({ ...prev, [entry.tag_name]: !isExpanded }))}
className="w-full text-left py-2 flex items-center gap-2 hover:bg-base transition duration-base ease-standard transform" className="w-full text-left py-2 flex items-center gap-2 hover:bg-surface transition duration-base ease-standard transform"
> >
<ChevronRightIcon <ChevronRightIcon
className={`w-3.5 h-3.5 shrink-0 text-muted transition-transform ${ className={`w-3.5 h-3.5 shrink-0 text-soft transition-transform ${
isExpanded ? 'rotate-90 text-foreground' : '' isExpanded ? 'rotate-90 text-foreground' : ''
}`} }`}
/> />
<div className="min-w-0 flex items-center gap-2 text-sm w-full"> <div className="min-w-0 flex items-center gap-2 text-sm w-full">
<span className="font-semibold text-foreground shrink-0">{entry.tag_name}</span> <span className="font-semibold text-foreground shrink-0">{entry.tag_name}</span>
{entry.prerelease && ( {entry.prerelease && (
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-muted shrink-0"> <span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-surface-sunken text-soft shrink-0">
prerelease prerelease
</span> </span>
)} )}
{isCurrent && ( {isCurrent && (
<span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-offbase text-accent shrink-0"> <span className="text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 bg-surface-sunken text-accent shrink-0">
current current
</span> </span>
)} )}
{showName && ( {showName && (
<span className="text-xs text-muted truncate"> <span className="text-xs text-soft truncate">
{entry.name} {entry.name}
</span> </span>
)} )}
<span className="text-[11px] text-muted shrink-0"> <span className="text-[11px] text-soft shrink-0">
{new Date(entry.published_at).toLocaleDateString()} {new Date(entry.published_at).toLocaleDateString()}
</span> </span>
</div> </div>
@ -1530,13 +1530,13 @@ function SettingsChangelogPanel({
{isExpanded && ( {isExpanded && (
<div className="pl-6 pr-1 pb-3 pt-1 space-y-2"> <div className="pl-6 pr-1 pb-3 pt-1 space-y-2">
{body ? ( {body ? (
<div className="text-sm text-foreground leading-6 space-y-2 [&_h1]:text-base [&_h1]:font-semibold [&_h2]:text-sm [&_h2]:font-semibold [&_ul]:pl-5 [&_ol]:pl-5 [&_code]:bg-offbase [&_code]:rounded [&_code]:px-1 [&_pre]:bg-offbase [&_pre]:rounded [&_pre]:p-2 [&_pre]:overflow-x-auto"> <div className="text-sm text-foreground leading-6 space-y-2 [&_h1]:text-base [&_h1]:font-semibold [&_h2]:text-sm [&_h2]:font-semibold [&_ul]:pl-5 [&_ol]:pl-5 [&_code]:bg-surface-sunken [&_code]:rounded [&_code]:px-1 [&_pre]:bg-surface-sunken [&_pre]:rounded [&_pre]:p-2 [&_pre]:overflow-x-auto">
<ReactMarkdown remarkPlugins={[remarkGfm]}> <ReactMarkdown remarkPlugins={[remarkGfm]}>
{body.body || '_No release notes provided._'} {body.body || '_No release notes provided._'}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
) : ( ) : (
<p className="text-xs text-muted">Loading release notes</p> <p className="text-xs text-soft">Loading release notes</p>
)} )}
<a <a
href={entry.html_url} href={entry.html_url}

View file

@ -252,11 +252,11 @@ export function AuthLoader({ children }: { children: ReactNode }) {
if (isLoading) { if (isLoading) {
return ( return (
<div className="fixed inset-0 bg-base z-50 flex flex-col items-center justify-center gap-4"> <div className="fixed inset-0 bg-surface z-50 flex flex-col items-center justify-center gap-4">
<LoadingSpinner className="w-8 h-8 text-accent" /> <LoadingSpinner className="w-8 h-8 text-accent" />
{bootstrapError ? ( {bootstrapError ? (
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<p className="text-sm text-muted text-center">{bootstrapError}</p> <p className="text-sm text-soft text-center">{bootstrapError}</p>
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
@ -270,7 +270,7 @@ export function AuthLoader({ children }: { children: ReactNode }) {
</button> </button>
</div> </div>
) : ( ) : (
<p className="text-sm text-muted animate-pulse"> <p className="text-sm text-soft animate-pulse">
{isAutoLoggingIn ? 'Starting anonymous session...' : 'Loading...'} {isAutoLoggingIn ? 'Starting anonymous session...' : 'Loading...'}
</p> </p>
)} )}

View file

@ -99,7 +99,7 @@ export default function ClaimDataModal({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<DialogPanel data-testid="claim-modal" className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition"> <DialogPanel data-testid="claim-modal" className="w-full max-w-md transform rounded-lg bg-surface p-6 text-left align-middle shadow-elev-3 transition">
<DialogTitle <DialogTitle
as="h3" as="h3"
className="text-lg font-semibold leading-6 text-foreground mb-4" className="text-lg font-semibold leading-6 text-foreground mb-4"
@ -107,13 +107,13 @@ export default function ClaimDataModal({
Existing Data Found Existing Data Found
</DialogTitle> </DialogTitle>
<p className="text-sm text-muted mb-2"> <p className="text-sm text-soft mb-2">
We found existing anonymous data from before auth was enabled. We found existing anonymous data from before auth was enabled.
Claim it now to attach it to your account. Claim it now to attach it to your account.
</p> </p>
<div className="mb-4 rounded-lg border border-offbase bg-surface-sunken p-3"> <div className="mb-4 rounded-lg border border-line bg-surface-sunken p-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted">Claimable data</div> <div className="text-xs font-semibold uppercase tracking-wide text-soft">Claimable data</div>
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-soft"> <ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-soft">
<li>{claimableCounts.documents} document(s)</li> <li>{claimableCounts.documents} document(s)</li>
<li>{claimableCounts.audiobooks} audiobook(s)</li> <li>{claimableCounts.audiobooks} audiobook(s)</li>
@ -133,7 +133,7 @@ export default function ClaimDataModal({
onClick={onDismiss} onClick={onDismiss}
disabled={isClaiming} disabled={isClaiming}
className="inline-flex justify-center rounded-lg bg-background px-3 py-1.5 text-sm 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 font-medium text-foreground hover:bg-accent-wash focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-base ease-standard hover:text-accent transform transition-transform duration-base ease-standard hover:text-accent
disabled:opacity-50" disabled:opacity-50"

View file

@ -72,7 +72,7 @@ export function RateLimitIndicator({ className = '' }: RateLimitBannerProps) {
if (isWarning) { if (isWarning) {
return ( return (
<span className={`text-xs text-muted ${className}`}> <span className={`text-xs text-soft ${className}`}>
{formatCharCount(status.remainingChars)} chars left {formatCharCount(status.remainingChars)} chars left
</span> </span>
); );

View file

@ -32,19 +32,19 @@ export function UserMenu({
}; };
const rowClass = const rowClass =
'w-full inline-flex items-center gap-2 px-2 py-1 rounded-md text-[12px] border border-transparent transition duration-base ease-standard text-left hover:border-accent hover:bg-offbase hover:text-accent'; 'w-full inline-flex items-center gap-2 px-2 py-1 rounded-md text-[12px] border border-transparent transition duration-base ease-standard text-left hover:border-accent hover:bg-accent-wash hover:text-accent';
if (!session || session.user.isAnonymous) { if (!session || session.user.isAnonymous) {
if (variant === 'sidebar') { if (variant === 'sidebar') {
return ( return (
<div className={`flex w-full flex-col gap-0.5 ${className}`}> <div className={`flex w-full flex-col gap-0.5 ${className}`}>
<Link href="/signin" className={rowClass}> <Link href="/signin" className={rowClass}>
<UserIcon className="h-3.5 w-3.5 text-muted" /> <UserIcon className="h-3.5 w-3.5 text-soft" />
<span className="truncate">Connect</span> <span className="truncate">Connect</span>
</Link> </Link>
{enableUserSignups && ( {enableUserSignups && (
<Link href="/signup" className={rowClass}> <Link href="/signup" className={rowClass}>
<UserIcon className="h-3.5 w-3.5 text-muted" /> <UserIcon className="h-3.5 w-3.5 text-soft" />
<span className="truncate">Create account</span> <span className="truncate">Create account</span>
</Link> </Link>
)} )}
@ -55,13 +55,13 @@ export function UserMenu({
return ( return (
<div className={`flex gap-2 ${className}`}> <div className={`flex gap-2 ${className}`}>
<Link href="/signin"> <Link href="/signin">
<Button className="inline-flex items-center rounded-md bg-base border border-offbase px-2 py-1 text-xs font-medium text-foreground hover:bg-offbase focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 transform transition duration-base ease-standard hover:text-accent"> <Button className="inline-flex items-center rounded-md bg-surface border border-line px-2 py-1 text-xs font-medium text-foreground hover:bg-accent-wash focus:outline-none focus:ring-2 focus:ring-accent-line focus:ring-offset-2 transform transition duration-base ease-standard hover:text-accent">
Connect Connect
</Button> </Button>
</Link> </Link>
{enableUserSignups && ( {enableUserSignups && (
<Link href="/signup"> <Link href="/signup">
<Button className="inline-flex items-center rounded-md bg-accent px-2 py-1 text-xs font-medium text-background hover:bg-secondary-accent focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 transform transition duration-base ease-standard"> <Button className="inline-flex items-center rounded-md bg-accent px-2 py-1 text-xs font-medium text-background hover:bg-secondary-accent focus:outline-none focus:ring-2 focus:ring-accent-line focus:ring-offset-2 transform transition duration-base ease-standard">
Create account Create account
</Button> </Button>
</Link> </Link>
@ -78,7 +78,7 @@ export function UserMenu({
title="Disconnect account" title="Disconnect account"
aria-label="Disconnect account" aria-label="Disconnect account"
> >
<UserIcon className="h-3.5 w-3.5 text-muted" /> <UserIcon className="h-3.5 w-3.5 text-soft" />
<span className="truncate flex-1">{session.user.email || 'Account'}</span> <span className="truncate flex-1">{session.user.email || 'Account'}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path> <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
@ -90,7 +90,7 @@ export function UserMenu({
} }
return ( return (
<div className={`flex items-center gap-2 px-2 py-1 rounded-md border border-offbase bg-base ${className}`}> <div className={`flex items-center gap-2 px-2 py-1 rounded-md border border-line bg-surface ${className}`}>
<span className="hidden sm:block text-xs font-medium text-foreground truncate max-w-[160px]"> <span className="hidden sm:block text-xs font-medium text-foreground truncate max-w-[160px]">
{session.user.email || 'Account'} {session.user.email || 'Account'}
</span> </span>

View file

@ -42,7 +42,7 @@ export function CreateFolderDialog({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition"> <DialogPanel className="w-full max-w-md transform rounded-lg bg-surface p-6 text-left align-middle shadow-elev-3 transition">
<DialogTitle as="h3" className="text-lg font-semibold text-foreground"> <DialogTitle as="h3" className="text-lg font-semibold text-foreground">
Create New Folder Create New Folder
</DialogTitle> </DialogTitle>
@ -53,10 +53,10 @@ export function CreateFolderDialog({
onChange={(e) => onFolderNameChange(e.target.value)} onChange={(e) => onFolderNameChange(e.target.value)}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
placeholder="Enter folder name" placeholder="Enter folder name"
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-1 focus:ring-accent" className="w-full rounded-lg bg-surface-sunken py-2 px-3 text-foreground shadow-elev-1 focus:outline-none focus:ring-1 focus:ring-accent-line"
autoFocus autoFocus
/> />
<p className="mt-2 text-xs text-muted">Press Enter to create or Escape to cancel</p> <p className="mt-2 text-xs text-soft">Press Enter to create or Escape to cancel</p>
</div> </div>
</DialogPanel> </DialogPanel>
</TransitionChild> </TransitionChild>

View file

@ -139,14 +139,14 @@ function SidebarUploadLoader({
const dashOffset = circumference - (progress / 100) * circumference; const dashOffset = circumference - (progress / 100) * circumference;
return ( return (
<div className="rounded-md border border-offbase bg-surface-sunken px-2 py-1.5"> <div className="rounded-md border border-line bg-surface-sunken px-2 py-1.5">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="min-w-0 flex items-center gap-1.5 text-[11px] leading-tight"> <div className="min-w-0 flex items-center gap-1.5 text-[11px] leading-tight">
<span className="font-medium text-foreground">{label}</span> <span className="font-medium text-foreground">{label}</span>
<span className="shrink-0 tabular-nums text-muted">{completedFiles}/{totalFiles}</span> <span className="shrink-0 tabular-nums text-soft">{completedFiles}/{totalFiles}</span>
</div> </div>
<div className="shrink-0 flex items-center gap-1 text-accent" aria-label={`Upload progress ${progress}%`}> <div className="shrink-0 flex items-center gap-1 text-accent" aria-label={`Upload progress ${progress}%`}>
<span className="text-[10px] tabular-nums text-muted">{progress}%</span> <span className="text-[10px] tabular-nums text-soft">{progress}%</span>
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className={phase === 'converting' ? 'animate-spin' : ''}> <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} className={phase === 'converting' ? 'animate-spin' : ''}>
<circle <circle
cx={size / 2} cx={size / 2}
@ -174,7 +174,7 @@ function SidebarUploadLoader({
</div> </div>
</div> </div>
{currentFileName && ( {currentFileName && (
<p className="mt-0.5 truncate text-[10px] text-muted" title={currentFileName}> <p className="mt-0.5 truncate text-[10px] text-soft" title={currentFileName}>
{currentFileName} {currentFileName}
</p> </p>
)} )}
@ -649,15 +649,15 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
}} }}
> >
{!isLoading && showHint && allDocuments.length > 1 && ( {!isLoading && showHint && allDocuments.length > 1 && (
<div className="px-3 pt-3 shrink-0 bg-background"> <div className="px-3 pt-3 shrink-0 bg-surface-sunken">
<div className="flex items-center justify-between bg-base border border-offbase rounded-md px-3 py-1 text-[12px]"> <div className="flex items-center justify-between bg-surface border border-line rounded-md px-3 py-1 text-[12px]">
<p className="text-foreground"> <p className="text-foreground">
Drag files onto each other to make folders. Drop into the sidebar to move. Drag files onto each other to make folders. Drop into the sidebar to move.
</p> </p>
<button <button
type="button" type="button"
onClick={() => setShowHint(false)} onClick={() => setShowHint(false)}
className="h-6 w-6 inline-flex items-center justify-center text-muted hover:text-accent hover:bg-base rounded transition duration-base ease-standard" className="h-6 w-6 inline-flex items-center justify-center text-soft hover:text-accent hover:bg-surface rounded transition duration-base ease-standard"
aria-label="Dismiss hint" aria-label="Dismiss hint"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View file

@ -15,11 +15,11 @@ function IconsSkeleton({ iconSize }: { iconSize: IconSize }) {
<div className="h-full min-h-0 overflow-y-auto p-3"> <div className="h-full min-h-0 overflow-y-auto p-3">
<div className="grid" style={iconsGridStyle(iconSize, ICON_SKELETON_ITEM_COUNT)}> <div className="grid" style={iconsGridStyle(iconSize, ICON_SKELETON_ITEM_COUNT)}>
{Array.from({ length: ICON_SKELETON_ITEM_COUNT }).map((_, index) => ( {Array.from({ length: ICON_SKELETON_ITEM_COUNT }).map((_, index) => (
<div key={index} className="overflow-hidden rounded-md border border-offbase bg-base"> <div key={index} className="overflow-hidden rounded-md border border-line bg-surface">
<div className="aspect-[3/4] w-full bg-offbase" /> <div className="aspect-[3/4] w-full bg-surface-sunken" />
<div className="flex items-center gap-2 px-2 py-2"> <div className="flex items-center gap-2 px-2 py-2">
<div className="h-3.5 w-3.5 rounded bg-offbase" /> <div className="h-3.5 w-3.5 rounded bg-surface-sunken" />
<div className="h-2.5 w-4/5 rounded bg-offbase" /> <div className="h-2.5 w-4/5 rounded bg-surface-sunken" />
</div> </div>
</div> </div>
))} ))}
@ -31,18 +31,18 @@ function IconsSkeleton({ iconSize }: { iconSize: IconSize }) {
function ListSkeleton() { function ListSkeleton() {
return ( return (
<div className="h-full min-h-0 overflow-y-auto"> <div className="h-full min-h-0 overflow-y-auto">
<div className="sticky top-0 z-10 bg-base border-b border-offbase grid grid-cols-[minmax(0,1fr)_44px_72px_104px_28px] sm:grid-cols-[minmax(0,1fr)_56px_96px_140px_32px]"> <div className="sticky top-0 z-10 bg-surface border-b border-line-soft grid grid-cols-[minmax(0,1fr)_44px_72px_104px_28px] sm:grid-cols-[minmax(0,1fr)_56px_96px_140px_32px]">
<div className="h-8 flex items-center px-2"> <div className="h-8 flex items-center px-2">
<div className="h-2.5 w-12 rounded bg-offbase" /> <div className="h-2.5 w-12 rounded bg-surface-sunken" />
</div> </div>
<div className="h-8 flex items-center px-2"> <div className="h-8 flex items-center px-2">
<div className="h-2.5 w-10 rounded bg-offbase" /> <div className="h-2.5 w-10 rounded bg-surface-sunken" />
</div> </div>
<div className="h-8 flex items-center justify-end px-2"> <div className="h-8 flex items-center justify-end px-2">
<div className="h-2.5 w-10 rounded bg-offbase" /> <div className="h-2.5 w-10 rounded bg-surface-sunken" />
</div> </div>
<div className="h-8 flex items-center px-2"> <div className="h-8 flex items-center px-2">
<div className="h-2.5 w-14 rounded bg-offbase" /> <div className="h-2.5 w-14 rounded bg-surface-sunken" />
</div> </div>
<div /> <div />
</div> </div>
@ -50,23 +50,23 @@ function ListSkeleton() {
{Array.from({ length: 10 }).map((_, index) => ( {Array.from({ length: 10 }).map((_, index) => (
<div <div
key={index} key={index}
className="grid grid-cols-[minmax(0,1fr)_44px_72px_104px_28px] sm:grid-cols-[minmax(0,1fr)_56px_96px_140px_32px] items-center border-b border-offbase h-[35px]" className="grid grid-cols-[minmax(0,1fr)_44px_72px_104px_28px] sm:grid-cols-[minmax(0,1fr)_56px_96px_140px_32px] items-center border-b border-line-soft h-[35px]"
> >
<div className="px-2 flex items-center gap-2"> <div className="px-2 flex items-center gap-2">
<div className="h-3.5 w-3.5 rounded bg-offbase" /> <div className="h-3.5 w-3.5 rounded bg-surface-sunken" />
<div className="h-2.5 w-2/3 rounded bg-offbase" /> <div className="h-2.5 w-2/3 rounded bg-surface-sunken" />
</div> </div>
<div className="px-2"> <div className="px-2">
<div className="h-2.5 w-8 rounded bg-offbase" /> <div className="h-2.5 w-8 rounded bg-surface-sunken" />
</div> </div>
<div className="px-2 flex justify-end"> <div className="px-2 flex justify-end">
<div className="h-2.5 w-12 rounded bg-offbase" /> <div className="h-2.5 w-12 rounded bg-surface-sunken" />
</div> </div>
<div className="px-2"> <div className="px-2">
<div className="h-2.5 w-16 rounded bg-offbase" /> <div className="h-2.5 w-16 rounded bg-surface-sunken" />
</div> </div>
<div className="px-2 flex justify-center"> <div className="px-2 flex justify-center">
<div className="h-4 w-4 rounded bg-offbase" /> <div className="h-4 w-4 rounded bg-surface-sunken" />
</div> </div>
</div> </div>
))} ))}
@ -78,24 +78,24 @@ function ListSkeleton() {
function GallerySkeleton() { function GallerySkeleton() {
return ( return (
<div className="h-full min-h-0 flex flex-col"> <div className="h-full min-h-0 flex flex-col">
<div className="flex-1 min-h-0 flex items-center justify-center p-6 bg-background"> <div className="flex-1 min-h-0 flex items-center justify-center p-6 bg-surface-sunken">
<div className="flex flex-col items-center gap-3 max-w-[420px]"> <div className="flex flex-col items-center gap-3 max-w-[420px]">
<div className="w-[260px] sm:w-[320px] aspect-[3/4] rounded-lg border border-offbase bg-offbase" /> <div className="w-[260px] sm:w-[320px] aspect-[3/4] rounded-lg border border-line bg-surface-sunken" />
<div className="h-3 w-40 rounded bg-offbase" /> <div className="h-3 w-40 rounded bg-surface-sunken" />
<div className="h-2.5 w-28 rounded bg-offbase" /> <div className="h-2.5 w-28 rounded bg-surface-sunken" />
<div className="flex gap-2"> <div className="flex gap-2">
<div className="h-8 w-20 rounded-md bg-offbase" /> <div className="h-8 w-20 rounded-md bg-surface-sunken" />
<div className="h-8 w-20 rounded-md bg-offbase" /> <div className="h-8 w-20 rounded-md bg-surface-sunken" />
</div> </div>
</div> </div>
</div> </div>
<div className="shrink-0 border-t border-offbase bg-base"> <div className="shrink-0 border-t border-line-soft bg-surface">
<div className="flex gap-2 overflow-x-auto p-2"> <div className="flex gap-2 overflow-x-auto p-2">
{Array.from({ length: 10 }).map((_, index) => ( {Array.from({ length: 10 }).map((_, index) => (
<div key={index} className="shrink-0 w-[88px] rounded-md overflow-hidden border border-offbase bg-base"> <div key={index} className="shrink-0 w-[88px] rounded-md overflow-hidden border border-line bg-surface">
<div className="aspect-[3/4] bg-offbase" /> <div className="aspect-[3/4] bg-surface-sunken" />
<div className="p-1.5"> <div className="p-1.5">
<div className="h-2.5 w-5/6 rounded bg-offbase" /> <div className="h-2.5 w-5/6 rounded bg-surface-sunken" />
</div> </div>
</div> </div>
))} ))}

View file

@ -243,7 +243,7 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className="relative w-full aspect-[3/4] overflow-hidden rounded-t-md bg-base" className="relative w-full aspect-[3/4] overflow-hidden rounded-t-md bg-surface"
> >
{imagePreview ? ( {imagePreview ? (
<> <>

View file

@ -140,9 +140,9 @@ export function DocumentTile({
className={ className={
'group relative flex flex-col rounded-md overflow-hidden border transition duration-base ease-standard ' + 'group relative flex flex-col rounded-md overflow-hidden border transition duration-base ease-standard ' +
(isSelected (isSelected
? 'border-accent bg-offbase' ? 'border-accent-line bg-surface-sunken'
: 'border-offbase bg-base hover:bg-offbase hover:border-accent') + : 'border-line bg-surface hover:bg-accent-wash hover:border-accent-line') +
(isDropTarget ? ' ring-1 ring-accent' : '') + (isDropTarget ? ' ring-1 ring-accent-line' : '') +
(isDragging ? ' opacity-50' : '') (isDragging ? ' opacity-50' : '')
} }
> >
@ -170,7 +170,7 @@ export function DocumentTile({
) : doc.type === 'epub' ? ( ) : doc.type === 'epub' ? (
<EPUBIcon className={`${FILE_ICON_CLASSES[iconSize]} text-accent`} /> <EPUBIcon className={`${FILE_ICON_CLASSES[iconSize]} text-accent`} />
) : ( ) : (
<FileIcon className={`${FILE_ICON_CLASSES[iconSize]} text-muted`} /> <FileIcon className={`${FILE_ICON_CLASSES[iconSize]} text-soft`} />
)} )}
</span> </span>
<span <span
@ -187,7 +187,7 @@ export function DocumentTile({
{showDeleteButton && ( {showDeleteButton && (
<Button <Button
onClick={() => onDelete(doc)} onClick={() => onDelete(doc)}
className={`inline-flex items-center justify-center text-muted hover:text-accent hover:bg-base focus:outline-none transition-colors duration-base ${TRASH_BTN_CLASSES[iconSize]}`} className={`inline-flex items-center justify-center text-soft hover:text-accent hover:bg-surface focus:outline-none transition-colors duration-base ${TRASH_BTN_CLASSES[iconSize]}`}
aria-label={`Delete ${doc.name}`} aria-label={`Delete ${doc.name}`}
> >
<svg className={TRASH_ICON_CLASSES[iconSize]} fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className={TRASH_ICON_CLASSES[iconSize]} fill="none" stroke="currentColor" viewBox="0 0 24 24">

View file

@ -40,7 +40,7 @@ function formatParseStatus(status: DocumentListDocument['parseStatus']): string
function KindIcon({ doc, className }: { doc: DocumentListDocument; className?: string }) { function KindIcon({ doc, className }: { doc: DocumentListDocument; className?: string }) {
if (doc.type === 'pdf') return <PDFIcon className={className ?? 'w-4 h-4 text-danger'} />; if (doc.type === 'pdf') return <PDFIcon className={className ?? 'w-4 h-4 text-danger'} />;
if (doc.type === 'epub') return <EPUBIcon className={className ?? 'w-4 h-4 text-accent'} />; if (doc.type === 'epub') return <EPUBIcon className={className ?? 'w-4 h-4 text-accent'} />;
return <FileIcon className={className ?? 'w-4 h-4 text-muted'} />; return <FileIcon className={className ?? 'w-4 h-4 text-soft'} />;
} }
function GalleryThumb({ function GalleryThumb({
@ -92,29 +92,29 @@ function GalleryThumb({
onClick={onClick} onClick={onClick}
aria-current={active ? 'true' : undefined} aria-current={active ? 'true' : undefined}
className={ className={
'group relative w-[98px] sm:w-[110px] shrink-0 cursor-pointer rounded-lg overflow-hidden border bg-base snap-start transition duration-base ease-standard ' + 'group relative w-[98px] sm:w-[110px] shrink-0 cursor-pointer rounded-lg overflow-hidden border bg-surface snap-start transition duration-base ease-standard ' +
(active (active
? 'border-accent shadow-[0_10px_24px_-18px_rgba(0,0,0,0.85)] -translate-y-0.5' ? 'border-accent-line shadow-elev-2 -translate-y-px'
: 'border-offbase hover:border-accent hover:-translate-y-0.5') + : 'border-line hover:border-accent-line hover:-translate-y-px hover:shadow-elev-2') +
(isOver && canDrop ? ' border-accent' : '') + (isOver && canDrop ? ' border-accent-line' : '') +
(isDragging ? ' opacity-50' : '') (isDragging ? ' opacity-50' : '')
} }
title={doc.name} title={doc.name}
> >
<div className="aspect-[3/4] bg-base"> <div className="aspect-[3/4] bg-surface">
<DocumentPreview doc={doc} /> <DocumentPreview doc={doc} />
</div> </div>
<div <div
className={ className={
'px-2 py-1.5 flex items-center gap-1.5 border-t transition-colors duration-base ' + 'px-2 py-1.5 flex items-center gap-1.5 border-t transition-colors duration-base ' +
(active ? 'bg-offbase border-accent' : 'bg-base border-offbase') (active ? 'bg-surface-sunken border-accent-line' : 'bg-surface border-line')
} }
> >
<KindIcon <KindIcon
doc={doc} doc={doc}
className={ className={
'w-3 h-3 shrink-0 transition-colors duration-base ' + 'w-3 h-3 shrink-0 transition-colors duration-base ' +
(active ? 'text-accent' : 'text-muted') (active ? 'text-accent' : 'text-soft')
} }
/> />
<span <span
@ -181,19 +181,19 @@ export function GalleryView({
return ( return (
<div className="flex-1 min-h-0 flex flex-col"> <div className="flex-1 min-h-0 flex flex-col">
<div className="flex-1 min-h-0 overflow-y-auto bg-background"> <div className="flex-1 min-h-0 overflow-y-auto bg-surface-sunken">
<div className="min-h-full flex items-center justify-center p-3 sm:p-6"> <div className="min-h-full flex items-center justify-center p-3 sm:p-6">
{activeDoc ? ( {activeDoc ? (
<div className="w-full max-w-[920px] flex flex-col md:flex-row items-center md:items-start justify-center gap-4 md:gap-6"> <div className="w-full max-w-[920px] flex flex-col md:flex-row items-center md:items-start justify-center gap-4 md:gap-6">
<div className="flex flex-col items-center gap-3 w-[180px] sm:w-[260px] md:w-[320px] shrink-0"> <div className="flex flex-col items-center gap-3 w-[180px] sm:w-[260px] md:w-[320px] shrink-0">
<div className="w-full aspect-[3/4] rounded-lg overflow-hidden border border-offbase shadow-lg"> <div className="w-full aspect-[3/4] rounded-lg overflow-hidden border border-line shadow-elev-2">
<DocumentPreview doc={activeDoc} /> <DocumentPreview doc={activeDoc} />
</div> </div>
<div className="text-center"> <div className="text-center">
<h2 className="text-[14px] font-semibold text-foreground truncate max-w-[320px]"> <h2 className="text-[14px] font-semibold text-foreground truncate max-w-[320px]">
{activeDoc.name} {activeDoc.name}
</h2> </h2>
<p className="text-[11px] text-muted"> <p className="text-[11px] text-soft">
{activeDoc.type.toUpperCase()} {formatDocumentSize(activeDoc.size)} {activeDoc.type.toUpperCase()} {formatDocumentSize(activeDoc.size)}
</p> </p>
</div> </div>
@ -214,18 +214,18 @@ export function GalleryView({
</button> </button>
</div> </div>
</div> </div>
<dl className="w-full max-w-[280px] sm:max-w-[360px] md:max-w-[340px] grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 rounded-md border border-offbase bg-base px-3 py-2 text-[11px] md:self-center"> <dl className="w-full max-w-[280px] sm:max-w-[360px] md:max-w-[340px] grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 rounded-md border border-line bg-surface px-3 py-2 text-[11px] md:self-center">
<dt className="text-muted">Type</dt> <dt className="text-soft">Type</dt>
<dd className="text-foreground text-right uppercase tracking-wide">{activeDoc.type}</dd> <dd className="text-foreground text-right uppercase tracking-wide">{activeDoc.type}</dd>
<dt className="text-muted">Size</dt> <dt className="text-soft">Size</dt>
<dd className="text-foreground text-right tabular-nums">{formatDocumentSize(activeDoc.size)}</dd> <dd className="text-foreground text-right tabular-nums">{formatDocumentSize(activeDoc.size)}</dd>
<dt className="text-muted">Last opened</dt> <dt className="text-soft">Last opened</dt>
<dd className="text-foreground text-right">{formatDateTime(activeDoc.recentlyOpenedAt)}</dd> <dd className="text-foreground text-right">{formatDateTime(activeDoc.recentlyOpenedAt)}</dd>
<dt className="text-muted">Last modified</dt> <dt className="text-soft">Last modified</dt>
<dd className="text-foreground text-right">{formatDateTime(activeDoc.lastModified)}</dd> <dd className="text-foreground text-right">{formatDateTime(activeDoc.lastModified)}</dd>
{activeDoc.folderId && ( {activeDoc.folderId && (
<> <>
<dt className="text-muted">Folder</dt> <dt className="text-soft">Folder</dt>
<dd className="text-foreground text-right truncate" title={folderNameById?.[activeDoc.folderId] ?? activeDoc.folderId}> <dd className="text-foreground text-right truncate" title={folderNameById?.[activeDoc.folderId] ?? activeDoc.folderId}>
{folderNameById?.[activeDoc.folderId] ?? activeDoc.folderId} {folderNameById?.[activeDoc.folderId] ?? activeDoc.folderId}
</dd> </dd>
@ -233,19 +233,19 @@ export function GalleryView({
)} )}
{activeDoc.type === 'pdf' && ( {activeDoc.type === 'pdf' && (
<> <>
<dt className="text-muted">Parse status</dt> <dt className="text-soft">Parse status</dt>
<dd className="text-foreground text-right">{formatParseStatus(activeDoc.parseStatus)}</dd> <dd className="text-foreground text-right">{formatParseStatus(activeDoc.parseStatus)}</dd>
</> </>
)} )}
</dl> </dl>
</div> </div>
) : ( ) : (
<p className="text-[12px] text-muted">No documents to show</p> <p className="text-[12px] text-soft">No documents to show</p>
)} )}
</div> </div>
</div> </div>
<div className="shrink-0 border-t border-offbase bg-gradient-to-b from-base to-offbase/30"> <div className="shrink-0 border-t border-line-soft bg-gradient-to-b from-surface to-surface-sunken">
<div <div
ref={railRef} ref={railRef}
className="flex gap-2.5 overflow-x-auto pl-4 pr-3 pt-2.5 pb-1.5 snap-x snap-proximity scroll-pl-4 sm:scroll-pl-5 scroll-pr-3 sm:scroll-pr-4" className="flex gap-2.5 overflow-x-auto pl-4 pr-3 pt-2.5 pb-1.5 snap-x snap-proximity scroll-pl-4 sm:scroll-pl-5 scroll-pr-3 sm:scroll-pr-4"

View file

@ -31,7 +31,7 @@ function formatDate(ms: number): string {
function KindIcon({ doc }: { doc: DocumentListDocument }) { function KindIcon({ doc }: { doc: DocumentListDocument }) {
if (doc.type === 'pdf') return <PDFIcon className="w-4 h-4 shrink-0 text-danger" />; if (doc.type === 'pdf') return <PDFIcon className="w-4 h-4 shrink-0 text-danger" />;
if (doc.type === 'epub') return <EPUBIcon className="w-4 h-4 shrink-0 text-accent" />; if (doc.type === 'epub') return <EPUBIcon className="w-4 h-4 shrink-0 text-accent" />;
return <FileIcon className="w-4 h-4 shrink-0 text-muted" />; return <FileIcon className="w-4 h-4 shrink-0 text-soft" />;
} }
function HeaderCell({ function HeaderCell({
@ -63,7 +63,7 @@ function HeaderCell({
}} }}
className={ className={
'flex items-center gap-1 px-2 py-1.5 text-[10px] uppercase tracking-wide font-semibold transition-colors duration-base ease-standard hover:text-accent ' + 'flex items-center gap-1 px-2 py-1.5 text-[10px] uppercase tracking-wide font-semibold transition-colors duration-base ease-standard hover:text-accent ' +
(active ? 'text-accent' : 'text-muted') + (active ? 'text-accent' : 'text-soft') +
(align === 'right' ? ' justify-end' : '') + (align === 'right' ? ' justify-end' : '') +
' ' + ' ' +
(className ?? '') (className ?? '')
@ -131,11 +131,11 @@ function DocRow({
data-doc-tile data-doc-tile
aria-selected={isSelected} aria-selected={isSelected}
className={ className={
'grid grid-cols-[minmax(0,1fr)_44px_72px_104px_28px] sm:grid-cols-[minmax(0,1fr)_56px_96px_140px_32px] items-center text-[12px] border-b border-offbase transition-colors duration-base ease-standard ' + 'grid grid-cols-[minmax(0,1fr)_44px_72px_104px_28px] sm:grid-cols-[minmax(0,1fr)_56px_96px_140px_32px] items-center text-[12px] border-b border-line-soft transition-colors duration-base ease-standard ' +
(isSelected (isSelected
? 'bg-offbase text-accent' ? 'bg-surface-sunken text-accent'
: 'text-foreground hover:bg-offbase') + : 'text-foreground hover:bg-accent-wash') +
(isTarget ? ' ring-1 ring-accent ring-inset' : '') + (isTarget ? ' ring-1 ring-accent-line ring-inset' : '') +
(isDragging ? ' opacity-50' : '') (isDragging ? ' opacity-50' : '')
} }
> >
@ -149,11 +149,11 @@ function DocRow({
<KindIcon doc={doc} /> <KindIcon doc={doc} />
<span className="truncate">{doc.name}</span> <span className="truncate">{doc.name}</span>
</Link> </Link>
<span className="px-2 text-[11px] text-muted uppercase tracking-wide">{doc.type}</span> <span className="px-2 text-[11px] text-soft uppercase tracking-wide">{doc.type}</span>
<span className="px-2 text-[11px] text-muted text-right tabular-nums"> <span className="px-2 text-[11px] text-soft text-right tabular-nums">
{formatDocumentSize(doc.size)} {formatDocumentSize(doc.size)}
</span> </span>
<span className="px-2 text-[11px] text-muted tabular-nums"> <span className="px-2 text-[11px] text-soft tabular-nums">
{formatDate(doc.lastModified)} {formatDate(doc.lastModified)}
</span> </span>
<Button <Button
@ -161,7 +161,7 @@ function DocRow({
e.stopPropagation(); e.stopPropagation();
onDeleteDoc(doc); onDeleteDoc(doc);
}} }}
className="h-7 w-7 flex items-center justify-center text-muted hover:text-accent hover:bg-offbase rounded transition duration-base ease-standard" className="h-7 w-7 flex items-center justify-center text-soft hover:text-accent hover:bg-accent-wash rounded transition duration-base ease-standard"
aria-label={`Delete ${doc.name}`} aria-label={`Delete ${doc.name}`}
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -198,7 +198,7 @@ export function ListView({
return ( return (
<div onClick={handleBackgroundClick} className="flex-1 min-h-0 overflow-y-auto"> <div onClick={handleBackgroundClick} className="flex-1 min-h-0 overflow-y-auto">
<div className="sticky top-0 z-10 bg-base border-b border-offbase grid grid-cols-[minmax(0,1fr)_44px_72px_104px_28px] sm:grid-cols-[minmax(0,1fr)_56px_96px_140px_32px]"> <div className="sticky top-0 z-10 bg-surface border-b border-line-soft grid grid-cols-[minmax(0,1fr)_44px_72px_104px_28px] sm:grid-cols-[minmax(0,1fr)_56px_96px_140px_32px]">
<HeaderCell label="Name" field="name" sortBy={sortBy} sortDirection={sortDirection} onSortChange={onSortChange} /> <HeaderCell label="Name" field="name" sortBy={sortBy} sortDirection={sortDirection} onSortChange={onSortChange} />
<HeaderCell label="Kind" field="type" sortBy={sortBy} sortDirection={sortDirection} onSortChange={onSortChange} /> <HeaderCell label="Kind" field="type" sortBy={sortBy} sortDirection={sortDirection} onSortChange={onSortChange} />
<HeaderCell label="Size" field="size" sortBy={sortBy} sortDirection={sortDirection} onSortChange={onSortChange} align="right" /> <HeaderCell label="Size" field="size" sortBy={sortBy} sortDirection={sortDirection} onSortChange={onSortChange} align="right" />

View file

@ -58,15 +58,15 @@ function SidebarRow({
className={ className={
'group w-full flex items-center gap-2 px-2 py-1 rounded-md text-[12px] border transform transition duration-base ease-standard text-left ' + 'group w-full flex items-center gap-2 px-2 py-1 rounded-md text-[12px] border transform transition duration-base ease-standard text-left ' +
(active (active
? 'border-accent bg-offbase text-accent' ? 'border-accent-line bg-surface-sunken text-accent'
: 'border-transparent bg-transparent text-foreground hover:border-accent hover:text-accent') + : 'border-transparent bg-transparent text-foreground hover:border-accent-line hover:text-accent') +
(isDropTarget ? ' ring-1 ring-accent' : '') (isDropTarget ? ' ring-1 ring-accent-line' : '')
} }
> >
<span <span
className={ className={
'w-4 h-4 shrink-0 flex items-center justify-center transition-colors duration-base ' + 'w-4 h-4 shrink-0 flex items-center justify-center transition-colors duration-base ' +
(active ? 'text-accent' : 'text-muted group-hover:text-accent') (active ? 'text-accent' : 'text-soft group-hover:text-accent')
} }
> >
{icon} {icon}
@ -74,7 +74,7 @@ function SidebarRow({
<span className="truncate flex-1">{label}</span> <span className="truncate flex-1">{label}</span>
{typeof count === 'number' && count > 0 && ( {typeof count === 'number' && count > 0 && (
<span <span
className={`text-[10px] text-muted tabular-nums transition-transform duration-base ease-standard ${countClassName ?? ''}`} className={`text-[10px] text-soft tabular-nums transition-transform duration-base ease-standard ${countClassName ?? ''}`}
> >
{count} {count}
</span> </span>
@ -137,7 +137,7 @@ function FolderRow({
e.stopPropagation(); e.stopPropagation();
onDelete(); onDelete();
}} }}
className="absolute right-1 top-1/2 -translate-y-1/2 h-5 w-5 inline-flex items-center justify-center rounded text-muted opacity-0 group-hover/folder:opacity-100 group-focus-within/folder:opacity-100 hover:text-accent hover:bg-offbase transition" className="absolute right-1 top-1/2 -translate-y-1/2 h-5 w-5 inline-flex items-center justify-center rounded text-soft opacity-0 group-hover/folder:opacity-100 group-focus-within/folder:opacity-100 hover:text-accent hover:bg-accent-wash transition"
aria-label={`Delete ${folder.name}`} aria-label={`Delete ${folder.name}`}
title={`Delete ${folder.name}`} title={`Delete ${folder.name}`}
> >
@ -161,7 +161,7 @@ function SectionHeader({
return ( return (
<div <div
className={ className={
'px-2 pb-1 text-[10px] uppercase tracking-[0.08em] text-muted font-semibold leading-none flex items-center justify-between ' + 'px-2 pb-1 text-[10px] uppercase tracking-[0.08em] text-soft font-semibold leading-none flex items-center justify-between ' +
(isFirst ? 'pt-1.5' : 'pt-3') (isFirst ? 'pt-1.5' : 'pt-3')
} }
> >
@ -206,7 +206,7 @@ export function FinderSidebar({
return ( return (
<aside <aside
style={{ '--sidebar-width': `${width}px` } as CSSProperties} style={{ '--sidebar-width': `${width}px` } as CSSProperties}
className="relative h-full w-full md:[width:var(--sidebar-width)] bg-base border-r border-offbase shrink-0 flex flex-col" className="relative h-full w-full md:[width:var(--sidebar-width)] bg-surface border-r border-line-soft shrink-0 flex flex-col"
> >
<div className="min-h-0 flex-1 overflow-y-auto"> <div className="min-h-0 flex-1 overflow-y-auto">
<div className="p-2 flex flex-col gap-0.5"> <div className="p-2 flex flex-col gap-0.5">
@ -272,7 +272,7 @@ export function FinderSidebar({
rightSlot={( rightSlot={(
<Menu as="div" className="relative inline-flex items-center leading-none text-left shrink-0 normal-case tracking-normal font-normal"> <Menu as="div" className="relative inline-flex items-center leading-none text-left shrink-0 normal-case tracking-normal font-normal">
<MenuButton <MenuButton
className="inline-flex items-center justify-center h-3.5 w-5 rounded-sm text-muted hover:text-accent transition-colors duration-base ease-standard focus:outline-none" className="inline-flex items-center justify-center h-3.5 w-5 rounded-sm text-soft hover:text-accent transition-colors duration-base ease-standard focus:outline-none"
title="Folder actions" title="Folder actions"
aria-label="Folder actions" aria-label="Folder actions"
> >
@ -289,7 +289,7 @@ export function FinderSidebar({
> >
<MenuItems <MenuItems
anchor="bottom start" anchor="bottom start"
className="z-50 mt-2 min-w-[180px] rounded-md bg-base shadow-lg ring-1 ring-black/5 focus:outline-none p-1 normal-case tracking-normal font-normal" className="z-50 mt-2 min-w-[180px] rounded-md bg-surface shadow-elev-2 ring-1 ring-line-soft focus:outline-none p-1 normal-case tracking-normal font-normal"
> >
<MenuItem> <MenuItem>
{({ active }) => ( {({ active }) => (
@ -299,7 +299,7 @@ export function FinderSidebar({
onNewFolder(); onNewFolder();
onRowAction?.(); onRowAction?.();
}} }}
className={`${active ? 'bg-offbase text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`} className={`${active ? 'bg-surface-sunken text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
> >
<FolderPlusIcon className="h-4 w-4" /> <FolderPlusIcon className="h-4 w-4" />
New Folder New Folder
@ -315,7 +315,7 @@ export function FinderSidebar({
onRowAction?.(); onRowAction?.();
}} }}
disabled={disabled} disabled={disabled}
className={`${disabled ? 'text-faint cursor-not-allowed' : active ? 'bg-offbase text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`} className={`${disabled ? 'text-faint cursor-not-allowed' : active ? 'bg-surface-sunken text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
> >
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
@ -332,7 +332,7 @@ export function FinderSidebar({
Folders Folders
</SectionHeader> </SectionHeader>
{folders.length === 0 ? ( {folders.length === 0 ? (
<p className="px-2 py-1 text-[11px] text-muted">No folders yet</p> <p className="px-2 py-1 text-[11px] text-soft">No folders yet</p>
) : ( ) : (
folders.map((folder) => ( folders.map((folder) => (
<FolderRow <FolderRow
@ -355,7 +355,7 @@ export function FinderSidebar({
</div> </div>
{bottomSlot && ( {bottomSlot && (
<div <div
className="shrink-0 border-t border-offbase px-2 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] md:pb-2" className="shrink-0 border-t border-line-soft px-2 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] md:pb-2"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
{bottomSlot} {bottomSlot}
@ -370,7 +370,7 @@ export function FinderSidebar({
onPointerMove={onResizeMove} onPointerMove={onResizeMove}
onPointerUp={onResizeEnd} onPointerUp={onResizeEnd}
onPointerCancel={onResizeEnd} onPointerCancel={onResizeEnd}
className="hidden md:block absolute top-0 right-0 h-full w-1 cursor-col-resize hover:bg-offbase active:bg-accent transition-colors duration-base ease-standard" className="hidden md:block absolute top-0 right-0 h-full w-1 cursor-col-resize hover:bg-accent-wash active:bg-accent transition-colors duration-base ease-standard"
/> />
</aside> </aside>
); );

View file

@ -19,14 +19,14 @@ export function FinderStatusBar({
<div <div
role="status" role="status"
aria-live="polite" aria-live="polite"
className="min-h-6 px-3 pb-[env(safe-area-inset-bottom)] flex items-center justify-between gap-3 text-[11px] text-muted bg-base border-t border-offbase select-none" className="min-h-6 px-3 pb-[env(safe-area-inset-bottom)] flex items-center justify-between gap-3 text-[11px] text-soft bg-surface border-t border-line-soft select-none"
> >
<span className="truncate">{summary}</span> <span className="truncate">{summary}</span>
<span className="shrink-0"> <span className="shrink-0">
{selectedCount > 0 {selectedCount > 0
? `${selectedCount} of ${itemCount} selected` ? `${selectedCount} of ${itemCount} selected`
: `${itemCount} item${itemCount === 1 ? '' : 's'}`} : `${itemCount} item${itemCount === 1 ? '' : 's'}`}
<span className="mx-1.5 text-muted"></span> <span className="mx-1.5 text-soft"></span>
{formatDocumentSize(totalSize)} {formatDocumentSize(totalSize)}
</span> </span>
</div> </div>

View file

@ -54,19 +54,19 @@ const ICON_SIZES: Array<{ value: IconSize; label: string }> = [
// Match SettingsModal / UserMenu trigger sizing exactly so all bar buttons share one rhythm. // Match SettingsModal / UserMenu trigger sizing exactly so all bar buttons share one rhythm.
const TOOLBAR_BTN = const TOOLBAR_BTN =
'inline-flex items-center py-1 px-2 rounded-md border bg-base text-xs transition duration-base ease-standard'; 'inline-flex items-center py-1 px-2 rounded-md border bg-surface text-xs transition duration-base ease-standard';
const TOOLBAR_BTN_INACTIVE = const TOOLBAR_BTN_INACTIVE =
'border-offbase text-foreground hover:text-accent hover:border-accent hover:bg-offbase'; 'border-line text-foreground hover:text-accent hover:border-accent-line hover:bg-accent-wash';
const TOOLBAR_BTN_ACTIVE = 'border-accent bg-offbase text-accent'; const TOOLBAR_BTN_ACTIVE = 'border-accent-line bg-surface-sunken text-accent';
// Pill-grouped segmented control. Outer pill carries the border; inner segments are // Pill-grouped segmented control. Outer pill carries the border; inner segments are
// borderless and rely on bg/text color to show active/hover. Sized so the whole pill // borderless and rely on bg/text color to show active/hover. Sized so the whole pill
// matches the height of a standalone TOOLBAR_BTN. // matches the height of a standalone TOOLBAR_BTN.
const PILL = 'inline-flex items-center rounded-md border border-offbase bg-base p-0.5 gap-0.5 shrink-0'; const PILL = 'inline-flex items-center rounded-md border border-line bg-surface p-0.5 gap-0.5 shrink-0';
const PILL_SEGMENT = const PILL_SEGMENT =
'inline-flex items-center justify-center rounded-[5px] text-xs transition-colors duration-base ease-standard'; 'inline-flex items-center justify-center rounded-sm text-xs transition-colors duration-base ease-standard';
const PILL_SEGMENT_INACTIVE = 'text-muted hover:bg-offbase hover:text-accent'; const PILL_SEGMENT_INACTIVE = 'text-soft hover:bg-accent-wash hover:text-accent';
const PILL_SEGMENT_ACTIVE = 'bg-offbase text-accent'; const PILL_SEGMENT_ACTIVE = 'bg-surface-sunken text-accent';
export function FinderToolbar({ export function FinderToolbar({
viewMode, viewMode,
@ -89,10 +89,10 @@ export function FinderToolbar({
const directionLabel = sortDirection === 'asc' ? currentSort.asc : currentSort.desc; const directionLabel = sortDirection === 'asc' ? currentSort.asc : currentSort.desc;
return ( return (
<div className="sticky top-0 z-40 w-full border-b border-offbase bg-base"> <div className="sticky top-0 z-40 w-full border-b border-line-soft bg-surface">
<div className="px-2 sm:px-3 py-1 min-h-10 flex items-center gap-1.5 sm:gap-2"> <div className="px-2 sm:px-3 py-1 min-h-10 flex items-center gap-1.5 sm:gap-2">
{leftSlot && ( {leftSlot && (
<div className="shrink-0 flex items-center gap-2 pr-1 sm:pr-2 sm:border-r sm:border-offbase"> <div className="shrink-0 flex items-center gap-2 pr-1 sm:pr-2 sm:border-r sm:border-line">
{leftSlot} {leftSlot}
</div> </div>
)} )}
@ -135,7 +135,7 @@ export function FinderToolbar({
<div <div
className="absolute top-full left-1/2 z-30 -translate-x-1/2 pt-1 opacity-0 pointer-events-none transition-opacity duration-fast group-hover/icons:opacity-100 group-hover/icons:pointer-events-auto group-focus-within/icons:opacity-100 group-focus-within/icons:pointer-events-auto" className="absolute top-full left-1/2 z-30 -translate-x-1/2 pt-1 opacity-0 pointer-events-none transition-opacity duration-fast group-hover/icons:opacity-100 group-hover/icons:pointer-events-auto group-focus-within/icons:opacity-100 group-focus-within/icons:pointer-events-auto"
> >
<div className={`${PILL} shadow-lg`}> <div className={`${PILL} shadow-elev-2`}>
{ICON_SIZES.map(({ value: sizeValue, label: sizeLabel }) => { {ICON_SIZES.map(({ value: sizeValue, label: sizeLabel }) => {
const sizeActive = iconSize === sizeValue; const sizeActive = iconSize === sizeValue;
return ( return (
@ -182,7 +182,7 @@ export function FinderToolbar({
</ListboxButton> </ListboxButton>
<ListboxOptions <ListboxOptions
anchor="bottom end" anchor="bottom end"
className="z-50 mt-1 rounded-md bg-background border border-offbase shadow-lg p-1 focus:outline-none" className="z-50 mt-1 rounded-md bg-surface-sunken border border-line shadow-elev-2 p-1 focus:outline-none"
> >
{SORT_OPTIONS.map((opt) => ( {SORT_OPTIONS.map((opt) => (
<ListboxOption <ListboxOption
@ -190,7 +190,7 @@ export function FinderToolbar({
value={opt.value} value={opt.value}
className={({ active, selected }) => className={({ active, selected }) =>
`cursor-pointer select-none rounded-sm py-1.5 px-2.5 text-xs ${ `cursor-pointer select-none rounded-sm py-1.5 px-2.5 text-xs ${
active ? 'bg-offbase text-accent' : 'text-foreground' active ? 'bg-surface-sunken text-accent' : 'text-foreground'
} ${selected ? 'font-semibold' : ''}` } ${selected ? 'font-semibold' : ''}`
} }
> >
@ -204,19 +204,19 @@ export function FinderToolbar({
<div className="flex-1 min-w-0" /> <div className="flex-1 min-w-0" />
<div className="hidden sm:flex items-center gap-1.5 px-2 py-1 rounded-md bg-background border border-offbase hover:border-accent focus-within:ring-1 focus-within:ring-accent focus-within:border-accent transition-colors duration-base ease-standard w-[160px] md:w-[200px]"> <div className="hidden sm:flex items-center gap-1.5 px-2 py-1 rounded-md bg-surface-sunken border border-line hover:border-accent-line focus-within:ring-1 focus-within:ring-accent-line focus-within:border-accent-line transition-colors duration-base ease-standard w-[160px] md:w-[200px]">
<SearchIcon className="w-3.5 h-3.5 text-muted shrink-0" /> <SearchIcon className="w-3.5 h-3.5 text-soft shrink-0" />
<input <input
type="search" type="search"
value={query} value={query}
onChange={(e) => onQueryChange(e.target.value)} onChange={(e) => onQueryChange(e.target.value)}
placeholder="Search" placeholder="Search"
className="flex-1 min-w-0 bg-transparent outline-none text-xs text-foreground placeholder:text-muted" className="flex-1 min-w-0 bg-transparent outline-none text-xs text-foreground placeholder:text-soft"
/> />
</div> </div>
{rightSlot && ( {rightSlot && (
<div className="shrink-0 flex items-center gap-2 pl-1 sm:pl-2 sm:border-l sm:border-offbase ml-0.5"> <div className="shrink-0 flex items-center gap-2 pl-1 sm:pl-2 sm:border-l sm:border-line ml-0.5">
{rightSlot} {rightSlot}
</div> </div>
)} )}

View file

@ -46,7 +46,7 @@ export function FinderWindow({
const isNarrow = useIsNarrow(); const isNarrow = useIsNarrow();
return ( return (
<div className="flex flex-col h-full w-full bg-background overflow-hidden"> <div className="flex flex-col h-full w-full bg-surface-sunken overflow-hidden">
{toolbar} {toolbar}
<div className="flex flex-1 min-h-0"> <div className="flex flex-1 min-h-0">
@ -89,7 +89,7 @@ export function FinderWindow({
leaveTo="-translate-x-full" leaveTo="-translate-x-full"
> >
<DialogPanel <DialogPanel
className="w-[80vw] max-w-[280px] h-full bg-base shadow-xl" className="w-[80vw] max-w-[280px] h-full bg-surface shadow-elev-3"
> >
{sidebar} {sidebar}
</DialogPanel> </DialogPanel>

View file

@ -161,12 +161,12 @@ export function DexieMigrationModal({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<DialogPanel data-testid="migration-modal" className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition"> <DialogPanel data-testid="migration-modal" className="w-full max-w-md transform rounded-lg bg-surface p-6 text-left align-middle shadow-elev-3 transition">
<DialogTitle as="h3" className="text-lg font-semibold leading-6 text-foreground mb-4"> <DialogTitle as="h3" className="text-lg font-semibold leading-6 text-foreground mb-4">
{title} {title}
</DialogTitle> </DialogTitle>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm text-muted mb-2"> <p className="text-sm text-soft mb-2">
Found {localCount} document{localCount === 1 ? '' : 's'} stored locally from an older version. Found {localCount} document{localCount === 1 ? '' : 's'} stored locally from an older version.
{displayMissingCount > 0 ? ( {displayMissingCount > 0 ? (
<> {displayMissingCount} {displayMissingCount === 1 ? 'is' : 'are'} not here yet.</> <> {displayMissingCount} {displayMissingCount === 1 ? 'is' : 'are'} not here yet.</>
@ -175,8 +175,8 @@ export function DexieMigrationModal({
</p> </p>
{isUploading && ( {isUploading && (
<div className="space-y-1"> <div className="space-y-1">
<p className="text-xs text-muted">{status}</p> <p className="text-xs text-soft">{status}</p>
<div className="h-2 w-full rounded bg-offbase"> <div className="h-2 w-full rounded bg-surface-sunken">
<div className="h-2 rounded bg-accent" style={{ width: `${Math.max(1, Math.round(progress))}%` }} /> <div className="h-2 rounded bg-accent" style={{ width: `${Math.max(1, Math.round(progress))}%` }} />
</div> </div>
</div> </div>
@ -190,7 +190,7 @@ export function DexieMigrationModal({
onClick={handleSkip} onClick={handleSkip}
disabled={isUploading} disabled={isUploading}
className="inline-flex justify-center rounded-lg bg-background px-3 py-1.5 text-sm 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 font-medium text-foreground hover:bg-accent-wash focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-base ease-standard hover:text-accent transform transition-transform duration-base ease-standard hover:text-accent
disabled:opacity-50" disabled:opacity-50"

View file

@ -49,10 +49,10 @@ export function DocumentHeaderMenu({
{onOpenSegments && ( {onOpenSegments && (
<button <button
onClick={onOpenSegments} onClick={onOpenSegments}
className={`inline-flex items-center py-1 px-2 rounded-md border bg-base text-xs transition duration-base ease-standard ${ className={`inline-flex items-center py-1 px-2 rounded-md border bg-surface text-xs transition duration-base ease-standard ${
isSegmentsOpen isSegmentsOpen
? 'border-accent text-accent bg-offbase' ? 'border-accent text-accent bg-surface-sunken'
: 'border-offbase text-foreground hover:bg-offbase hover:text-accent' : 'border-line text-foreground hover:bg-accent-wash hover:text-accent'
}`} }`}
aria-label={isSegmentsOpen ? 'Hide segments sidebar' : 'Open segments sidebar'} aria-label={isSegmentsOpen ? 'Hide segments sidebar' : 'Open segments sidebar'}
title={isSegmentsOpen ? 'Hide Segments' : 'Segments'} title={isSegmentsOpen ? 'Hide Segments' : 'Segments'}
@ -63,10 +63,10 @@ export function DocumentHeaderMenu({
{showAudiobookExport && onOpenAudiobook && ( {showAudiobookExport && onOpenAudiobook && (
<button <button
onClick={onOpenAudiobook} onClick={onOpenAudiobook}
className={`inline-flex items-center py-1 px-2 rounded-md border bg-base text-xs transition duration-base ease-standard ${ className={`inline-flex items-center py-1 px-2 rounded-md border bg-surface text-xs transition duration-base ease-standard ${
isAudiobookOpen isAudiobookOpen
? 'border-accent text-accent bg-offbase' ? 'border-accent text-accent bg-surface-sunken'
: 'border-offbase text-foreground hover:bg-offbase hover:text-accent' : 'border-line text-foreground hover:bg-accent-wash hover:text-accent'
}`} }`}
aria-label={isAudiobookOpen ? 'Hide audiobook export' : 'Open audiobook export'} aria-label={isAudiobookOpen ? 'Hide audiobook export' : 'Open audiobook export'}
title={isAudiobookOpen ? 'Hide Export Audiobook' : 'Export Audiobook'} title={isAudiobookOpen ? 'Hide Export Audiobook' : 'Export Audiobook'}
@ -76,10 +76,10 @@ export function DocumentHeaderMenu({
)} )}
<button <button
onClick={onOpenSettings} onClick={onOpenSettings}
className={`inline-flex items-center py-1 px-2 rounded-md border bg-base text-xs transition duration-base ease-standard ${ className={`inline-flex items-center py-1 px-2 rounded-md border bg-surface text-xs transition duration-base ease-standard ${
isSettingsOpen isSettingsOpen
? 'border-accent text-accent bg-offbase' ? 'border-accent text-accent bg-surface-sunken'
: 'border-offbase text-foreground hover:bg-offbase hover:text-accent' : 'border-line text-foreground hover:bg-accent-wash hover:text-accent'
}`} }`}
aria-label={isSettingsOpen ? 'Hide settings' : 'Open settings'} aria-label={isSettingsOpen ? 'Hide settings' : 'Open settings'}
title={isSettingsOpen ? 'Hide Settings' : 'Settings'} title={isSettingsOpen ? 'Hide Settings' : 'Settings'}
@ -95,7 +95,7 @@ export function DocumentHeaderMenu({
<div className="sm:hidden flex items-center"> <div className="sm:hidden flex items-center">
<Menu as="div" className="relative inline-block text-left"> <Menu as="div" className="relative inline-block text-left">
<MenuButton <MenuButton
className="inline-flex items-center justify-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition duration-base ease-standard hover:text-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-accent" className="inline-flex items-center justify-center py-1 px-2 rounded-md border border-line bg-surface text-foreground text-xs hover:bg-accent-wash transition duration-base ease-standard hover:text-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-accent"
title="Menu" title="Menu"
> >
<DotsVerticalIcon className="w-4 h-4 transform transition-transform duration-base ease-standard hover:text-accent" /> <DotsVerticalIcon className="w-4 h-4 transform transition-transform duration-base ease-standard hover:text-accent" />
@ -109,10 +109,10 @@ export function DocumentHeaderMenu({
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<MenuItems className="absolute right-0 mt-2 min-w-max origin-top-right divide-y divide-offbase rounded-md bg-base shadow-lg ring-1 ring-black/5 focus:outline-none z-50"> <MenuItems className="absolute right-0 mt-2 min-w-max origin-top-right divide-y divide-line-soft rounded-md bg-surface shadow-elev-2 ring-1 ring-line-soft focus:outline-none z-50">
{/* Zoom Controls Section */} {/* Zoom Controls Section */}
<div className="px-4 py-3"> <div className="px-4 py-3">
<p className="text-xs font-medium text-muted mb-2">Zoom / Padding</p> <p className="text-xs font-medium text-soft mb-2">Zoom / Padding</p>
<div className="flex justify-center"> <div className="flex justify-center">
<ZoomControl <ZoomControl
value={zoomLevel} value={zoomLevel}
@ -138,7 +138,7 @@ export function DocumentHeaderMenu({
{({ active }) => ( {({ active }) => (
<button <button
onClick={onOpenSegments} onClick={onOpenSegments}
className={`${active || isSegmentsOpen ? 'bg-offbase text-accent' : 'text-foreground' className={`${active || isSegmentsOpen ? 'bg-surface-sunken text-accent' : 'text-foreground'
} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`} } group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
> >
<ListIcon className="h-4 w-4" /> <ListIcon className="h-4 w-4" />
@ -152,7 +152,7 @@ export function DocumentHeaderMenu({
{({ active }) => ( {({ active }) => (
<button <button
onClick={onOpenAudiobook} onClick={onOpenAudiobook}
className={`${active || isAudiobookOpen ? 'bg-offbase text-accent' : 'text-foreground' className={`${active || isAudiobookOpen ? 'bg-surface-sunken text-accent' : 'text-foreground'
} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`} } group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
> >
<DownloadIcon className="h-4 w-4" /> <DownloadIcon className="h-4 w-4" />
@ -165,7 +165,7 @@ export function DocumentHeaderMenu({
{({ active }) => ( {({ active }) => (
<button <button
onClick={onOpenSettings} onClick={onOpenSettings}
className={`${active || isSettingsOpen ? 'bg-offbase text-accent' : 'text-foreground' className={`${active || isSettingsOpen ? 'bg-surface-sunken text-accent' : 'text-foreground'
} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`} } group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
> >
<FileSettingsIcon className="h-4 w-4" /> <FileSettingsIcon className="h-4 w-4" />
@ -176,7 +176,7 @@ export function DocumentHeaderMenu({
</div> </div>
{/* Auth Section */} {/* Auth Section */}
<div className="p-2 border-t border-offbase flex justify-center"> <div className="p-2 border-t border-line-soft flex justify-center">
<UserMenu /> <UserMenu />
</div> </div>
</MenuItems> </MenuItems>

View file

@ -139,7 +139,7 @@ export function DocumentSelectionModal({
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<DialogPanel className="w-full max-w-2xl transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition flex flex-col h-[80vh]"> <DialogPanel className="w-full max-w-2xl transform rounded-lg bg-surface p-6 text-left align-middle shadow-elev-3 transition flex flex-col h-[80vh]">
<DialogTitle <DialogTitle
as="h3" as="h3"
className="text-lg font-semibold leading-6 text-foreground mb-4 flex-shrink-0 flex justify-between items-center" className="text-lg font-semibold leading-6 text-foreground mb-4 flex-shrink-0 flex justify-between items-center"
@ -147,10 +147,10 @@ export function DocumentSelectionModal({
{title} {title}
{files.length > 0 && ( {files.length > 0 && (
<div className="flex items-center text-sm font-normal"> <div className="flex items-center text-sm font-normal">
<label className="flex items-center gap-2 cursor-pointer select-none text-muted hover:text-foreground transition-colors"> <label className="flex items-center gap-2 cursor-pointer select-none text-soft hover:text-foreground transition-colors">
<input <input
type="checkbox" type="checkbox"
className="rounded border-muted text-accent focus:ring-accent" className="rounded border-muted text-accent focus:ring-accent-line"
checked={allSelected} checked={allSelected}
ref={input => { ref={input => {
if (input) input.indeterminate = isIndeterminate; if (input) input.indeterminate = isIndeterminate;
@ -163,13 +163,13 @@ export function DocumentSelectionModal({
)} )}
</DialogTitle> </DialogTitle>
<div className="flex-1 overflow-auto border border-offbase rounded-lg bg-background p-2 min-h-0"> <div className="flex-1 overflow-auto border border-line rounded-lg bg-background p-2 min-h-0">
{isLoading ? ( {isLoading ? (
<DocumentSelectionSkeleton /> <DocumentSelectionSkeleton />
) : errorMessage ? ( ) : errorMessage ? (
<div className="flex items-center justify-center h-full text-danger">{errorMessage}</div> <div className="flex items-center justify-center h-full text-danger">{errorMessage}</div>
) : files.length === 0 ? ( ) : files.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted">No documents found.</div> <div className="flex items-center justify-center h-full text-soft">No documents found.</div>
) : ( ) : (
<div className="space-y-0.5"> <div className="space-y-0.5">
{files.map((file) => { {files.map((file) => {
@ -179,7 +179,7 @@ export function DocumentSelectionModal({
key={file.id} key={file.id}
onClick={(e) => handleRowClick(e, file.id)} onClick={(e) => handleRowClick(e, file.id)}
className={`flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer text-sm select-none className={`flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer text-sm select-none
${isSelected ? 'bg-accent-wash' : 'hover:bg-offbase'} ${isSelected ? 'bg-accent-wash' : 'hover:bg-accent-wash'}
`} `}
> >
<div className="flex-shrink-0" onClick={(e) => e.stopPropagation()}> <div className="flex-shrink-0" onClick={(e) => e.stopPropagation()}>
@ -187,7 +187,7 @@ export function DocumentSelectionModal({
type="checkbox" type="checkbox"
checked={isSelected} checked={isSelected}
onChange={(e) => handleCheckboxChange(file.id, e.target.checked)} onChange={(e) => handleCheckboxChange(file.id, e.target.checked)}
className="rounded border-muted text-accent focus:ring-accent" className="rounded border-muted text-accent focus:ring-accent-line"
/> />
</div> </div>
<div <div
@ -197,7 +197,7 @@ export function DocumentSelectionModal({
> >
{file.name} {file.name}
</div> </div>
<div className="text-muted text-xs whitespace-nowrap">{formatSize(file.size)}</div> <div className="text-soft text-xs whitespace-nowrap">{formatSize(file.size)}</div>
</div> </div>
); );
})} })}
@ -237,9 +237,9 @@ function DocumentSelectionSkeleton() {
<div className="h-full animate-pulse space-y-0.5" aria-label="Loading documents" aria-busy="true"> <div className="h-full animate-pulse space-y-0.5" aria-label="Loading documents" aria-busy="true">
{rows.map((_, index) => ( {rows.map((_, index) => (
<div key={index} className="flex items-center gap-3 px-3 py-2 rounded-md"> <div key={index} className="flex items-center gap-3 px-3 py-2 rounded-md">
<div className="h-4 w-4 rounded-sm bg-offbase border border-offbase" /> <div className="h-4 w-4 rounded-sm bg-surface-sunken border border-line" />
<div className="h-3.5 flex-1 rounded bg-offbase" /> <div className="h-3.5 flex-1 rounded bg-surface-sunken" />
<div className="h-3 w-14 rounded bg-offbase" /> <div className="h-3 w-14 rounded bg-surface-sunken" />
</div> </div>
))} ))}
</div> </div>

View file

@ -163,13 +163,13 @@ export function DocumentUploader({
variant === 'compact' variant === 'compact'
? `${ ? `${
isDragActive isDragActive
? 'border border-accent bg-offbase text-accent' ? 'border border-accent bg-surface-sunken text-accent'
: 'border border-dashed border-offbase text-foreground hover:border-accent hover:bg-offbase hover:text-accent' : 'border border-dashed border-line text-foreground hover:border-accent hover:bg-accent-wash hover:text-accent'
}` }`
: `${ : `${
isDragActive isDragActive
? 'border-2 border-dashed border-accent bg-base text-foreground' ? 'border-2 border-dashed border-accent bg-surface text-foreground'
: 'border-2 border-dashed border-muted bg-transparent text-foreground hover:border-accent hover:bg-base' : 'border-2 border-dashed border-muted bg-transparent text-foreground hover:border-accent hover:bg-surface'
}`; }`;
const paddingClass = variant === 'compact' ? 'py-1 px-2 rounded-md' : 'py-5 px-3 rounded-lg'; const paddingClass = variant === 'compact' ? 'py-1 px-2 rounded-md' : 'py-5 px-3 rounded-lg';
@ -182,7 +182,7 @@ export function DocumentUploader({
{children} {children}
{isDragActive && ( {isDragActive && (
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-surface backdrop-blur-md pointer-events-none p-6"> <div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-surface backdrop-blur-md pointer-events-none p-6">
<div className="w-full h-full border-2 border-dashed border-accent rounded-xl flex flex-col items-center justify-center bg-surface-solid text-center p-4"> <div className="w-full h-full border-2 border-dashed border-accent rounded-lg flex flex-col items-center justify-center bg-surface-solid text-center p-4">
<UploadIcon className="w-14 h-14 text-accent mb-4 animate-bounce" /> <UploadIcon className="w-14 h-14 text-accent mb-4 animate-bounce" />
<p className="text-xl font-bold text-foreground mb-1.5"> <p className="text-xl font-bold text-foreground mb-1.5">
Drop files here to upload Drop files here to upload
@ -217,7 +217,7 @@ export function DocumentUploader({
<input {...getInputProps()} /> <input {...getInputProps()} />
{variant === 'compact' ? ( {variant === 'compact' ? (
<div className="flex items-center gap-2 text-left w-full min-w-0"> <div className="flex items-center gap-2 text-left w-full min-w-0">
<UploadIcon className="w-3.5 h-3.5 text-muted group-hover:text-accent shrink-0 transition-colors duration-base" /> <UploadIcon className="w-3.5 h-3.5 text-soft group-hover:text-accent shrink-0 transition-colors duration-base" />
{isUploading ? ( {isUploading ? (
<p className="text-[12px] font-medium truncate flex-1">Uploading</p> <p className="text-[12px] font-medium truncate flex-1">Uploading</p>
) : isConverting ? ( ) : isConverting ? (
@ -233,7 +233,7 @@ export function DocumentUploader({
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center text-center"> <div className="flex flex-col items-center justify-center text-center">
<UploadIcon className="w-7 h-7 sm:w-10 sm:h-10 mb-2 text-muted" /> <UploadIcon className="w-7 h-7 sm:w-10 sm:h-10 mb-2 text-soft" />
{isUploading ? ( {isUploading ? (
<p className="text-sm sm:text-lg font-semibold text-foreground">Uploading file...</p> <p className="text-sm sm:text-lg font-semibold text-foreground">Uploading file...</p>
) : isConverting ? ( ) : isConverting ? (
@ -243,7 +243,7 @@ export function DocumentUploader({
<p className="mb-2 text-sm sm:text-lg font-semibold text-foreground"> <p className="mb-2 text-sm sm:text-lg font-semibold text-foreground">
{isDragActive ? 'Drop your file(s) here' : 'Drop your file(s) here, or click to select'} {isDragActive ? 'Drop your file(s) here' : 'Drop your file(s) here, or click to select'}
</p> </p>
<p className="text-xs sm:text-sm text-muted"> <p className="text-xs sm:text-sm text-soft">
{enableDocx ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'} {enableDocx ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'}
</p> </p>
{error && <p className="mt-2 text-sm text-danger">{error}</p>} {error && <p className="mt-2 text-sm text-danger">{error}</p>}

View file

@ -28,7 +28,7 @@ export function ZoomControl({
> >
</button> </button>
<span className="text-xs tabular-nums w-12 text-center text-muted">{value}%</span> <span className="text-xs tabular-nums w-12 text-center text-soft">{value}%</span>
<button <button
type="button" type="button"
onClick={onIncrease} onClick={onIncrease}

View file

@ -30,16 +30,16 @@ export {
type ButtonVariant, type ButtonVariant,
}; };
// Inputs use `bg-base` so they remain visible regardless of whether the // Inputs use `bg-surface` so they remain visible regardless of whether the
// surrounding container is `bg-background` (Card) or `bg-base` (Section). // surrounding container is `bg-background` (Card) or `bg-surface` (Section).
// Using the same `bg-background` as the Card would make the input blend in. // Using the same `bg-background` as the Card would make the input blend in.
// (Note: never use Tailwind alpha modifiers on these theme variables — they // (Note: never use Tailwind alpha modifiers on these theme variables — they
// resolve to CSS custom properties that don't accept opacity suffixes.) // resolve to CSS custom properties that don't accept opacity suffixes.)
export const inputClass = export const inputClass =
'w-full rounded-md bg-surface-sunken border border-line px-2.5 py-1.5 text-sm text-foreground transition-colors duration-fast ease-standard focus:outline-none focus:ring-2 focus:ring-accent focus:border-accent-line'; 'w-full rounded-md bg-surface-sunken border border-line px-2.5 py-1.5 text-sm text-foreground transition-colors duration-fast ease-standard focus:outline-none focus:ring-2 focus:ring-accent-line focus:border-accent-line';
export const listboxButtonClass = 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 hover:bg-accent-wash transition-colors duration-fast ease-standard'; '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 listboxOptionsClass = export const listboxOptionsClass =
'z-50 w-[var(--button-width)] max-h-60 overflow-y-auto overscroll-contain rounded-md bg-surface p-1 shadow-elev-2 ring-1 ring-line focus:outline-none [--anchor-gap:0.25rem]'; 'z-50 w-[var(--button-width)] max-h-60 overflow-y-auto overscroll-contain rounded-md bg-surface p-1 shadow-elev-2 ring-1 ring-line focus:outline-none [--anchor-gap:0.25rem]';
export const listboxOptionClass = (active: boolean) => export const listboxOptionClass = (active: boolean) =>

View file

@ -283,7 +283,7 @@ export function FileIcon(props: React.SVGProps<SVGSVGElement>) {
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
className={props.className || "w-8 h-8 text-muted"} className={props.className || "w-8 h-8 text-soft"}
{...props} {...props}
> >
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />

View file

@ -56,7 +56,7 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
<Button <Button
onClick={() => skipToLocation(currentPage - 1, true)} onClick={() => skipToLocation(currentPage - 1, true)}
disabled={currentPage <= 1} disabled={currentPage <= 1}
className="relative p-2 rounded-full text-foreground hover:bg-offbase data-[hover]:bg-offbase data-[active]:bg-offbase transition duration-base focus:outline-none disabled:opacity-50 transform ease-standard hover:text-accent" className="relative p-2 rounded-full text-foreground hover:bg-accent-wash data-[hover]:bg-accent-wash data-[active]:bg-accent-wash transition duration-base focus:outline-none disabled:opacity-50 transform ease-standard hover:text-accent"
aria-label="Previous page" aria-label="Previous page"
> >
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
@ -67,14 +67,14 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
{/* Page number popup */} {/* Page number popup */}
<Popover className="relative mb-1"> <Popover className="relative mb-1">
<PopoverButton <PopoverButton
className="bg-offbase px-2 py-0.5 rounded-full focus:outline-none cursor-pointer hover:bg-offbase transform transition-transform duration-base ease-standard hover:text-accent" className="bg-surface-sunken px-2 py-0.5 rounded-full focus:outline-none cursor-pointer hover:bg-accent-wash transform transition-transform duration-base ease-standard hover:text-accent"
onClick={handlePopoverOpen} onClick={handlePopoverOpen}
> >
<p className="text-xs whitespace-nowrap"> <p className="text-xs whitespace-nowrap">
{currentPage} / {numPages || 1} {currentPage} / {numPages || 1}
</p> </p>
</PopoverButton> </PopoverButton>
<PopoverPanel anchor="top" className="absolute z-50 bg-base p-3 rounded-md shadow-lg border border-offbase"> <PopoverPanel anchor="top" className="absolute z-50 bg-surface p-3 rounded-md shadow-elev-2 border border-line">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<div className="text-xs font-medium text-foreground">Go to page</div> <div className="text-xs font-medium text-foreground">Go to page</div>
<input <input
@ -82,7 +82,7 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
type="text" type="text"
inputMode="numeric" inputMode="numeric"
pattern="[0-9]*" pattern="[0-9]*"
className="w-20 px-2 py-1 text-xs text-accent bg-offbase rounded border-none outline-none appearance-none text-center" className="w-20 px-2 py-1 text-xs text-accent bg-surface-sunken rounded border-none outline-none appearance-none text-center"
value={inputValue} value={inputValue}
onChange={handleInputChange} onChange={handleInputChange}
onBlur={handleInputConfirm} onBlur={handleInputConfirm}
@ -90,7 +90,7 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
placeholder={currentPage.toString()} placeholder={currentPage.toString()}
aria-label="Page number" aria-label="Page number"
/> />
<div className="text-xs text-muted text-center">of {numPages || 1}</div> <div className="text-xs text-soft text-center">of {numPages || 1}</div>
</div> </div>
</PopoverPanel> </PopoverPanel>
</Popover> </Popover>
@ -99,7 +99,7 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
<Button <Button
onClick={() => skipToLocation(currentPage + 1, true)} onClick={() => skipToLocation(currentPage + 1, true)}
disabled={currentPage >= (numPages || 1)} disabled={currentPage >= (numPages || 1)}
className="relative p-2 rounded-full text-foreground hover:bg-offbase data-[hover]:bg-offbase data-[active]:bg-offbase transition duration-base focus:outline-none disabled:opacity-50 transform ease-standard hover:text-accent" className="relative p-2 rounded-full text-foreground hover:bg-accent-wash data-[hover]:bg-accent-wash data-[active]:bg-accent-wash transition duration-base focus:outline-none disabled:opacity-50 transform ease-standard hover:text-accent"
aria-label="Next page" aria-label="Next page"
> >
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">

View file

@ -16,7 +16,7 @@ export function RateLimitPauseButton() {
onClick={() => { onClick={() => {
if (isPlaying) togglePlay(); if (isPlaying) togglePlay();
}} }}
className="relative p-1.5 rounded-md text-foreground hover:bg-offbase transition duration-base focus:outline-none h-8 w-8 flex items-center justify-center transform ease-standard hover:text-accent" className="relative p-1.5 rounded-md text-foreground hover:bg-accent-wash transition duration-base focus:outline-none h-8 w-8 flex items-center justify-center transform ease-standard hover:text-accent"
aria-label="Pause" aria-label="Pause"
> >
<PauseIcon className="w-5 h-5" /> <PauseIcon className="w-5 h-5" />

View file

@ -88,16 +88,16 @@ export const SpeedControl = ({
return ( return (
<Popover className="relative"> <Popover className="relative">
<PopoverButton className="flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-foreground text-xs sm:text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1 transform transition-transform duration-base ease-standard hover:text-accent"> <PopoverButton className="flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-foreground text-xs sm:text-sm focus:outline-none cursor-pointer hover:bg-accent-wash rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1 transform transition-transform duration-base ease-standard hover:text-accent">
<SpeedometerIcon className="h-3 w-3 sm:h-3.5 sm:w-3.5" /> <SpeedometerIcon className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
<span className="sm:hidden">{compactTriggerLabel}</span> <span className="sm:hidden">{compactTriggerLabel}</span>
<span className="hidden sm:inline">{triggerLabel}</span> <span className="hidden sm:inline">{triggerLabel}</span>
<ChevronUpDownIcon className="h-2.5 w-2.5 sm:h-3 sm:w-3" /> <ChevronUpDownIcon className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
</PopoverButton> </PopoverButton>
<PopoverPanel anchor="top" className="absolute z-50 bg-base p-3 rounded-md shadow-lg border border-offbase"> <PopoverPanel anchor="top" className="absolute z-50 bg-surface p-3 rounded-md shadow-elev-2 border border-line">
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
{!nativeSpeedSupported && ( {!nativeSpeedSupported && (
<div className="rounded-md border border-offbase bg-background px-2 py-1.5 text-[11px] text-muted"> <div className="rounded-md border border-line bg-background px-2 py-1.5 text-[11px] text-soft">
Native model speed is not available for this model. Native model speed is not available for this model.
</div> </div>
)} )}
@ -122,7 +122,7 @@ export const SpeedControl = ({
onMouseUp={handleVoiceSpeedChangeComplete} onMouseUp={handleVoiceSpeedChangeComplete}
onKeyUp={handleVoiceSpeedChangeComplete} onKeyUp={handleVoiceSpeedChangeComplete}
onTouchEnd={handleVoiceSpeedChangeComplete} onTouchEnd={handleVoiceSpeedChangeComplete}
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent" className="w-full bg-surface-sunken rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-surface-sunken [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-surface-sunken [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
/> />
</div> </div>
)} )}
@ -146,7 +146,7 @@ export const SpeedControl = ({
onMouseUp={handleAudioSpeedChangeComplete} onMouseUp={handleAudioSpeedChangeComplete}
onKeyUp={handleAudioSpeedChangeComplete} onKeyUp={handleAudioSpeedChangeComplete}
onTouchEnd={handleAudioSpeedChangeComplete} onTouchEnd={handleAudioSpeedChangeComplete}
className="w-full bg-offbase rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-offbase [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-offbase [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent" className="w-full bg-surface-sunken rounded-lg appearance-none cursor-pointer accent-accent [&::-webkit-slider-runnable-track]:bg-surface-sunken [&::-webkit-slider-runnable-track]:rounded-lg [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-accent [&::-moz-range-track]:bg-surface-sunken [&::-moz-range-track]:rounded-lg [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:h-4 [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-accent"
/> />
</div> </div>
</div> </div>

View file

@ -31,7 +31,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
} = useTTS(); } = useTTS();
return ( return (
<div className="sticky bottom-0 z-30 w-full border-t border-offbase bg-base" data-app-ttsbar> <div className="sticky bottom-0 z-30 w-full border-t border-line-soft bg-surface" data-app-ttsbar>
<div className="px-2 md:px-3 pt-1 pb-[max(0.375rem,env(safe-area-inset-bottom))] flex items-center justify-center gap-1 min-h-10"> <div className="px-2 md:px-3 pt-1 pb-[max(0.375rem,env(safe-area-inset-bottom))] flex items-center justify-center gap-1 min-h-10">
{/* Speed control */} {/* Speed control */}
<SpeedControl <SpeedControl
@ -51,7 +51,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
{/* Playback Controls */} {/* Playback Controls */}
<Button <Button
onClick={skipBackward} onClick={skipBackward}
className="relative p-1.5 rounded-md text-foreground hover:bg-offbase transition duration-base focus:outline-none disabled:opacity-50 h-8 w-8 flex items-center justify-center transform ease-standard hover:text-accent" className="relative p-1.5 rounded-md text-foreground hover:bg-accent-wash transition duration-base focus:outline-none disabled:opacity-50 h-8 w-8 flex items-center justify-center transform ease-standard hover:text-accent"
aria-label="Skip backward" aria-label="Skip backward"
disabled={isProcessing} disabled={isProcessing}
> >
@ -60,7 +60,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
<Button <Button
onClick={togglePlay} onClick={togglePlay}
className="relative p-1.5 rounded-md text-foreground hover:bg-offbase transition duration-base focus:outline-none h-8 w-8 flex items-center justify-center transform ease-standard hover:text-accent" className="relative p-1.5 rounded-md text-foreground hover:bg-accent-wash transition duration-base focus:outline-none h-8 w-8 flex items-center justify-center transform ease-standard hover:text-accent"
aria-label={isPlaying ? 'Pause' : 'Play'} aria-label={isPlaying ? 'Pause' : 'Play'}
disabled={isProcessing && !isPlaying} disabled={isProcessing && !isPlaying}
> >
@ -69,7 +69,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
<Button <Button
onClick={skipForward} onClick={skipForward}
className="relative p-1.5 rounded-md text-foreground hover:bg-offbase transition duration-base focus:outline-none disabled:opacity-50 h-8 w-8 flex items-center justify-center transform ease-standard hover:text-accent" className="relative p-1.5 rounded-md text-foreground hover:bg-accent-wash transition duration-base focus:outline-none disabled:opacity-50 h-8 w-8 flex items-center justify-center transform ease-standard hover:text-accent"
aria-label="Skip forward" aria-label="Skip forward"
disabled={isProcessing} disabled={isProcessing}
> >

View file

@ -34,15 +34,15 @@ export function VoicesControlBase({
: 'bottom-full right-0 mb-1'; : 'bottom-full right-0 mb-1';
const buttonClass = variant === 'field' const buttonClass = variant === 'field'
? 'relative cursor-pointer rounded-lg bg-base py-1.5 pl-3 pr-10 text-left text-foreground focus:outline-none focus:ring-2 focus:ring-accent transform transition-transform duration-base ease-standard hover:text-accent w-full' ? 'relative cursor-pointer rounded-lg bg-surface py-1.5 pl-3 pr-10 text-left text-foreground focus:outline-none focus:ring-2 focus:ring-accent-line transform transition-transform duration-base ease-standard hover:text-accent w-full'
: 'flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-foreground text-xs sm:text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1 transform transition-transform duration-base ease-standard hover:text-accent'; : 'flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-foreground text-xs sm:text-sm focus:outline-none cursor-pointer hover:bg-accent-wash rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1 transform transition-transform duration-base ease-standard hover:text-accent';
const iconClass = variant === 'field' const iconClass = variant === 'field'
? 'h-3.5 w-3.5 shrink-0' ? 'h-3.5 w-3.5 shrink-0'
: 'h-3 w-3 sm:h-3.5 sm:w-3.5'; : 'h-3 w-3 sm:h-3.5 sm:w-3.5';
const chevronClass = variant === 'field' const chevronClass = variant === 'field'
? 'h-4 w-4 text-muted' ? 'h-4 w-4 text-soft'
: 'h-2.5 w-2.5 sm:h-3 sm:w-3'; : 'h-2.5 w-2.5 sm:h-3 sm:w-3';
const providerModelPolicy = resolveTtsProviderModelPolicy({ const providerModelPolicy = resolveTtsProviderModelPolicy({
@ -85,7 +85,7 @@ export function VoicesControlBase({
if (availableVoices.length === 0) { if (availableVoices.length === 0) {
return ( return (
<div className="relative"> <div className="relative">
<div className="flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-muted text-xs sm:text-sm rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1"> <div className="flex items-center space-x-0.5 sm:space-x-1 bg-transparent text-soft text-xs sm:text-sm rounded pl-1.5 sm:pl-2 pr-0.5 sm:pr-1 py-0.5 sm:py-1">
<AudioWaveIcon className="h-3 w-3 sm:h-3.5 sm:w-3.5" /> <AudioWaveIcon className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
<span>No voices</span> <span>No voices</span>
</div> </div>
@ -140,13 +140,13 @@ export function VoicesControlBase({
</> </>
)} )}
</ListboxButton> </ListboxButton>
<ListboxOptions className={`absolute ${dropdownPosition} z-50 ${dropdownWidth} !h-auto !min-h-0 !max-h-[50vh] overflow-y-auto overscroll-contain rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none`}> <ListboxOptions className={`absolute ${dropdownPosition} z-50 ${dropdownWidth} !h-auto !min-h-0 !max-h-[50vh] overflow-y-auto overscroll-contain rounded-lg bg-surface shadow-elev-2 ring-1 ring-line-soft focus:outline-none`}>
{availableVoices.map((voiceId) => ( {availableVoices.map((voiceId) => (
<ListboxOption <ListboxOption
key={voiceId} key={voiceId}
value={voiceId} value={voiceId}
className={({ active, selected }) => className={({ active, selected }) =>
`relative cursor-pointer select-none py-1 px-2 sm:py-2 sm:px-3 flex items-center gap-2 ${active ? 'bg-offbase' : ''} ${selected ? 'font-medium bg-accent text-background' : ''} ${selected && active ? 'text-foreground' : ''}` `relative cursor-pointer select-none py-1 px-2 sm:py-2 sm:px-3 flex items-center gap-2 ${active ? 'bg-surface-sunken' : ''} ${selected ? 'font-medium bg-accent text-background' : ''} ${selected && active ? 'text-foreground' : ''}`
} }
> >
{({ selected }) => ( {({ selected }) => (
@ -184,13 +184,13 @@ export function VoicesControlBase({
</> </>
)} )}
</ListboxButton> </ListboxButton>
<ListboxOptions className={`absolute ${dropdownPosition} z-50 ${dropdownWidth} !h-auto !min-h-0 !max-h-[50vh] overflow-y-auto overscroll-contain rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none`}> <ListboxOptions className={`absolute ${dropdownPosition} z-50 ${dropdownWidth} !h-auto !min-h-0 !max-h-[50vh] overflow-y-auto overscroll-contain rounded-lg bg-surface shadow-elev-2 ring-1 ring-line-soft focus:outline-none`}>
{availableVoices.map((voiceId) => ( {availableVoices.map((voiceId) => (
<ListboxOption <ListboxOption
key={voiceId} key={voiceId}
value={voiceId} value={voiceId}
className={({ active, selected }) => className={({ active, selected }) =>
`relative cursor-pointer select-none py-1 px-2 sm:py-2 sm:px-3 ${active ? 'bg-offbase' : ''} ${selected ? 'font-medium bg-accent text-background' : ''} ${selected && active ? 'text-foreground' : ''}` `relative cursor-pointer select-none py-1 px-2 sm:py-2 sm:px-3 ${active ? 'bg-surface-sunken' : ''} ${selected ? 'font-medium bg-accent text-background' : ''} ${selected && active ? 'text-foreground' : ''}`
} }
> >
<span className="text-xs sm:text-sm">{voiceId}</span> <span className="text-xs sm:text-sm">{voiceId}</span>

View file

@ -68,12 +68,12 @@ export function ReaderSidebarShell({
<aside <aside
role="dialog" role="dialog"
aria-label={ariaLabel} aria-label={ariaLabel}
className={`reader-sidebar-panel absolute inset-y-0 right-0 sm:right-3 sm:top-3 sm:bottom-3 pointer-events-auto bg-base border-l sm:border border-offbase shadow-xl sm:rounded-xl flex flex-col ${panelClassName}`} className={`reader-sidebar-panel absolute inset-y-0 right-0 sm:right-3 sm:top-3 sm:bottom-3 pointer-events-auto bg-surface border-l sm:border border-line shadow-elev-3 sm:rounded-lg flex flex-col ${panelClassName}`}
> >
<div className="border-b border-offbase px-4 py-3 flex items-start justify-between gap-3"> <div className="border-b border-line-soft px-4 py-3 flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<h2 className="text-sm font-medium text-foreground">{title}</h2> <h2 className="text-sm font-medium text-foreground">{title}</h2>
{subtitle ? <p className="mt-0.5 text-xs text-muted">{subtitle}</p> : null} {subtitle ? <p className="mt-0.5 text-xs text-soft">{subtitle}</p> : null}
</div> </div>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
{headerActions} {headerActions}
@ -85,7 +85,7 @@ export function ReaderSidebarShell({
className={buttonClass({ className={buttonClass({
variant: 'secondary', variant: 'secondary',
size: 'icon', size: 'icon',
className: 'h-8 w-8 text-muted', className: 'h-8 w-8 text-soft',
})} })}
> >
<XCircleIcon className="w-4 h-4" /> <XCircleIcon className="w-4 h-4" />

View file

@ -641,7 +641,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
className={buttonClass({ className={buttonClass({
variant: 'secondary', variant: 'secondary',
size: 'xs', size: 'xs',
className: 'h-8 px-2 text-muted', className: 'h-8 px-2 text-soft',
})} })}
> >
{isClearingSegments ? 'Clearing…' : 'Clear'} {isClearingSegments ? 'Clearing…' : 'Clear'}
@ -654,7 +654,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
className={buttonClass({ className={buttonClass({
variant: 'secondary', variant: 'secondary',
size: 'icon', size: 'icon',
className: 'h-8 w-8 text-muted', className: 'h-8 w-8 text-soft',
})} })}
> >
<RefreshIcon className="w-3.5 h-3.5" /> <RefreshIcon className="w-3.5 h-3.5" />
@ -672,8 +672,8 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
headerActions={headerActions} headerActions={headerActions}
bodyClassName="flex-1 overflow-y-auto px-0 py-0" bodyClassName="flex-1 overflow-y-auto px-0 py-0"
> >
<div className="px-4 py-2 border-b border-offbase"> <div className="px-4 py-2 border-b border-line-soft">
<div className="text-xs text-muted"> <div className="text-xs text-soft">
{hasLoadedManifest ? ( {hasLoadedManifest ? (
<> <>
{rowsToRender.length} indexed {rowsToRender.length} indexed
@ -687,7 +687,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
) : null} ) : null}
</> </>
) : isManifestLoading ? ( ) : isManifestLoading ? (
<div className="animate-pulse h-3 w-36 rounded bg-offbase" aria-label="Loading segment summary" aria-busy="true" /> <div className="animate-pulse h-3 w-36 rounded bg-surface-sunken" aria-label="Loading segment summary" aria-busy="true" />
) : hasManifestError ? ( ) : hasManifestError ? (
<span className="text-danger">error</span> <span className="text-danger">error</span>
) : ( ) : (
@ -705,16 +705,16 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
)} )}
{hasLoadedManifest && rowsToRender.length === 0 && ( {hasLoadedManifest && rowsToRender.length === 0 && (
<div className="px-4 py-10 flex flex-col items-center text-center gap-2"> <div className="px-4 py-10 flex flex-col items-center text-center gap-2">
<div className="text-sm font-medium text-muted"> <div className="text-sm font-medium text-soft">
No segments No segments
</div> </div>
<p className="text-sm text-muted leading-relaxed max-w-[24ch]"> <p className="text-sm text-soft leading-relaxed max-w-[24ch]">
Press play in the reader to generate audio segments. Press play in the reader to generate audio segments.
</p> </p>
</div> </div>
)} )}
{hasLoadedManifest && rowsToRender.length > 0 && ( {hasLoadedManifest && rowsToRender.length > 0 && (
<ul className="divide-y divide-offbase"> <ul className="divide-y divide-line-soft">
{rowsToRender.map(({ segmentIndex, sentenceText, row, isCurrentLocation, groupKey, groupLabel, isSynthesized }, rowIndex) => { {rowsToRender.map(({ segmentIndex, sentenceText, row, isCurrentLocation, groupKey, groupLabel, isSynthesized }, rowIndex) => {
const previousGroupKey = rowIndex > 0 ? rowsToRender[rowIndex - 1]?.groupKey : null; const previousGroupKey = rowIndex > 0 ? rowsToRender[rowIndex - 1]?.groupKey : null;
const showGroupHeader = previousGroupKey !== groupKey; const showGroupHeader = previousGroupKey !== groupKey;
@ -744,8 +744,8 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
className={`relative px-4 py-3 ${isCurrent ? 'bg-surface-sunken' : ''}`} className={`relative px-4 py-3 ${isCurrent ? 'bg-surface-sunken' : ''}`}
> >
{showGroupHeader && ( {showGroupHeader && (
<div className="mb-2 -mx-4 px-4 py-1.5 bg-surface-sunken border-y border-offbase"> <div className="mb-2 -mx-4 px-4 py-1.5 bg-surface-sunken border-y border-line">
<span className="text-[10px] uppercase tracking-[0.14em] text-muted"> <span className="text-[10px] uppercase tracking-[0.14em] text-soft">
{groupLabel} {groupLabel}
</span> </span>
</div> </div>
@ -758,7 +758,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
type="button" type="button"
onClick={() => { if (canJump) handleJump(segmentIndex, row.locator); }} onClick={() => { if (canJump) handleJump(segmentIndex, row.locator); }}
disabled={!canJump} disabled={!canJump}
className={`text-xs font-medium shrink-0 pt-0.5 ${canJump ? 'text-muted hover:text-accent' : 'text-faint cursor-not-allowed'}`} className={`text-xs font-medium shrink-0 pt-0.5 ${canJump ? 'text-soft hover:text-accent' : 'text-faint cursor-not-allowed'}`}
title={canJump ? (playable ? 'Play this segment' : 'Jump to this segment') : 'Text not loaded yet'} title={canJump ? (playable ? 'Play this segment' : 'Jump to this segment') : 'Text not loaded yet'}
aria-label={`Segment ${segmentIndex + 1}`} aria-label={`Segment ${segmentIndex + 1}`}
> >
@ -774,7 +774,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
> >
<p className={`text-sm leading-snug ${isCurrent ? 'text-foreground' : 'text-soft'} line-clamp-2`}> <p className={`text-sm leading-snug ${isCurrent ? 'text-foreground' : 'text-soft'} line-clamp-2`}>
{sentenceText || ( {sentenceText || (
<span className="text-muted italic text-xs"> <span className="text-soft italic text-xs">
[text not loaded press play to fetch] [text not loaded press play to fetch]
</span> </span>
)} )}
@ -787,7 +787,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
aria-label={`Status ${status}`} aria-label={`Status ${status}`}
title={status} title={status}
/> />
<span className="text-xs text-muted"> <span className="text-xs text-soft">
{formatDuration(activeVariant?.durationMs)} {formatDuration(activeVariant?.durationMs)}
</span> </span>
{isCurrent && isPlaying && ( {isCurrent && isPlaying && (
@ -796,7 +796,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
</span> </span>
)} )}
{!canJump && ( {!canJump && (
<span className="text-[10px] text-faint border border-offbase rounded px-1 py-0.5"> <span className="text-[10px] text-faint border border-line rounded px-1 py-0.5">
not loaded not loaded
</span> </span>
)} )}
@ -822,8 +822,8 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
isActive isActive
? 'border-accent text-accent bg-surface-sunken' ? 'border-accent text-accent bg-surface-sunken'
: known : known
? 'border-offbase text-muted hover:border-accent hover:text-accent' ? 'border-line text-soft hover:border-accent hover:text-accent'
: 'border-offbase text-muted opacity-60 cursor-not-allowed', : 'border-line text-soft opacity-60 cursor-not-allowed',
].join(' ')} ].join(' ')}
> >
{formatVoiceLabel(variant.settings)} {formatVoiceLabel(variant.settings)}
@ -854,21 +854,21 @@ function SegmentsListSkeleton() {
return ( return (
<div className="px-4 py-3"> <div className="px-4 py-3">
<div className="animate-pulse space-y-3" aria-label="Loading segments" aria-busy="true"> <div className="animate-pulse space-y-3" aria-label="Loading segments" aria-busy="true">
<div className="h-3 w-40 rounded bg-offbase" /> <div className="h-3 w-40 rounded bg-surface-sunken" />
{Array.from({ length: 8 }).map((_, index) => ( {Array.from({ length: 8 }).map((_, index) => (
<div key={index} className="rounded-md border border-offbase bg-base px-3 py-2.5"> <div key={index} className="rounded-md border border-line bg-surface px-3 py-2.5">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="h-3.5 w-8 rounded bg-offbase mt-0.5 shrink-0" /> <div className="h-3.5 w-8 rounded bg-surface-sunken mt-0.5 shrink-0" />
<div className="min-w-0 flex-1 space-y-2"> <div className="min-w-0 flex-1 space-y-2">
<div className="h-3.5 w-11/12 rounded bg-offbase" /> <div className="h-3.5 w-11/12 rounded bg-surface-sunken" />
<div className="h-3.5 w-3/4 rounded bg-offbase" /> <div className="h-3.5 w-3/4 rounded bg-surface-sunken" />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-offbase" /> <div className="h-2 w-2 rounded-full bg-surface-sunken" />
<div className="h-3 w-12 rounded bg-offbase" /> <div className="h-3 w-12 rounded bg-surface-sunken" />
<div className="h-4 w-20 rounded bg-offbase" /> <div className="h-4 w-20 rounded bg-surface-sunken" />
</div> </div>
</div> </div>
<div className="h-6 w-6 rounded bg-offbase shrink-0" /> <div className="h-6 w-6 rounded bg-surface-sunken shrink-0" />
</div> </div>
</div> </div>
))} ))}
@ -882,7 +882,7 @@ function SegmentsListSkeletonRows() {
<div className="px-4 py-3 animate-pulse" aria-label="Loading more segments" aria-busy="true"> <div className="px-4 py-3 animate-pulse" aria-label="Loading more segments" aria-busy="true">
<div className="space-y-2"> <div className="space-y-2">
{Array.from({ length: 3 }).map((_, index) => ( {Array.from({ length: 3 }).map((_, index) => (
<div key={index} className="h-12 rounded-md border border-offbase bg-base" /> <div key={index} className="h-12 rounded-md border border-line bg-surface" />
))} ))}
</div> </div>
</div> </div>
@ -895,7 +895,7 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
<PopoverButton <PopoverButton
aria-label="Segment metadata" aria-label="Segment metadata"
title="Metadata" title="Metadata"
className="inline-flex items-center justify-center w-7 h-7 rounded-md border border-transparent text-muted hover:bg-offbase hover:border-offbase hover:text-accent transition-colors" className="inline-flex items-center justify-center w-7 h-7 rounded-md border border-transparent text-soft hover:bg-accent-wash hover:border-line hover:text-accent transition-colors"
> >
<InfoIcon className="w-3.5 h-3.5" /> <InfoIcon className="w-3.5 h-3.5" />
</PopoverButton> </PopoverButton>
@ -910,7 +910,7 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
> >
<PopoverPanel <PopoverPanel
anchor="bottom end" anchor="bottom end"
className="z-[60] w-[300px] mt-1 rounded-lg border border-offbase bg-base shadow-xl p-3" className="z-[60] w-[300px] mt-1 rounded-lg border border-line bg-surface shadow-elev-3 p-3"
> >
<dl className="space-y-2"> <dl className="space-y-2">
<Row label="locator"> <Row label="locator">
@ -925,7 +925,7 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
: `${row.locator.readerType || '?'} (legacy)`} : `${row.locator.readerType || '?'} (legacy)`}
</span> </span>
) : ( ) : (
<span className="text-muted text-[11px]">none</span> <span className="text-soft text-[11px]">none</span>
)} )}
</Row> </Row>
<Row label="variants"> <Row label="variants">
@ -934,9 +934,9 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
</span> </span>
</Row> </Row>
{row.variants.map((v) => ( {row.variants.map((v) => (
<div key={v.segmentId} className="border-t border-offbase pt-2"> <div key={v.segmentId} className="border-t border-line-soft pt-2">
<Row label="segment_id"> <Row label="segment_id">
<span className="font-mono text-[10px] text-muted break-all"> <span className="font-mono text-[10px] text-soft break-all">
{v.segmentId.slice(0, 16)} {v.segmentId.slice(0, 16)}
</span> </span>
</Row> </Row>
@ -972,7 +972,7 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
function Row({ label, children }: { label: string; children: ReactNode }) { function Row({ label, children }: { label: string; children: ReactNode }) {
return ( return (
<div className="grid grid-cols-[80px_1fr] gap-2 items-baseline"> <div className="grid grid-cols-[80px_1fr] gap-2 items-baseline">
<dt className="font-mono uppercase tracking-[0.16em] text-[9px] text-muted">{label}</dt> <dt className="font-mono uppercase tracking-[0.16em] text-[9px] text-soft">{label}</dt>
<dd className="min-w-0">{children}</dd> <dd className="min-w-0">{children}</dd>
</div> </div>
); );

View file

@ -158,12 +158,12 @@ export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
return ( return (
<div className={`h-full flex flex-col relative z-0 ${className}`} ref={containerRef}> <div className={`h-full flex flex-col relative z-0 ${className}`} ref={containerRef}>
<div className="flex items-center justify-between px-2 py-1 border-b border-offbase bg-base text-xs text-muted"> <div className="flex items-center justify-between px-2 py-1 border-b border-line-soft bg-surface text-xs text-soft">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => setIsTocOpen(open => !open)} onClick={() => setIsTocOpen(open => !open)}
className="inline-flex items-center py-1 px-1 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition duration-base ease-standard transform hover:text-accent" className="inline-flex items-center py-1 px-1 rounded-md border border-line bg-surface text-foreground text-xs hover:bg-accent-wash transition duration-base ease-standard transform hover:text-accent"
aria-label={isTocOpen ? 'Hide chapters' : 'Show chapters'} aria-label={isTocOpen ? 'Hide chapters' : 'Show chapters'}
> >
<DotsVerticalIcon className="w-4 h-4" /> <DotsVerticalIcon className="w-4 h-4" />
@ -171,7 +171,7 @@ export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
<button <button
type="button" type="button"
onClick={() => handleLocationChanged('prev')} onClick={() => handleLocationChanged('prev')}
className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition duration-base ease-standard transform hover:text-accent" className="inline-flex items-center py-1 px-2 rounded-md border border-line bg-surface text-foreground text-xs hover:bg-accent-wash transition duration-base ease-standard transform hover:text-accent"
aria-label="Previous section" aria-label="Previous section"
> >
<ChevronLeftIcon className="w-4 h-4" /> <ChevronLeftIcon className="w-4 h-4" />
@ -185,15 +185,15 @@ export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
<button <button
type="button" type="button"
onClick={() => handleLocationChanged('next')} onClick={() => handleLocationChanged('next')}
className="inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase transition duration-base ease-standard transform hover:text-accent" className="inline-flex items-center py-1 px-2 rounded-md border border-line bg-surface text-foreground text-xs hover:bg-accent-wash transition duration-base ease-standard transform hover:text-accent"
aria-label="Next section" aria-label="Next section"
> >
<ChevronRightIcon className="w-4 h-4" /> <ChevronRightIcon className="w-4 h-4" />
</button> </button>
</div> </div>
{isTocOpen && tocRef.current && tocRef.current.length > 0 && ( {isTocOpen && tocRef.current && tocRef.current.length > 0 && (
<div className="border-b border-offbase bg-background text-xs overflow-y-auto max-h-64 p-2"> <div className="border-b border-line-soft bg-background text-xs overflow-y-auto max-h-64 p-2">
<div className="font-semibold text-muted pb-1">Skip to chapters</div> <div className="font-semibold text-soft pb-1">Skip to chapters</div>
<div className="flex flex-wrap gap-1 w-full"> <div className="flex flex-wrap gap-1 w-full">
{tocRef.current.map((item, index) => ( {tocRef.current.map((item, index) => (
<button <button
@ -204,8 +204,8 @@ export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
setIsTocOpen(false); setIsTocOpen(false);
}} }}
className=" className="
px-2 py-1 rounded-md font-medium text-foreground text-center bg-base px-2 py-1 rounded-md font-medium text-foreground text-center bg-surface
hover:bg-offbase hover:text-accent transition-colors duration-fast hover:bg-accent-wash hover:text-accent transition-colors duration-fast
whitespace-nowrap whitespace-nowrap
flex-1 min-w-[140px] flex-1 min-w-[140px]
" "

View file

@ -480,7 +480,7 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro
return ( return (
<div <div
key={`${block.id}:${fragment.page}:${fragment.readingOrder}`} key={`${block.id}:${fragment.page}:${fragment.readingOrder}`}
className="absolute border border-accent-line rounded-[2px]" className="absolute border border-accent-line rounded-sm"
style={{ style={{
left: `${leftPct}%`, left: `${leftPct}%`,
top: `${topPct}%`, top: `${topPct}%`,
@ -536,7 +536,7 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro
pageNumber={i + 1} pageNumber={i + 1}
renderAnnotationLayer={true} renderAnnotationLayer={true}
renderTextLayer={i + 1 === currDocPage} renderTextLayer={i + 1 === currDocPage}
className="shadow-lg" className="shadow-elev-2"
scale={currentScale()} scale={currentScale()}
onRenderSuccess={() => { onRenderSuccess={() => {
lastRenderedLayoutKeyRef.current = layoutKey; lastRenderedLayoutKeyRef.current = layoutKey;
@ -562,7 +562,7 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro
pageNumber={leftPage} pageNumber={leftPage}
renderAnnotationLayer={true} renderAnnotationLayer={true}
renderTextLayer={leftPage === currDocPage} renderTextLayer={leftPage === currDocPage}
className="shadow-lg" className="shadow-elev-2"
scale={currentScale()} scale={currentScale()}
onRenderSuccess={() => { onRenderSuccess={() => {
lastRenderedLayoutKeyRef.current = layoutKey; lastRenderedLayoutKeyRef.current = layoutKey;
@ -584,7 +584,7 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro
pageNumber={rightPage} pageNumber={rightPage}
renderAnnotationLayer={true} renderAnnotationLayer={true}
renderTextLayer={rightPage === currDocPage} renderTextLayer={rightPage === currDocPage}
className="shadow-lg" className="shadow-elev-2"
scale={currentScale()} scale={currentScale()}
onRenderSuccess={() => { onRenderSuccess={() => {
lastRenderedLayoutKeyRef.current = layoutKey; lastRenderedLayoutKeyRef.current = layoutKey;