diff --git a/eslint.config.mjs b/eslint.config.mjs index 7797007..ab1841c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -23,6 +23,47 @@ 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 in migrated app surfaces; formPrimitives is a compatibility shim.", + }, + { + name: "@/components/ui/buttonPrimitives", + message: + "Use '@/components/ui' button exports in migrated app surfaces; buttonPrimitives is a compatibility shim.", + }, + { + 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/]", @@ -83,18 +124,7 @@ 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, }, ], }, @@ -105,6 +135,18 @@ const eslintConfig = [ "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, + }, + ], + }, + }, { files: ["src/app/api/**/*.ts", "src/lib/server/**/*.ts"], rules: { diff --git a/src/components/auth/UserMenu.tsx b/src/components/auth/UserMenu.tsx index c6487d8..e771f3e 100644 --- a/src/components/auth/UserMenu.tsx +++ b/src/components/auth/UserMenu.tsx @@ -1,6 +1,5 @@ 'use client'; -import { Button } from '@headlessui/react'; import Link from 'next/link'; import { useAuthConfig } from '@/contexts/AuthRateLimitContext'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; @@ -8,6 +7,7 @@ import { useAuthSession } from '@/hooks/useAuthSession'; import { getAuthClient } from '@/lib/client/auth-client'; import { useRouter } from 'next/navigation'; import { UserIcon } from '@/components/icons/Icons'; +import { IconButton, buttonClass } from '@/components/ui'; type UserMenuVariant = 'toolbar' | 'sidebar'; @@ -55,15 +55,15 @@ export function UserMenu({ return (
- + {enableUserSignups && ( - + )}
@@ -95,17 +95,17 @@ export function UserMenu({ {session.user.email || 'Account'} - + ); } diff --git a/src/components/doclist/views/DocumentTile.tsx b/src/components/doclist/views/DocumentTile.tsx index 30eb152..e74cabc 100644 --- a/src/components/doclist/views/DocumentTile.tsx +++ b/src/components/doclist/views/DocumentTile.tsx @@ -2,10 +2,10 @@ import Link from 'next/link'; import { useDrag, useDrop, type DragSourceMonitor } from 'react-dnd'; -import { Button } from '@headlessui/react'; import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons'; import type { DocumentListDocument, IconSize } from '@/types/documents'; import { DocumentPreview } from '@/components/doclist/DocumentPreview'; +import { IconButton } from '@/components/ui'; import { useDocumentSelection } from '../dnd/DocumentSelectionContext'; import { DND_DOCUMENT, documentIdentityKey, type DocumentDragItem } from '../dnd/dndTypes'; @@ -185,9 +185,10 @@ export function DocumentTile({ {showDeleteButton && ( - + )} diff --git a/src/components/doclist/views/ListView.tsx b/src/components/doclist/views/ListView.tsx index fecd45e..d729b72 100644 --- a/src/components/doclist/views/ListView.tsx +++ b/src/components/doclist/views/ListView.tsx @@ -3,7 +3,6 @@ import Link from 'next/link'; import { useEffect } from 'react'; import { useDrag, useDrop } from 'react-dnd'; -import { Button } from '@headlessui/react'; import type { DocumentListDocument, SortBy, @@ -11,6 +10,7 @@ import type { } from '@/types/documents'; import { PDFIcon, EPUBIcon, FileIcon } from '@/components/icons/Icons'; import { formatDocumentSize } from '@/components/doclist/formatSize'; +import { IconButton } from '@/components/ui'; import { useDocumentSelection } from '../dnd/DocumentSelectionContext'; import { DND_DOCUMENT, documentIdentityKey, type DocumentDragItem } from '../dnd/dndTypes'; @@ -156,12 +156,12 @@ function DocRow({ {formatDate(doc.lastModified)} - + ); } diff --git a/src/components/reader/ReaderSidebarShell.tsx b/src/components/reader/ReaderSidebarShell.tsx index 5ceeaed..89a5ae3 100644 --- a/src/components/reader/ReaderSidebarShell.tsx +++ b/src/components/reader/ReaderSidebarShell.tsx @@ -4,7 +4,7 @@ import { Fragment, type ReactNode } from 'react'; import { Transition } from '@headlessui/react'; import { XCircleIcon } from '@/components/icons/Icons'; import { useReaderSidebarBounds } from '@/hooks/useReaderSidebarBounds'; -import { buttonClass } from '@/components/ui/buttonPrimitives'; +import { IconButton } from '@/components/ui'; interface ReaderSidebarShellProps { isOpen: boolean; @@ -77,19 +77,15 @@ export function ReaderSidebarShell({
{headerActions} - +
diff --git a/src/components/reader/SegmentsSidebar.tsx b/src/components/reader/SegmentsSidebar.tsx index 49cf99e..aeacbdf 100644 --- a/src/components/reader/SegmentsSidebar.tsx +++ b/src/components/reader/SegmentsSidebar.tsx @@ -8,7 +8,7 @@ import toast from 'react-hot-toast'; import { useTTS } from '@/contexts/TTSContext'; import { useConfig } from '@/contexts/ConfigContext'; import { RefreshIcon, InfoIcon } from '@/components/icons/Icons'; -import { buttonClass } from '@/components/ui/buttonPrimitives'; +import { buttonClass } from '@/components/ui'; import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell'; import { compareSegmentLocators, locatorGroupKey, locatorIdentityKey } from '@/lib/shared/tts-locator'; import { buildSegmentKey, buildSegmentKeyPrefix } from '@/lib/shared/tts-segment-plan';