phase 2: primitives and semantic tokens
This commit is contained in:
parent
11e138ae36
commit
745c846a6e
28 changed files with 196 additions and 108 deletions
|
|
@ -170,7 +170,7 @@ export default function EPUBPage() {
|
|||
if (error) {
|
||||
return (
|
||||
<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
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export default function HTMLPage() {
|
|||
if (error) {
|
||||
return (
|
||||
<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
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export default function PDFViewerPage() {
|
|||
if (error) {
|
||||
return (
|
||||
<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
|
||||
href="/app"
|
||||
onClick={handleBackToDocuments}
|
||||
|
|
@ -311,18 +311,18 @@ export default function PDFViewerPage() {
|
|||
<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'}`}>
|
||||
{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="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-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" />
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted">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-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">
|
||||
<p className="text-[11px] font-semibold text-foreground">
|
||||
{hasMeasuredProgress ? `Page ${pagesParsed} / ${totalPages}` : 'Awaiting first page'}
|
||||
|
|
@ -354,7 +354,7 @@ export default function PDFViewerPage() {
|
|||
</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="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
|
|
@ -366,9 +366,9 @@ export default function PDFViewerPage() {
|
|||
</span>
|
||||
</div>
|
||||
<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/20 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" />
|
||||
<span className="h-1.5 rounded-full bg-accent-wash animate-pulse [animation-delay:120ms]" />
|
||||
<span className="h-1.5 rounded-full bg-accent-wash animate-pulse [animation-delay:220ms]" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -132,16 +132,16 @@ function SignInContent() {
|
|||
|
||||
{/* Alerts */}
|
||||
{sessionExpired && (
|
||||
<div className="mt-4 p-3 bg-amber-500/10 border border-amber-500/30 rounded-lg">
|
||||
<p className="text-sm text-amber-700 dark:text-amber-400">
|
||||
<div className="mt-4 p-3 bg-accent-wash border border-accent-line rounded-lg">
|
||||
<p className="text-sm text-accent ">
|
||||
Your session has expired. Please sign in again.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">{error}</p>
|
||||
<div className="mt-4 p-3 bg-danger-wash border border-danger rounded-lg">
|
||||
<p className="text-sm text-danger dark:text-danger">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export default function SignUpPage() {
|
|||
|
||||
const { checks, strength } = validatePassword(password);
|
||||
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 (
|
||||
<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>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
|
||||
<p className="text-sm text-red-700 dark:text-red-400">{error}</p>
|
||||
<div className="mt-4 p-3 bg-danger-wash border border-danger rounded-lg">
|
||||
<p className="text-sm text-danger dark:text-danger">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -190,12 +190,12 @@ export default function SignUpPage() {
|
|||
/>
|
||||
))}
|
||||
</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'}
|
||||
</p>
|
||||
<div className="text-xs space-y-0.5 text-muted">
|
||||
{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>
|
||||
{key === 'length' && 'At least 8 characters'}
|
||||
|
|
@ -223,7 +223,7 @@ export default function SignUpPage() {
|
|||
focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
/>
|
||||
{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'}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -490,7 +490,7 @@ export function AudiobookExportModal({
|
|||
|
||||
<div className="p-4">
|
||||
{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="mt-1 text-muted">
|
||||
This audiobook was likely created before v1 metadata was introduced, so OpenReader can't know
|
||||
|
|
@ -694,9 +694,9 @@ export function AudiobookExportModal({
|
|||
<Button
|
||||
onClick={() => setShowResetConfirm(true)}
|
||||
disabled={isGenerating}
|
||||
className="inline-flex justify-center rounded-lg border border-red-500 bg-transparent px-3 py-2 text-sm
|
||||
font-medium text-red-500 hover:bg-red-500 hover:text-background focus:outline-none
|
||||
focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2
|
||||
className="inline-flex justify-center rounded-lg border border-danger bg-transparent px-3 py-2 text-sm
|
||||
font-medium text-danger hover:bg-danger-wash hover:text-background focus:outline-none
|
||||
focus-visible:ring-2 focus-visible:ring-danger focus-visible:ring-offset-2
|
||||
transform transition-transform duration-base ease-standard"
|
||||
title="Delete all generated chapters/pages for this document"
|
||||
>
|
||||
|
|
@ -760,7 +760,7 @@ 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/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'}
|
||||
>
|
||||
<RefreshIcon className={`h-4 w-4 ${regeneratingChapter === chapter.index || chapter.status === 'generating' ? 'animate-spin' : ''}`} />
|
||||
|
|
@ -807,7 +807,7 @@ export function AudiobookExportModal({
|
|||
{({ active }) => (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<XCircleIcon className="h-4 w-4" />
|
||||
|
|
@ -833,7 +833,7 @@ export function AudiobookExportModal({
|
|||
{({ active }) => (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<XCircleIcon className="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function ConfirmDialog({
|
|||
{title}
|
||||
</DialogTitle>
|
||||
<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 className="mt-6 flex justify-end space-x-3">
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function CookieConsentBanner() {
|
|||
<h3 className="mb-2 text-lg font-bold">
|
||||
🍪 We use cookies
|
||||
</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.
|
||||
See our <Link href="/privacy" className="font-medium text-accent hover:underline">Privacy Policy</Link> for details.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ interface PrivacyModalProps {
|
|||
|
||||
function PrivacyModalBody({ origin }: { origin: string }) {
|
||||
return (
|
||||
<div className="mt-4 space-y-4 text-sm text-foreground/90">
|
||||
<div className="rounded-lg border border-offbase bg-offbase/40 p-3">
|
||||
<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="mt-1">
|
||||
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} />
|
||||
|
||||
<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">
|
||||
<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-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 className="text-sm leading-6">
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function ProgressCard({
|
|||
className={buttonClass({
|
||||
variant: 'ghost',
|
||||
size: 'xs',
|
||||
className: 'shrink-0 hover:bg-background/50',
|
||||
className: 'shrink-0 hover:bg-surface-sunken',
|
||||
})}
|
||||
onClick={(e) => onCancel(e)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -630,7 +630,7 @@ export function SettingsModal({
|
|||
<div className="space-y-1.5">
|
||||
<label className={fieldLabelClass}>TTS Provider</label>
|
||||
{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.
|
||||
</p>
|
||||
) : (
|
||||
|
|
@ -1253,7 +1253,7 @@ export function SettingsModal({
|
|||
</Button>
|
||||
|
||||
<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
|
||||
onClick={() => setShowDeleteAccountConfirm(true)}
|
||||
className={buttonClass({ variant: 'danger', size: 'md' })}
|
||||
|
|
|
|||
|
|
@ -674,7 +674,7 @@ export function AdminProvidersPanel() {
|
|||
type="button"
|
||||
onClick={() => setDefault(p.slug)}
|
||||
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
|
||||
</button>
|
||||
|
|
@ -696,7 +696,7 @@ export function AdminProvidersPanel() {
|
|||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -112,9 +112,9 @@ export default function ClaimDataModal({
|
|||
Claim it now to attach it to your account.
|
||||
</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>
|
||||
<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.audiobooks} audiobook(s)</li>
|
||||
<li>{claimableCounts.preferences} preference set(s)</li>
|
||||
|
|
@ -122,7 +122,7 @@ export default function ClaimDataModal({
|
|||
</ul>
|
||||
</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.
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,13 +19,13 @@ export function RateLimitBanner({ className = '' }: RateLimitBannerProps) {
|
|||
const isAnonymous = status.userType === 'anonymous';
|
||||
|
||||
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="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.
|
||||
</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.`}
|
||||
{' Resets in '}{timeUntilReset}.
|
||||
</span>
|
||||
|
|
@ -64,7 +64,7 @@ export function RateLimitIndicator({ className = '' }: RateLimitBannerProps) {
|
|||
|
||||
if (isAtLimit) {
|
||||
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
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ function SidebarUploadLoader({
|
|||
const dashOffset = circumference - (progress / 100) * circumference;
|
||||
|
||||
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="min-w-0 flex items-center gap-1.5 text-[11px] leading-tight">
|
||||
<span className="font-medium text-foreground">{label}</span>
|
||||
|
|
|
|||
|
|
@ -285,16 +285,16 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
|
|||
</>
|
||||
) : 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="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 ? (
|
||||
<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}
|
||||
</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
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
|
|
@ -311,11 +311,11 @@ export function DocumentPreview({ doc }: DocumentPreviewProps) {
|
|||
a: ({ children }) => <span>{children}</span>,
|
||||
img: () => null,
|
||||
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
|
||||
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}
|
||||
/>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -166,9 +166,9 @@ export function DocumentTile({
|
|||
>
|
||||
<span className="flex-shrink-0 flex items-center">
|
||||
{doc.type === 'pdf' ? (
|
||||
<PDFIcon className={`${FILE_ICON_CLASSES[iconSize]} text-red-500`} />
|
||||
<PDFIcon className={`${FILE_ICON_CLASSES[iconSize]} text-danger`} />
|
||||
) : 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`} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ 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-red-500'} />;
|
||||
if (doc.type === 'epub') return <EPUBIcon className={className ?? 'w-4 h-4 text-blue-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-accent'} />;
|
||||
return <FileIcon className={className ?? 'w-4 h-4 text-muted'} />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ function formatDate(ms: number): string {
|
|||
}
|
||||
|
||||
function KindIcon({ doc }: { doc: DocumentListDocument }) {
|
||||
if (doc.type === 'pdf') return <PDFIcon className="w-4 h-4 shrink-0 text-red-500" />;
|
||||
if (doc.type === 'epub') return <EPUBIcon className="w-4 h-4 shrink-0 text-blue-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-accent" />;
|
||||
return <FileIcon className="w-4 h-4 shrink-0 text-muted" />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ export function FinderSidebar({
|
|||
onRowAction?.();
|
||||
}}
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ export function DexieMigrationModal({
|
|||
</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 className="flex justify-end gap-3 mt-6">
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ export function DocumentSelectionModal({
|
|||
{isLoading ? (
|
||||
<DocumentSelectionSkeleton />
|
||||
) : 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 ? (
|
||||
<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}
|
||||
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/10' : 'hover:bg-offbase'}
|
||||
${isSelected ? 'bg-accent-wash' : 'hover:bg-offbase'}
|
||||
`}
|
||||
>
|
||||
<div className="flex-shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||
|
|
|
|||
|
|
@ -214,14 +214,14 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
|
|||
action={
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<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>
|
||||
<span className="flex items-center gap-1 text-xs text-muted">
|
||||
<span>{pdf.parseStatus ?? 'pending'}</span>
|
||||
<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}
|
||||
disabled={isForceReparseDisabled(pdf.parseStatus)}
|
||||
title="Force reparse"
|
||||
|
|
@ -240,7 +240,7 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
|
|||
disabled={pdf.parseStatus !== 'ready'}
|
||||
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">
|
||||
Skip Block Kinds While Reading Aloud
|
||||
</summary>
|
||||
|
|
|
|||
|
|
@ -181,19 +181,19 @@ export function DocumentUploader({
|
|||
<input {...getInputProps()} />
|
||||
{children}
|
||||
{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="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="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">
|
||||
<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
|
||||
</p>
|
||||
<p className="text-sm text-foreground/70">
|
||||
<p className="text-sm text-soft">
|
||||
{enableDocx
|
||||
? 'Accepts PDF, EPUB, TXT, MD, or DOCX'
|
||||
: 'Accepts PDF, EPUB, TXT, or MD'}
|
||||
</p>
|
||||
{error && (
|
||||
<p className="mt-3 text-sm text-red-500">
|
||||
<p className="mt-3 text-sm text-danger">
|
||||
Upload failed: {error} — try again.
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -201,7 +201,7 @@ export function DocumentUploader({
|
|||
</div>
|
||||
)}
|
||||
{!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.
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -227,7 +227,7 @@ export function DocumentUploader({
|
|||
<p className="text-[12px] truncate flex-1">
|
||||
{isDragActive ? 'Drop files here' : 'Upload documents'}
|
||||
</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>
|
||||
|
|
@ -246,7 +246,7 @@ export function DocumentUploader({
|
|||
<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'}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useId, type ReactNode } from 'react';
|
||||
import { useId, type ButtonHTMLAttributes, type ReactNode } from 'react';
|
||||
import {
|
||||
btnBase,
|
||||
btnDanger,
|
||||
|
|
@ -36,24 +36,112 @@ export {
|
|||
// (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-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 =
|
||||
'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 =
|
||||
'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) =>
|
||||
`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 =
|
||||
'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) =>
|
||||
`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
|
||||
? '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({
|
||||
title,
|
||||
subtitle,
|
||||
|
|
@ -69,11 +157,11 @@ export function Section({
|
|||
}) {
|
||||
if (variant === 'flat') {
|
||||
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="min-w-0">
|
||||
<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>
|
||||
{action ? <div className="shrink-0">{action}</div> : null}
|
||||
</div>
|
||||
|
|
@ -83,12 +171,12 @@ export function Section({
|
|||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-offbase bg-base overflow-hidden">
|
||||
<div className="px-3 py-2 bg-background border-b border-offbase">
|
||||
<section className="rounded-lg border border-line bg-surface overflow-hidden">
|
||||
<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="min-w-0">
|
||||
<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>
|
||||
{action ? <div className="shrink-0">{action}</div> : null}
|
||||
</div>
|
||||
|
|
@ -108,7 +196,7 @@ export function Card({
|
|||
className?: string;
|
||||
}) {
|
||||
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}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -159,13 +247,13 @@ export function Switch({
|
|||
aria-describedby={ariaDescribedBy}
|
||||
disabled={disabled}
|
||||
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} ${
|
||||
checked ? 'bg-accent' : 'bg-offbase'
|
||||
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-line-strong'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
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
|
||||
}`}
|
||||
/>
|
||||
|
|
@ -194,8 +282,8 @@ export function ToggleRow({
|
|||
const descId = useId();
|
||||
const rowClass =
|
||||
variant === 'flat'
|
||||
? 'px-0.5 pt-1 pb-2 border-b border-offbase 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';
|
||||
? '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-line bg-surface px-2.5 py-1.5 transition-colors duration-fast ease-standard';
|
||||
const handleTextToggle = () => {
|
||||
if (!disabled) onChange(!checked);
|
||||
};
|
||||
|
|
@ -207,7 +295,7 @@ export function ToggleRow({
|
|||
onClick={handleTextToggle}
|
||||
>
|
||||
<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>
|
||||
{right ? <div className="shrink-0 self-start pl-1.5">{right}</div> : null}
|
||||
<Switch
|
||||
|
|
@ -273,9 +361,9 @@ export function Field({
|
|||
}) {
|
||||
return (
|
||||
<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}
|
||||
{hint ? <p className="text-[11px] text-muted">{hint}</p> : null}
|
||||
{hint ? <p className="text-[11px] text-faint">{hint}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -289,10 +377,10 @@ export function Badge({
|
|||
}) {
|
||||
const toneClass =
|
||||
tone === 'accent'
|
||||
? 'text-accent'
|
||||
? 'text-accent bg-accent-wash'
|
||||
: tone === 'foreground'
|
||||
? 'text-foreground bg-offbase'
|
||||
: 'text-muted bg-offbase';
|
||||
? 'text-foreground bg-surface-sunken'
|
||||
: 'text-soft bg-surface-sunken';
|
||||
return (
|
||||
<span className={`inline-flex items-center text-[10px] uppercase tracking-wide font-semibold rounded px-1.5 py-0.5 ${toneClass}`}>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ export function GithubIcon(props: React.SVGProps<SVGSVGElement>) {
|
|||
export function PDFIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<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"
|
||||
stroke="currentColor"
|
||||
viewBox="-4 0 40 40"
|
||||
|
|
@ -261,7 +261,7 @@ export function PDFIcon(props: React.SVGProps<SVGSVGElement>) {
|
|||
export function EPUBIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<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"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ function settingsAreEqual(a: TTSSegmentSettings | null, b: TTSSegmentSettings |
|
|||
|
||||
function statusColor(status: TTSSegmentVariant['status']): string {
|
||||
if (status === 'completed') return 'bg-accent';
|
||||
if (status === 'error') return 'bg-red-500';
|
||||
if (status === 'error') return 'bg-danger';
|
||||
return 'bg-muted';
|
||||
}
|
||||
|
||||
|
|
@ -689,7 +689,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
|
|||
) : isManifestLoading ? (
|
||||
<div className="animate-pulse h-3 w-36 rounded bg-offbase" aria-label="Loading segment summary" aria-busy="true" />
|
||||
) : hasManifestError ? (
|
||||
<span className="text-red-500">error</span>
|
||||
<span className="text-danger">error</span>
|
||||
) : (
|
||||
<span>—</span>
|
||||
)}
|
||||
|
|
@ -698,7 +698,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
|
|||
|
||||
<div ref={listRef} className="flex-1 overflow-y-auto">
|
||||
{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 && (
|
||||
<SegmentsListSkeleton />
|
||||
|
|
@ -741,10 +741,10 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
|
|||
<li
|
||||
key={`${isSynthesized ? 'syn' : 'mfr'}::${groupKey}::${segmentIndex}::${rowIndex}`}
|
||||
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 && (
|
||||
<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">
|
||||
{groupLabel}
|
||||
</span>
|
||||
|
|
@ -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-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'}
|
||||
aria-label={`Segment ${segmentIndex + 1}`}
|
||||
>
|
||||
|
|
@ -772,7 +772,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
|
|||
disabled={!canJump}
|
||||
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 || (
|
||||
<span className="text-muted italic text-xs">
|
||||
[text not loaded — press play to fetch]
|
||||
|
|
@ -796,7 +796,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
|
|||
</span>
|
||||
)}
|
||||
{!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
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -820,7 +820,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
|
|||
className={[
|
||||
'max-w-full whitespace-normal break-words text-left leading-none text-[10px] px-1 py-0.5 rounded border transition-colors',
|
||||
isActive
|
||||
? 'border-accent text-accent bg-offbase/60'
|
||||
? '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',
|
||||
|
|
|
|||
|
|
@ -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/70 rounded-[2px]"
|
||||
className="absolute border border-accent-line rounded-[2px]"
|
||||
style={{
|
||||
left: `${leftPct}%`,
|
||||
top: `${topPct}%`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue