feat(settings): add option to show titles under grid cards#181
Conversation
Add a user preference to configure the label displayed underneath book and series cards in grid view. The option allows users to select between hiding labels completely, showing book titles, or showing series titles. Closes #159
📝 WalkthroughWalkthroughThis PR implements configurable grid card labels and card info display modes. Users can select which book/series metadata appears below covers (book title, series title, author, or hidden) and choose whether card info appears in hover overlays or below-cover rows. Implementation includes type definitions, client composable state management, component UI updates, grid layout adjustments, settings controls, and comprehensive test coverage. ChangesGrid card labels and card info display modes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
server/src/modules/user-preferences/user-preferences.controller.test.ts (1)
35-55:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd the required
cardInfoModefield to this fixture.
DisplayPreferencesnow requirescardInfoMode, but this fixture (explicitly typed asDisplayPreferences) omits it. This will fail type-checking with "Property 'cardInfoMode' is missing in type ... but required in type 'DisplayPreferences'". The siblingservice.test.tsfixture already includes it.🐛 Proposed fix
seriesCardCoverMode: 'mosaic', gridCardPrimaryLabel: 'hidden', gridCardSecondaryLabel: 'hidden', + cardInfoMode: 'hover-overlay', };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/modules/user-preferences/user-preferences.controller.test.ts` around lines 35 - 55, The fixture object validDisplayPreferences (typed DisplayPreferences) is missing the required cardInfoMode property; update the validDisplayPreferences object to include cardInfoMode with the same expected value used in the sibling service.test.ts fixture (e.g., the project's standard card info mode string/enum), ensuring the property name exactly matches cardInfoMode so the DisplayPreferences type-checks correctly.
🧹 Nitpick comments (3)
client/src/features/book/components/BookCoverCard.vue (2)
200-216: ⚖️ Poor tradeoffConsider adding loading feedback for author lookup.
The async author lookup has no visible loading state. If the API call is slow, users clicking the author label won't receive feedback until navigation completes. While this doesn't break functionality, a brief loading indicator could improve the user experience.
💡 Optional enhancement
If this becomes a concern, you could add a loading state:
+const authorLookupLoading = ref(false) + async function openAuthorDetails() { const authorName = authorQuery.value?.trim() if (!authorName) return + + authorLookupLoading.value = true try { const page = await fetchAuthors({ q: authorName, page: 0, size: 5, sort: 'name', order: 'asc' }) const author = page.items.find((item) => item.name.trim().toLocaleLowerCase() === authorName.toLocaleLowerCase()) if (author) { void router.push({ name: 'author-detail', params: { id: author.id }, query: { from: route.fullPath } }) return } } catch { // Fall back to the filtered author list below. + } finally { + authorLookupLoading.value = false } void router.push({ name: 'authors', query: { q: authorName } }) }Then show a spinner on the label button during lookup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/features/book/components/BookCoverCard.vue` around lines 200 - 216, Add a brief loading state to openAuthorDetails so users get feedback while fetchAuthors runs: introduce a reactive flag (e.g., isAuthorLookupLoading) used by the label/button UI to show a spinner or disabled state, set it true before calling fetchAuthors and false in both the success and catch branches (and before returning after router.push). Update the button/label component to bind its disabled/aria-busy and spinner visibility to isAuthorLookupLoading so the UI indicates progress during openAuthorDetails, while preserving the existing fallback navigation to the authors list.
524-637: 🏗️ Heavy liftSignificant code duplication in kebab menu.
The dropdown menu structure is duplicated across two contexts (hover overlay and below-cover label row), repeating ~110 lines of nearly identical code. This creates maintenance burden where any menu change must be applied in both locations.
♻️ Recommended refactor to extract the menu
Consider extracting the menu content into a reusable component or helper:
Option 1: Extract menu content to a separate component
Create
client/src/features/book/components/BookCoverCardMenu.vue:<script setup lang="ts"> import type { BookCard, BookFileRef } from '`@bookorbit/types`' // ... import all menu-related composables and icons const props = defineProps<{ book: BookCard openableFiles: BookFileRef[] isMissing: boolean anyRefreshing: boolean reExtractingCover: boolean localReadStatus: ReadStatus | null }>() const emit = defineEmits<{ 'open-file': [file: BookFileRef] 'download-file': [file: BookFileRef] 'export-all': [] 'open-details': [] 'edit-metadata': [] 'refresh-metadata': [] 'regenerate-cover': [] 'add-to-collection': [] 'set-status': [status: ReadStatus] 'send-email': [] 'delete': [] }>() </script> <template> <DropdownMenuContent align="end"> <!-- All menu items here --> </DropdownMenuContent> </template>Then use it in both contexts:
<!-- Kebab menu in hover overlay --> <div v-if="!showBelowCoverLabelArea" class="absolute bottom-2 right-2 z-20"> <DropdownMenu> <DropdownMenuTrigger as-child> <button>...</button> </DropdownMenuTrigger> + <BookCoverCardMenu + :book="book" + :openable-files="openableFiles" + :is-missing="isMissing" + :any-refreshing="anyRefreshing" + :re-extracting-cover="reExtractingCover" + :local-read-status="localReadStatus" + `@open-file`="openFile" + `@download-file`="handleDownloadFile" + `@export-all`="handleExportAll" + `@open-details`="openBookDetails" + ... + /> </DropdownMenu> </div>Option 2: Use a shared template ref
If extracting is too complex, at minimum add a TODO comment noting the duplication and plan for future refactoring.
Also applies to: 656-762
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/features/book/components/BookCoverCard.vue` around lines 524 - 637, The kebab menu in BookCoverCard.vue is duplicated; extract it into a reusable component (e.g., BookCoverCardMenu.vue) that accepts props (book, openableFiles, isMissing, anyRefreshing, reExtractingCover, localReadStatus) and emits actions (open-file, download-file, export-all, open-details, refresh-metadata, regenerate-cover, add-to-collection, set-status, send-email, delete). Move the DropdownMenuContent block into BookCoverCardMenu and replace both duplicated menu blocks in BookCoverCard.vue with <BookCoverCardMenu /> instances wired to the existing methods (openFile, handleDownloadFile, handleExportAll, openBookDetails, handleRefreshMetadata, reExtractCover, emit('action', ...), handleSetStatus, setting showSendDialog) and permissions checks; ensure STATUS_ICONS/STATUS_COLORS and FORMAT_TO_GROUP remain available via props or imports.client/src/features/book/components/CollapsedSeriesCard.vue (1)
249-298: ⚡ Quick winMissing explicit focus-visible styles for keyboard accessibility.
The label buttons lack explicit
focus-visiblestyles. While browsers provide default focus indicators, BookCoverCard defines custom focus styles for consistency:.grid-card-label__button:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: 2px; }♻️ Add explicit focus styles for consistency
.grid-card-label__primary:hover { text-decoration: underline; } +.grid-card-label__primary:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + border-radius: 2px; +} + .grid-card-label__secondary { font-size: 0.625rem; ... } .grid-card-label__secondary:hover { text-decoration: underline; } + +.grid-card-label__secondary:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + border-radius: 2px; +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/features/book/components/CollapsedSeriesCard.vue` around lines 249 - 298, The label buttons (.grid-card-label__primary and .grid-card-label__secondary) lack explicit :focus-visible styles for keyboard accessibility; add matching :focus-visible rules for both selectors that set an outline (e.g., 2px solid var(--ring)), an outline-offset (e.g., 2px) and a small border-radius to mirror BookCoverCard’s custom focus visuals so keyboard users get a consistent visible focus indicator.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@server/src/modules/user-preferences/user-preferences.controller.test.ts`:
- Around line 35-55: The fixture object validDisplayPreferences (typed
DisplayPreferences) is missing the required cardInfoMode property; update the
validDisplayPreferences object to include cardInfoMode with the same expected
value used in the sibling service.test.ts fixture (e.g., the project's standard
card info mode string/enum), ensuring the property name exactly matches
cardInfoMode so the DisplayPreferences type-checks correctly.
---
Nitpick comments:
In `@client/src/features/book/components/BookCoverCard.vue`:
- Around line 200-216: Add a brief loading state to openAuthorDetails so users
get feedback while fetchAuthors runs: introduce a reactive flag (e.g.,
isAuthorLookupLoading) used by the label/button UI to show a spinner or disabled
state, set it true before calling fetchAuthors and false in both the success and
catch branches (and before returning after router.push). Update the button/label
component to bind its disabled/aria-busy and spinner visibility to
isAuthorLookupLoading so the UI indicates progress during openAuthorDetails,
while preserving the existing fallback navigation to the authors list.
- Around line 524-637: The kebab menu in BookCoverCard.vue is duplicated;
extract it into a reusable component (e.g., BookCoverCardMenu.vue) that accepts
props (book, openableFiles, isMissing, anyRefreshing, reExtractingCover,
localReadStatus) and emits actions (open-file, download-file, export-all,
open-details, refresh-metadata, regenerate-cover, add-to-collection, set-status,
send-email, delete). Move the DropdownMenuContent block into BookCoverCardMenu
and replace both duplicated menu blocks in BookCoverCard.vue with
<BookCoverCardMenu /> instances wired to the existing methods (openFile,
handleDownloadFile, handleExportAll, openBookDetails, handleRefreshMetadata,
reExtractCover, emit('action', ...), handleSetStatus, setting showSendDialog)
and permissions checks; ensure STATUS_ICONS/STATUS_COLORS and FORMAT_TO_GROUP
remain available via props or imports.
In `@client/src/features/book/components/CollapsedSeriesCard.vue`:
- Around line 249-298: The label buttons (.grid-card-label__primary and
.grid-card-label__secondary) lack explicit :focus-visible styles for keyboard
accessibility; add matching :focus-visible rules for both selectors that set an
outline (e.g., 2px solid var(--ring)), an outline-offset (e.g., 2px) and a small
border-radius to mirror BookCoverCard’s custom focus visuals so keyboard users
get a consistent visible focus indicator.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: e8216b9e-66a5-4429-84f2-d6b46e61a7bc
📒 Files selected for processing (18)
client/src/composables/__tests__/useDisplaySettings.spec.tsclient/src/composables/__tests__/useDisplaySettingsSync.spec.tsclient/src/composables/useDisplaySettings.tsclient/src/composables/useDisplaySettingsSync.tsclient/src/features/book/components/BookCoverCard.test.tsclient/src/features/book/components/BookCoverCard.vueclient/src/features/book/components/CollapsedSeriesCard.vueclient/src/features/book/components/VirtualBookGrid.spec.tsclient/src/features/book/components/VirtualBookGrid.vueclient/src/features/book/components/__tests__/BookCoverCard.spec.tsclient/src/features/book/components/__tests__/CollapsedSeriesCard.spec.tsclient/src/features/series/views/SeriesDetailView.spec.tsclient/src/features/settings/AppearanceLayoutSettings.vueclient/src/features/settings/__tests__/AppearanceSettings.spec.tspackages/types/src/display-preferences.tsserver/src/modules/user-preferences/user-preferences.controller.test.tsserver/src/modules/user-preferences/user-preferences.service.test.tsserver/src/modules/user-preferences/user-preferences.service.ts
|
🎉 This PR is included in version 1.8.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Add a user preference to configure the label displayed underneath book and series cards in grid view. The option allows users to select between hiding labels completely, showing book titles, or showing series titles.
Closes #159
Summary by CodeRabbit
Release Notes