chore: implement local quality foundation

This commit is contained in:
neon
2026-03-06 09:08:15 -06:00
parent aeae0ac8ce
commit 891cd19f46
46 changed files with 891 additions and 189 deletions
+2 -1
View File
@@ -43,7 +43,8 @@ src/
## Testing
```bash
pnpm test:unit # run Vitest
pnpm test:unit # run unit tests
pnpm test:cov # run unit tests with coverage
```
## Key conventions
+28 -21
View File
@@ -8,57 +8,64 @@
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"test:cov": "vitest --coverage",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache",
"format": "prettier --write --experimental-cli src/"
"lint": "pnpm run lint:check",
"lint:check": "run-s lint:oxlint:check lint:eslint:check",
"lint:fix": "run-s lint:oxlint:fix lint:eslint:fix",
"lint:oxlint:check": "oxlint src",
"lint:oxlint:fix": "oxlint src --fix",
"lint:eslint:check": "eslint src --cache",
"lint:eslint:fix": "eslint src --fix --cache",
"format": "prettier --write --experimental-cli src/",
"format:check": "prettier --check src/"
},
"dependencies": {
"@fontsource-variable/fraunces": "^5.2.9",
"@fontsource-variable/inter": "^5.2.8",
"@projectx/types": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
"@tailwindcss/vite": "^4.2.1",
"@vueuse/core": "^14.2.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.564.0",
"pdfjs-dist": "^5.4.624",
"lucide-vue-next": "^0.577.0",
"pdfjs-dist": "^5.5.207",
"pinia": "^3.0.4",
"reka-ui": "^2.8.0",
"reka-ui": "^2.9.0",
"socket.io-client": "^4.8.3",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"vue": "^3.5.28",
"vue-router": "^5.0.2",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1",
"vue": "^3.5.29",
"vue-router": "^5.0.3",
"vue-sonner": "^2.0.9",
"vue-virtual-scroller": "^2.0.0-beta.8"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
"@types/jsdom": "^27.0.0",
"@types/node": "^24.10.13",
"@types/jsdom": "^28.0.0",
"@types/node": "^24.11.0",
"@vitejs/plugin-vue": "^6.0.4",
"@vitest/eslint-plugin": "^1.6.7",
"@vue/eslint-config-typescript": "^14.6.0",
"@vitest/coverage-v8": "^4.0.18",
"@vitest/eslint-plugin": "^1.6.9",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.8.1",
"eslint": "^10.0.0",
"eslint": "^10.0.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-oxlint": "~1.46.0",
"eslint-plugin-oxlint": "~1.50.0",
"eslint-plugin-vue": "~10.8.0",
"jiti": "^2.6.1",
"jsdom": "^28.0.0",
"jsdom": "^28.1.0",
"npm-run-all2": "^8.0.4",
"oxlint": "~1.47.0",
"oxlint": "~1.50.0",
"prettier": "3.8.1",
"tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vite-plugin-vue-devtools": "^8.0.6",
"vitest": "^4.0.18",
"vue-tsc": "^3.2.4"
"vue-tsc": "^3.2.5"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
+10 -3
View File
@@ -4,7 +4,6 @@ import { CheckSquare, LayoutGrid, List, MoreHorizontal, SlidersHorizontal, Squar
import * as LucideIcons from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Separator } from '@/components/ui/separator'
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import {
DropdownMenu,
@@ -64,7 +63,11 @@ function getIconComponent(name: string) {
variant="ghost"
size="sm"
class="hidden md:flex h-8 gap-1.5 text-[11px] font-bold uppercase tracking-tight px-2.5 rounded-lg transition-all"
:class="selectionMode ? 'text-primary bg-primary/10 hover:bg-primary/20 ring-1 ring-primary/20' : 'text-muted-foreground/60 hover:text-foreground hover:bg-primary/5'"
:class="
selectionMode
? 'text-primary bg-primary/10 hover:bg-primary/20 ring-1 ring-primary/20'
: 'text-muted-foreground/60 hover:text-foreground hover:bg-primary/5'
"
@click="emit('toggle-selection')"
>
<CheckSquare v-if="selectionMode" :size="13" />
@@ -101,7 +104,11 @@ function getIconComponent(name: string) {
<!-- Desktop: display settings popover -->
<Popover>
<PopoverTrigger as-child>
<Button variant="ghost" size="icon" class="hidden md:flex h-8 w-8 rounded-lg text-muted-foreground/50 hover:text-foreground hover:bg-primary/5">
<Button
variant="ghost"
size="icon"
class="hidden md:flex h-8 w-8 rounded-lg text-muted-foreground/50 hover:text-foreground hover:bg-primary/5"
>
<SlidersHorizontal :size="14" />
</Button>
</PopoverTrigger>
@@ -1,5 +1,4 @@
import type { OidcPublicConfig } from '@projectx/types'
import { api } from '@/lib/api'
async function generatePkce(): Promise<{ codeVerifier: string; codeChallenge: string }> {
const array = new Uint8Array(32)
@@ -104,8 +104,12 @@ function openFile(file: BookFileRef) {
<template>
<div
class="flex items-center gap-3 py-3 px-2 rounded-md transition-colors cursor-pointer hover:bg-muted/50"
:class="[selectionMode ? 'cursor-pointer select-none' : '', selected ? 'bg-primary/8 ring-1 ring-primary/30' : '']"
class="flex items-center gap-3 py-3 px-2 rounded-md transition-colors cursor-pointer"
:class="[
selectionMode ? 'cursor-pointer select-none' : '',
selected ? 'bg-primary/8 ring-1 ring-primary/30' : '',
isMissing ? 'grayscale opacity-60' : 'hover:bg-muted/50',
]"
@click="selectionMode ? emit('select', $event) : emit('action', 'quick-view')"
>
<!-- Selection checkbox -->
@@ -160,11 +164,7 @@ function openFile(file: BookFileRef) {
<div class="hidden sm:flex items-center gap-0.5" @mouseleave="hoverRating = null">
<Tooltip v-for="star in 5" :key="star">
<TooltipTrigger as-child>
<button
class="p-0.5 transition-colors"
@mouseenter="hoverRating = star"
@click="setRating(star)"
>
<button class="p-0.5 transition-colors" @mouseenter="hoverRating = star" @click="setRating(star)">
<Star class="size-3" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/40'" />
</button>
</TooltipTrigger>
@@ -1,9 +1,12 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi, beforeAll } from 'vitest'
import { describe, it, expect, vi } from 'vitest'
import BookCoverCard from '../BookCoverCard.vue'
import type { BookCard } from '@projectx/types'
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
vi.mock('vue-router', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-router')>()
return { ...actual, useRouter: () => ({ push: vi.fn() }) }
})
vi.mock('@/features/book/composables/useCoverVersions', () => ({
useCoverVersions: () => ({ coverUrl: () => '/cover.jpg', bumpVersion: vi.fn() }),
}))
@@ -30,6 +33,13 @@ const missingBook: BookCard = {
seriesName: null,
seriesIndex: null,
files: [],
publishedYear: null,
language: null,
genres: [],
tags: [],
rating: null,
readingProgress: null,
addedAt: '2026-01-01T00:00:00.000Z',
}
const presentBook: BookCard = {
@@ -40,6 +50,13 @@ const presentBook: BookCard = {
seriesName: null,
seriesIndex: null,
files: [{ id: 10, format: 'epub', role: 'primary' }],
publishedYear: 2024,
language: 'en',
genres: [],
tags: [],
rating: null,
readingProgress: null,
addedAt: '2026-01-01T00:00:00.000Z',
}
describe('BookCoverCard — missing state', () => {
@@ -66,7 +83,8 @@ describe('BookCoverCard — missing state', () => {
it('uses cursor-default on the root when book is missing', () => {
const wrapper = mount(BookCoverCard, { props: { book: missingBook }, global: globalStubs })
expect(wrapper.classes()).toContain('cursor-default')
const root = wrapper.find('div.group')
expect(root.classes()).toContain('cursor-default')
})
})
@@ -3,7 +3,10 @@ import { describe, it, expect, vi } from 'vitest'
import BookListRow from '../BookListRow.vue'
import type { BookCard } from '@projectx/types'
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
vi.mock('vue-router', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-router')>()
return { ...actual, useRouter: () => ({ push: vi.fn() }) }
})
vi.mock('@/features/book/composables/useCoverVersions', () => ({
useCoverVersions: () => ({ coverUrl: () => '/cover.jpg', bumpVersion: vi.fn() }),
}))
@@ -18,6 +21,9 @@ const globalStubs = {
DropdownMenuItem: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuSeparator: { template: '<div />' },
Tooltip: { template: '<div><slot /></div>' },
TooltipTrigger: { template: '<div><slot /></div>' },
TooltipContent: { template: '<div><slot /></div>' },
},
}
@@ -29,6 +35,13 @@ const missingBook: BookCard = {
seriesName: null,
seriesIndex: null,
files: [],
publishedYear: null,
language: null,
genres: [],
tags: [],
rating: null,
readingProgress: null,
addedAt: '2026-01-01T00:00:00.000Z',
}
const presentBook: BookCard = {
@@ -39,6 +52,13 @@ const presentBook: BookCard = {
seriesName: null,
seriesIndex: null,
files: [{ id: 10, format: 'epub', role: 'primary' }],
publishedYear: 2024,
language: 'en',
genres: ['Fiction'],
tags: [],
rating: null,
readingProgress: null,
addedAt: '2026-01-01T00:00:00.000Z',
}
describe('BookListRow — missing state', () => {
@@ -3,7 +3,10 @@ import { describe, it, expect, vi } from 'vitest'
import DetailsTab from '../detail/tabs/DetailsTab.vue'
import type { BookDetail } from '@projectx/types'
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn(), replace: vi.fn() }) }))
vi.mock('vue-router', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-router')>()
return { ...actual, useRouter: () => ({ push: vi.fn(), replace: vi.fn() }) }
})
vi.mock('@/features/auth/composables/usePermissions', () => ({
usePermissions: () => ({ hasPermission: () => false }),
}))
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import { Search, Loader2, X, Image as ImageIcon, ExternalLink, Check } from 'lucide-vue-next'
import { Search, Loader2, X, Image as ImageIcon, Check } from 'lucide-vue-next'
import type { CoverSearchResult } from '@projectx/types'
const props = defineProps<{
@@ -120,9 +120,11 @@ onMounted(() => {
@click="handleSelect(String(res.url))"
>
<img :src="res.previewUrl" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" loading="lazy" />
<!-- Hover Overlay -->
<div class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center gap-2">
<div
class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center gap-2"
>
<div class="p-2 rounded-full bg-primary text-white scale-75 group-hover:scale-100 transition-transform">
<Check class="size-5" />
</div>
@@ -130,8 +132,10 @@ onMounted(() => {
</div>
<!-- Resolution Badge -->
<div class="absolute top-2 right-2 px-1.5 py-0.5 rounded-md text-[10px] font-bold text-white backdrop-blur-md shadow-sm"
:class="res.width >= 1000 ? 'bg-green-500/80' : 'bg-black/50'">
<div
class="absolute top-2 right-2 px-1.5 py-0.5 rounded-md text-[10px] font-bold text-white backdrop-blur-md shadow-sm"
:class="res.width >= 1000 ? 'bg-green-500/80' : 'bg-black/50'"
>
{{ res.width }}x{{ res.height }}
</div>
@@ -182,10 +186,10 @@ onMounted(() => {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.1);
background: rgba(0, 0, 0, 0.1);
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(0,0,0,0.2);
background: rgba(0, 0, 0, 0.2);
}
</style>
@@ -1,4 +1,4 @@
import { computed, ref, type Ref } from 'vue'
import { computed, ref } from 'vue'
const bookIds = ref<number[]>([])
const total = ref(0)
@@ -75,9 +75,17 @@ function handleImageClick(e: MouseEvent) {
return
}
const x = e.clientX / window.innerWidth
if (x < 0.25) direction.value === 'rtl' ? nextPage() : prevPage()
else if (x > 0.75) direction.value === 'rtl' ? prevPage() : nextPage()
else handleMiddleTap()
if (x < 0.25) {
if (direction.value === 'rtl') nextPage()
else prevPage()
return
}
if (x > 0.75) {
if (direction.value === 'rtl') prevPage()
else nextPage()
return
}
handleMiddleTap()
}
// ── Touch / swipe ──────────────────────────────────────────────────────────────
@@ -90,8 +98,13 @@ function onTouchStart(e: TouchEvent) {
function onTouchEnd(e: TouchEvent) {
const dx = e.changedTouches[0].clientX - touchStartX
if (Math.abs(dx) < 50) return
if (dx < 0) direction.value === 'rtl' ? prevPage() : nextPage()
else direction.value === 'rtl' ? nextPage() : prevPage()
if (dx < 0) {
if (direction.value === 'rtl') prevPage()
else nextPage()
return
}
if (direction.value === 'rtl') nextPage()
else prevPage()
}
// ── Wheel (paginated mode only) ────────────────────────────────────────────────
@@ -111,16 +124,19 @@ function onKeyDown(e: KeyboardEvent) {
case 'ArrowRight':
case 'PageDown':
e.preventDefault()
isRtl ? prevPage() : nextPage()
if (isRtl) prevPage()
else nextPage()
break
case 'ArrowLeft':
case 'PageUp':
e.preventDefault()
isRtl ? nextPage() : prevPage()
if (isRtl) nextPage()
else prevPage()
break
case ' ':
e.preventDefault()
e.shiftKey ? prevPage() : nextPage()
if (e.shiftKey) prevPage()
else nextPage()
break
case 'Home':
goToPage(0)
@@ -2,7 +2,7 @@
import { nextTick, ref, watch } from 'vue'
import { ChevronDown, ChevronUp, X } from 'lucide-vue-next'
const props = defineProps<{
defineProps<{
matchCount: number
currentIndex: number
}>()
@@ -42,10 +42,7 @@ defineExpose({
</script>
<template>
<div
class="flex items-center gap-1.5 px-3 py-1.5 shrink-0"
style="background: rgba(60, 65, 70, 1); border-bottom: 1px solid rgba(0,0,0,0.3)"
>
<div class="flex items-center gap-1.5 px-3 py-1.5 shrink-0" style="background: rgba(60, 65, 70, 1); border-bottom: 1px solid rgba(0, 0, 0, 0.3)">
<input
ref="inputRef"
v-model="query"
@@ -53,8 +50,8 @@ defineExpose({
placeholder="Find in document..."
class="flex-1 min-w-0 bg-transparent text-sm text-white/90 outline-none placeholder:text-white/30"
style="max-width: 260px"
@keydown.enter="emit('next')"
@keydown.shift.enter.prevent="emit('prev')"
@keydown.enter.exact="emit('next')"
@keydown.enter.shift.exact.prevent="emit('prev')"
@keydown.escape="emit('close')"
/>
@@ -95,4 +92,3 @@ defineExpose({
</button>
</div>
</template>
@@ -1,4 +1,4 @@
import { computed, ref, watch } from 'vue'
import { computed, ref } from 'vue'
import { api } from '@/lib/api'
export interface Bookmark {
@@ -1,4 +1,4 @@
import { ref, shallowRef } from 'vue'
import { ref } from 'vue'
import type * as pdfjsLib from 'pdfjs-dist'
import type { Ref, ShallowRef } from 'vue'
@@ -39,4 +39,3 @@ export function usePdfThumbnails(pdfDoc: ShallowRef<pdfjsLib.PDFDocumentProxy |
return { thumbnails, loadThumbnail, loadRange }
}
@@ -43,8 +43,8 @@ export function useReaderSettings(bookFileId: number, format: string) {
() =>
({
...(READER_GROUP_DEFAULTS[group] as ReaderSettings),
...(defaultSettings.value ?? {}),
...(bookDelta.value ?? {}),
...(defaultSettings.value ?? undefined),
...(bookDelta.value ?? undefined),
}) as ReaderSettings,
)
@@ -84,7 +84,7 @@ export function useReaderSettings(bookFileId: number, format: string) {
// Merges only the changed field(s) into the existing delta — never saves a full snapshot.
function updateBookSettings(patch: Partial<ReaderSettings>) {
const next = { ...(bookDelta.value ?? {}), ...patch } as Partial<ReaderSettings>
const next = { ...(bookDelta.value ?? undefined), ...patch } as Partial<ReaderSettings>
bookDelta.value = next
isCustomized.value = Object.keys(next).length > 0
writeLs(lsBookKey(bookFileId), next)
@@ -1,4 +1,4 @@
import { ref, watch } from 'vue'
import { ref } from 'vue'
import { api } from '@/lib/api'
import type { StagingFile, StagingMetadata } from '@projectx/types'
@@ -1,4 +1,4 @@
import { computed, reactive, ref, watch } from 'vue'
import { computed, reactive, ref } from 'vue'
import { api } from '@/lib/api'
import type { StagingFile, StagingFilesPage, StagingFileStatus } from '@projectx/types'
@@ -142,4 +142,3 @@ export function useStagingFiles() {
getSelectionPayload,
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*", "eslint.config.*"],
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "eslint.config.*"],
"compilerOptions": {
// Most tools use transpilation instead of Node.js's native type-stripping.
// Bundler mode provides a smoother developer experience.
+64
View File
@@ -0,0 +1,64 @@
# Client typecheck baseline
# Auto-generated by: pnpm run typecheck:client:baseline:update
# One diagnostic per line. Do not edit manually unless necessary.
src/components/IconPicker.vue(174,39): error TS18046: 'item' is of type 'unknown'.
src/features/book/components/detail/BookDetailHeader.vue(22,5): error TS2322: Type 'number | null | undefined' is not assignable to type 'number | null'.
src/features/book/components/detail/BookDetailHeader.vue(23,5): error TS2322: Type 'number | null | undefined' is not assignable to type 'number | null'.
src/features/dashboard/components/DashboardSettingsSheet.vue(45,26): error TS2345: Argument of type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to parameter of type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'.
src/features/dashboard/components/DashboardSettingsSheet.vue(80,23): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'.
src/features/dashboard/components/DashboardSettingsSheet.vue(80,5): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'.
src/features/dashboard/components/DashboardSettingsSheet.vue(87,19): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'.
src/features/dashboard/components/DashboardSettingsSheet.vue(87,5): error TS2322: Type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; } | undefined' is not assignable to type '{ id: string; type: ScrollerType; label: string; enabled: boolean; order: number; limit: number; lensId?: number | undefined; }'.
src/features/library/components/LibraryCreatorDetails.vue(109,43): error TS18046: 'item' is of type 'unknown'.
src/features/library/components/LibraryCreatorScanner.vue(36,36): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
src/features/reader/components/CbzReaderView.vue(95,17): error TS2532: Object is possibly 'undefined'.
src/features/reader/components/CbzReaderView.vue(99,14): error TS2532: Object is possibly 'undefined'.
src/features/reader/components/NoteDialog.vue(14,25): error TS2345: Argument of type '"update:modelValue"' is not assignable to parameter of type '"save"'.
src/features/reader/components/PdfToolbar.vue(216,93): error TS2769: No overload matches this call.
src/features/reader/components/ReaderSidebar.vue(128,10): error TS2300: Duplicate identifier 'stripFragment'.
src/features/reader/components/ReaderSidebar.vue(130,7): error TS7022: 'TocList' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
src/features/reader/components/ReaderSidebar.vue(144,12): error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
src/features/reader/components/ReaderSidebar.vue(148,25): error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
src/features/reader/components/ReaderSidebar.vue(7,10): error TS2300: Duplicate identifier 'stripFragment'.
src/features/reader/composables/useFoliate.ts(112,36): error TS2352: Conversion of type 'Window & typeof globalThis' to type 'Record<string, unknown>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
src/features/reader/composables/useFoliateAnnotations.ts(81,48): error TS2349: This expression is not callable.
src/features/reader/composables/useFoliateAnnotations.ts(87,50): error TS2349: This expression is not callable.
src/features/reader/composables/useFoliateAnnotations.ts(93,48): error TS2349: This expression is not callable.
src/features/reader/composables/useFoliateAnnotations.ts(99,52): error TS2349: This expression is not callable.
src/features/reader/composables/useFoliateInput.ts(31,19): error TS18048: 'touch' is possibly 'undefined'.
src/features/reader/composables/useFoliateInput.ts(32,19): error TS18048: 'touch' is possibly 'undefined'.
src/features/reader/composables/useFoliateInput.ts(43,29): error TS18048: 'touch' is possibly 'undefined'.
src/features/reader/composables/useFoliateInput.ts(44,29): error TS18048: 'touch' is possibly 'undefined'.
src/features/reader/composables/useFoliateInput.ts(71,22): error TS18048: 'touch' is possibly 'undefined'.
src/features/reader/composables/useFoliateInput.ts(72,31): error TS18048: 'touch' is possibly 'undefined'.
src/features/reader/composables/useFoliateInput.ts(87,45): error TS18048: 'touch' is possibly 'undefined'.
src/features/reader/composables/useFoliateInput.ts(92,39): error TS18048: 'touch' is possibly 'undefined'.
src/features/reader/composables/useFoliateInput.ts(95,27): error TS18048: 'touch' is possibly 'undefined'.
src/features/reader/composables/usePdfFind.ts(33,22): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
src/features/reader/composables/usePdfFind.ts(33,43): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
src/features/reader/composables/usePdfOutline.ts(22,72): error TS2694: Namespace '"node_modules/.pnpm/pdfjs-dist@5.5.207/node_modules/pdfjs-dist/types/src/pdf"' has no exported member 'RefProxy'.
src/features/reader/composables/usePdfOutline.ts(29,74): error TS2694: Namespace '"node_modules/.pnpm/pdfjs-dist@5.5.207/node_modules/pdfjs-dist/types/src/pdf"' has no exported member 'OutlineNode'.
src/features/reader/composables/usePdfRenderer.ts(96,17): error TS18048: 'e' is possibly 'undefined'.
src/features/reader/composables/usePdfRenderer.ts(97,34): error TS18048: 'f' is possibly 'undefined'.
src/features/reader/composables/usePdfRenderer.ts(98,31): error TS18048: 'a' is possibly 'undefined'.
src/features/reader/composables/usePdfRenderer.ts(98,50): error TS18048: 'b' is possibly 'undefined'.
src/features/reader/composables/usePdfThumbnails.ts(23,25): error TS2345: Argument of type '{ canvasContext: CanvasRenderingContext2D; viewport: PageViewport; }' is not assignable to parameter of type 'RenderParameters'.
src/features/reader/composables/useReaderState.ts(212,47): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
src/features/reader/composables/useReaderState.ts(69,27): error TS18048: 'theme' is possibly 'undefined'.
src/features/reader/composables/useReaderState.ts(69,40): error TS18048: 'theme' is possibly 'undefined'.
src/features/reader/composables/useReaderState.ts(76,23): error TS18048: 'theme' is possibly 'undefined'.
src/features/reader/composables/useReaderState.ts(77,22): error TS18048: 'theme' is possibly 'undefined'.
src/features/reader/ReaderView.vue(149,15): error TS2345: Argument of type 'unknown[]' is not assignable to parameter of type 'RawTocItem[]'.
src/features/reader/ReaderView.vue(241,12): error TS2345: Argument of type '{}' is not assignable to parameter of type 'FoliateView'.
src/features/reader/ReaderView.vue(253,15): error TS2345: Argument of type 'unknown' is not assignable to parameter of type 'FoliateView | null'.
src/features/reader/utils.ts(2,3): error TS2322: Type 'string | undefined' is not assignable to type 'string'.
src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(57,25): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'.
src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(57,5): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'.
src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(65,21): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'.
src/features/settings/metadata-preferences/components/FieldConfigSheet.vue(65,5): error TS2322: Type 'MetadataProviderKey | undefined' is not assignable to type 'MetadataProviderKey'.
src/features/settings/metadata-preferences/components/ProviderChipList.vue(56,34): error TS2345: Argument of type 'MetadataProviderKey | undefined' is not assignable to parameter of type 'MetadataProviderKey'.
src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue(130,25): error TS2352: Conversion of type '{ enabled: boolean; apiKey: string; } | { enabled: boolean; domain: string; cookie: string; } | { enabled: boolean; } | { enabled: boolean; apiKey: string; }' to type 'Record<string, string>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue(138,27): error TS2352: Conversion of type '{ enabled: boolean; apiKey: string; } | { enabled: boolean; domain: string; cookie: string; } | { enabled: boolean; } | { enabled: boolean; apiKey: string; }' to type 'Record<string, string>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
src/features/settings/PdfSettings.vue(45,17): error TS2367: This comparison appears to be unintentional because the types '"vertical" | "horizontal" | "page" | "wrapped"' and '"continuous"' have no overlap.
src/features/settings/PdfSettings.vue(47,32): error TS2322: Type '"continuous"' is not assignable to type '"vertical" | "horizontal" | "page" | "wrapped" | undefined'.
src/lib/api.ts(55,37): error TS2353: Object literal may only specify known properties, and '_isRetry' does not exist in type 'RequestInit'.
+11
View File
@@ -9,6 +9,17 @@ export default mergeConfig(
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
reportsDirectory: './coverage',
thresholds: {
lines: 1,
statements: 1,
functions: 1,
branches: 1,
},
},
},
}),
)