fix(client): adjust cover defaults and metadata menu/layout

This commit is contained in:
neon
2026-05-10 23:52:15 -06:00
parent 35100a0fd0
commit 4fd91b6d1f
6 changed files with 26 additions and 10 deletions
+6 -4
View File
@@ -6,7 +6,9 @@ export type AuthorCoverShape = 'square' | 'circle'
export type CoverSizeScope = 'per-view' | 'synced'
export type TableDensity = 'compact' | 'comfortable' | 'roomy'
const DEFAULT_BOOK_COVER_SIZE = 130
const DEFAULT_PORTRAIT_COVER_SIZE = 130
const DEFAULT_SQUARE_COVER_SIZE = 150
const DEFAULT_GRID_GAP = 28
const CARD_OVERLAY_KEYS: CardOverlayKey[] = ['progress-bar', 'format', 'rating', 'read-status', 'lock-status']
const DEFAULT_CARD_OVERLAYS: CardOverlayKey[] = ['progress-bar', 'format', 'rating', 'read-status']
@@ -24,10 +26,10 @@ function normalizeCardOverlays(value: unknown): CardOverlayKey[] {
return normalized
}
const portraitCoverSize = ref(Math.max(storage.get('portraitCoverSize', DEFAULT_BOOK_COVER_SIZE), 100))
const squareCoverSize = ref(Math.max(storage.get('squareCoverSize', DEFAULT_BOOK_COVER_SIZE), 100))
const portraitCoverSize = ref(Math.max(storage.get('portraitCoverSize', DEFAULT_PORTRAIT_COVER_SIZE), 100))
const squareCoverSize = ref(Math.max(storage.get('squareCoverSize', DEFAULT_SQUARE_COVER_SIZE), 100))
const coverSizeScope = ref<CoverSizeScope>(storage.get('coverSizeScope', 'per-view'))
const gridGap = ref(storage.get('gridGap', 20))
const gridGap = ref(storage.get('gridGap', DEFAULT_GRID_GAP))
const portraitGridGap = ref(storage.get('portraitGridGap', gridGap.value))
const squareGridGap = ref(storage.get('squareGridGap', gridGap.value))
export type BookViewMode = 'grid' | 'list' | 'table'
@@ -23,13 +23,15 @@ function legacyGridGapKey(type: ViewType, id: number) {
return `bookorbit:gridGap:${type}:${id}`
}
const DEFAULT_GRID_GAP = 28
export function useViewDisplaySettings(
viewType: ViewType,
viewId: Readonly<Ref<number | null>>,
coverAspectRatio: Readonly<Ref<CoverAspectRatio>> = ref(DEFAULT_COVER_ASPECT_RATIO),
) {
const { portraitCoverSize, squareCoverSize, coverSizeScope, portraitGridGap, squareGridGap } = useDisplaySettings()
const fallbackGridGap = storage.get('gridGap', 20)
const fallbackGridGap = storage.get('gridGap', DEFAULT_GRID_GAP)
const coverShapeGroup = computed<CoverShapeGroup>(() => (coverAspectRatio.value === '1/1' ? 'square' : 'portrait'))
const globalCoverSizeForGroup = computed(() => (coverShapeGroup.value === 'square' ? squareCoverSize.value : portraitCoverSize.value))
@@ -504,7 +504,7 @@ async function handleSetStatus(status: ReadStatus) {
<Send class="size-4 mr-2" />
Send via Email
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSeparator v-if="hasPermission('email_send') || hasPermission('library_delete_books')" />
<DropdownMenuItem
v-if="hasPermission('library_delete_books')"
class="text-destructive focus:text-destructive"
@@ -25,6 +25,9 @@ vi.mock('@/features/book/lib/book-cover', () => ({
}),
titleFontSizeClass: () => 'text-[11cqi]',
}))
vi.mock('@/features/auth/composables/usePermissions', () => ({
usePermissions: () => ({ hasPermission: () => false }),
}))
// Stub complex UI sub-components
const globalStubs = {
@@ -33,7 +36,7 @@ const globalStubs = {
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuSeparator: { template: '<div />' },
DropdownMenuSeparator: { template: '<div data-test="dropdown-separator" />' },
DropdownMenuSub: { template: '<div><slot /></div>' },
DropdownMenuSubTrigger: { template: '<div><slot /></div>' },
DropdownMenuSubContent: { template: '<div><slot /></div>' },
@@ -190,6 +193,14 @@ describe('BookCoverCard — present state', () => {
expect(wrapper.find('div.absolute.bottom-2.right-2.z-20').exists()).toBe(true)
})
it('does not render a trailing separator when no post-status actions are visible', () => {
const wrapper = mountCard(presentBook)
expect(wrapper.text()).toContain('Set Status')
expect(wrapper.text()).not.toContain('Send via Email')
expect(wrapper.text()).not.toContain('Delete')
expect(wrapper.findAll('[data-test="dropdown-separator"]')).toHaveLength(0)
})
it('renders an orange lock pill when lock-status overlay is enabled and metadata is locked', () => {
cardOverlays.value = ['lock-status']
@@ -748,7 +748,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
</MetadataFieldLabel>
<MetadataFieldLabel
v-if="isPrimaryAudio"
class="sm:w-24 sm:shrink-0"
class="sm:w-30 sm:shrink-0"
label="Duration (s)"
field="durationSeconds"
:locked="isLocked('durationSeconds')"
@@ -782,9 +782,9 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
v-model="form.abridged"
type="checkbox"
class="h-4 w-4 rounded border-input accent-primary"
aria-label="Abridged"
:disabled="isLocked('abridged')"
/>
<label for="abridged-check" class="ml-2 text-sm text-foreground select-none">Abridged</label>
</div>
</MetadataFieldLabel>
</div>
+2 -1
View File
@@ -147,6 +147,7 @@ export const BACKGROUND_OPTIONS: { id: Background; label: string; cssClass: stri
const ACCENT_IDS = ACCENT_OPTIONS.map((a) => a.id)
const RADIUS_IDS = RADIUS_OPTIONS.map((r) => r.id)
const BACKGROUND_IDS = BACKGROUND_OPTIONS.map((b) => b.id)
const DEFAULT_SURFACE_BRIGHTNESS = 35
export const useThemeStore = defineStore('theme', () => {
const theme = ref<Theme>(storage.get<Theme>('theme', 'dark'))
@@ -160,7 +161,7 @@ export const useThemeStore = defineStore('theme', () => {
const storedBackground = storage.get<Background>('background', 'vinyl')
const background = ref<Background>(BACKGROUND_IDS.includes(storedBackground) ? storedBackground : 'dots')
const brightness = ref<number>(storage.get<number>('brightness', 50))
const brightness = ref<number>(storage.get<number>('brightness', DEFAULT_SURFACE_BRIGHTNESS))
function applyTheme(t: Theme) {
document.documentElement.classList.toggle('dark', t === 'dark')