mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
chore: implement local quality foundation
This commit is contained in:
+2
-1
@@ -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
@@ -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"
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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'.
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user