phase 2: primitives and semantic tokens

This commit is contained in:
Richard R 2026-05-31 20:15:44 -06:00
parent 11e138ae36
commit 745c846a6e
28 changed files with 196 additions and 108 deletions

View file

@ -170,7 +170,7 @@ export default function EPUBPage() {
if (error) { if (error) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen">
<p className="text-red-500 mb-4">{error}</p> <p className="text-danger mb-4">{error}</p>
<Link <Link
href="/app" href="/app"
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition duration-base ease-standard hover:text-accent" className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition duration-base ease-standard hover:text-accent"

View file

@ -157,7 +157,7 @@ export default function HTMLPage() {
if (error) { if (error) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen">
<p className="text-red-500 mb-4">{error}</p> <p className="text-danger mb-4">{error}</p>
<Link <Link
href="/app" href="/app"
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition duration-base ease-standard hover:text-accent" className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition duration-base ease-standard hover:text-accent"

View file

@ -251,7 +251,7 @@ export default function PDFViewerPage() {
if (error) { if (error) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-screen"> <div className="flex flex-col items-center justify-center min-h-screen">
<p className="text-red-500 mb-4">{error}</p> <p className="text-danger mb-4">{error}</p>
<Link <Link
href="/app" href="/app"
onClick={handleBackToDocuments} onClick={handleBackToDocuments}
@ -311,18 +311,18 @@ export default function PDFViewerPage() {
<div className="h-full w-full bg-base"> <div className="h-full w-full bg-base">
<div className={`mx-auto flex h-full items-center px-4 py-6 transition duration-slow ease-standard ${showDetailedParseLoader ? 'max-w-lg' : 'max-w-md'}`}> <div className={`mx-auto flex h-full items-center px-4 py-6 transition duration-slow ease-standard ${showDetailedParseLoader ? 'max-w-lg' : 'max-w-md'}`}>
{showDetailedParseLoader ? ( {showDetailedParseLoader ? (
<div className="w-full rounded-xl border border-offbase bg-offbase/95 shadow-sm overflow-hidden"> <div className="w-full rounded-xl border border-offbase bg-surface-sunken shadow-sm overflow-hidden">
<div className="h-1 bg-[linear-gradient(90deg,var(--accent),transparent_80%)]" /> <div className="h-1 bg-[linear-gradient(90deg,var(--accent),transparent_80%)]" />
<div className="p-3.5 sm:p-4"> <div className="p-3.5 sm:p-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="inline-flex items-center gap-2 rounded-md border border-offbase bg-base/70 px-2.5 py-1"> <div className="inline-flex items-center gap-2 rounded-md border border-offbase bg-surface-solid px-2.5 py-1">
<LoadingSpinner className="h-3.5 w-3.5 text-accent" /> <LoadingSpinner className="h-3.5 w-3.5 text-accent" />
<span className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted">PDF Layout Parse</span> <span className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted">PDF Layout Parse</span>
</div> </div>
<p className="text-sm font-semibold text-foreground">{statusText}</p> <p className="text-sm font-semibold text-foreground">{statusText}</p>
</div> </div>
<div className="mt-3 rounded-lg border border-offbase bg-base/75 p-2.5"> <div className="mt-3 rounded-lg border border-offbase bg-surface-solid p-2.5">
<div className="mb-1.5 flex items-end justify-between gap-2"> <div className="mb-1.5 flex items-end justify-between gap-2">
<p className="text-[11px] font-semibold text-foreground"> <p className="text-[11px] font-semibold text-foreground">
{hasMeasuredProgress ? `Page ${pagesParsed} / ${totalPages}` : 'Awaiting first page'} {hasMeasuredProgress ? `Page ${pagesParsed} / ${totalPages}` : 'Awaiting first page'}
@ -354,7 +354,7 @@ export default function PDFViewerPage() {
</div> </div>
</div> </div>
) : ( ) : (
<div className="w-full rounded-xl border border-offbase bg-offbase/95 p-4 shadow-sm transition duration-slow ease-standard overflow-hidden"> <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="h-0.5 -mx-4 -mt-4 mb-3 bg-[linear-gradient(90deg,var(--accent),transparent_75%)]" /> <div className="h-0.5 -mx-4 -mt-4 mb-3 bg-[linear-gradient(90deg,var(--accent),transparent_75%)]" />
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
@ -366,9 +366,9 @@ export default function PDFViewerPage() {
</span> </span>
</div> </div>
<div className="mt-3 grid grid-cols-3 gap-1.5"> <div className="mt-3 grid grid-cols-3 gap-1.5">
<span className="h-1.5 rounded-full bg-accent/30 animate-pulse" /> <span className="h-1.5 rounded-full bg-accent-wash animate-pulse" />
<span className="h-1.5 rounded-full bg-accent/20 animate-pulse [animation-delay:120ms]" /> <span className="h-1.5 rounded-full bg-accent-wash animate-pulse [animation-delay:120ms]" />
<span className="h-1.5 rounded-full bg-accent/15 animate-pulse [animation-delay:220ms]" /> <span className="h-1.5 rounded-full bg-accent-wash animate-pulse [animation-delay:220ms]" />
</div> </div>
</div> </div>
)} )}

View file

@ -132,16 +132,16 @@ function SignInContent() {
{/* Alerts */} {/* Alerts */}
{sessionExpired && ( {sessionExpired && (
<div className="mt-4 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg"> <div className="mt-4 p-3 bg-accent-wash border border-accent-line rounded-lg">
<p className="text-sm text-amber-700 dark:text-amber-400"> <p className="text-sm text-accent ">
Your session has expired. Please sign in again. Your session has expired. Please sign in again.
</p> </p>
</div> </div>
)} )}
{error && ( {error && (
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg"> <div className="mt-4 p-3 bg-danger-wash border border-danger rounded-lg">
<p className="text-sm text-red-700 dark:text-red-400">{error}</p> <p className="text-sm text-danger dark:text-danger">{error}</p>
</div> </div>
)} )}

View file

@ -125,7 +125,7 @@ export default function SignUpPage() {
const { checks, strength } = validatePassword(password); const { checks, strength } = validatePassword(password);
const strengthLabels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong']; const strengthLabels = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong'];
const strengthColors = ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500']; const strengthColors = ['bg-danger', 'bg-danger', 'bg-accent', 'bg-accent', 'bg-accent'];
return ( return (
<div className="min-h-screen flex items-center justify-center p-4 bg-background"> <div className="min-h-screen flex items-center justify-center p-4 bg-background">
@ -134,8 +134,8 @@ export default function SignUpPage() {
<p className="text-sm text-muted mt-1">Create your account to get started</p> <p className="text-sm text-muted mt-1">Create your account to get started</p>
{error && ( {error && (
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg"> <div className="mt-4 p-3 bg-danger-wash border border-danger rounded-lg">
<p className="text-sm text-red-700 dark:text-red-400">{error}</p> <p className="text-sm text-danger dark:text-danger">{error}</p>
</div> </div>
)} )}
@ -190,12 +190,12 @@ export default function SignUpPage() {
/> />
))} ))}
</div> </div>
<p className={`text-xs ${strength >= 3 ? 'text-green-600' : 'text-red-600'}`}> <p className={`text-xs ${strength >= 3 ? 'text-accent' : 'text-danger'}`}>
{strengthLabels[strength - 1] || 'Very Weak'} {strengthLabels[strength - 1] || 'Very Weak'}
</p> </p>
<div className="text-xs space-y-0.5 text-muted"> <div className="text-xs space-y-0.5 text-muted">
{Object.entries(checks).map(([key, passed]) => ( {Object.entries(checks).map(([key, passed]) => (
<div key={key} className={`flex items-center gap-1 ${passed ? 'text-green-600' : ''}`}> <div key={key} className={`flex items-center gap-1 ${passed ? 'text-accent' : ''}`}>
<span>{passed ? '✓' : '○'}</span> <span>{passed ? '✓' : '○'}</span>
<span> <span>
{key === 'length' && 'At least 8 characters'} {key === 'length' && 'At least 8 characters'}
@ -223,7 +223,7 @@ export default function SignUpPage() {
focus:outline-none focus:ring-2 focus:ring-accent" focus:outline-none focus:ring-2 focus:ring-accent"
/> />
{passwordConfirmation && password && ( {passwordConfirmation && password && (
<p className={`text-xs mt-1 ${password === passwordConfirmation ? 'text-green-600' : 'text-red-600'}`}> <p className={`text-xs mt-1 ${password === passwordConfirmation ? 'text-accent' : 'text-danger'}`}>
{password === passwordConfirmation ? '✓ Passwords match' : '✗ Passwords do not match'} {password === passwordConfirmation ? '✓ Passwords match' : '✗ Passwords do not match'}
</p> </p>
)} )}

View file

@ -490,7 +490,7 @@ export function AudiobookExportModal({
<div className="p-4"> <div className="p-4">
{isLegacyAudiobookMissingSettings && ( {isLegacyAudiobookMissingSettings && (
<div className="mb-4 rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3 text-xs text-foreground"> <div className="mb-4 rounded-lg border border-accent-line bg-accent-wash p-3 text-xs text-foreground">
<div className="font-medium">Saved generation settings not found</div> <div className="font-medium">Saved generation settings not found</div>
<div className="mt-1 text-muted"> <div className="mt-1 text-muted">
This audiobook was likely created before v1 metadata was introduced, so OpenReader can&apos;t know This audiobook was likely created before v1 metadata was introduced, so OpenReader can&apos;t know
@ -694,9 +694,9 @@ export function AudiobookExportModal({
<Button <Button
onClick={() => setShowResetConfirm(true)} onClick={() => setShowResetConfirm(true)}
disabled={isGenerating} disabled={isGenerating}
className="inline-flex justify-center rounded-lg border border-red-500 bg-transparent px-3 py-2 text-sm className="inline-flex justify-center rounded-lg border border-danger bg-transparent px-3 py-2 text-sm
font-medium text-red-500 hover:bg-red-500 hover:text-background focus:outline-none font-medium text-danger hover:bg-danger-wash hover:text-background focus:outline-none
focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2 focus-visible:ring-2 focus-visible:ring-danger focus-visible:ring-offset-2
transform transition-transform duration-base ease-standard" transform transition-transform duration-base ease-standard"
title="Delete all generated chapters/pages for this document" title="Delete all generated chapters/pages for this document"
> >
@ -760,7 +760,7 @@ export function AudiobookExportModal({
<Button <Button
onClick={() => handleRegenerateChapter(chapter)} onClick={() => handleRegenerateChapter(chapter)}
disabled={regeneratingChapter !== null || chapter.status === 'generating' || isGenerating} disabled={regeneratingChapter !== null || chapter.status === 'generating' || isGenerating}
className="inline-flex items-center justify-center rounded-full bg-offbase text-accent hover:bg-accent/20 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-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"
title={chapter.status === 'generating' ? 'Generating...' : 'Regenerate this chapter'} title={chapter.status === 'generating' ? 'Generating...' : 'Regenerate this chapter'}
> >
<RefreshIcon className={`h-4 w-4 ${regeneratingChapter === chapter.index || chapter.status === 'generating' ? 'animate-spin' : ''}`} /> <RefreshIcon className={`h-4 w-4 ${regeneratingChapter === chapter.index || chapter.status === 'generating' ? 'animate-spin' : ''}`} />
@ -807,7 +807,7 @@ export function AudiobookExportModal({
{({ active }) => ( {({ active }) => (
<button <button
onClick={() => setPendingDeleteChapter(chapter)} onClick={() => setPendingDeleteChapter(chapter)}
className={`${active ? 'bg-offbase' : ''} text-red-500 group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`} className={`${active ? 'bg-offbase' : ''} text-danger group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`}
title="Delete this chapter" title="Delete this chapter"
> >
<XCircleIcon className="h-4 w-4" /> <XCircleIcon className="h-4 w-4" />
@ -833,7 +833,7 @@ export function AudiobookExportModal({
{({ active }) => ( {({ active }) => (
<button <button
onClick={handleCancel} onClick={handleCancel}
className={`${active ? 'bg-offbase text-red-500' : 'text-red-500'} group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`} className={`${active ? 'bg-offbase text-danger' : 'text-danger'} group flex w-full items-center gap-2 rounded px-2 py-2 text-sm`}
title="Cancel this chapter regeneration" title="Cancel this chapter regeneration"
> >
<XCircleIcon className="h-4 w-4" /> <XCircleIcon className="h-4 w-4" />

View file

@ -70,7 +70,7 @@ export function ConfirmDialog({
{title} {title}
</DialogTitle> </DialogTitle>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-foreground/90 break-words">{message}</p> <p className="text-sm text-soft break-words">{message}</p>
</div> </div>
<div className="mt-6 flex justify-end space-x-3"> <div className="mt-6 flex justify-end space-x-3">

View file

@ -48,7 +48,7 @@ export function CookieConsentBanner() {
<h3 className="mb-2 text-lg font-bold"> <h3 className="mb-2 text-lg font-bold">
🍪 We use cookies 🍪 We use cookies
</h3> </h3>
<p className="text-sm leading-relaxed text-foreground/90"> <p className="text-sm leading-relaxed text-soft">
We use strictly necessary cookies for authentication. Optional analytics is enabled only when you consent. We use strictly necessary cookies for authentication. Optional analytics is enabled only when you consent.
See our <Link href="/privacy" className="font-medium text-accent hover:underline">Privacy Policy</Link> for details. See our <Link href="/privacy" className="font-medium text-accent hover:underline">Privacy Policy</Link> for details.
</p> </p>

View file

@ -19,8 +19,8 @@ interface PrivacyModalProps {
function PrivacyModalBody({ origin }: { origin: string }) { function PrivacyModalBody({ origin }: { origin: string }) {
return ( return (
<div className="mt-4 space-y-4 text-sm text-foreground/90"> <div className="mt-4 space-y-4 text-sm text-soft">
<div className="rounded-lg border border-offbase bg-offbase/40 p-3"> <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="text-xs font-semibold uppercase tracking-wide text-muted">Service Operator</div>
<div className="mt-1"> <div className="mt-1">
This instance is hosted at <span className="font-bold">{origin || 'this server'}</span>. This instance is hosted at <span className="font-bold">{origin || 'this server'}</span>.
@ -113,7 +113,7 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
<PrivacyModalBody origin={origin} /> <PrivacyModalBody origin={origin} />
<div className="mt-6 space-y-4"> <div className="mt-6 space-y-4">
<div className="flex items-start gap-3 rounded-lg border border-offbase p-3 bg-offbase/20"> <div className="flex items-start gap-3 rounded-lg border border-offbase p-3 bg-surface-sunken">
<div className="flex h-6 items-center"> <div className="flex h-6 items-center">
<input <input
data-testid="privacy-agree-checkbox" data-testid="privacy-agree-checkbox"
@ -121,7 +121,7 @@ export function PrivacyModal({ isOpen, onAccept, onDismiss }: PrivacyModalProps)
type="checkbox" type="checkbox"
checked={agreed} checked={agreed}
onChange={(e) => setAgreed(e.target.checked)} onChange={(e) => setAgreed(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-accent focus:ring-accent bg-base" className="h-4 w-4 rounded border-line text-accent focus:ring-accent bg-base"
/> />
</div> </div>
<div className="text-sm leading-6"> <div className="text-sm leading-6">

View file

@ -57,7 +57,7 @@ export function ProgressCard({
className={buttonClass({ className={buttonClass({
variant: 'ghost', variant: 'ghost',
size: 'xs', size: 'xs',
className: 'shrink-0 hover:bg-background/50', className: 'shrink-0 hover:bg-surface-sunken',
})} })}
onClick={(e) => onCancel(e)} onClick={(e) => onCancel(e)}
> >

View file

@ -630,7 +630,7 @@ export function SettingsModal({
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className={fieldLabelClass}>TTS Provider</label> <label className={fieldLabelClass}>TTS Provider</label>
{ttsProviders.length === 0 ? ( {ttsProviders.length === 0 ? (
<p className="text-xs text-amber-500"> <p className="text-xs text-accent">
User API keys are restricted and no shared provider is configured. Ask an admin to add one. User API keys are restricted and no shared provider is configured. Ask an admin to add one.
</p> </p>
) : ( ) : (
@ -1253,7 +1253,7 @@ export function SettingsModal({
</Button> </Button>
<div className="pt-4 mt-4 border-t border-offbase"> <div className="pt-4 mt-4 border-t border-offbase">
<label className="block text-sm font-medium text-red-500 mb-2">Danger Zone</label> <label className="block text-sm font-medium text-danger mb-2">Danger Zone</label>
<Button <Button
onClick={() => setShowDeleteAccountConfirm(true)} onClick={() => setShowDeleteAccountConfirm(true)}
className={buttonClass({ variant: 'danger', size: 'md' })} className={buttonClass({ variant: 'danger', size: 'md' })}

View file

@ -674,7 +674,7 @@ export function AdminProvidersPanel() {
type="button" type="button"
onClick={() => setDefault(p.slug)} onClick={() => setDefault(p.slug)}
disabled={disabled} disabled={disabled}
className={`${disabled ? 'text-muted/60 cursor-not-allowed' : active ? 'bg-offbase text-accent' : 'text-foreground'} flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`} className={`${disabled ? 'text-faint cursor-not-allowed' : active ? 'bg-offbase text-accent' : 'text-foreground'} flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
> >
Set as default Set as default
</button> </button>
@ -696,7 +696,7 @@ export function AdminProvidersPanel() {
<button <button
type="button" type="button"
onClick={() => remove(p.id)} onClick={() => remove(p.id)}
className={`${active ? 'bg-offbase' : ''} text-red-500 flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`} className={`${active ? 'bg-offbase' : ''} text-danger flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
> >
Delete Delete
</button> </button>

View file

@ -112,9 +112,9 @@ export default function ClaimDataModal({
Claim it now to attach it to your account. Claim it now to attach it to your account.
</p> </p>
<div className="mb-4 rounded-lg border border-offbase bg-offbase/40 p-3"> <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="text-xs font-semibold uppercase tracking-wide text-muted">Claimable data</div>
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-foreground/90"> <ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-soft">
<li>{claimableCounts.documents} document(s)</li> <li>{claimableCounts.documents} document(s)</li>
<li>{claimableCounts.audiobooks} audiobook(s)</li> <li>{claimableCounts.audiobooks} audiobook(s)</li>
<li>{claimableCounts.preferences} preference set(s)</li> <li>{claimableCounts.preferences} preference set(s)</li>
@ -122,7 +122,7 @@ export default function ClaimDataModal({
</ul> </ul>
</div> </div>
<p className="text-xs text-muted/70 mb-6 italic"> <p className="text-xs text-faint mb-6 italic">
First user to claim this data will own it and revoke access for anyone else. First user to claim this data will own it and revoke access for anyone else.
</p> </p>

View file

@ -19,13 +19,13 @@ export function RateLimitBanner({ className = '' }: RateLimitBannerProps) {
const isAnonymous = status.userType === 'anonymous'; const isAnonymous = status.userType === 'anonymous';
return ( return (
<div className={`bg-amber-500/10 border border-amber-500/30 rounded-lg px-3 py-2 ${className}`}> <div className={`bg-accent-wash border border-accent-line rounded-lg px-3 py-2 ${className}`}>
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2"> <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
<div className="text-xs sm:text-sm"> <div className="text-xs sm:text-sm">
<span className="font-medium text-amber-700 dark:text-amber-400"> <span className="font-medium text-accent ">
Daily TTS limit reached. Daily TTS limit reached.
</span> </span>
<span className="text-amber-600 dark:text-amber-500 ml-1.5"> <span className="text-accent ml-1.5">
{`Used ${formatCharCount(status.currentCount)} / ${formatCharCount(status.limit)} characters.`} {`Used ${formatCharCount(status.currentCount)} / ${formatCharCount(status.limit)} characters.`}
{' Resets in '}{timeUntilReset}. {' Resets in '}{timeUntilReset}.
</span> </span>
@ -64,7 +64,7 @@ export function RateLimitIndicator({ className = '' }: RateLimitBannerProps) {
if (isAtLimit) { if (isAtLimit) {
return ( return (
<span className={`text-xs font-medium text-amber-600 dark:text-amber-400 ${className}`}> <span className={`text-xs font-medium text-accent ${className}`}>
Limit reached Limit reached
</span> </span>
); );

View file

@ -139,7 +139,7 @@ function SidebarUploadLoader({
const dashOffset = circumference - (progress / 100) * circumference; const dashOffset = circumference - (progress / 100) * circumference;
return ( return (
<div className="rounded-md border border-offbase bg-offbase/60 px-2 py-1.5"> <div className="rounded-md border border-offbase bg-surface-sunken px-2 py-1.5">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="min-w-0 flex items-center gap-1.5 text-[11px] leading-tight"> <div className="min-w-0 flex items-center gap-1.5 text-[11px] leading-tight">
<span className="font-medium text-foreground">{label}</span> <span className="font-medium text-foreground">{label}</span>

View file

@ -285,16 +285,16 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
</> </>
) : textPreview ? ( ) : textPreview ? (
<> <>
<div className="absolute inset-0 bg-gradient-to-br from-slate-50 to-slate-200" /> <div className="absolute inset-0 bg-gradient-to-br from-surface-solid to-surface-sunken" />
<div className="absolute inset-0 opacity-[0.06] bg-[radial-gradient(circle_at_1px_1px,rgba(0,0,0,1)_1px,transparent_0)] [background-size:12px_12px]" /> <div className="absolute inset-0 opacity-[0.06] bg-[radial-gradient(circle_at_1px_1px,rgba(0,0,0,1)_1px,transparent_0)] [background-size:12px_12px]" />
<div className="relative z-10 h-full w-full p-2 flex flex-col"> <div className="relative z-10 h-full w-full p-2 flex flex-col">
<div className="mt-auto rounded-md bg-white/70 backdrop-blur-[1px] shadow-sm ring-1 ring-black/5 p-2.5 max-h-[70%] overflow-hidden"> <div className="mt-auto rounded-md bg-surface-solid backdrop-blur-[1px] shadow-elev-1 ring-1 ring-line-soft p-2.5 max-h-[70%] overflow-hidden">
{isTxtFile ? ( {isTxtFile ? (
<pre className="text-[10px] sm:text-[11px] leading-snug text-slate-900 whitespace-pre-wrap font-mono"> <pre className="text-[10px] sm:text-[11px] leading-snug text-foreground whitespace-pre-wrap font-mono">
{textPreview} {textPreview}
</pre> </pre>
) : ( ) : (
<div className="text-[10px] sm:text-[11px] leading-snug text-slate-900 break-words [overflow-wrap:anywhere]"> <div className="text-[10px] sm:text-[11px] leading-snug text-foreground break-words [overflow-wrap:anywhere]">
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
components={{ components={{
@ -311,11 +311,11 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
a: ({ children }) => <span>{children}</span>, a: ({ children }) => <span>{children}</span>,
img: () => null, img: () => null,
blockquote: (props) => ( blockquote: (props) => (
<blockquote className="m-0 pl-2 border-l-2 border-slate-300 text-slate-700" {...props} /> <blockquote className="m-0 pl-2 border-l-2 border-line text-soft" {...props} />
), ),
code: (props) => ( code: (props) => (
<code <code
className="font-mono text-[10px] bg-slate-900/5 rounded px-1 whitespace-pre-wrap break-words [overflow-wrap:anywhere]" className="font-mono text-[10px] bg-surface-sunken rounded px-1 whitespace-pre-wrap break-words [overflow-wrap:anywhere]"
{...props} {...props}
/> />
), ),

View file

@ -166,9 +166,9 @@ export function DocumentTile({
> >
<span className="flex-shrink-0 flex items-center"> <span className="flex-shrink-0 flex items-center">
{doc.type === 'pdf' ? ( {doc.type === 'pdf' ? (
<PDFIcon className={`${FILE_ICON_CLASSES[iconSize]} text-red-500`} /> <PDFIcon className={`${FILE_ICON_CLASSES[iconSize]} text-danger`} />
) : doc.type === 'epub' ? ( ) : doc.type === 'epub' ? (
<EPUBIcon className={`${FILE_ICON_CLASSES[iconSize]} text-blue-500`} /> <EPUBIcon className={`${FILE_ICON_CLASSES[iconSize]} text-accent`} />
) : ( ) : (
<FileIcon className={`${FILE_ICON_CLASSES[iconSize]} text-muted`} /> <FileIcon className={`${FILE_ICON_CLASSES[iconSize]} text-muted`} />
)} )}

View file

@ -38,8 +38,8 @@ function formatParseStatus(status: DocumentListDocument['parseStatus']): string
} }
function KindIcon({ doc, className }: { doc: DocumentListDocument; className?: string }) { function KindIcon({ doc, className }: { doc: DocumentListDocument; className?: string }) {
if (doc.type === 'pdf') return <PDFIcon className={className ?? 'w-4 h-4 text-red-500'} />; 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-blue-500'} />; 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-muted'} />;
} }

View file

@ -29,8 +29,8 @@ function formatDate(ms: number): string {
} }
function KindIcon({ doc }: { doc: DocumentListDocument }) { function KindIcon({ doc }: { doc: DocumentListDocument }) {
if (doc.type === 'pdf') return <PDFIcon className="w-4 h-4 shrink-0 text-red-500" />; 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-blue-500" />; 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-muted" />;
} }

View file

@ -315,7 +315,7 @@ export function FinderSidebar({
onRowAction?.(); onRowAction?.();
}} }}
disabled={disabled} disabled={disabled}
className={`${disabled ? 'text-muted/60 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-offbase text-accent' : 'text-foreground'} group flex w-full items-center gap-2 rounded-md px-2 py-2 text-xs`}
> >
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />

View file

@ -181,7 +181,7 @@ export function DexieMigrationModal({
</div> </div>
</div> </div>
)} )}
{!isUploading && status ? <p className="text-xs text-red-500">{status}</p> : null} {!isUploading && status ? <p className="text-xs text-danger">{status}</p> : null}
</div> </div>
<div className="flex justify-end gap-3 mt-6"> <div className="flex justify-end gap-3 mt-6">

View file

@ -167,7 +167,7 @@ export function DocumentSelectionModal({
{isLoading ? ( {isLoading ? (
<DocumentSelectionSkeleton /> <DocumentSelectionSkeleton />
) : errorMessage ? ( ) : errorMessage ? (
<div className="flex items-center justify-center h-full text-red-500">{errorMessage}</div> <div className="flex items-center justify-center h-full text-danger">{errorMessage}</div>
) : files.length === 0 ? ( ) : files.length === 0 ? (
<div className="flex items-center justify-center h-full text-muted">No documents found.</div> <div className="flex items-center justify-center h-full text-muted">No documents found.</div>
) : ( ) : (
@ -179,7 +179,7 @@ export function DocumentSelectionModal({
key={file.id} key={file.id}
onClick={(e) => handleRowClick(e, file.id)} onClick={(e) => handleRowClick(e, file.id)}
className={`flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer text-sm select-none className={`flex items-center gap-3 px-3 py-2 rounded-md cursor-pointer text-sm select-none
${isSelected ? 'bg-accent/10' : 'hover:bg-offbase'} ${isSelected ? 'bg-accent-wash' : 'hover:bg-offbase'}
`} `}
> >
<div className="flex-shrink-0" onClick={(e) => e.stopPropagation()}> <div className="flex-shrink-0" onClick={(e) => e.stopPropagation()}>

View file

@ -214,14 +214,14 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
action={ action={
<div className="flex flex-col items-end gap-1"> <div className="flex flex-col items-end gap-1">
<span className="flex items-center gap-1 text-muted"> <span className="flex items-center gap-1 text-muted">
<SparkleIcon className="h-3 w-3 text-accent/70" /> <SparkleIcon className="h-3 w-3 text-accent" />
<span className="text-xs">PP-DocLayout-V3</span> <span className="text-xs">PP-DocLayout-V3</span>
</span> </span>
<span className="flex items-center gap-1 text-xs text-muted"> <span className="flex items-center gap-1 text-xs text-muted">
<span>{pdf.parseStatus ?? 'pending'}</span> <span>{pdf.parseStatus ?? 'pending'}</span>
<button <button
type="button" type="button"
className={buttonClass({ variant: 'ghost', size: 'icon', className: '!h-5 !w-5 hover:scale-[1.1] shrink-0' })} className={buttonClass({ variant: 'ghost', size: 'icon', className: '!h-5 !w-5 shrink-0' })}
onClick={pdf.onForceReparse} onClick={pdf.onForceReparse}
disabled={isForceReparseDisabled(pdf.parseStatus)} disabled={isForceReparseDisabled(pdf.parseStatus)}
title="Force reparse" title="Force reparse"
@ -240,7 +240,7 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
disabled={pdf.parseStatus !== 'ready'} disabled={pdf.parseStatus !== 'ready'}
variant="flat" variant="flat"
/> />
<details className="rounded-md border border-offbase bg-base/50 px-3 py-2"> <details className="rounded-md border border-offbase bg-surface-solid px-3 py-2">
<summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-wide text-muted"> <summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-wide text-muted">
Skip Block Kinds While Reading Aloud Skip Block Kinds While Reading Aloud
</summary> </summary>

View file

@ -181,19 +181,19 @@ export function DocumentUploader({
<input {...getInputProps()} /> <input {...getInputProps()} />
{children} {children}
{isDragActive && ( {isDragActive && (
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-background/90 backdrop-blur-md pointer-events-none p-6"> <div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-surface backdrop-blur-md pointer-events-none p-6">
<div className="w-full h-full border-2 border-dashed border-accent rounded-xl flex flex-col items-center justify-center bg-base/60 text-center p-4"> <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">
<UploadIcon className="w-14 h-14 text-accent mb-4 animate-bounce" /> <UploadIcon className="w-14 h-14 text-accent mb-4 animate-bounce" />
<p className="text-xl font-bold text-foreground mb-1.5"> <p className="text-xl font-bold text-foreground mb-1.5">
Drop files here to upload Drop files here to upload
</p> </p>
<p className="text-sm text-foreground/70"> <p className="text-sm text-soft">
{enableDocx {enableDocx
? 'Accepts PDF, EPUB, TXT, MD, or DOCX' ? 'Accepts PDF, EPUB, TXT, MD, or DOCX'
: 'Accepts PDF, EPUB, TXT, or MD'} : 'Accepts PDF, EPUB, TXT, or MD'}
</p> </p>
{error && ( {error && (
<p className="mt-3 text-sm text-red-500"> <p className="mt-3 text-sm text-danger">
Upload failed: {error} try again. Upload failed: {error} try again.
</p> </p>
)} )}
@ -201,7 +201,7 @@ export function DocumentUploader({
</div> </div>
)} )}
{!isDragActive && error && ( {!isDragActive && error && (
<div className="absolute inset-x-4 bottom-4 z-40 rounded-md border border-red-500/40 bg-red-500/10 px-3 py-2 text-center text-sm text-red-500 pointer-events-none"> <div className="absolute inset-x-4 bottom-4 z-40 rounded-md border border-danger bg-danger-wash px-3 py-2 text-center text-sm text-danger pointer-events-none">
Upload failed: {error} try again. Upload failed: {error} try again.
</div> </div>
)} )}
@ -227,7 +227,7 @@ export function DocumentUploader({
<p className="text-[12px] truncate flex-1"> <p className="text-[12px] truncate flex-1">
{isDragActive ? 'Drop files here' : 'Upload documents'} {isDragActive ? 'Drop files here' : 'Upload documents'}
</p> </p>
{error && <p className="text-[10px] text-red-500 truncate shrink-0">{error}</p>} {error && <p className="text-[10px] text-danger truncate shrink-0">{error}</p>}
</div> </div>
)} )}
</div> </div>
@ -246,7 +246,7 @@ export function DocumentUploader({
<p className="text-xs sm:text-sm text-muted"> <p className="text-xs sm:text-sm text-muted">
{enableDocx ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'} {enableDocx ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'}
</p> </p>
{error && <p className="mt-2 text-sm text-red-500">{error}</p>} {error && <p className="mt-2 text-sm text-danger">{error}</p>}
</> </>
)} )}
</div> </div>

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useId, type ReactNode } from 'react'; import { useId, type ButtonHTMLAttributes, type ReactNode } from 'react';
import { import {
btnBase, btnBase,
btnDanger, btnDanger,
@ -36,24 +36,112 @@ export {
// (Note: never use Tailwind alpha modifiers on these theme variables — they // (Note: never use Tailwind alpha modifiers on these theme variables — they
// resolve to CSS custom properties that don't accept opacity suffixes.) // resolve to CSS custom properties that don't accept opacity suffixes.)
export const inputClass = export const inputClass =
'w-full rounded-md bg-background border border-offbase px-2.5 py-1.5 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-accent'; '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';
export const listboxButtonClass = export const listboxButtonClass =
'relative w-full cursor-pointer rounded-md bg-background border border-offbase 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-base 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 hover:bg-accent-wash transition-colors duration-fast ease-standard';
export const listboxOptionsClass = export const listboxOptionsClass =
'z-50 w-[var(--button-width)] max-h-60 overflow-y-auto overscroll-contain rounded-md bg-background p-1 shadow-lg ring-1 ring-offbase focus:outline-none [--anchor-gap:0.25rem]'; 'z-50 w-[var(--button-width)] max-h-60 overflow-y-auto overscroll-contain rounded-md bg-surface p-1 shadow-elev-2 ring-1 ring-line focus:outline-none [--anchor-gap:0.25rem]';
export const listboxOptionClass = (active: boolean) => export const listboxOptionClass = (active: boolean) =>
`relative cursor-pointer select-none rounded-sm py-1.5 pl-9 pr-3 text-sm ${active ? 'bg-offbase text-foreground' : 'text-foreground'}`; `relative cursor-pointer select-none rounded-sm py-1.5 pl-9 pr-3 text-sm ${active ? 'bg-accent-wash text-foreground' : 'text-foreground'}`;
export const segmentedGroupClass = export const segmentedGroupClass =
'grid gap-1 rounded-full border border-offbase bg-background p-1'; 'grid gap-1 rounded-full border border-line bg-surface-sunken p-1';
export const segmentedButtonClass = (active: boolean) => export const segmentedButtonClass = (active: boolean) =>
`rounded-full px-2.5 py-1.5 text-xs font-medium transition-colors duration-fast ease-standard focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${ `rounded-full px-2.5 py-1.5 text-xs font-medium transition-colors duration-fast ease-standard focus:outline-none focus-visible:ring-2 focus-visible:ring-accent ${
active active
? 'bg-accent text-background' ? 'bg-accent text-background'
: 'text-muted hover:bg-base hover:text-foreground' : 'text-soft hover:bg-accent-wash hover:text-foreground'
}`; }`;
type SurfaceElevation = 'none' | '1' | '2' | '3';
const SURFACE_ELEVATION_CLASS: Record<SurfaceElevation, string> = {
none: '',
'1': 'shadow-elev-1',
'2': 'shadow-elev-2',
'3': 'shadow-elev-3',
};
export function Surface({
children,
className = '',
elevation = 'none',
}: {
children: ReactNode;
className?: string;
elevation?: SurfaceElevation;
}) {
return (
<div className={`rounded-lg border border-line bg-surface text-foreground ${SURFACE_ELEVATION_CLASS[elevation]} ${className}`}>
{children}
</div>
);
}
export function Panel({
children,
className = '',
}: {
children: ReactNode;
className?: string;
}) {
return (
<Surface elevation="1" className={`overflow-hidden ${className}`}>
{children}
</Surface>
);
}
export function Sidebar({
children,
className = '',
}: {
children: ReactNode;
className?: string;
}) {
return (
<aside className={`rounded-lg border border-line bg-surface text-foreground shadow-elev-2 ${className}`}>
{children}
</aside>
);
}
export function Divider({ className = '' }: { className?: string }) {
return <div aria-hidden="true" className={`border-t border-line-soft ${className}`} />;
}
export function IconButton({
className = '',
children,
type = 'button',
...props
}: ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
type={type}
className={`inline-flex h-8 w-8 items-center justify-center rounded-md text-soft transition-colors duration-fast ease-standard hover:bg-accent-wash hover:text-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-accent disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
{...props}
>
{children}
</button>
);
}
export function Menu({
children,
className = '',
}: {
children: ReactNode;
className?: string;
}) {
return (
<div className={`rounded-md border border-line bg-surface p-1 shadow-elev-2 ring-1 ring-line-soft ${className}`}>
{children}
</div>
);
}
export function Section({ export function Section({
title, title,
subtitle, subtitle,
@ -69,11 +157,11 @@ export function Section({
}) { }) {
if (variant === 'flat') { if (variant === 'flat') {
return ( return (
<section className="space-y-2 pb-3 border-b border-offbase last:border-b-0"> <section className="space-y-2 pb-3 border-b border-line-soft last:border-b-0">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-sm font-semibold text-foreground">{title}</h3> <h3 className="text-sm font-semibold text-foreground">{title}</h3>
{subtitle ? <p className="text-xs text-muted mt-0.5">{subtitle}</p> : null} {subtitle ? <p className="text-xs text-soft mt-0.5">{subtitle}</p> : null}
</div> </div>
{action ? <div className="shrink-0">{action}</div> : null} {action ? <div className="shrink-0">{action}</div> : null}
</div> </div>
@ -83,12 +171,12 @@ export function Section({
} }
return ( return (
<section className="rounded-xl border border-offbase bg-base overflow-hidden"> <section className="rounded-lg border border-line bg-surface overflow-hidden">
<div className="px-3 py-2 bg-background border-b border-offbase"> <div className="px-3 py-2 bg-surface-solid border-b border-line-soft">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-sm font-semibold text-foreground">{title}</h3> <h3 className="text-sm font-semibold text-foreground">{title}</h3>
{subtitle ? <p className="text-xs text-muted mt-0.5">{subtitle}</p> : null} {subtitle ? <p className="text-xs text-soft mt-0.5">{subtitle}</p> : null}
</div> </div>
{action ? <div className="shrink-0">{action}</div> : null} {action ? <div className="shrink-0">{action}</div> : null}
</div> </div>
@ -108,7 +196,7 @@ export function Card({
className?: string; className?: string;
}) { }) {
return ( return (
<div className={`rounded-lg border border-offbase bg-background px-3 py-2 transition-colors duration-fast ease-standard ${className}`}> <div className={`rounded-lg border border-line bg-surface px-3 py-2 transition-colors duration-fast ease-standard ${className}`}>
{children} {children}
</div> </div>
); );
@ -159,13 +247,13 @@ export function Switch({
aria-describedby={ariaDescribedBy} aria-describedby={ariaDescribedBy}
disabled={disabled} disabled={disabled}
onClick={() => onChange(!checked)} onClick={() => onChange(!checked)}
className={`relative inline-flex shrink-0 cursor-pointer items-center rounded-full border border-offbase transition-colors duration-fast ease-standard focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${s.track} ${ className={`relative inline-flex shrink-0 cursor-pointer items-center rounded-full border border-line transition-colors duration-fast ease-standard focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${s.track} ${
checked ? 'bg-accent' : 'bg-offbase' checked ? 'bg-accent' : 'bg-line-strong'
}`} }`}
> >
<span <span
aria-hidden="true" aria-hidden="true"
className={`pointer-events-none inline-block rounded-full bg-background shadow-sm ring-0 transition-transform duration-fast ease-standard ${s.thumb} ${ className={`pointer-events-none inline-block rounded-full bg-surface shadow-elev-1 ring-0 transition-transform duration-fast ease-standard ${s.thumb} ${
checked ? s.on : s.off checked ? s.on : s.off
}`} }`}
/> />
@ -194,8 +282,8 @@ export function ToggleRow({
const descId = useId(); const descId = useId();
const rowClass = const rowClass =
variant === 'flat' variant === 'flat'
? 'px-0.5 pt-1 pb-2 border-b border-offbase last:border-b-0 transition-colors duration-fast ease-standard' ? 'px-0.5 pt-1 pb-2 border-b border-line-soft last:border-b-0 transition-colors duration-fast ease-standard'
: 'rounded-md border border-offbase bg-background px-2.5 py-1.5 transition-colors duration-fast ease-standard'; : 'rounded-md border border-line bg-surface px-2.5 py-1.5 transition-colors duration-fast ease-standard';
const handleTextToggle = () => { const handleTextToggle = () => {
if (!disabled) onChange(!checked); if (!disabled) onChange(!checked);
}; };
@ -207,7 +295,7 @@ export function ToggleRow({
onClick={handleTextToggle} onClick={handleTextToggle}
> >
<span id={labelId} className="block text-sm font-medium leading-5 text-foreground">{label}</span> <span id={labelId} className="block text-sm font-medium leading-5 text-foreground">{label}</span>
<span id={descId} className="block text-xs leading-4 text-muted">{description}</span> <span id={descId} className="block text-xs leading-4 text-soft">{description}</span>
</div> </div>
{right ? <div className="shrink-0 self-start pl-1.5">{right}</div> : null} {right ? <div className="shrink-0 self-start pl-1.5">{right}</div> : null}
<Switch <Switch
@ -273,9 +361,9 @@ export function Field({
}) { }) {
return ( return (
<div className={`space-y-1 ${className}`}> <div className={`space-y-1 ${className}`}>
{label ? <label className="block text-[11px] font-semibold uppercase tracking-wide text-muted">{label}</label> : null} {label ? <label className="block text-[11px] font-semibold uppercase tracking-wide text-faint">{label}</label> : null}
{children} {children}
{hint ? <p className="text-[11px] text-muted">{hint}</p> : null} {hint ? <p className="text-[11px] text-faint">{hint}</p> : null}
</div> </div>
); );
} }
@ -289,10 +377,10 @@ export function Badge({
}) { }) {
const toneClass = const toneClass =
tone === 'accent' tone === 'accent'
? 'text-accent' ? 'text-accent bg-accent-wash'
: tone === 'foreground' : tone === 'foreground'
? 'text-foreground bg-offbase' ? 'text-foreground bg-surface-sunken'
: 'text-muted bg-offbase'; : 'text-soft bg-surface-sunken';
return ( return (
<span className={`inline-flex items-center text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 ${toneClass}`}> <span className={`inline-flex items-center text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 ${toneClass}`}>
{children} {children}

View file

@ -246,7 +246,7 @@ export function GithubIcon(props: React.SVGProps<SVGSVGElement>) {
export function PDFIcon(props: React.SVGProps<SVGSVGElement>) { export function PDFIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg
className={`w-6 h-6 sm:w-8 sm:h-8 text-red-500 ${props.className || ''}`} className={`w-6 h-6 sm:w-8 sm:h-8 text-danger ${props.className || ''}`}
fill="currentColor" fill="currentColor"
stroke="currentColor" stroke="currentColor"
viewBox="-4 0 40 40" viewBox="-4 0 40 40"
@ -261,7 +261,7 @@ export function PDFIcon(props: React.SVGProps<SVGSVGElement>) {
export function EPUBIcon(props: React.SVGProps<SVGSVGElement>) { export function EPUBIcon(props: React.SVGProps<SVGSVGElement>) {
return ( return (
<svg <svg
className={`w-6 h-6 sm:w-8 sm:h-8 text-blue-500 ${props.className || ''}`} className={`w-6 h-6 sm:w-8 sm:h-8 text-accent ${props.className || ''}`}
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"

View file

@ -99,7 +99,7 @@ function settingsAreEqual(a: TTSSegmentSettings | null, b: TTSSegmentSettings |
function statusColor(status: TTSSegmentVariant['status']): string { function statusColor(status: TTSSegmentVariant['status']): string {
if (status === 'completed') return 'bg-accent'; if (status === 'completed') return 'bg-accent';
if (status === 'error') return 'bg-red-500'; if (status === 'error') return 'bg-danger';
return 'bg-muted'; return 'bg-muted';
} }
@ -689,7 +689,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
) : isManifestLoading ? ( ) : isManifestLoading ? (
<div className="animate-pulse h-3 w-36 rounded bg-offbase" aria-label="Loading segment summary" aria-busy="true" /> <div className="animate-pulse h-3 w-36 rounded bg-offbase" aria-label="Loading segment summary" aria-busy="true" />
) : hasManifestError ? ( ) : hasManifestError ? (
<span className="text-red-500">error</span> <span className="text-danger">error</span>
) : ( ) : (
<span></span> <span></span>
)} )}
@ -698,7 +698,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
<div ref={listRef} className="flex-1 overflow-y-auto"> <div ref={listRef} className="flex-1 overflow-y-auto">
{hasManifestError && ( {hasManifestError && (
<div className="px-4 py-6 text-sm text-red-500">{manifestErrorMessage}</div> <div className="px-4 py-6 text-sm text-danger">{manifestErrorMessage}</div>
)} )}
{isManifestLoading && ( {isManifestLoading && (
<SegmentsListSkeleton /> <SegmentsListSkeleton />
@ -741,10 +741,10 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
<li <li
key={`${isSynthesized ? 'syn' : 'mfr'}::${groupKey}::${segmentIndex}::${rowIndex}`} key={`${isSynthesized ? 'syn' : 'mfr'}::${groupKey}::${segmentIndex}::${rowIndex}`}
data-active-segment={isCurrent ? 'true' : undefined} data-active-segment={isCurrent ? 'true' : undefined}
className={`relative px-4 py-3 ${isCurrent ? 'bg-offbase/40' : ''}`} className={`relative px-4 py-3 ${isCurrent ? 'bg-surface-sunken' : ''}`}
> >
{showGroupHeader && ( {showGroupHeader && (
<div className="mb-2 -mx-4 px-4 py-1.5 bg-offbase/30 border-y border-offbase"> <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"> <span className="text-[10px] uppercase tracking-[0.14em] text-muted">
{groupLabel} {groupLabel}
</span> </span>
@ -758,7 +758,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
type="button" type="button"
onClick={() => { if (canJump) handleJump(segmentIndex, row.locator); }} onClick={() => { if (canJump) handleJump(segmentIndex, row.locator); }}
disabled={!canJump} disabled={!canJump}
className={`text-xs font-medium shrink-0 pt-0.5 ${canJump ? 'text-muted hover:text-accent' : 'text-muted/50 cursor-not-allowed'}`} className={`text-xs font-medium shrink-0 pt-0.5 ${canJump ? 'text-muted hover:text-accent' : 'text-faint cursor-not-allowed'}`}
title={canJump ? (playable ? 'Play this segment' : 'Jump to this segment') : 'Text not loaded yet'} title={canJump ? (playable ? 'Play this segment' : 'Jump to this segment') : 'Text not loaded yet'}
aria-label={`Segment ${segmentIndex + 1}`} aria-label={`Segment ${segmentIndex + 1}`}
> >
@ -772,7 +772,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
disabled={!canJump} disabled={!canJump}
className={`block w-full text-left ${canJump ? '' : 'cursor-not-allowed'}`} className={`block w-full text-left ${canJump ? '' : 'cursor-not-allowed'}`}
> >
<p className={`text-sm leading-snug ${isCurrent ? 'text-foreground' : 'text-foreground/90'} line-clamp-2`}> <p className={`text-sm leading-snug ${isCurrent ? 'text-foreground' : 'text-soft'} line-clamp-2`}>
{sentenceText || ( {sentenceText || (
<span className="text-muted italic text-xs"> <span className="text-muted italic text-xs">
[text not loaded press play to fetch] [text not loaded press play to fetch]
@ -796,7 +796,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
</span> </span>
)} )}
{!canJump && ( {!canJump && (
<span className="text-[10px] text-muted/80 border border-offbase rounded px-1 py-0.5"> <span className="text-[10px] text-faint border border-offbase rounded px-1 py-0.5">
not loaded not loaded
</span> </span>
)} )}
@ -820,7 +820,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
className={[ className={[
'max-w-full whitespace-normal break-words text-left leading-none text-[10px] px-1 py-0.5 rounded border transition-colors', 'max-w-full whitespace-normal break-words text-left leading-none text-[10px] px-1 py-0.5 rounded border transition-colors',
isActive isActive
? 'border-accent text-accent bg-offbase/60' ? 'border-accent text-accent bg-surface-sunken'
: known : known
? 'border-offbase text-muted hover:border-accent hover:text-accent' ? 'border-offbase text-muted hover:border-accent hover:text-accent'
: 'border-offbase text-muted opacity-60 cursor-not-allowed', : 'border-offbase text-muted opacity-60 cursor-not-allowed',

View file

@ -480,7 +480,7 @@ export function PDFViewer({ zoomLevel, onDocumentReady, pdfState }: PDFViewerPro
return ( return (
<div <div
key={`${block.id}:${fragment.page}:${fragment.readingOrder}`} key={`${block.id}:${fragment.page}:${fragment.readingOrder}`}
className="absolute border border-accent/70 rounded-[2px]" className="absolute border border-accent-line rounded-[2px]"
style={{ style={{
left: `${leftPct}%`, left: `${leftPct}%`,
top: `${topPct}%`, top: `${topPct}%`,