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>
<Link
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" />
</svg>
Back to Documents
@ -190,7 +190,7 @@ export default function EPUBPage() {
left={
<Link
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"
>
<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 ? (
<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">
<RateLimitPauseButton />
<RateLimitBanner />

View file

@ -160,7 +160,7 @@ export default function HTMLPage() {
<p className="text-danger mb-4">{error}</p>
<Link
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">
<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={
<Link
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"
>
<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 ? (
<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">
<RateLimitPauseButton />
<RateLimitBanner />

View file

@ -255,9 +255,9 @@ export default function PDFViewerPage() {
<Link
href="/app"
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" />
</svg>
Back to Documents
@ -308,34 +308,34 @@ export default function PDFViewerPage() {
: (isMerging ? 'Stage: merge' : 'Stage: infer'));
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'}`}>
{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="p-3.5 sm:p-4">
<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" />
<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>
<p className="text-sm font-semibold text-foreground">{statusText}</p>
</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">
<p className="text-[11px] font-semibold text-foreground">
{hasMeasuredProgress ? `Page ${pagesParsed} / ${totalPages}` : 'Awaiting first page'}
</p>
<p className="text-[10px] text-muted">{stageLabel}</p>
<p className="text-[10px] text-soft">{stageLabel}</p>
</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
className="h-full bg-accent transition duration-slow ease-standard"
style={{ width: `${hasMeasuredProgress ? progressPercent : 6}%` }}
/>
</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}
</p>
</div>
@ -345,7 +345,7 @@ export default function PDFViewerPage() {
<button
type="button"
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
</button>
@ -354,14 +354,14 @@ export default function PDFViewerPage() {
</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="flex items-start justify-between gap-3">
<div className="min-w-0">
<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>
<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" />
</span>
</div>
@ -384,7 +384,7 @@ export default function PDFViewerPage() {
<Link
href="/app"
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"
>
<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 ? (
<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">
<RateLimitPauseButton />
<RateLimitBanner />

View file

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

View file

@ -105,13 +105,13 @@ export default function SignUpPage() {
if (!enableUserSignups) {
return (
<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>
<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.
</p>
<div className="mt-6 pt-4 border-t border-offbase text-center">
<p className="text-xs text-muted">
<div className="mt-6 pt-4 border-t border-line-soft text-center">
<p className="text-xs text-soft">
Already have an account?{' '}
<Link href="/signin" className="underline hover:text-foreground">
Sign in
@ -129,9 +129,9 @@ export default function SignUpPage() {
return (
<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>
<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 && (
<div className="mt-4 p-3 bg-danger-wash border border-danger rounded-lg">
@ -148,8 +148,8 @@ export default function SignUpPage() {
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="me@example.com"
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm
focus:outline-none focus:ring-2 focus:ring-accent"
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-line"
/>
</div>
@ -162,8 +162,8 @@ export default function SignUpPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="w-full rounded-lg bg-background py-2 px-3 pr-10 text-foreground shadow-sm
focus:outline-none focus:ring-2 focus:ring-accent"
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-line"
/>
<button
type="button"
@ -171,7 +171,7 @@ export default function SignUpPage() {
className={buttonClass({
variant: 'ghost',
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 ? '👁️' : '👁️‍🗨️'}
@ -185,7 +185,7 @@ export default function SignUpPage() {
{[0, 1, 2, 3, 4].map((i) => (
<div
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'}`}>
{strengthLabels[strength - 1] || 'Very Weak'}
</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]) => (
<div key={key} className={`flex items-center gap-1 ${passed ? 'text-accent' : ''}`}>
<span>{passed ? '✓' : '○'}</span>
@ -219,8 +219,8 @@ export default function SignUpPage() {
value={passwordConfirmation}
onChange={(e) => setPasswordConfirmation(e.target.value)}
placeholder="Confirm Password"
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm
focus:outline-none focus:ring-2 focus:ring-accent"
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-line"
/>
{passwordConfirmation && password && (
<p className={`text-xs mt-1 ${password === passwordConfirmation ? 'text-accent' : 'text-danger'}`}>
@ -241,14 +241,14 @@ export default function SignUpPage() {
</div>
{/* Footer */}
<div className="mt-6 pt-4 border-t border-offbase text-center space-y-2">
<p className="text-xs text-muted">
<div className="mt-6 pt-4 border-t border-line-soft text-center space-y-2">
<p className="text-xs text-soft">
Already have an account?{' '}
<Link href="/signin" className="underline hover:text-foreground">
Sign in
</Link>
</p>
<p className="text-xs text-muted">
<p className="text-xs text-soft">
By creating an account, you agree to our{' '}
<button
onClick={() => showPrivacyModal()}

View file

@ -476,12 +476,12 @@ export function AudiobookExportModal({
<>
<div className="space-y-4">
{!isGenerating && (
<div className="w-full rounded-xl border border-offbase bg-background">
<div className="w-full rounded-lg border border-line bg-background">
{/* 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>
{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>
Locked
</span>
@ -492,7 +492,7 @@ export function AudiobookExportModal({
{isLegacyAudiobookMissingSettings && (
<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="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
which voice/speeds/format were used. Consider resetting this audiobook to regenerate it with
v1 metadata (so settings are saved for resumes across devices).
@ -503,18 +503,18 @@ export function AudiobookExportModal({
{settingsLocked && savedSettings ? (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg bg-base p-3">
<div className="text-[11px] uppercase tracking-wider text-muted mb-1">Voice</div>
<div className="rounded-lg bg-surface p-3">
<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>
<div className="rounded-lg bg-base p-3">
<div className="text-[11px] uppercase tracking-wider text-muted mb-1">Format</div>
<div className="rounded-lg bg-surface p-3">
<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>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg bg-base p-3">
<div className="text-[11px] uppercase tracking-wider text-muted mb-1">Native speed</div>
<div className="rounded-lg bg-surface p-3">
<div className="text-[11px] uppercase tracking-wider text-soft mb-1">Native speed</div>
<div className="text-sm font-medium text-foreground">
{resolveTtsProviderModelPolicy({
providerRef: savedSettings.providerRef,
@ -525,12 +525,12 @@ export function AudiobookExportModal({
: 'Not supported'}
</div>
</div>
<div className="rounded-lg bg-base p-3">
<div className="text-[11px] uppercase tracking-wider text-muted mb-1">Post speed</div>
<div className="rounded-lg bg-surface p-3">
<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>
</div>
<p className="text-xs text-muted">
<p className="text-xs text-soft">
Reset the audiobook to change generation settings.
</p>
</div>
@ -539,7 +539,7 @@ export function AudiobookExportModal({
{/* Voice & Format row */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<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
availableVoices={availableVoices}
voice={audiobookVoice}
@ -552,7 +552,7 @@ export function AudiobookExportModal({
</div>
<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 ? (
<Listbox
value={format}
@ -560,10 +560,10 @@ export function AudiobookExportModal({
disabled={chapters.length > 0 || settingsLocked}
>
<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="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>
</ListboxButton>
<Transition
@ -572,11 +572,11 @@ export function AudiobookExportModal({
leaveFrom="opacity-100"
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
value="m4b"
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 }) => (
@ -588,7 +588,7 @@ export function AudiobookExportModal({
<ListboxOption
value="mp3"
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 }) => (
@ -608,9 +608,9 @@ export function AudiobookExportModal({
</div>
{/* 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 && (
<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.
</div>
)}
@ -619,7 +619,7 @@ export function AudiobookExportModal({
<>
<div className="space-y-2">
<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>
</div>
<input
@ -629,21 +629,21 @@ export function AudiobookExportModal({
step="0.1"
value={nativeSpeed}
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>3x</span>
</div>
</div>
<div className="border-t border-offbase" />
<div className="border-t border-line-soft" />
</>
)}
<div className="space-y-2">
<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>
</div>
<input
@ -653,9 +653,9 @@ export function AudiobookExportModal({
step="0.1"
value={postSpeed}
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>3x</span>
</div>
@ -708,7 +708,7 @@ export function AudiobookExportModal({
</div>
)}
{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">
TTS audio for this chapter may be cached
<br />
@ -716,7 +716,7 @@ export function AudiobookExportModal({
</p>
<Button
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"
>
<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">
<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>
{displayChapters.map((chapter) => (
<div
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">
{chapter.status === 'completed' ? (
@ -760,20 +760,20 @@ export function AudiobookExportModal({
<Button
onClick={() => handleRegenerateChapter(chapter)}
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'}
>
<RefreshIcon className={`h-4 w-4 ${regeneratingChapter === chapter.index || chapter.status === 'generating' ? 'animate-spin' : ''}`} />
</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">
<p className="text-sm font-medium text-foreground">
{chapter.title}
</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)}
</p>
</div>
@ -782,7 +782,7 @@ export function AudiobookExportModal({
{((onRegenerateChapter && !isGenerating) || chapter.status === 'completed') && (
<Menu as="div" className="relative inline-block text-left">
<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"
>
<DotsVerticalIcon className="h-5 w-5" />
@ -799,7 +799,7 @@ export function AudiobookExportModal({
<MenuItems
anchor={{ to: 'bottom end', gap: '8px', padding: '12px' }}
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' && (
<>
@ -807,7 +807,7 @@ export function AudiobookExportModal({
{({ active }) => (
<button
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"
>
<XCircleIcon className="h-4 w-4" />
@ -819,7 +819,7 @@ export function AudiobookExportModal({
{({ active }) => (
<button
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" />
<span>Download</span>
@ -833,7 +833,7 @@ export function AudiobookExportModal({
{({ active }) => (
<button
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"
>
<XCircleIcon className="h-4 w-4" />
@ -848,7 +848,7 @@ export function AudiobookExportModal({
<button
onClick={() => handleRegenerateChapter(chapter)}
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"
>
<RefreshIcon className={`h-4 w-4 ${regeneratingChapter === chapter.index ? 'animate-spin' : ''}`} />
@ -868,7 +868,7 @@ export function AudiobookExportModal({
</div>
{bookId && !isGenerating && (
<div className="pt-4 border-t border-offbase">
<div className="pt-4 border-t border-line-soft">
<Button
onClick={handleDownloadComplete}
disabled={isCombining}
@ -888,7 +888,7 @@ export function AudiobookExportModal({
{chapters.length === 0 && !isGenerating && !isLoadingExisting && (
<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.
</p>
</div>
@ -938,43 +938,43 @@ export function AudiobookExportModal({
function AudiobookSettingsSkeleton() {
return (
<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="flex items-center justify-between px-4 py-3 border-b border-offbase bg-base">
<div className="h-4 w-40 rounded bg-offbase" />
<div className="h-5 w-14 rounded bg-offbase" />
<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-line-soft bg-surface">
<div className="h-4 w-40 rounded bg-surface-sunken" />
<div className="h-5 w-14 rounded bg-surface-sunken" />
</div>
<div className="p-4 space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<div className="h-3 w-16 rounded bg-offbase" />
<div className="h-9 w-full rounded-md bg-offbase" />
<div className="h-3 w-16 rounded bg-surface-sunken" />
<div className="h-9 w-full rounded-md bg-surface-sunken" />
</div>
<div className="space-y-1.5">
<div className="h-3 w-16 rounded bg-offbase" />
<div className="h-9 w-full rounded-md bg-offbase" />
<div className="h-3 w-16 rounded bg-surface-sunken" />
<div className="h-9 w-full rounded-md bg-surface-sunken" />
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<div className="h-3 w-24 rounded bg-offbase" />
<div className="h-2 w-full rounded bg-offbase" />
<div className="h-3 w-24 rounded bg-surface-sunken" />
<div className="h-2 w-full rounded bg-surface-sunken" />
</div>
<div className="space-y-1.5">
<div className="h-3 w-20 rounded bg-offbase" />
<div className="h-2 w-full rounded bg-offbase" />
<div className="h-3 w-20 rounded bg-surface-sunken" />
<div className="h-2 w-full rounded bg-surface-sunken" />
</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 className="w-full rounded-xl border border-offbase bg-background overflow-hidden">
<div className="px-4 py-3 border-b border-offbase bg-base">
<div className="h-4 w-28 rounded bg-offbase" />
<div className="w-full rounded-lg border border-line bg-background overflow-hidden">
<div className="px-4 py-3 border-b border-line-soft bg-surface">
<div className="h-4 w-28 rounded bg-surface-sunken" />
</div>
<div className="p-4 space-y-2">
{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>

View file

@ -16,7 +16,7 @@ export function CodeBlock({ children }: { children: string }) {
return (
<div className="relative group my-2">
<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>
</pre>
<button

View file

@ -91,7 +91,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
<PopoverPanel
anchor="bottom start"
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"
>
{/* Label */}
@ -107,7 +107,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
className="p-1"
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>
<input
ref={nativeRef}
@ -166,7 +166,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps)
}}
spellCheck={false}
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>
</PopoverPanel>

View file

@ -62,7 +62,7 @@ export function ConfirmDialog({
leaveFrom="opacity-100 scale-100"
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
as="h3"
className="text-lg font-semibold leading-6 text-foreground"

View file

@ -43,7 +43,7 @@ export function CookieConsentBanner() {
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))]"
>
<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">
<h3 className="mb-2 text-lg font-bold">
🍪 We use cookies
@ -70,7 +70,7 @@ export function CookieConsentBanner() {
className={buttonClass({
variant: 'primary',
size: 'md',
className: 'whitespace-nowrap font-bold shadow-sm',
className: 'whitespace-nowrap font-bold shadow-elev-1',
})}
>
Accept All

View file

@ -10,7 +10,7 @@ export function Header({
right?: ReactNode;
}) {
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="flex items-center gap-2 min-w-0 flex-1">
{left}

View file

@ -20,8 +20,8 @@ interface PrivacyModalProps {
function PrivacyModalBody({ origin }: { origin: string }) {
return (
<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="text-xs font-semibold uppercase tracking-wide text-muted">Service Operator</div>
<div className="rounded-lg border border-line bg-surface-sunken p-3">
<div className="text-xs font-semibold uppercase tracking-wide text-soft">Service Operator</div>
<div className="mt-1">
This instance is hosted at <span className="font-bold">{origin || 'this server'}</span>.
</div>
@ -102,7 +102,7 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
leaveFrom="opacity-100 scale-100"
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
as="h3"
className="text-lg font-semibold leading-6 text-foreground"
@ -113,7 +113,7 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
<PrivacyModalBody origin={origin} />
<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">
<input
data-testid="privacy-agree-checkbox"
@ -121,7 +121,7 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
type="checkbox"
checked={agreed}
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 className="text-sm leading-6">
@ -216,7 +216,7 @@ export function showPrivacyModal(): void {
leaveFrom="opacity-100 scale-100"
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 leading-6 text-foreground"

View file

@ -32,7 +32,7 @@ export function ProgressCard({
const operationLabel = getOperationLabel();
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 */}
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0 space-y-1">
@ -57,7 +57,7 @@ export function ProgressCard({
className={buttonClass({
variant: 'ghost',
size: 'xs',
className: 'shrink-0 hover:bg-surface-sunken',
className: 'shrink-0 hover:bg-accent-wash',
})}
onClick={(e) => onCancel(e)}
>
@ -74,7 +74,7 @@ export function ProgressCard({
</div>
{/* 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 && (
<>
<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="w-full max-w-md mx-auto">
<div
className={`pointer-events-auto shadow-xl ${
className={`pointer-events-auto shadow-elev-3 ${
onClick ? 'cursor-pointer' : ''
}`}
onClick={onClick}

View file

@ -142,7 +142,7 @@ export function SettingsTrigger({
return (
<Button
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"
tabIndex={0}
>
@ -483,8 +483,8 @@ export function SettingsModal({
setActiveSection(visibleSections[0]?.id ?? 'theme');
}, [activeSection, visibleSections]);
const fieldLabelClass = 'block text-[11px] font-semibold uppercase tracking-wide text-muted';
const sectionShellClass = 'space-y-2 pb-3 border-b border-offbase px-0.5';
const fieldLabelClass = 'block text-[11px] font-semibold uppercase tracking-wide text-soft';
const sectionShellClass = 'space-y-2 pb-3 border-b border-line-soft px-0.5';
const sectionHeadingClass = 'text-sm font-semibold text-foreground';
const effectiveProviderType = resolveEffectiveProviderType({
providerRef: selectedProviderRef,
@ -540,16 +540,16 @@ export function SettingsModal({
leaveFrom="opacity-100 scale-100"
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 */}
<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">
<DialogTitle as="h3" className="text-lg font-semibold leading-6 text-foreground">
Settings
</DialogTitle>
<Button
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'}
</Button>
@ -557,7 +557,7 @@ export function SettingsModal({
<div className="flex items-center">
<Button
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
</Button>
@ -573,7 +573,7 @@ export function SettingsModal({
) : (
<>
{/* 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) => {
const Icon = section.icon;
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 ${
activeSection === section.id
? '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" />
@ -595,7 +595,7 @@ export function SettingsModal({
<div className="flex flex-row h-[490px]">
{/* 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">
{visibleSections.map((section) => {
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 ${
active
? '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" />
@ -661,7 +661,7 @@ export function SettingsModal({
{selectedProviderOption?.name || 'Select Provider'}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-muted" />
<ChevronUpDownIcon className="h-5 w-5 text-soft" />
</span>
</ListboxButton>
<Transition
@ -700,7 +700,7 @@ export function SettingsModal({
)}
</div>
{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.
</p>
)}
@ -737,7 +737,7 @@ export function SettingsModal({
</div>
)}
{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.
</p>
)}
@ -745,7 +745,7 @@ export function SettingsModal({
<div className="space-y-1.5">
<label className={fieldLabelClass}>TTS Model</label>
{!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.
</p>
)}
@ -768,7 +768,7 @@ export function SettingsModal({
{selectedModel.name}
</span>
{selectedModelVersion && (
<span className="block truncate text-xs text-muted">
<span className="block truncate text-xs text-soft">
{selectedModelVersion}
</span>
)}
@ -777,7 +777,7 @@ export function SettingsModal({
<span className="block truncate">Select Model</span>
)}
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-muted" />
<ChevronUpDownIcon className="h-5 w-5 text-soft" />
</span>
</ListboxButton>
<Transition
@ -801,7 +801,7 @@ export function SettingsModal({
<span className={`block ${selected ? 'font-medium' : 'font-normal'}`}>
<span className="block truncate">{model.name}</span>
{model.id.includes(':') && (
<span className="block truncate text-xs text-muted">
<span className="block truncate text-xs text-soft">
{model.id.slice(model.id.indexOf(':'))}
</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
${isActive
? 'border-accent'
: 'border-offbase hover:border-muted'
: 'border-line hover:border-accent-line'
}`}
style={{ backgroundColor: colors.base }}
>
@ -927,8 +927,8 @@ export function SettingsModal({
{systemTheme.name}
</span>
<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-offbase" style={{ backgroundColor: colors.offbase }} />
<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-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.secondaryAccent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
@ -940,7 +940,7 @@ export function SettingsModal({
{/* Custom theme */}
<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 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
${isActive
? 'border-accent'
: 'border-offbase hover:border-muted'
: 'border-line hover:border-accent-line'
}`}
style={{ backgroundColor: colors.base }}
>
@ -971,8 +971,8 @@ export function SettingsModal({
Custom
</span>
<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-offbase" style={{ backgroundColor: colors.offbase }} />
<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-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.secondaryAccent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
@ -980,7 +980,7 @@ export function SettingsModal({
</button>
<button
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 }}
aria-label={isCustomExpanded ? 'Collapse color picker' : 'Expand color picker'}
>
@ -1043,7 +1043,7 @@ export function SettingsModal({
{/* Light themes */}
<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">
{lightThemes.map((t) => {
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
${isActive
? 'border-accent'
: 'border-offbase hover:border-muted'
: 'border-line hover:border-accent-line'
}`}
style={{ backgroundColor: colors.base }}
>
@ -1071,8 +1071,8 @@ export function SettingsModal({
{t.name}
</span>
<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-offbase" style={{ backgroundColor: colors.offbase }} />
<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-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.secondaryAccent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
@ -1085,7 +1085,7 @@ export function SettingsModal({
{/* Dark themes */}
<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">
{darkThemes.map((t) => {
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
${isActive
? 'border-accent'
: 'border-offbase hover:border-muted'
: 'border-line hover:border-accent-line'
}`}
style={{ backgroundColor: colors.base }}
>
@ -1113,8 +1113,8 @@ export function SettingsModal({
{t.name}
</span>
<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-offbase" style={{ backgroundColor: colors.offbase }} />
<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-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.secondaryAccent }} />
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: colors.muted }} />
@ -1199,10 +1199,10 @@ export function SettingsModal({
{activeSection === 'account' && (
<div className="space-y-2">
{/* 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>
<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 ? (
<>
<p className="font-medium text-foreground">
@ -1211,7 +1211,7 @@ export function SettingsModal({
: (session.user.name || session.user.email || 'Account')}
</p>
{!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 && (
<p className="text-xs text-accent mt-1">Anonymous session</p>
@ -1229,14 +1229,14 @@ export function SettingsModal({
onClick={() => {
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" />
</div>
<div className="flex-1 min-w-0">
<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>
</button>
)}
@ -1252,7 +1252,7 @@ export function SettingsModal({
Disconnect account
</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>
<Button
onClick={() => setShowDeleteAccountConfirm(true)}
@ -1260,14 +1260,14 @@ export function SettingsModal({
>
Delete Account
</Button>
<p className="text-xs text-muted mt-2">
<p className="text-xs text-soft mt-2">
Permanently deletes your account and all data.
</p>
</div>
</>
) : (
<div className="pt-2 border-t border-offbase">
<p className="text-sm text-muted mb-3">
<div className="pt-2 border-t border-line-soft">
<p className="text-sm text-soft mb-3">
{session?.user?.isAnonymous
? (runtimeConfig.enableUserSignups
? '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]);
return (
<div className="h-[490px] flex flex-col bg-base">
<div className="flex items-center gap-3 px-4 py-3 border-b border-offbase bg-background">
<div className="h-[490px] flex flex-col bg-surface">
<div className="flex items-center gap-3 px-4 py-3 border-b border-line-soft bg-background">
<Button
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"
title="Back"
>
@ -1449,7 +1449,7 @@ function SettingsChangelogPanel({
</Button>
<div className="min-w-0">
<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
? `Current version: v${normalizedAppVersion}`
: 'Release history from GitHub'}
@ -1459,15 +1459,15 @@ function SettingsChangelogPanel({
<div className="flex-1 overflow-y-auto px-3 pb-3">
{loading && (
<div className="py-3 text-sm text-muted">
<div className="py-3 text-sm text-soft">
Loading changelog
</div>
)}
{!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-xs text-muted break-words">{error}</p>
<p className="text-xs text-soft break-words">{error}</p>
<a
href="https://github.com/richardr1126/openreader/releases"
target="_blank"
@ -1480,7 +1480,7 @@ function SettingsChangelogPanel({
)}
{!loading && !error && manifest.length === 0 && (
<div className="py-3 text-sm text-muted">
<div className="py-3 text-sm text-soft">
No releases found.
</div>
)}
@ -1493,35 +1493,35 @@ function SettingsChangelogPanel({
const normalizedName = normalizeVersion(entry.name || '');
const showName = Boolean(entry.name) && normalizedName !== normalizedTag;
return (
<div key={entry.tag_name} className="border-b border-offbase">
<div key={entry.tag_name} className="border-b border-line-soft">
<button
type="button"
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
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' : ''
}`}
/>
<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>
{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
</span>
)}
{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
</span>
)}
{showName && (
<span className="text-xs text-muted truncate">
<span className="text-xs text-soft truncate">
{entry.name}
</span>
)}
<span className="text-[11px] text-muted shrink-0">
<span className="text-[11px] text-soft shrink-0">
{new Date(entry.published_at).toLocaleDateString()}
</span>
</div>
@ -1530,13 +1530,13 @@ function SettingsChangelogPanel({
{isExpanded && (
<div className="pl-6 pr-1 pb-3 pt-1 space-y-2">
{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]}>
{body.body || '_No release notes provided._'}
</ReactMarkdown>
</div>
) : (
<p className="text-xs text-muted">Loading release notes</p>
<p className="text-xs text-soft">Loading release notes</p>
)}
<a
href={entry.html_url}

View file

@ -252,11 +252,11 @@ export function AuthLoader({ children }: { children: ReactNode }) {
if (isLoading) {
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" />
{bootstrapError ? (
<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
type="button"
onClick={() => {
@ -270,7 +270,7 @@ export function AuthLoader({ children }: { children: ReactNode }) {
</button>
</div>
) : (
<p className="text-sm text-muted animate-pulse">
<p className="text-sm text-soft animate-pulse">
{isAutoLoggingIn ? 'Starting anonymous session...' : 'Loading...'}
</p>
)}

View file

@ -99,7 +99,7 @@ export default function ClaimDataModal({
leaveFrom="opacity-100 scale-100"
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
as="h3"
className="text-lg font-semibold leading-6 text-foreground mb-4"
@ -107,13 +107,13 @@ export default function ClaimDataModal({
Existing Data Found
</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.
Claim it now to attach it to your account.
</p>
<div className="mb-4 rounded-lg border border-offbase bg-surface-sunken p-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted">Claimable data</div>
<div className="mb-4 rounded-lg border border-line bg-surface-sunken p-3">
<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">
<li>{claimableCounts.documents} document(s)</li>
<li>{claimableCounts.audiobooks} audiobook(s)</li>
@ -132,8 +132,8 @@ export default function ClaimDataModal({
type="button"
onClick={onDismiss}
disabled={isClaiming}
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
className="inline-flex justify-center rounded-lg bg-background px-3 py-1.5 text-sm
font-medium text-foreground hover:bg-accent-wash focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-base ease-standard hover:text-accent
disabled:opacity-50"
@ -145,8 +145,8 @@ export default function ClaimDataModal({
type="button"
onClick={handleClaim}
disabled={isClaiming}
className="inline-flex justify-center rounded-lg bg-accent px-3 py-1.5 text-sm
font-medium text-background hover:bg-secondary-accent focus:outline-none
className="inline-flex justify-center rounded-lg bg-accent px-3 py-1.5 text-sm
font-medium text-background hover:bg-secondary-accent focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-base ease-standard hover:text-background
disabled:opacity-50"

View file

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

View file

@ -32,19 +32,19 @@ export function UserMenu({
};
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 (variant === 'sidebar') {
return (
<div className={`flex w-full flex-col gap-0.5 ${className}`}>
<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>
</Link>
{enableUserSignups && (
<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>
</Link>
)}
@ -55,13 +55,13 @@ export function UserMenu({
return (
<div className={`flex gap-2 ${className}`}>
<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
</Button>
</Link>
{enableUserSignups && (
<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
</Button>
</Link>
@ -78,7 +78,7 @@ export function UserMenu({
title="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>
<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>
@ -90,7 +90,7 @@ export function UserMenu({
}
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]">
{session.user.email || 'Account'}
</span>

View file

@ -42,7 +42,7 @@ export function CreateFolderDialog({
leaveFrom="opacity-100 scale-100"
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">
Create New Folder
</DialogTitle>
@ -53,10 +53,10 @@ export function CreateFolderDialog({
onChange={(e) => onFolderNameChange(e.target.value)}
onKeyDown={onKeyDown}
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
/>
<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>
</DialogPanel>
</TransitionChild>

View file

@ -139,14 +139,14 @@ function SidebarUploadLoader({
const dashOffset = circumference - (progress / 100) * circumference;
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="min-w-0 flex items-center gap-1.5 text-[11px] leading-tight">
<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 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' : ''}>
<circle
cx={size / 2}
@ -174,7 +174,7 @@ function SidebarUploadLoader({
</div>
</div>
{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}
</p>
)}
@ -649,15 +649,15 @@ function DocumentListInner({ brand, appActions }: DocumentListInnerProps) {
}}
>
{!isLoading && showHint && allDocuments.length > 1 && (
<div className="px-3 pt-3 shrink-0 bg-background">
<div className="flex items-center justify-between bg-base border border-offbase rounded-md px-3 py-1 text-[12px]">
<div className="px-3 pt-3 shrink-0 bg-surface-sunken">
<div className="flex items-center justify-between bg-surface border border-line rounded-md px-3 py-1 text-[12px]">
<p className="text-foreground">
Drag files onto each other to make folders. Drop into the sidebar to move.
</p>
<button
type="button"
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"
>
<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="grid" style={iconsGridStyle(iconSize, ICON_SKELETON_ITEM_COUNT)}>
{Array.from({ length: ICON_SKELETON_ITEM_COUNT }).map((_, index) => (
<div key={index} className="overflow-hidden rounded-md border border-offbase bg-base">
<div className="aspect-[3/4] w-full bg-offbase" />
<div key={index} className="overflow-hidden rounded-md border border-line bg-surface">
<div className="aspect-[3/4] w-full bg-surface-sunken" />
<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-2.5 w-4/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-surface-sunken" />
</div>
</div>
))}
@ -31,18 +31,18 @@ function IconsSkeleton({ iconSize }: { iconSize: IconSize }) {
function ListSkeleton() {
return (
<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-2.5 w-12 rounded bg-offbase" />
<div className="h-2.5 w-12 rounded bg-surface-sunken" />
</div>
<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 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 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>
@ -50,23 +50,23 @@ function ListSkeleton() {
{Array.from({ length: 10 }).map((_, index) => (
<div
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="h-3.5 w-3.5 rounded bg-offbase" />
<div className="h-2.5 w-2/3 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-surface-sunken" />
</div>
<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 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 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 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>
))}
@ -78,24 +78,24 @@ function ListSkeleton() {
function GallerySkeleton() {
return (
<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="w-[260px] sm:w-[320px] aspect-[3/4] rounded-lg border border-offbase bg-offbase" />
<div className="h-3 w-40 rounded bg-offbase" />
<div className="h-2.5 w-28 rounded 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-surface-sunken" />
<div className="h-2.5 w-28 rounded bg-surface-sunken" />
<div className="flex gap-2">
<div className="h-8 w-20 rounded-md bg-offbase" />
<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-surface-sunken" />
</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">
{Array.from({ length: 10 }).map((_, index) => (
<div key={index} className="shrink-0 w-[88px] rounded-md overflow-hidden border border-offbase bg-base">
<div className="aspect-[3/4] bg-offbase" />
<div key={index} className="shrink-0 w-[88px] rounded-md overflow-hidden border border-line bg-surface">
<div className="aspect-[3/4] bg-surface-sunken" />
<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>
))}

View file

@ -243,7 +243,7 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
return (
<div
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 ? (
<>

View file

@ -140,9 +140,9 @@ export function DocumentTile({
className={
'group relative flex flex-col rounded-md overflow-hidden border transition duration-base ease-standard ' +
(isSelected
? 'border-accent bg-offbase'
: 'border-offbase bg-base hover:bg-offbase hover:border-accent') +
(isDropTarget ? ' ring-1 ring-accent' : '') +
? 'border-accent-line bg-surface-sunken'
: 'border-line bg-surface hover:bg-accent-wash hover:border-accent-line') +
(isDropTarget ? ' ring-1 ring-accent-line' : '') +
(isDragging ? ' opacity-50' : '')
}
>
@ -170,7 +170,7 @@ export function DocumentTile({
) : doc.type === 'epub' ? (
<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
@ -187,7 +187,7 @@ export function DocumentTile({
{showDeleteButton && (
<Button
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}`}
>
<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 }) {
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'} />;
return <FileIcon className={className ?? 'w-4 h-4 text-muted'} />;
return <FileIcon className={className ?? 'w-4 h-4 text-soft'} />;
}
function GalleryThumb({
@ -92,29 +92,29 @@ function GalleryThumb({
onClick={onClick}
aria-current={active ? 'true' : undefined}
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
? 'border-accent shadow-[0_10px_24px_-18px_rgba(0,0,0,0.85)] -translate-y-0.5'
: 'border-offbase hover:border-accent hover:-translate-y-0.5') +
(isOver && canDrop ? ' border-accent' : '') +
? 'border-accent-line shadow-elev-2 -translate-y-px'
: 'border-line hover:border-accent-line hover:-translate-y-px hover:shadow-elev-2') +
(isOver && canDrop ? ' border-accent-line' : '') +
(isDragging ? ' opacity-50' : '')
}
title={doc.name}
>
<div className="aspect-[3/4] bg-base">
<div className="aspect-[3/4] bg-surface">
<DocumentPreview doc={doc} />
</div>
<div
className={
'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
doc={doc}
className={
'w-3 h-3 shrink-0 transition-colors duration-base ' +
(active ? 'text-accent' : 'text-muted')
(active ? 'text-accent' : 'text-soft')
}
/>
<span
@ -181,19 +181,19 @@ export function GalleryView({
return (
<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">
{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="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} />
</div>
<div className="text-center">
<h2 className="text-[14px] font-semibold text-foreground truncate max-w-[320px]">
{activeDoc.name}
</h2>
<p className="text-[11px] text-muted">
<p className="text-[11px] text-soft">
{activeDoc.type.toUpperCase()} {formatDocumentSize(activeDoc.size)}
</p>
</div>
@ -214,18 +214,18 @@ export function GalleryView({
</button>
</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">
<dt className="text-muted">Type</dt>
<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-soft">Type</dt>
<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>
<dt className="text-muted">Last opened</dt>
<dt className="text-soft">Last opened</dt>
<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>
{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}>
{folderNameById?.[activeDoc.folderId] ?? activeDoc.folderId}
</dd>
@ -233,19 +233,19 @@ export function GalleryView({
)}
{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>
</>
)}
</dl>
</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 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
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"

View file

@ -31,7 +31,7 @@ function formatDate(ms: number): string {
function KindIcon({ doc }: { doc: DocumentListDocument }) {
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" />;
return <FileIcon className="w-4 h-4 shrink-0 text-muted" />;
return <FileIcon className="w-4 h-4 shrink-0 text-soft" />;
}
function HeaderCell({
@ -63,7 +63,7 @@ function HeaderCell({
}}
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 ' +
(active ? 'text-accent' : 'text-muted') +
(active ? 'text-accent' : 'text-soft') +
(align === 'right' ? ' justify-end' : '') +
' ' +
(className ?? '')
@ -131,11 +131,11 @@ function DocRow({
data-doc-tile
aria-selected={isSelected}
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
? 'bg-offbase text-accent'
: 'text-foreground hover:bg-offbase') +
(isTarget ? ' ring-1 ring-accent ring-inset' : '') +
? 'bg-surface-sunken text-accent'
: 'text-foreground hover:bg-accent-wash') +
(isTarget ? ' ring-1 ring-accent-line ring-inset' : '') +
(isDragging ? ' opacity-50' : '')
}
>
@ -149,11 +149,11 @@ function DocRow({
<KindIcon doc={doc} />
<span className="truncate">{doc.name}</span>
</Link>
<span className="px-2 text-[11px] text-muted 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 uppercase tracking-wide">{doc.type}</span>
<span className="px-2 text-[11px] text-soft text-right tabular-nums">
{formatDocumentSize(doc.size)}
</span>
<span className="px-2 text-[11px] text-muted tabular-nums">
<span className="px-2 text-[11px] text-soft tabular-nums">
{formatDate(doc.lastModified)}
</span>
<Button
@ -161,7 +161,7 @@ function DocRow({
e.stopPropagation();
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}`}
>
<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 (
<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="Kind" field="type" sortBy={sortBy} sortDirection={sortDirection} onSortChange={onSortChange} />
<HeaderCell label="Size" field="size" sortBy={sortBy} sortDirection={sortDirection} onSortChange={onSortChange} align="right" />

View file

@ -58,15 +58,15 @@ function SidebarRow({
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 ' +
(active
? 'border-accent bg-offbase text-accent'
: 'border-transparent bg-transparent text-foreground hover:border-accent hover:text-accent') +
(isDropTarget ? ' ring-1 ring-accent' : '')
? 'border-accent-line bg-surface-sunken text-accent'
: 'border-transparent bg-transparent text-foreground hover:border-accent-line hover:text-accent') +
(isDropTarget ? ' ring-1 ring-accent-line' : '')
}
>
<span
className={
'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}
@ -74,7 +74,7 @@ function SidebarRow({
<span className="truncate flex-1">{label}</span>
{typeof count === 'number' && count > 0 && (
<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}
</span>
@ -137,7 +137,7 @@ function FolderRow({
e.stopPropagation();
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}`}
title={`Delete ${folder.name}`}
>
@ -161,7 +161,7 @@ function SectionHeader({
return (
<div
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')
}
>
@ -206,7 +206,7 @@ export function FinderSidebar({
return (
<aside
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="p-2 flex flex-col gap-0.5">
@ -272,7 +272,7 @@ export function FinderSidebar({
rightSlot={(
<Menu as="div" className="relative inline-flex items-center leading-none text-left shrink-0 normal-case tracking-normal font-normal">
<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"
aria-label="Folder actions"
>
@ -289,7 +289,7 @@ export function FinderSidebar({
>
<MenuItems
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>
{({ active }) => (
@ -299,7 +299,7 @@ export function FinderSidebar({
onNewFolder();
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" />
New Folder
@ -315,7 +315,7 @@ export function FinderSidebar({
onRowAction?.();
}}
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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
@ -332,7 +332,7 @@ export function FinderSidebar({
Folders
</SectionHeader>
{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) => (
<FolderRow
@ -355,7 +355,7 @@ export function FinderSidebar({
</div>
{bottomSlot && (
<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()}
>
{bottomSlot}
@ -370,7 +370,7 @@ export function FinderSidebar({
onPointerMove={onResizeMove}
onPointerUp={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>
);

View file

@ -19,14 +19,14 @@ export function FinderStatusBar({
<div
role="status"
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="shrink-0">
{selectedCount > 0
? `${selectedCount} of ${itemCount} selected`
: `${itemCount} item${itemCount === 1 ? '' : 's'}`}
<span className="mx-1.5 text-muted"></span>
<span className="mx-1.5 text-soft"></span>
{formatDocumentSize(totalSize)}
</span>
</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.
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 =
'border-offbase text-foreground hover:text-accent hover:border-accent hover:bg-offbase';
const TOOLBAR_BTN_ACTIVE = 'border-accent bg-offbase text-accent';
'border-line text-foreground hover:text-accent hover:border-accent-line hover:bg-accent-wash';
const TOOLBAR_BTN_ACTIVE = 'border-accent-line bg-surface-sunken text-accent';
// 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
// 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 =
'inline-flex items-center justify-center rounded-[5px] text-xs transition-colors duration-base ease-standard';
const PILL_SEGMENT_INACTIVE = 'text-muted hover:bg-offbase hover:text-accent';
const PILL_SEGMENT_ACTIVE = 'bg-offbase text-accent';
'inline-flex items-center justify-center rounded-sm text-xs transition-colors duration-base ease-standard';
const PILL_SEGMENT_INACTIVE = 'text-soft hover:bg-accent-wash hover:text-accent';
const PILL_SEGMENT_ACTIVE = 'bg-surface-sunken text-accent';
export function FinderToolbar({
viewMode,
@ -89,10 +89,10 @@ export function FinderToolbar({
const directionLabel = sortDirection === 'asc' ? currentSort.asc : currentSort.desc;
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">
{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}
</div>
)}
@ -135,7 +135,7 @@ export function FinderToolbar({
<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"
>
<div className={`${PILL} shadow-lg`}>
<div className={`${PILL} shadow-elev-2`}>
{ICON_SIZES.map(({ value: sizeValue, label: sizeLabel }) => {
const sizeActive = iconSize === sizeValue;
return (
@ -182,7 +182,7 @@ export function FinderToolbar({
</ListboxButton>
<ListboxOptions
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) => (
<ListboxOption
@ -190,7 +190,7 @@ export function FinderToolbar({
value={opt.value}
className={({ active, selected }) =>
`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' : ''}`
}
>
@ -204,19 +204,19 @@ export function FinderToolbar({
<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]">
<SearchIcon className="w-3.5 h-3.5 text-muted shrink-0" />
<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-soft shrink-0" />
<input
type="search"
value={query}
onChange={(e) => onQueryChange(e.target.value)}
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>
{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}
</div>
)}

View file

@ -46,7 +46,7 @@ export function FinderWindow({
const isNarrow = useIsNarrow();
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}
<div className="flex flex-1 min-h-0">
@ -89,7 +89,7 @@ export function FinderWindow({
leaveTo="-translate-x-full"
>
<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}
</DialogPanel>

View file

@ -161,12 +161,12 @@ export function DexieMigrationModal({
leaveFrom="opacity-100 scale-100"
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">
{title}
</DialogTitle>
<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.
{displayMissingCount > 0 ? (
<> {displayMissingCount} {displayMissingCount === 1 ? 'is' : 'are'} not here yet.</>
@ -175,8 +175,8 @@ export function DexieMigrationModal({
</p>
{isUploading && (
<div className="space-y-1">
<p className="text-xs text-muted">{status}</p>
<div className="h-2 w-full rounded bg-offbase">
<p className="text-xs text-soft">{status}</p>
<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>
</div>
@ -189,8 +189,8 @@ export function DexieMigrationModal({
data-testid="migration-skip-button"
onClick={handleSkip}
disabled={isUploading}
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
className="inline-flex justify-center rounded-lg bg-background px-3 py-1.5 text-sm
font-medium text-foreground hover:bg-accent-wash focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-base ease-standard hover:text-accent
disabled:opacity-50"
@ -200,8 +200,8 @@ export function DexieMigrationModal({
<Button
onClick={handleUpload}
disabled={isUploading}
className="inline-flex justify-center rounded-lg bg-accent px-3 py-1.5 text-sm
font-medium text-background hover:bg-secondary-accent focus:outline-none
className="inline-flex justify-center rounded-lg bg-accent px-3 py-1.5 text-sm
font-medium text-background hover:bg-secondary-accent focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-base ease-standard hover:text-background
disabled:opacity-50"

View file

@ -49,10 +49,10 @@ export function DocumentHeaderMenu({
{onOpenSegments && (
<button
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
? 'border-accent text-accent bg-offbase'
: 'border-offbase text-foreground hover:bg-offbase hover:text-accent'
? 'border-accent text-accent bg-surface-sunken'
: 'border-line text-foreground hover:bg-accent-wash hover:text-accent'
}`}
aria-label={isSegmentsOpen ? 'Hide segments sidebar' : 'Open segments sidebar'}
title={isSegmentsOpen ? 'Hide Segments' : 'Segments'}
@ -63,10 +63,10 @@ export function DocumentHeaderMenu({
{showAudiobookExport && onOpenAudiobook && (
<button
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
? 'border-accent text-accent bg-offbase'
: 'border-offbase text-foreground hover:bg-offbase hover:text-accent'
? 'border-accent text-accent bg-surface-sunken'
: 'border-line text-foreground hover:bg-accent-wash hover:text-accent'
}`}
aria-label={isAudiobookOpen ? 'Hide audiobook export' : 'Open audiobook export'}
title={isAudiobookOpen ? 'Hide Export Audiobook' : 'Export Audiobook'}
@ -76,10 +76,10 @@ export function DocumentHeaderMenu({
)}
<button
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
? 'border-accent text-accent bg-offbase'
: 'border-offbase text-foreground hover:bg-offbase hover:text-accent'
? 'border-accent text-accent bg-surface-sunken'
: 'border-line text-foreground hover:bg-accent-wash hover:text-accent'
}`}
aria-label={isSettingsOpen ? 'Hide settings' : 'Open settings'}
title={isSettingsOpen ? 'Hide Settings' : 'Settings'}
@ -95,7 +95,7 @@ export function DocumentHeaderMenu({
<div className="sm:hidden flex items-center">
<Menu as="div" className="relative inline-block text-left">
<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"
>
<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"
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 */}
<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">
<ZoomControl
value={zoomLevel}
@ -138,7 +138,7 @@ export function DocumentHeaderMenu({
{({ active }) => (
<button
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`}
>
<ListIcon className="h-4 w-4" />
@ -152,7 +152,7 @@ export function DocumentHeaderMenu({
{({ active }) => (
<button
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`}
>
<DownloadIcon className="h-4 w-4" />
@ -165,7 +165,7 @@ export function DocumentHeaderMenu({
{({ active }) => (
<button
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`}
>
<FileSettingsIcon className="h-4 w-4" />
@ -176,7 +176,7 @@ export function DocumentHeaderMenu({
</div>
{/* 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 />
</div>
</MenuItems>

View file

@ -139,7 +139,7 @@ export function DocumentSelectionModal({
leaveFrom="opacity-100 scale-100"
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
as="h3"
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}
{files.length > 0 && (
<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
type="checkbox"
className="rounded border-muted text-accent focus:ring-accent"
className="rounded border-muted text-accent focus:ring-accent-line"
checked={allSelected}
ref={input => {
if (input) input.indeterminate = isIndeterminate;
@ -163,13 +163,13 @@ export function DocumentSelectionModal({
)}
</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 ? (
<DocumentSelectionSkeleton />
) : errorMessage ? (
<div className="flex items-center justify-center h-full text-danger">{errorMessage}</div>
) : 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">
{files.map((file) => {
@ -179,7 +179,7 @@ export function DocumentSelectionModal({
key={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
${isSelected ? 'bg-accent-wash' : 'hover:bg-offbase'}
${isSelected ? 'bg-accent-wash' : 'hover:bg-accent-wash'}
`}
>
<div className="flex-shrink-0" onClick={(e) => e.stopPropagation()}>
@ -187,7 +187,7 @@ export function DocumentSelectionModal({
type="checkbox"
checked={isSelected}
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
@ -197,7 +197,7 @@ export function DocumentSelectionModal({
>
{file.name}
</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>
);
})}
@ -237,9 +237,9 @@ function DocumentSelectionSkeleton() {
<div className="h-full animate-pulse space-y-0.5" aria-label="Loading documents" aria-busy="true">
{rows.map((_, index) => (
<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-3.5 flex-1 rounded bg-offbase" />
<div className="h-3 w-14 rounded bg-offbase" />
<div className="h-4 w-4 rounded-sm bg-surface-sunken border border-line" />
<div className="h-3.5 flex-1 rounded bg-surface-sunken" />
<div className="h-3 w-14 rounded bg-surface-sunken" />
</div>
))}
</div>

View file

@ -163,13 +163,13 @@ export function DocumentUploader({
variant === 'compact'
? `${
isDragActive
? 'border border-accent bg-offbase text-accent'
: 'border border-dashed border-offbase text-foreground hover:border-accent hover:bg-offbase hover:text-accent'
? 'border border-accent bg-surface-sunken text-accent'
: 'border border-dashed border-line text-foreground hover:border-accent hover:bg-accent-wash hover:text-accent'
}`
: `${
isDragActive
? 'border-2 border-dashed border-accent bg-base text-foreground'
: 'border-2 border-dashed border-muted bg-transparent text-foreground hover:border-accent hover:bg-base'
? '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-surface'
}`;
const paddingClass = variant === 'compact' ? 'py-1 px-2 rounded-md' : 'py-5 px-3 rounded-lg';
@ -182,7 +182,7 @@ export function DocumentUploader({
{children}
{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="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" />
<p className="text-xl font-bold text-foreground mb-1.5">
Drop files here to upload
@ -217,7 +217,7 @@ export function DocumentUploader({
<input {...getInputProps()} />
{variant === 'compact' ? (
<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 ? (
<p className="text-[12px] font-medium truncate flex-1">Uploading</p>
) : isConverting ? (
@ -233,7 +233,7 @@ export function DocumentUploader({
</div>
) : (
<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 ? (
<p className="text-sm sm:text-lg font-semibold text-foreground">Uploading file...</p>
) : isConverting ? (
@ -243,7 +243,7 @@ export function DocumentUploader({
<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'}
</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'}
</p>
{error && <p className="mt-2 text-sm text-danger">{error}</p>}

View file

@ -28,7 +28,7 @@ export function ZoomControl({
>
</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
type="button"
onClick={onIncrease}

View file

@ -30,16 +30,16 @@ export {
type ButtonVariant,
};
// Inputs use `bg-base` so they remain visible regardless of whether the
// surrounding container is `bg-background` (Card) or `bg-base` (Section).
// Inputs use `bg-surface` so they remain visible regardless of whether the
// surrounding container is `bg-background` (Card) or `bg-surface` (Section).
// 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
// resolve to CSS custom properties that don't accept opacity suffixes.)
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 =
'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 =
'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) =>

View file

@ -283,7 +283,7 @@ export function FileIcon(props: React.SVGProps<SVGSVGElement>) {
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
className={props.className || "w-8 h-8 text-muted"}
className={props.className || "w-8 h-8 text-soft"}
{...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" />

View file

@ -56,7 +56,7 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
<Button
onClick={() => skipToLocation(currentPage - 1, true)}
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"
>
<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 */}
<Popover className="relative mb-1">
<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}
>
<p className="text-xs whitespace-nowrap">
{currentPage} / {numPages || 1}
</p>
</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="text-xs font-medium text-foreground">Go to page</div>
<input
@ -82,7 +82,7 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
type="text"
inputMode="numeric"
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}
onChange={handleInputChange}
onBlur={handleInputConfirm}
@ -90,7 +90,7 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
placeholder={currentPage.toString()}
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>
</PopoverPanel>
</Popover>
@ -99,7 +99,7 @@ export const Navigator = ({ currentPage, numPages, skipToLocation }: {
<Button
onClick={() => skipToLocation(currentPage + 1, true)}
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"
>
<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={() => {
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"
>
<PauseIcon className="w-5 h-5" />

View file

@ -88,16 +88,16 @@ export const SpeedControl = ({
return (
<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" />
<span className="sm:hidden">{compactTriggerLabel}</span>
<span className="hidden sm:inline">{triggerLabel}</span>
<ChevronUpDownIcon className="h-2.5 w-2.5 sm:h-3 sm:w-3" />
</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">
{!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.
</div>
)}
@ -122,7 +122,7 @@ export const SpeedControl = ({
onMouseUp={handleVoiceSpeedChangeComplete}
onKeyUp={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>
)}
@ -146,7 +146,7 @@ export const SpeedControl = ({
onMouseUp={handleAudioSpeedChangeComplete}
onKeyUp={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>

View file

@ -31,7 +31,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
} = useTTS();
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">
{/* Speed control */}
<SpeedControl
@ -51,7 +51,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
{/* Playback Controls */}
<Button
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"
disabled={isProcessing}
>
@ -60,7 +60,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
<Button
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'}
disabled={isProcessing && !isPlaying}
>
@ -69,7 +69,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
<Button
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"
disabled={isProcessing}
>

View file

@ -34,15 +34,15 @@ export function VoicesControlBase({
: 'bottom-full right-0 mb-1';
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'
: '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';
? '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-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'
? 'h-3.5 w-3.5 shrink-0'
: 'h-3 w-3 sm:h-3.5 sm:w-3.5';
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';
const providerModelPolicy = resolveTtsProviderModelPolicy({
@ -85,7 +85,7 @@ export function VoicesControlBase({
if (availableVoices.length === 0) {
return (
<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" />
<span>No voices</span>
</div>
@ -140,13 +140,13 @@ export function VoicesControlBase({
</>
)}
</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) => (
<ListboxOption
key={voiceId}
value={voiceId}
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 }) => (
@ -184,13 +184,13 @@ export function VoicesControlBase({
</>
)}
</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) => (
<ListboxOption
key={voiceId}
value={voiceId}
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>

View file

@ -68,12 +68,12 @@ export function ReaderSidebarShell({
<aside
role="dialog"
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">
<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 className="flex items-center gap-1 shrink-0">
{headerActions}
@ -85,7 +85,7 @@ export function ReaderSidebarShell({
className={buttonClass({
variant: 'secondary',
size: 'icon',
className: 'h-8 w-8 text-muted',
className: 'h-8 w-8 text-soft',
})}
>
<XCircleIcon className="w-4 h-4" />

View file

@ -641,7 +641,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
className={buttonClass({
variant: 'secondary',
size: 'xs',
className: 'h-8 px-2 text-muted',
className: 'h-8 px-2 text-soft',
})}
>
{isClearingSegments ? 'Clearing…' : 'Clear'}
@ -654,7 +654,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
className={buttonClass({
variant: 'secondary',
size: 'icon',
className: 'h-8 w-8 text-muted',
className: 'h-8 w-8 text-soft',
})}
>
<RefreshIcon className="w-3.5 h-3.5" />
@ -672,8 +672,8 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
headerActions={headerActions}
bodyClassName="flex-1 overflow-y-auto px-0 py-0"
>
<div className="px-4 py-2 border-b border-offbase">
<div className="text-xs text-muted">
<div className="px-4 py-2 border-b border-line-soft">
<div className="text-xs text-soft">
{hasLoadedManifest ? (
<>
{rowsToRender.length} indexed
@ -687,7 +687,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
) : null}
</>
) : 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 ? (
<span className="text-danger">error</span>
) : (
@ -705,16 +705,16 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
)}
{hasLoadedManifest && rowsToRender.length === 0 && (
<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
</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.
</p>
</div>
)}
{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) => {
const previousGroupKey = rowIndex > 0 ? rowsToRender[rowIndex - 1]?.groupKey : null;
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' : ''}`}
>
{showGroupHeader && (
<div className="mb-2 -mx-4 px-4 py-1.5 bg-surface-sunken border-y border-offbase">
<span className="text-[10px] uppercase tracking-[0.14em] text-muted">
<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-soft">
{groupLabel}
</span>
</div>
@ -758,7 +758,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
type="button"
onClick={() => { if (canJump) handleJump(segmentIndex, row.locator); }}
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'}
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`}>
{sentenceText || (
<span className="text-muted italic text-xs">
<span className="text-soft italic text-xs">
[text not loaded press play to fetch]
</span>
)}
@ -787,7 +787,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
aria-label={`Status ${status}`}
title={status}
/>
<span className="text-xs text-muted">
<span className="text-xs text-soft">
{formatDuration(activeVariant?.durationMs)}
</span>
{isCurrent && isPlaying && (
@ -796,7 +796,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
</span>
)}
{!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
</span>
)}
@ -822,8 +822,8 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
isActive
? 'border-accent text-accent bg-surface-sunken'
: known
? 'border-offbase text-muted hover:border-accent hover:text-accent'
: 'border-offbase text-muted opacity-60 cursor-not-allowed',
? 'border-line text-soft hover:border-accent hover:text-accent'
: 'border-line text-soft opacity-60 cursor-not-allowed',
].join(' ')}
>
{formatVoiceLabel(variant.settings)}
@ -854,21 +854,21 @@ function SegmentsListSkeleton() {
return (
<div className="px-4 py-3">
<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) => (
<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="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="h-3.5 w-11/12 rounded bg-offbase" />
<div className="h-3.5 w-3/4 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-surface-sunken" />
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-offbase" />
<div className="h-3 w-12 rounded bg-offbase" />
<div className="h-4 w-20 rounded bg-offbase" />
<div className="h-2 w-2 rounded-full bg-surface-sunken" />
<div className="h-3 w-12 rounded bg-surface-sunken" />
<div className="h-4 w-20 rounded bg-surface-sunken" />
</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>
))}
@ -882,7 +882,7 @@ function SegmentsListSkeletonRows() {
<div className="px-4 py-3 animate-pulse" aria-label="Loading more segments" aria-busy="true">
<div className="space-y-2">
{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>
@ -895,7 +895,7 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
<PopoverButton
aria-label="Segment 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" />
</PopoverButton>
@ -910,7 +910,7 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
>
<PopoverPanel
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">
<Row label="locator">
@ -925,7 +925,7 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
: `${row.locator.readerType || '?'} (legacy)`}
</span>
) : (
<span className="text-muted text-[11px]">none</span>
<span className="text-soft text-[11px]">none</span>
)}
</Row>
<Row label="variants">
@ -934,9 +934,9 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
</span>
</Row>
{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">
<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)}
</span>
</Row>
@ -972,7 +972,7 @@ function SegmentMetadataPopover({ row }: { row: TTSSegmentRow }) {
function Row({ label, children }: { label: string; children: ReactNode }) {
return (
<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>
</div>
);

View file

@ -158,12 +158,12 @@ export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
return (
<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">
<button
type="button"
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'}
>
<DotsVerticalIcon className="w-4 h-4" />
@ -171,7 +171,7 @@ export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
<button
type="button"
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"
>
<ChevronLeftIcon className="w-4 h-4" />
@ -185,15 +185,15 @@ export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
<button
type="button"
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"
>
<ChevronRightIcon className="w-4 h-4" />
</button>
</div>
{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="font-semibold text-muted pb-1">Skip to chapters</div>
<div className="border-b border-line-soft bg-background text-xs overflow-y-auto max-h-64 p-2">
<div className="font-semibold text-soft pb-1">Skip to chapters</div>
<div className="flex flex-wrap gap-1 w-full">
{tocRef.current.map((item, index) => (
<button
@ -204,9 +204,9 @@ export function EPUBViewer({ className = '', epubState }: EPUBViewerProps) {
setIsTocOpen(false);
}}
className="
px-2 py-1 rounded-md font-medium text-foreground text-center bg-base
hover:bg-offbase hover:text-accent transition-colors duration-fast
whitespace-nowrap
px-2 py-1 rounded-md font-medium text-foreground text-center bg-surface
hover:bg-accent-wash hover:text-accent transition-colors duration-fast
whitespace-nowrap
flex-1 min-w-[140px]
"
>

View file

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