Skip to content

feat(settings): add option to show titles under grid cards#181

Merged
neonsolstice merged 1 commit into
mainfrom
BO-159-grid-card-titles
Jun 1, 2026
Merged

feat(settings): add option to show titles under grid cards#181
neonsolstice merged 1 commit into
mainfrom
BO-159-grid-card-titles

Conversation

@neonsolstice

@neonsolstice neonsolstice commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

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

  • New Features
    • Customizable card labels: choose which information displays as primary and secondary labels on book cards
    • Card info display modes: select whether card information appears on hover, below the cover, or hidden
    • Preferences automatically persist and sync across sessions

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
@github-actions github-actions Bot added server Changes affecting server-side code, APIs, or backend behavior. client Changes affecting the client application or frontend behavior. packages labels Jun 1, 2026
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Grid card labels and card info display modes

Layer / File(s) Summary
Type system and server validation
packages/types/src/display-preferences.ts, server/src/modules/user-preferences/user-preferences.service.ts, server/src/modules/user-preferences/user-preferences.*.test.ts
New GridCardLabelField enum (hidden, book-title, series-title, author) and CardInfoMode enum (hover-overlay, below-cover, off) exported as types. DisplayPreferences interface extended with gridCardPrimaryLabel, gridCardSecondaryLabel, and cardInfoMode fields. Server Zod schema validates these fields with appropriate defaults ('hidden' for labels, 'hover-overlay' for info mode).
Client composable and sync
client/src/composables/useDisplaySettings.ts, client/src/composables/useDisplaySettingsSync.ts, client/src/composables/__tests__/useDisplaySettings*.spec.ts
useDisplaySettings composable now initializes, persists, validates, and exports the new label and info-mode refs. Normalization helpers validate values against allowed enums. Snapshot, sanitize, and apply functions updated to handle the three new fields. Watch handlers persist changes to storage. Sync composable includes new refs in debounced server updates. Tests cover snapshot inclusion, validation, defaulting, and round-trip sync behavior.
Book cover card UI and navigation
client/src/features/book/components/BookCoverCard.vue, client/src/features/book/components/BookCoverCard.test.ts, client/src/features/book/components/__tests__/BookCoverCard.spec.ts
Component accepts showLabel prop and receives label/info-mode settings from composable. Label text computed via resolveBookLabel helper selecting between book title, series title, position, and author. Navigation handlers open book/series details or fetch authors for detail/search navigation, with route.fullPath passed as from query param. Template conditionally renders below-cover label row with resolved text and kebab menu. Hover title visibility and overlay styling updated based on cardInfoMode. Tests verify label rendering conditions, click navigation, async author fetch, and route parameter passing.
Series card UI and navigation
client/src/features/book/components/CollapsedSeriesCard.vue, client/src/features/book/components/__tests__/CollapsedSeriesCard.spec.ts
Component added showLabel prop and label display logic for below-cover mode. Hover overlay redesigned with action button and optional title/author text. Read progress bar computed from readCount/bookCount with clamping. Label resolution helpers compute primary/secondary text; author navigation performs async fetchAuthors lookup with case-insensitive matching, fallback to authors search, and error handling. Below-cover label buttons call handlers to navigate to series details or author routes. Tests cover series badge, hover button behavior, progress bar rendering/clamping, and label button navigation with author lookup scenarios.
Grid layout and virtualization
client/src/features/book/components/VirtualBookGrid.vue, client/src/features/book/components/VirtualBookGrid.spec.ts
Grid imports useDisplaySettings and derives labelAreaHeight and showLabel computed values based on cardInfoMode. Label area height included in virtualization itemSize and buffer calculations. CSS variable --book-grid-label-height added to scrollerStyle. Both static and virtualized rendering paths pass :show-label prop to child cards. Virtualized cell height adjusted to calc(var(--book-grid-height) + var(--book-grid-label-height, 0px)). Mock and test helper updated to support showLabel-driven label slot assertions for different cardInfoMode values.
Settings UI for label and info mode
client/src/features/settings/AppearanceLayoutSettings.vue, client/src/features/settings/__tests__/AppearanceSettings.spec.ts
Component destructures label fields and cardInfoMode from composable. Computed flag showLabelFields gates visibility of label selection UI based on cardInfoMode. Button group allows switching between three card info modes (hover-overlay, below-cover, off). Conditional section with animated Transition shows primary/secondary label field dropdown selectors when enabled. Change handlers update refs via composable. Tests mock display settings refs and snapshot including new fields.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • bookorbit/bookorbit#169: Both PRs modify the core display-preferences plumbing in useDisplaySettings.ts (updating snapshot/sanitize/apply and persist logic); main PR adds gridCardPrimaryLabel/gridCardSecondaryLabel and cardInfoMode, while the related PR adds bookCoverDisplayMode.

Suggested labels

client, packages, server

Poem

📚✨ A rabbit hops through grid rows,
Beneath each cover, a label glows—
Book title, series, author's name flow,
Choose hover-over or below,
Grid browsing now truly knows! 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main feature being added: a configurable option to display titles beneath grid cards in the UI.
Description check ✅ Passed The PR description is minimal but covers the core change (user preference for grid card labels with three options). However, it lacks sections from the template such as testing details, screenshots, and a pre-submission checklist.
Linked Issues check ✅ Passed The PR comprehensively implements all requirements from issue #159: configurable grid card label display with three states (hidden/book title/series title), applied to both book and series cards, with persistence and user preference storage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with adding configurable grid card labels. Test enhancements, component updates, type definitions, and settings UI are all necessary for the feature implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Add the required cardInfoMode field to this fixture.

DisplayPreferences now requires cardInfoMode, but this fixture (explicitly typed as DisplayPreferences) omits it. This will fail type-checking with "Property 'cardInfoMode' is missing in type ... but required in type 'DisplayPreferences'". The sibling service.test.ts fixture 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 tradeoff

Consider 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 lift

Significant 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 win

Missing explicit focus-visible styles for keyboard accessibility.

The label buttons lack explicit focus-visible styles. 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

📥 Commits

Reviewing files that changed from the base of the PR and between cbcdf01 and f2685a3.

📒 Files selected for processing (18)
  • client/src/composables/__tests__/useDisplaySettings.spec.ts
  • client/src/composables/__tests__/useDisplaySettingsSync.spec.ts
  • client/src/composables/useDisplaySettings.ts
  • client/src/composables/useDisplaySettingsSync.ts
  • client/src/features/book/components/BookCoverCard.test.ts
  • client/src/features/book/components/BookCoverCard.vue
  • client/src/features/book/components/CollapsedSeriesCard.vue
  • client/src/features/book/components/VirtualBookGrid.spec.ts
  • client/src/features/book/components/VirtualBookGrid.vue
  • client/src/features/book/components/__tests__/BookCoverCard.spec.ts
  • client/src/features/book/components/__tests__/CollapsedSeriesCard.spec.ts
  • client/src/features/series/views/SeriesDetailView.spec.ts
  • client/src/features/settings/AppearanceLayoutSettings.vue
  • client/src/features/settings/__tests__/AppearanceSettings.spec.ts
  • packages/types/src/display-preferences.ts
  • server/src/modules/user-preferences/user-preferences.controller.test.ts
  • server/src/modules/user-preferences/user-preferences.service.test.ts
  • server/src/modules/user-preferences/user-preferences.service.ts

@neonsolstice
neonsolstice merged commit c09dac6 into main Jun 1, 2026
28 checks passed
@neonsolstice
neonsolstice deleted the BO-159-grid-card-titles branch June 1, 2026 01:37
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.8.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions github-actions Bot added the released Issue or PR is included in a released version. label Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client Changes affecting the client application or frontend behavior. packages released Issue or PR is included in a released version. server Changes affecting server-side code, APIs, or backend behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add option to show book title or series title under grid cards

1 participant