phase 9: enforce ui architecture imports
This commit is contained in:
parent
b4f30cf072
commit
740366a027
6 changed files with 77 additions and 38 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={`flex gap-2 ${className}`}>
|
||||
<Link href="/signin">
|
||||
<Button className="inline-flex items-center rounded-md bg-surface border border-line px-2 py-1 text-xs font-medium text-foreground hover:bg-accent-wash focus:outline-none focus:ring-2 focus:ring-accent-line focus:ring-offset-2 transform transition duration-base ease-standard hover:text-accent">
|
||||
<span className={buttonClass({ variant: 'secondary', size: 'sm' })}>
|
||||
Connect
|
||||
</Button>
|
||||
</span>
|
||||
</Link>
|
||||
{enableUserSignups && (
|
||||
<Link href="/signup">
|
||||
<Button className="inline-flex items-center rounded-md bg-accent px-2 py-1 text-xs font-medium text-background hover:bg-secondary-accent focus:outline-none focus:ring-2 focus:ring-accent-line focus:ring-offset-2 transform transition duration-base ease-standard">
|
||||
<span className={buttonClass({ variant: 'primary', size: 'sm' })}>
|
||||
Create account
|
||||
</Button>
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -95,17 +95,17 @@ export function UserMenu({
|
|||
{session.user.email || 'Account'}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
<IconButton
|
||||
onClick={handleDisconnectAccount}
|
||||
className="inline-flex items-center text-foreground text-xs hover:text-accent transform transition duration-base ease-standard"
|
||||
title="Disconnect account"
|
||||
aria-label="Disconnect account"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
</Button>
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</span>
|
||||
</Link>
|
||||
{showDeleteButton && (
|
||||
<Button
|
||||
<IconButton
|
||||
onClick={() => onDelete(doc)}
|
||||
className={`inline-flex items-center justify-center text-soft hover:text-accent hover:bg-surface focus:outline-none transition-colors duration-base ${TRASH_BTN_CLASSES[iconSize]}`}
|
||||
size="xs"
|
||||
className={TRASH_BTN_CLASSES[iconSize]}
|
||||
aria-label={`Delete ${doc.name}`}
|
||||
>
|
||||
<svg className={TRASH_ICON_CLASSES[iconSize]} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -198,7 +199,7 @@ export function DocumentTile({
|
|||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<span className="px-2 text-[11px] text-soft tabular-nums">
|
||||
{formatDate(doc.lastModified)}
|
||||
</span>
|
||||
<Button
|
||||
<IconButton
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onDeleteDoc(doc);
|
||||
}}
|
||||
className="h-7 w-7 flex items-center justify-center text-soft hover:text-accent hover:bg-accent-wash rounded transition duration-base ease-standard"
|
||||
size="sm"
|
||||
aria-label={`Delete ${doc.name}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -172,7 +172,7 @@ function DocRow({
|
|||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{headerActions}
|
||||
<button
|
||||
type="button"
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
title="Close"
|
||||
className={buttonClass({
|
||||
variant: 'secondary',
|
||||
size: 'icon',
|
||||
className: 'h-8 w-8 text-soft',
|
||||
})}
|
||||
tone="surface"
|
||||
className="text-soft"
|
||||
>
|
||||
<XCircleIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue