chore(book): unify rating star colors across views

This commit is contained in:
neon
2026-05-15 00:16:06 -06:00
parent 7d1435cbd3
commit 9fdb3f46bb
7 changed files with 47 additions and 31 deletions
@@ -28,6 +28,7 @@ import { COVER_ASPECT_RATIO_KEY, DEFAULT_COVER_ASPECT_RATIO } from '../lib/cover
import { useRefreshMetadata } from '../composables/useRefreshMetadata'
import { usePermissions } from '@/features/auth/composables/usePermissions'
import SendBookDialog from '@/features/email/components/SendBookDialog.vue'
import { RATING_STARS, getRatingStarClass } from '@/features/book/lib/rating-stars'
const router = useRouter()
@@ -197,10 +198,10 @@ function openAuthorBrowse() {
<div v-if="!selectionMode" class="flex items-center gap-1.5 shrink-0" @click.stop>
<!-- Star rating -->
<div class="hidden sm:flex items-center gap-0.5" @mouseleave="hoverRating = null">
<Tooltip v-for="star in 5" :key="star">
<Tooltip v-for="star in RATING_STARS" :key="star">
<TooltipTrigger as-child>
<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/60'" />
<Star class="size-3" :class="getRatingStarClass(star, displayRating)" />
</button>
</TooltipTrigger>
<TooltipContent>Rate {{ star }}</TooltipContent>
@@ -134,7 +134,7 @@ describe('BookListRow — present state', () => {
it('syncs the displayed star rating when the book prop changes externally', async () => {
const wrapper = mount(BookListRow, { props: { book: presentBook }, global: globalStubs })
expect(wrapper.findAll('.fill-amber-400')).toHaveLength(0)
expect(wrapper.findAll('.fill-lime-400')).toHaveLength(0)
await wrapper.setProps({
book: {
@@ -143,6 +143,6 @@ describe('BookListRow — present state', () => {
},
})
expect(wrapper.findAll('.fill-amber-400')).toHaveLength(4)
expect(wrapper.findAll('.fill-lime-400')).toHaveLength(4)
})
})
@@ -29,6 +29,7 @@ import MetadataScoreBreakdown from '@/features/metadata-score/components/Metadat
import { useMetadataScoreWeights } from '@/features/metadata-score/composables/useMetadataScoreWeights'
import { useSafeHtml } from '@/features/book/composables/useSafeHtml'
import { useKoreaderBookProgress } from '@/features/koreader/composables/useKoreaderBookProgress'
import { RATING_STARS, getRatingStarClass } from '@/features/book/lib/rating-stars'
type FileProgress = {
percentage: number
@@ -323,7 +324,7 @@ async function setRating(star: number) {
}
}
const ratingStars = [1, 2, 3, 4, 5]
const ratingStars = RATING_STARS
const { setStatus } = useBookStatus()
@@ -794,19 +795,14 @@ watch(
@mouseenter="hoverRating = star"
@click="setRating(star)"
>
<Star class="size-4" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'" />
<Star class="size-4" :class="getRatingStarClass(star, displayRating)" />
</button>
</TooltipTrigger>
<TooltipContent>{{ isRatingLocked ? 'Rating is locked' : `Rate ${star}` }}</TooltipContent>
</Tooltip>
</template>
<template v-else>
<Star
v-for="star in ratingStars"
:key="star"
class="size-4"
:class="(localRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'"
/>
<Star v-for="star in ratingStars" :key="star" class="size-4" :class="getRatingStarClass(star, localRating)" />
</template>
</div>
<template v-if="isRatingLocked">
@@ -1156,19 +1152,14 @@ watch(
@mouseenter="hoverRating = star"
@click="setRating(star)"
>
<Star class="size-3.5" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'" />
<Star class="size-3.5" :class="getRatingStarClass(star, displayRating)" />
</button>
</TooltipTrigger>
<TooltipContent>{{ isRatingLocked ? 'Rating is locked' : `Rate ${star}` }}</TooltipContent>
</Tooltip>
</template>
<template v-else>
<Star
v-for="star in ratingStars"
:key="star"
class="size-3.5"
:class="(localRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'"
/>
<Star v-for="star in ratingStars" :key="star" class="size-3.5" :class="getRatingStarClass(star, localRating)" />
</template>
</div>
@@ -19,6 +19,7 @@ import { useNarratorSearch } from '../../../composables/useNarratorSearch'
import { useGenreSearch, useTagSearch } from '../../../composables/useTagSearch'
import { usePublisherSearch, useSeriesNameSearch, useLanguageSearch } from '../../../composables/useMetadataFieldSearch'
import InputWithSuggestions from '@/components/ui/InputWithSuggestions.vue'
import { RATING_STARS, getRatingStarClass } from '@/features/book/lib/rating-stars'
const props = defineProps<{ book: BookDetail }>()
const emit = defineEmits<{
@@ -581,7 +582,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
:class="isLocked('rating') ? 'opacity-50 cursor-not-allowed' : ''"
@mouseleave="hoverRating = null"
>
<Tooltip v-for="star in 5" :key="star">
<Tooltip v-for="star in RATING_STARS" :key="star">
<TooltipTrigger as-child>
<button
type="button"
@@ -590,10 +591,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
@mouseenter="hoverRating = star"
@click="setRating(star)"
>
<Star
class="size-5 sm:size-4"
:class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'"
/>
<Star class="size-5 sm:size-4" :class="getRatingStarClass(star, displayRating)" />
</button>
</TooltipTrigger>
<TooltipContent>Rate {{ star }}</TooltipContent>
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Star } from 'lucide-vue-next'
import { RATING_STARS, getRatingStarClass } from '@/features/book/lib/rating-stars'
const props = defineProps<{
value: number | null
@@ -11,7 +12,7 @@ const emit = defineEmits<{
save: [value: number | null]
}>()
const STARS = [1, 2, 3, 4, 5]
const STARS = RATING_STARS
const groupRef = ref<HTMLDivElement | null>(null)
function getStarFromEvent(event: Event): number | null {
@@ -73,10 +74,7 @@ function getTabIndex(star: number): number {
:aria-pressed="star <= (value ?? 0)"
type="button"
class="transition-colors"
:class="[
isReadOnly ? 'cursor-default' : 'cursor-pointer hover:scale-110',
star <= (value ?? 0) ? 'text-amber-400' : 'text-muted-foreground/30',
]"
:class="[isReadOnly ? 'cursor-default' : 'cursor-pointer hover:scale-110', getRatingStarClass(star, value, 'text-muted-foreground/30')]"
@click="handleStarClick"
@keydown="handleStarKeydown"
>
@@ -0,0 +1,28 @@
export const RATING_STARS = [1, 2, 3, 4, 5] as const
export type RatingStarValue = (typeof RATING_STARS)[number]
export const RATING_VALUE_COLOR_CLASSES: Record<RatingStarValue, string> = {
1: 'fill-red-500 text-red-500',
2: 'fill-orange-400 text-orange-400',
3: 'fill-yellow-400 text-yellow-400',
4: 'fill-lime-400 text-lime-400',
5: 'fill-green-600 text-green-600',
}
function normalizeRatingValue(rating: number | null | undefined): RatingStarValue | null {
if (rating == null) return null
const rounded = Math.round(rating)
if (rounded < 1 || rounded > 5) return null
return rounded as RatingStarValue
}
export function getRatingFilledClass(rating: number | null | undefined): string {
const value = normalizeRatingValue(rating)
return value ? RATING_VALUE_COLOR_CLASSES[value] : 'fill-amber-400 text-amber-400'
}
export function getRatingStarClass(star: number, rating: number | null | undefined, emptyClass = 'text-muted-foreground/60'): string {
if ((rating ?? 0) < star) return emptyClass
return getRatingFilledClass(rating)
}
@@ -36,8 +36,8 @@ export const themes: Theme[] = [
{
name: 'crimson',
label: 'Crimson',
light: { fg: '#212529', bg: '#ffffff', link: '#dd0031' },
dark: { fg: '#dee2e6', bg: '#343a40', link: '#ff4081' },
light: { fg: '#2f1f25', bg: '#fdf1f4', link: '#dd0031' },
dark: { fg: '#f3dbe2', bg: '#3a252d', link: '#ff5a86' },
},
{
name: 'meadow',