diff --git a/eslint.config.mjs b/eslint.config.mjs index 2dac3b5..2a0310d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -19,6 +19,103 @@ const LOGGER_RECEIVER_SELECTOR = const SERVER_LOGGER_CALL_SELECTOR = `:matches(${STATIC_LOGGER_CALL_SELECTOR}${LOGGER_RECEIVER_SELECTOR},${DYNAMIC_LOGGER_CALL_SELECTOR}${LOGGER_RECEIVER_SELECTOR})`; const NEXT_RESPONSE_ERROR_JSON_SELECTOR = "CallExpression[callee.type='MemberExpression'][callee.object.name='NextResponse'][callee.property.name='json'][arguments.0.type='ObjectExpression']:has(Property[key.name='error'])"; +const APP_DESIGN_SYSTEM_FILES = [ + "src/app/(app)/**/*.{ts,tsx}", + "src/components/**/*.{ts,tsx}", +]; +const UI_ARCHITECTURE_FILES = [ + "src/app/(app)/**/*.{ts,tsx}", + "src/components/auth/**/*.{ts,tsx}", + "src/components/doclist/**/*.{ts,tsx}", + "src/components/documents/DocumentUploader.tsx", + "src/components/documents/DocumentSelectionModal.tsx", + "src/components/documents/DexieMigrationModal.tsx", + "src/components/documents/ZoomControl.tsx", + "src/components/player/**/*.{ts,tsx}", + "src/components/reader/**/*.{ts,tsx}", +]; +const COMPUTE_CORE_IMPORT_PATTERNS = [ + { + group: [ + "@openreader/compute-core/*", + "!@openreader/compute-core/local-runtime", + "!@openreader/compute-core/types", + "!@openreader/compute-core/api-contracts", + ], + message: + "Use '@openreader/compute-core' root imports for light APIs. Allowed subpaths are '@openreader/compute-core/local-runtime', '@openreader/compute-core/types', and '@openreader/compute-core/api-contracts'.", + }, +]; +const UI_ARCHITECTURE_IMPORT_PATHS = [ + { + name: "@/components/formPrimitives", + message: + "Use '@/components/ui' primitives; formPrimitives has been removed.", + }, + { + name: "@/components/ui/buttonPrimitives", + message: + "Use '@/components/ui' button exports; buttonPrimitives has been removed.", + }, + { + name: "@headlessui/react", + importNames: ["Button", "Input"], + message: + "Use app UI Button/Input primitives; Headless UI should be reserved for structural primitives such as Menu, Listbox, Popover, Dialog, and Transition.", + }, +]; +const RESTRICTED_APP_CLASS_PATTERNS = [ + { + selector: "Literal[value=/(bg|text|border|ring)-(gray|slate|zinc|neutral|stone|red|orange|yellow|amber|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\\d/]", + message: + "Use semantic design-system tokens instead of literal Tailwind palette color classes in app surfaces.", + }, + { + selector: "TemplateElement[value.raw=/(bg|text|border|ring)-(gray|slate|zinc|neutral|stone|red|orange|yellow|amber|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\\d/]", + message: + "Use semantic design-system tokens instead of literal Tailwind palette color classes in app surfaces.", + }, + { + selector: "Literal[value=/transition-all/]", + message: + "Use scoped transition utilities; transition-all is banned by the app design system.", + }, + { + selector: "TemplateElement[value.raw=/transition-all/]", + message: + "Use scoped transition utilities; transition-all is banned by the app design system.", + }, + { + selector: "Literal[value=/(bg|text|border|from|via|to)-(accent|background|foreground|base|offbase|muted|secondary-accent|surface|surface-solid|surface-sunken|line|line-soft|line-strong|soft|faint|accent-wash|accent-line|accent-strong|danger|danger-wash)\\/\\d/]", + message: + "Do not use Tailwind alpha modifiers on theme-token colors; use dedicated wash/semantic tokens.", + }, + { + selector: "TemplateElement[value.raw=/(bg|text|border|from|via|to)-(accent|background|foreground|base|offbase|muted|secondary-accent|surface|surface-solid|surface-sunken|line|line-soft|line-strong|soft|faint|accent-wash|accent-line|accent-strong|danger|danger-wash)\\/\\d/]", + message: + "Do not use Tailwind alpha modifiers on theme-token colors; use dedicated wash/semantic tokens.", + }, + { + selector: "Literal[value=/rounded-\\[[^\\]]*px\\]/]", + message: + "Use the app radius scale instead of arbitrary rounded-[…px] utilities.", + }, + { + selector: "TemplateElement[value.raw=/rounded-\\[[^\\]]*px\\]/]", + message: + "Use the app radius scale instead of arbitrary rounded-[…px] utilities.", + }, + { + selector: "Literal[value=/duration-\\[[^\\]]*ms\\]/]", + message: + "Use the shared motion duration tokens instead of arbitrary duration-[…ms] utilities.", + }, + { + selector: "TemplateElement[value.raw=/duration-\\[[^\\]]*ms\\]/]", + message: + "Use the shared motion duration tokens instead of arbitrary duration-[…ms] utilities.", + }, +]; const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), @@ -27,18 +124,25 @@ const eslintConfig = [ "no-restricted-imports": [ "error", { - patterns: [ - { - group: [ - "@openreader/compute-core/*", - "!@openreader/compute-core/local-runtime", - "!@openreader/compute-core/types", - "!@openreader/compute-core/api-contracts", - ], - message: - "Use '@openreader/compute-core' root imports for light APIs. Allowed subpaths are '@openreader/compute-core/local-runtime', '@openreader/compute-core/types', and '@openreader/compute-core/api-contracts'.", - }, - ], + patterns: COMPUTE_CORE_IMPORT_PATTERNS, + }, + ], + }, + }, + { + files: APP_DESIGN_SYSTEM_FILES, + rules: { + "no-restricted-syntax": ["error", ...RESTRICTED_APP_CLASS_PATTERNS], + }, + }, + { + files: UI_ARCHITECTURE_FILES, + rules: { + "no-restricted-imports": [ + "error", + { + paths: UI_ARCHITECTURE_IMPORT_PATHS, + patterns: COMPUTE_CORE_IMPORT_PATTERNS, }, ], }, diff --git a/src/app/(app)/epub/[id]/page.tsx b/src/app/(app)/epub/[id]/page.tsx index 19e1806..4a25c2e 100644 --- a/src/app/(app)/epub/[id]/page.tsx +++ b/src/app/(app)/epub/[id]/page.tsx @@ -1,7 +1,6 @@ 'use client'; import { useParams, useRouter } from "next/navigation"; -import Link from 'next/link'; import { useCallback, useEffect, useRef, useState } from 'react'; import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton'; import { EPUBViewer } from '@/components/views/EPUBViewer'; @@ -20,6 +19,7 @@ import { RateLimitBanner } from '@/components/auth/RateLimitBanner'; import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; import { useUnmountCleanupRef } from '@/hooks/useUnmountCleanupRef'; +import { ButtonLink } from '@/components/ui'; import { useEpubDocument } from './useEpubDocument'; export default function EPUBPage() { @@ -170,16 +170,13 @@ export default function EPUBPage() { if (error) { return (
{error}
- -{error}
- -{error}
+{error}
- -{error}
+{statusText}
{hasMeasuredProgress ? `Page ${pagesParsed} / ${totalPages}` : 'Awaiting first page'}
-{stageLabel}
+{stageLabel}
+
{hasMeasuredProgress ? `${Math.round(progressPercent)}% complete` : statusSubText}
{compactLabel}
-{compactSubLabel}
+{compactSubLabel}
+
{sessionExpired ? 'Please sign in again to continue' : 'Connect an email account to sync your data across devices'} @@ -132,45 +131,41 @@ function SignInContent() { {/* Alerts */} {sessionExpired && ( -
+
Your session has expired. Please sign in again.
{error}
+{error}
+
Don't have an account?{' '} Sign up
)} -+
By signing in, you agree to our{' '}
+
New account sign-ups are currently disabled by the site administrator.
-+
Already have an account?{' '} Sign in
Create your account to get started
+Create your account to get started
{error && ( -{error}
+{error}
= 3 ? 'text-green-600' : 'text-red-600'}`}> +
= 3 ? 'text-accent' : 'text-danger'}`}> {strengthLabels[strength - 1] || 'Very Weak'}
-+
{password === passwordConfirmation ? '✓ Passwords match' : '✗ Passwords do not match'}
)} -+
Already have an account?{' '} Sign in
-+
By creating an account, you agree to our{' '}
+
Reset the audiobook to change generation settings.
TTS audio for this chapter may be cached
Change the TTS playback options or restart the server to force uncached regeneration
{chapter.title}
•
-+
{chapter.status !== 'completed' && Missing • }{formatDuration(chapter.duration)}