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';