feat: refine theme tokens and apply consistent styling

This commit is contained in:
neon
2026-03-18 06:57:19 -06:00
parent e61486fae9
commit 416952a3a0
58 changed files with 172 additions and 149 deletions
+17 -14
View File
@@ -78,16 +78,6 @@
background-image: radial-gradient(ellipse 80% 50% at 50% -10%, color-mix(in oklch, var(--primary) 20%, transparent) 0%, transparent 100%);
}
.pattern-scanlines::before {
content: '';
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background: linear-gradient(to bottom, transparent 50%, color-mix(in oklch, var(--primary) 5%, transparent) 50%);
background-size: 100% 4px;
}
.pattern-brushed {
background-image: repeating-linear-gradient(
45deg,
@@ -222,6 +212,20 @@
background-image: repeating-linear-gradient(90deg, var(--primary), var(--primary) 1px, transparent 1px, transparent 40px);
}
.pattern-scanlines::before {
content: '';
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background: linear-gradient(
to bottom,
transparent 50%,
color-mix(in oklch, var(--primary) 5%, transparent) 50%
);
background-size: 100% 4px;
}
.pattern-vinyl::before {
content: '';
position: fixed;
@@ -232,7 +236,7 @@
circle at 50% -25%,
transparent,
transparent 3px,
color-mix(in oklch, var(--primary) 7%, transparent) 3.75px,
color-mix(in oklch, var(--primary) 8%, transparent) 3.75px,
transparent 4.5px
);
}
@@ -287,8 +291,7 @@
}
.glow-wrapper {
background-image:
url('/topbar.png'), radial-gradient(50% 50% at center -25px, color-mix(in oklch, var(--primary) 35%, transparent) 0%, transparent 100%);
background-image: radial-gradient(50% 50% at center -25px, color-mix(in oklch, var(--primary) 35%, transparent) 0%, transparent 100%);
background-blend-mode: hard-light, color-dodge;
background-position: top;
background-repeat: no-repeat;
@@ -350,7 +353,7 @@
────────────────────────────────────────────────────────────── */
.input-field {
@apply h-9 rounded-md border border-input bg-background px-3 text-sm text-foreground
placeholder:text-muted-foreground/50
placeholder:text-muted-foreground/70
focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary
disabled:opacity-50 transition-colors;
}
+22 -22
View File
@@ -31,7 +31,7 @@
--secondary-foreground: oklch(0.21 var(--tint-c-surface) var(--tint-h));
--muted: oklch(0.955 var(--tint-c-surface) var(--tint-h));
--muted-foreground: oklch(0.52 0.008 var(--tint-h));
--muted-foreground: oklch(0.52 0.001 var(--tint-h));
--accent: oklch(0.955 var(--tint-c-surface) var(--tint-h));
--accent-foreground: oklch(0.21 var(--tint-c-surface) var(--tint-h));
@@ -93,30 +93,30 @@
}
.dark {
--tint-c: 0.02;
--tint-c: 0.0175;
--tint-c-surface: var(--tint-c);
--bg-lift: 0;
--background: oklch(calc(0.145 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--foreground: oklch(0.985 0.004 var(--tint-h));
--foreground: oklch(0.985 0.001 var(--tint-h));
--card: oklch(calc(0.205 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--card-foreground: oklch(0.985 0.004 var(--tint-h));
--card: oklch(calc(0.18 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--card-foreground: oklch(0.985 0.001 var(--tint-h));
--popover: oklch(calc(0.205 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--popover-foreground: oklch(0.985 0.004 var(--tint-h));
--popover: oklch(calc(0.18 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--popover-foreground: oklch(0.985 0.001 var(--tint-h));
--primary: oklch(0.91 var(--tint-c-surface) var(--tint-h));
--primary-foreground: oklch(0.205 var(--tint-c-surface) var(--tint-h));
--primary-foreground: oklch(0.18 var(--tint-c-surface) var(--tint-h));
--secondary: oklch(calc(0.265 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--secondary-foreground: oklch(0.985 0.004 var(--tint-h));
--secondary: oklch(calc(0.245 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--secondary-foreground: oklch(0.985 0.001 var(--tint-h));
--muted: oklch(calc(0.265 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--muted-foreground: oklch(0.68 0.008 var(--tint-h));
--muted: oklch(calc(0.245 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--muted-foreground: oklch(0.725 0.001 var(--tint-h));
--accent: oklch(calc(0.265 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--accent-foreground: oklch(0.985 0.004 var(--tint-h));
--accent: oklch(calc(0.245 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--accent-foreground: oklch(0.985 0.001 var(--tint-h));
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
@@ -125,14 +125,14 @@
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.52 0.008 var(--tint-h));
--sidebar: oklch(calc(0.205 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--sidebar-foreground: oklch(0.985 0.004 var(--tint-h));
--sidebar: oklch(calc(0.18 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--sidebar-foreground: oklch(0.985 0.001 var(--tint-h));
--sidebar-primary: oklch(0.91 var(--tint-c-surface) var(--tint-h));
--sidebar-primary-foreground: oklch(0.205 var(--tint-c-surface) var(--tint-h));
--sidebar-accent: oklch(calc(0.265 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--sidebar-accent-foreground: oklch(0.985 0.004 var(--tint-h));
--sidebar-primary-foreground: oklch(0.18 var(--tint-c-surface) var(--tint-h));
--sidebar-accent: oklch(calc(0.245 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--sidebar-accent-foreground: oklch(0.985 0.001 var(--tint-h));
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.52 0.008 var(--tint-h));
--sidebar-ring: oklch(0.52 0.001 var(--tint-h));
--chart-1: oklch(0.7 0.16 var(--tint-h));
--chart-2: oklch(0.68 0.15 calc(var(--tint-h) + 72));
@@ -154,9 +154,9 @@
--chart-icon-8: oklch(0.7 0.17 calc(var(--tint-h) + 315));
/* Elevation surfaces */
--surface-1: oklch(calc(0.205 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--surface-1: oklch(calc(0.18 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--surface-2: oklch(calc(0.235 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--surface-3: oklch(calc(0.265 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--surface-3: oklch(calc(0.245 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
--surface-4: oklch(calc(0.295 + var(--bg-lift)) var(--tint-c-surface) var(--tint-h));
/* Shadows */
+13 -8
View File
@@ -229,7 +229,7 @@ function formatBadgeClass(fmt: string): string {
<span v-else>{{ seg.text }}</span>
</template>
</p>
<p v-if="result.seriesName" class="text-xs text-muted-foreground/70 truncate mt-0.5 italic">
<p v-if="result.seriesName" class="text-xs text-muted-foreground/85 truncate mt-0.5 italic">
<template v-for="seg in highlightSegments(result.seriesName, globalSearchQuery)" :key="seg.text + seg.match">
<span v-if="seg.match" class="bg-primary/20 text-foreground font-semibold rounded-sm px-0.5 not-italic">{{ seg.text }}</span>
<span v-else>{{ seg.text }}</span>
@@ -266,7 +266,7 @@ function formatBadgeClass(fmt: string): string {
class="hidden md:flex flex-1 mx-4 relative items-center transition-all duration-300"
:class="searchFocused || globalSearchQuery ? 'max-w-xl' : 'max-w-sm'"
>
<Search class="absolute left-3 text-muted-foreground/60 pointer-events-none" :size="14" />
<Search class="absolute left-3 text-muted-foreground/80 pointer-events-none" :size="14" />
<input
ref="desktopSearchInput"
v-model="globalSearchQuery"
@@ -274,17 +274,22 @@ function formatBadgeClass(fmt: string): string {
@blur="onSearchBlur"
@keydown="handleSearchKeydown"
placeholder="Search all books..."
class="w-full h-8 pl-9 pr-8 text-[13px] rounded-full border-none bg-primary/5 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1.5 focus:ring-primary/30 transition-all duration-300 shadow-inner shadow-black/5"
class="w-full h-8 pl-9 pr-8 text-[13.5px] rounded-full border-none bg-primary/5 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1.5 focus:ring-primary/30 transition-all duration-300 shadow-inner shadow-black/5"
/>
<div class="absolute right-2.5 flex items-center gap-1.5">
<button v-if="globalSearchQuery" @click="clearSearch()" class="text-muted-foreground/60 hover:text-foreground transition-colors">
<div class="absolute inset-y-0 right-2.5 flex items-center gap-1.5">
<button
v-if="globalSearchQuery"
@click="clearSearch()"
class="flex items-center justify-center text-muted-foreground/75 hover:text-foreground transition-colors"
>
<X :size="13" />
</button>
<kbd
v-else
class="hidden lg:inline-flex h-4.5 select-none items-center gap-1 rounded border border-sidebar-border/50 bg-background/50 px-1.5 font-mono text-[9px] font-medium text-muted-foreground/40 opacity-100"
class="hidden lg:inline-flex h-5.5 select-none items-center gap-1 rounded border border-sidebar-border/50 bg-background/50 px-2 font-mono text-[11px] font-bold text-muted-foreground/85 opacity-100"
>
<span class="text-xs">⌘</span>K
<span class="text-[16px] leading-none">⌘</span>
<span class="text-[12px] leading-none">K</span>
</kbd>
</div>
@@ -329,7 +334,7 @@ function formatBadgeClass(fmt: string): string {
<span v-else>{{ seg.text }}</span>
</template>
</p>
<p v-if="result.seriesName" class="text-xs text-muted-foreground/70 truncate mt-0.5 italic">
<p v-if="result.seriesName" class="text-xs text-muted-foreground/85 truncate mt-0.5 italic">
<template v-for="seg in highlightSegments(result.seriesName, globalSearchQuery)" :key="seg.text + seg.match">
<span v-if="seg.match" class="bg-primary/20 text-foreground font-semibold rounded-sm px-0.5 not-italic">{{ seg.text }}</span>
<span v-else>{{ seg.text }}</span>
+4 -4
View File
@@ -182,7 +182,7 @@ onUnmounted(() => stopUploadCompletedListener())
<span class="text-lg font-serif font-semibold text-sidebar-foreground leading-tight tracking-tight">
Project<span class="text-primary"> X</span>
</span>
<span class="mt-0.5 text-[10px] uppercase tracking-[0.16em] text-sidebar-foreground/50">Library Control</span>
<span class="mt-0.5 text-[10px] uppercase tracking-[0.16em] text-sidebar-foreground/65">Library Control</span>
</div>
</div>
</SidebarHeader>
@@ -234,7 +234,7 @@ onUnmounted(() => stopUploadCompletedListener())
</SidebarGroupContent>
</SidebarGroup>
<SidebarSeparator class="group-data-[collapsible=icon]:hidden" />
<SidebarSeparator />
<!-- Libraries -->
<SidebarGroup>
@@ -315,7 +315,7 @@ onUnmounted(() => stopUploadCompletedListener())
</div>
</SidebarGroup>
<SidebarSeparator class="group-data-[collapsible=icon]:hidden" />
<SidebarSeparator />
<!-- Lenses -->
<SidebarGroup>
@@ -374,7 +374,7 @@ onUnmounted(() => stopUploadCompletedListener())
</div>
</SidebarGroup>
<SidebarSeparator class="group-data-[collapsible=icon]:hidden" />
<SidebarSeparator />
<!-- Collections -->
<SidebarGroup>
+3 -3
View File
@@ -128,7 +128,7 @@ onUnmounted(() => document.removeEventListener('mousedown', handleOutsideClick))
<component v-if="selectedIconComponent" :is="selectedIconComponent" :size="16" class="shrink-0 text-foreground" />
<span v-else class="h-4 w-4 rounded bg-muted/60 shrink-0" />
<span v-if="modelValue" class="flex-1 text-left text-foreground truncate">{{ modelValue }}</span>
<span v-else class="flex-1 text-left text-muted-foreground/40 truncate">{{ placeholder ?? 'Choose an icon...' }}</span>
<span v-else class="flex-1 text-left text-muted-foreground/60 truncate">{{ placeholder ?? 'Choose an icon...' }}</span>
<button
v-if="modelValue"
type="button"
@@ -152,10 +152,10 @@ onUnmounted(() => document.removeEventListener('mousedown', handleOutsideClick))
v-model="query"
type="text"
placeholder="Search icons..."
class="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground/40 focus:outline-none"
class="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground/60 focus:outline-none"
/>
<div class="flex items-center gap-2 shrink-0">
<span class="text-[11px] text-muted-foreground/60">{{ filteredIcons.length.toLocaleString() }}</span>
<span class="text-[11px] text-muted-foreground/80">{{ filteredIcons.length.toLocaleString() }}</span>
<button v-if="query" type="button" @click="query = ''" class="text-muted-foreground hover:text-foreground transition-colors">
<X :size="13" />
</button>
+7 -7
View File
@@ -76,7 +76,7 @@ watch(
<button
:disabled="count === 0"
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
:class="count > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/30 cursor-not-allowed'"
:class="count > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/60 cursor-not-allowed'"
@click="emit('send')"
>
<Mail :size="ICON_SIZE" />
@@ -90,7 +90,7 @@ watch(
<button
:disabled="count === 0"
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
:class="count > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/30 cursor-not-allowed'"
:class="count > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/60 cursor-not-allowed'"
@click="exportMenuOpen = true"
>
<Download :size="ICON_SIZE" />
@@ -104,7 +104,7 @@ watch(
<button
:disabled="count === 0"
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
:class="count > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/30 cursor-not-allowed'"
:class="count > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/60 cursor-not-allowed'"
@click="emit('add-to-collection')"
>
<FolderPlus :size="ICON_SIZE" />
@@ -121,7 +121,7 @@ watch(
:class="
count > 0
? 'text-destructive hover:bg-destructive hover:text-destructive-foreground'
: 'text-muted-foreground/30 cursor-not-allowed'
: 'text-muted-foreground/60 cursor-not-allowed'
"
@click="emit('remove-from-collection')"
>
@@ -138,7 +138,7 @@ watch(
<button
:disabled="count === 0"
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
:class="count > 0 ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/30 cursor-not-allowed'"
:class="count > 0 ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/60 cursor-not-allowed'"
@click="emit('refresh-metadata')"
>
<RefreshCw :size="ICON_SIZE" />
@@ -152,7 +152,7 @@ watch(
<button
:disabled="count === 0"
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
:class="count > 0 ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/30 cursor-not-allowed'"
:class="count > 0 ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/60 cursor-not-allowed'"
@click="emit('re-extract-cover')"
>
<ImageDown :size="ICON_SIZE" />
@@ -171,7 +171,7 @@ watch(
:class="
count > 0
? 'text-destructive hover:bg-destructive hover:text-destructive-foreground'
: 'text-muted-foreground/30 cursor-not-allowed'
: 'text-muted-foreground/60 cursor-not-allowed'
"
@click="confirmingDelete = true"
>
+5 -5
View File
@@ -45,7 +45,7 @@ function getIconComponent(name: string) {
<div class="sticky top-0 z-20 mb-2 mt-2 flex h-10 shrink-0 items-center gap-2 p-2 transition-all duration-300 bg-background/80 backdrop-blur-md">
<!-- Left: optional icon + title + count -->
<div class="flex items-center gap-2 flex-1 min-w-0">
<component v-if="icon" :is="getIconComponent(icon)" :size="16" class="shrink-0 text-muted-foreground/90" />
<component v-if="icon" :is="getIconComponent(icon)" :size="16" class="shrink-0 text-muted-foreground" />
<span class="font-bold text-[16px] text-foreground/90 tracking-tight truncate">{{ title }}</span>
<span class="text-[12px] font-semibold text-primary/70 tabular-nums">({{ total.toLocaleString() }})</span>
</div>
@@ -63,7 +63,7 @@ function getIconComponent(name: string) {
: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'
: 'text-muted-foreground/80 hover:text-foreground hover:bg-primary/5'
"
@click="emit('toggle-selection')"
>
@@ -80,7 +80,7 @@ function getIconComponent(name: string) {
variant="ghost"
size="icon"
class="h-8 w-8 rounded-lg"
:class="viewMode === 'grid' ? 'text-primary bg-primary/10' : 'text-muted-foreground/50 hover:text-foreground hover:bg-primary/5'"
:class="viewMode === 'grid' ? 'text-primary bg-primary/10' : 'text-muted-foreground/70 hover:text-foreground hover:bg-primary/5'"
@click="emit('update:viewMode', 'grid')"
>
<LayoutGrid :size="14" />
@@ -89,7 +89,7 @@ function getIconComponent(name: string) {
variant="ghost"
size="icon"
class="h-8 w-8 rounded-lg"
:class="viewMode === 'list' ? 'text-primary bg-primary/10' : 'text-muted-foreground/50 hover:text-foreground hover:bg-primary/5'"
:class="viewMode === 'list' ? 'text-primary bg-primary/10' : 'text-muted-foreground/70 hover:text-foreground hover:bg-primary/5'"
@click="emit('update:viewMode', 'list')"
>
<List :size="14" />
@@ -102,7 +102,7 @@ function getIconComponent(name: string) {
<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"
class="hidden md:flex h-8 w-8 rounded-lg text-muted-foreground/70 hover:text-foreground hover:bg-primary/5"
>
<SlidersHorizontal :size="14" />
</Button>
@@ -11,20 +11,31 @@ withDefaults(
iconClass?: string
label: string
}>(),
{ iconSize: 16, iconClass: '' },
{ iconSize: 17, iconClass: '' },
)
const emit = defineEmits<{ click: [] }>()
const buttonClass = [
// layout & shape
'relative h-8.5 gap-1 rounded-xl pl-0.5 pr-2 group/item',
// transitions & hover
'transition-[background-color,box-shadow] duration-200 hover:bg-primary/8',
// active indicator pill (::before pseudo-element)
'before:absolute before:-left-1 before:top-1/2 before:h-4 before:w-[2.5px]',
'before:-translate-y-1/2 before:scale-y-75 before:rounded-full before:bg-primary',
'before:opacity-0 before:transition-all before:duration-200',
// active state
'data-[active=true]:bg-primary/12 data-[active=true]:shadow-[inset_0_0_0_1px_var(--sidebar-border)]',
'data-[active=true]:before:scale-y-100 data-[active=true]:before:opacity-100',
// collapsed icon mode
'group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-1.5',
].join(' ')
</script>
<template>
<SidebarMenuItem>
<SidebarMenuButton
:is-active="isActive"
:tooltip="tooltip"
class="relative h-8.5 gap-1 rounded-xl pl-0.5 pr-2 transition-[background-color,box-shadow] duration-200 before:absolute before:-left-1 before:top-1/2 before:h-4 before:w-[2.5px] before:-translate-y-1/2 before:scale-y-75 before:rounded-full before:bg-primary before:opacity-0 before:transition-all before:duration-200 hover:bg-primary/8 data-[active=true]:bg-primary/12 data-[active=true]:shadow-[inset_0_0_0_1px_var(--sidebar-border)] data-[active=true]:before:scale-y-100 data-[active=true]:before:opacity-100 group/item group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:px-1.5"
@click="emit('click')"
>
<SidebarMenuButton :is-active="isActive" :tooltip="tooltip" :class="buttonClass" @click="emit('click')">
<span class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg transition-colors">
<component
:is="icon"
@@ -34,7 +45,7 @@ const emit = defineEmits<{ click: [] }>()
/>
</span>
<span
class="text-sm font-medium tracking-tight text-sidebar-foreground/90 transition-colors group-hover/item:text-sidebar-foreground group-data-[active=true]/item:text-primary group-data-[collapsible=icon]:hidden"
class="text-[14px] tracking-tight text-sidebar-foreground/85 transition-colors group-hover/item:text-sidebar-foreground group-data-[active=true]/item:text-primary group-data-[collapsible=icon]:hidden"
>
{{ label }}
</span>
@@ -21,7 +21,7 @@ const hasMenu = () => props.canAdd || props.canReorder
<div class="flex h-8.5 items-center px-2 group-data-[collapsible=icon]:hidden">
<button class="group/hdr flex min-w-0 flex-1 items-center gap-1.5" @click="emit('toggle')">
<span
class="text-[11px] font-medium uppercase tracking-[0.16em] text-sidebar-foreground/45 transition-colors group-hover/hdr:text-sidebar-foreground/80"
class="text-[11px] font-medium uppercase tracking-[0.16em] text-sidebar-foreground/65 transition-colors group-hover/hdr:text-sidebar-foreground/80"
>
{{ label }}
</span>
@@ -8,7 +8,11 @@ const props = defineProps<{
</script>
<template>
<div data-slot="sidebar-group" data-sidebar="group" :class="cn('relative flex w-full min-w-0 flex-col p-2', props.class)">
<div
data-slot="sidebar-group"
data-sidebar="group"
:class="cn('relative flex w-full min-w-0 flex-col py-2 px-3 group-data-[collapsible=icon]:px-2', props.class)"
>
<slot />
</div>
</template>
@@ -9,7 +9,7 @@ const props = defineProps<{
</script>
<template>
<Separator data-slot="sidebar-separator" data-sidebar="separator" :class="cn('bg-sidebar-border mx-2 my-1 w-auto', props.class)">
<Separator data-slot="sidebar-separator" data-sidebar="separator" :class="cn('bg-sidebar-border my-1 w-auto', props.class)">
<slot />
</Separator>
</template>
@@ -75,7 +75,7 @@ function onMinBookCountChange(event: Event) {
:value="minBookCount ?? ''"
min="1"
placeholder="Any"
class="w-14 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground/70"
class="w-14 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground/85"
@change="onMinBookCountChange"
/>
</div>
@@ -86,8 +86,8 @@ function handleClick(event: MouseEvent) {
</span>
</div>
<Loader2 v-if="!selectionMode && refreshing" :size="14" class="animate-spin text-muted-foreground/70" />
<ArrowRight v-else-if="!selectionMode" :size="14" class="text-muted-foreground/60" />
<Loader2 v-if="!selectionMode && refreshing" :size="14" class="animate-spin text-muted-foreground/85" />
<ArrowRight v-else-if="!selectionMode" :size="14" class="text-muted-foreground/80" />
</div>
</button>
</template>
@@ -478,14 +478,14 @@ watch(
>
<template #toolbar>
<div class="hidden lg:flex h-8 w-64 items-center rounded-md border border-input bg-background px-2.5">
<Search :size="13" class="mr-1.5 shrink-0 text-muted-foreground/70" />
<Search :size="13" class="mr-1.5 shrink-0 text-muted-foreground/85" />
<input
v-model="q"
type="search"
placeholder="Search authors"
class="h-full w-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground/70"
class="h-full w-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground/85"
/>
<button v-if="q.trim()" class="ml-1 text-muted-foreground/70 transition-colors hover:text-foreground" @click="q = ''">
<button v-if="q.trim()" class="ml-1 text-muted-foreground/85 transition-colors hover:text-foreground" @click="q = ''">
<X :size="12" />
</button>
</div>
@@ -696,7 +696,7 @@ watch(
:disabled="selectedCount === 0"
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
:class="
selectedCount > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/30 cursor-not-allowed'
selectedCount > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/60 cursor-not-allowed'
"
@click="selectAllVisible"
>
@@ -711,7 +711,7 @@ watch(
<button
:disabled="selectedCount === 0 || bulkRefreshing"
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
:class="selectedCount > 0 ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/30 cursor-not-allowed'"
:class="selectedCount > 0 ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/60 cursor-not-allowed'"
@click="refreshSelectedAuthorsMetadata"
>
<RefreshCcw :size="17" :class="bulkRefreshing ? 'animate-spin' : ''" />
@@ -728,7 +728,7 @@ watch(
:class="
selectedCount > 0
? 'text-destructive hover:bg-destructive hover:text-destructive-foreground'
: 'text-muted-foreground/30 cursor-not-allowed'
: 'text-muted-foreground/60 cursor-not-allowed'
"
@click="confirmingBulkDelete = true"
>
@@ -194,7 +194,7 @@ onMounted(() => {
<div class="flex flex-col items-start gap-0.5 min-w-0">
<span class="text-xs font-semibold text-foreground leading-none">Fetching metadata</span>
<span class="text-xs text-muted-foreground leading-none">{{ bookRemainingLabel }}</span>
<span class="text-xs text-muted-foreground/60 leading-none truncate mt-0.5" :class="lastBookName ? '' : 'invisible'">{{
<span class="text-xs text-muted-foreground/80 leading-none truncate mt-0.5" :class="lastBookName ? '' : 'invisible'">{{
lastBookName ?? '\u00a0'
}}</span>
</div>
@@ -220,7 +220,7 @@ onMounted(() => {
<div class="flex flex-col items-start gap-0.5 min-w-0">
<span class="text-xs font-semibold text-foreground leading-none">Enriching authors</span>
<span class="text-xs text-muted-foreground leading-none">{{ authorRemainingLabel }}</span>
<span class="text-xs text-muted-foreground/60 leading-none truncate mt-0.5" :class="lastAuthorName ? '' : 'invisible'">{{
<span class="text-xs text-muted-foreground/80 leading-none truncate mt-0.5" :class="lastAuthorName ? '' : 'invisible'">{{
lastAuthorName ?? '\u00a0'
}}</span>
</div>
@@ -386,7 +386,7 @@ function showValueToInput(operator: RuleOperator): boolean {
<button
@click="removeNode(index)"
class="ml-auto h-8 w-8 flex items-center justify-center rounded-md text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 transition-colors shrink-0"
class="ml-auto h-8 w-8 flex items-center justify-center rounded-md text-muted-foreground/70 hover:text-destructive hover:bg-destructive/10 transition-colors shrink-0"
>
<Trash2 :size="13" />
</button>
@@ -402,7 +402,7 @@ function showValueToInput(operator: RuleOperator): boolean {
</div>
<button
@click="removeNode(index)"
class="h-8 w-8 flex items-center justify-center rounded-md text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 transition-colors shrink-0 mt-1"
class="h-8 w-8 flex items-center justify-center rounded-md text-muted-foreground/70 hover:text-destructive hover:bg-destructive/10 transition-colors shrink-0 mt-1"
>
<Trash2 :size="13" />
</button>
@@ -173,7 +173,7 @@ function openAuthorBrowse() {
<Tooltip v-for="star in 5" :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/40'" />
<Star class="size-3" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'" />
</button>
</TooltipTrigger>
<TooltipContent>Rate {{ star }}</TooltipContent>
@@ -242,7 +242,7 @@ function openDetails() {
v-for="star in ratingStars"
:key="star"
class="size-3"
:class="detail.rating >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/30'"
:class="detail.rating >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'"
/>
<span class="text-[10px] text-muted-foreground ml-1">{{ detail.rating }}/5</span>
</div>
@@ -111,7 +111,7 @@ const canAddMore = computed(() => props.modelValue.length < SORT_FIELDS.length)
<TooltipTrigger as-child>
<button
@click="remove(index)"
class="h-8 w-8 flex items-center justify-center rounded-md text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 transition-colors shrink-0"
class="h-8 w-8 flex items-center justify-center rounded-md text-muted-foreground/70 hover:text-destructive hover:bg-destructive/10 transition-colors shrink-0"
>
<Trash2 :size="13" />
</button>
@@ -18,7 +18,7 @@ defineProps<{
</span>
<template v-for="(rule, i) in node.rules" :key="i">
<span v-if="i > 0" class="text-[10px] text-muted-foreground/40 font-medium select-none">
<span v-if="i > 0" class="text-[10px] text-muted-foreground/60 font-medium select-none">
{{ node.join === 'AND' ? '·' : '|' }}
</span>
@@ -42,7 +42,7 @@ async function navigateToBook(id: number) {
@click="prevId && navigateToBook(prevId)"
:disabled="!prevId"
class="h-7 w-7 flex items-center justify-center rounded-md transition-colors"
:class="prevId ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/30 cursor-not-allowed'"
:class="prevId ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/60 cursor-not-allowed'"
title="Previous book"
>
<ChevronLeft :size="16" />
@@ -51,7 +51,7 @@ async function navigateToBook(id: number) {
@click="nextId && navigateToBook(nextId)"
:disabled="!nextId"
class="h-7 w-7 flex items-center justify-center rounded-md transition-colors"
:class="nextId ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/30 cursor-not-allowed'"
:class="nextId ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/60 cursor-not-allowed'"
title="Next book"
>
<ChevronRight :size="16" />
@@ -603,7 +603,7 @@ watch(
<span class="ml-1 font-medium text-foreground">{{ authorLine }}</span>
</p>
<template v-if="seriesLine">
<span class="text-muted-foreground/40 text-xs">·</span>
<span class="text-muted-foreground/60 text-xs">·</span>
<span class="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground">{{ seriesLine }}</span>
</template>
</div>
@@ -613,7 +613,7 @@ watch(
<Tooltip v-for="star in ratingStars" :key="star">
<TooltipTrigger as-child>
<button type="button" class="p-0.5 transition-colors" @mouseenter="hoverRating = star" @click="setRating(star)">
<Star class="size-3.5" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/30'" />
<Star class="size-3.5" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'" />
</button>
</TooltipTrigger>
<TooltipContent>Rate {{ star }}</TooltipContent>
@@ -624,7 +624,7 @@ watch(
v-for="star in ratingStars"
:key="star"
class="size-3.5"
:class="(localRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/30'"
:class="(localRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'"
/>
</template>
</div>
@@ -223,7 +223,7 @@ async function autoFill() {
<Tooltip v-for="star in 5" :key="star">
<TooltipTrigger as-child>
<button type="button" class="p-0.5 transition-colors" @mouseenter="hoverRating = star" @click="setRating(star)">
<Star class="size-4" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/40'" />
<Star class="size-4" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'" />
</button>
</TooltipTrigger>
<TooltipContent>Rate {{ star }}</TooltipContent>
@@ -178,7 +178,7 @@ async function toggleWriteLog() {
<div class="min-w-0 flex-1">
<p class="text-sm font-medium truncate">{{ file.filename ?? '-' }}</p>
<p class="text-[11px] font-mono text-muted-foreground/60 truncate mt-0.5" :title="file.absolutePath">{{ file.absolutePath }}</p>
<p class="text-[11px] font-mono text-muted-foreground/80 truncate mt-0.5" :title="file.absolutePath">{{ file.absolutePath }}</p>
<p class="text-xs text-muted-foreground mt-1">
{{ formatBytes(file.sizeBytes) }}
<span class="mx-1 opacity-40">·</span>
@@ -212,7 +212,7 @@ async function toggleWriteLog() {
<div v-if="book.files.length === 0" class="flex flex-col items-center justify-center py-20 text-center">
<div class="flex items-center justify-center w-12 h-12 rounded-xl bg-muted mb-3">
<Files class="size-5 text-muted-foreground/50" />
<Files class="size-5 text-muted-foreground/70" />
</div>
<p class="text-sm font-medium">No files attached</p>
<p class="text-xs text-muted-foreground mt-1">This book has no associated files.</p>
@@ -82,7 +82,7 @@ const lightboxSrc = ref<string | null>(null)
:class="!field.hasDiff ? 'bg-muted/30 opacity-50' : field.isCopied ? 'bg-muted/30 opacity-40' : 'bg-background ring-1 ring-border'"
>
<p class="text-[10px] font-medium text-muted-foreground mb-0.5 sm:hidden">Current</p>
<p class="wrap-break-word leading-snug text-sm w-full" :class="!field.currentDisplay ? 'text-muted-foreground/40 italic' : 'text-foreground'">
<p class="wrap-break-word leading-snug text-sm w-full" :class="!field.currentDisplay ? 'text-muted-foreground/60 italic' : 'text-foreground'">
{{ field.currentDisplay || 'empty' }}
</p>
</div>
@@ -120,7 +120,7 @@ function handleBookAction(book: BookCard, action: BookActionType) {
<!-- Empty -->
<div v-else-if="books.length === 0" class="flex flex-col items-center justify-center py-10 gap-3 text-center">
<div class="h-12 w-12 rounded-full bg-muted flex items-center justify-center">
<component :is="typeIcon" :size="20" class="text-muted-foreground/40" />
<component :is="typeIcon" :size="20" class="text-muted-foreground/60" />
</div>
<p class="text-sm text-muted-foreground">
<template v-if="type === 'continue-reading'">No books in progress yet. Start reading one to see it here.</template>
@@ -157,17 +157,17 @@ function resetToDefault() {
<!-- Drag handle (desktop) + up/down arrows (mobile fallback) -->
<div class="flex shrink-0 flex-col items-center">
<button
class="touch-reorder-btn flex h-5 w-5 items-center justify-center rounded text-muted-foreground/50 transition-colors hover:bg-muted hover:text-foreground disabled:opacity-20"
class="touch-reorder-btn flex h-5 w-5 items-center justify-center rounded text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground disabled:opacity-20"
:disabled="index === 0"
@click="moveUp(index)"
>
<ChevronUp :size="13" />
</button>
<div class="drag-handle cursor-grab text-muted-foreground/40 hover:text-muted-foreground active:cursor-grabbing">
<div class="drag-handle cursor-grab text-muted-foreground/60 hover:text-muted-foreground active:cursor-grabbing">
<GripVertical :size="16" />
</div>
<button
class="touch-reorder-btn flex h-5 w-5 items-center justify-center rounded text-muted-foreground/50 transition-colors hover:bg-muted hover:text-foreground disabled:opacity-20"
class="touch-reorder-btn flex h-5 w-5 items-center justify-center rounded text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground disabled:opacity-20"
:disabled="index === draft.length - 1"
@click="moveDown(index)"
>
@@ -200,7 +200,7 @@ function resetToDefault() {
<!-- Remove -->
<button
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground/50 transition-colors hover:bg-destructive/10 hover:text-destructive disabled:pointer-events-none disabled:opacity-30"
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-destructive/10 hover:text-destructive disabled:pointer-events-none disabled:opacity-30"
:disabled="draft.length <= 1"
@click="removeScroller(index)"
>
@@ -212,7 +212,7 @@ function availableRecipients(group: EmailGroup) {
<UserPlus :size="12" />
Add member
</button>
<p v-else class="text-xs text-muted-foreground/60">All recipients are already in this group.</p>
<p v-else class="text-xs text-muted-foreground/80">All recipients are already in this group.</p>
</div>
</div>
</div>
@@ -180,7 +180,7 @@ function close() {
class="text-xs font-semibold text-muted-foreground uppercase tracking-wider cursor-pointer hover:text-foreground transition-colors list-none flex items-center gap-1"
>
<span>Options</span>
<span class="text-muted-foreground/50 group-open:rotate-90 transition-transform inline-block">›</span>
<span class="text-muted-foreground/70 group-open:rotate-90 transition-transform inline-block">›</span>
</summary>
<div class="mt-3 space-y-3">
<!-- File picker -->
@@ -170,7 +170,7 @@ async function save() {
v-model="draftName"
type="text"
placeholder="e.g. Unread Sci-Fi"
class="h-10 rounded-lg border border-input bg-card text-foreground text-sm px-3 focus:outline-none focus:ring-2 focus:ring-primary placeholder:text-muted-foreground/40"
class="h-10 rounded-lg border border-input bg-card text-foreground text-sm px-3 focus:outline-none focus:ring-2 focus:ring-primary placeholder:text-muted-foreground/60"
/>
</div>
<div class="flex-1 flex flex-col gap-1.5">
@@ -180,7 +180,7 @@ function formatPillClass(filename: string): string {
cb7: 'bg-sky-500/15 text-sky-600 dark:text-sky-400',
fb2: 'bg-teal-500/15 text-teal-600 dark:text-teal-400',
}
return map[ext] ?? 'bg-muted text-muted-foreground/70'
return map[ext] ?? 'bg-muted text-muted-foreground/85'
}
</script>
@@ -327,7 +327,7 @@ function formatPillClass(filename: string): string {
<div class="shrink-0 flex items-center gap-0.5">
<button
v-if="item.status === 'error'"
class="flex items-center justify-center w-6 h-6 rounded text-muted-foreground/70 hover:text-primary hover:bg-primary/10 transition-colors"
class="flex items-center justify-center w-6 h-6 rounded text-muted-foreground/85 hover:text-primary hover:bg-primary/10 transition-colors"
title="Retry"
@click="retryFile(item.id)"
>
@@ -335,7 +335,7 @@ function formatPillClass(filename: string): string {
</button>
<button
v-if="item.status !== 'uploading'"
class="flex items-center justify-center w-6 h-6 rounded text-muted-foreground/50 hover:text-foreground hover:bg-muted transition-colors"
class="flex items-center justify-center w-6 h-6 rounded text-muted-foreground/70 hover:text-foreground hover:bg-muted transition-colors"
@click="removeFile(item.id)"
>
<X :size="12" />
@@ -114,7 +114,7 @@ function selectCurrent() {
<TooltipContent>Go up</TooltipContent>
</Tooltip>
<template v-for="(crumb, i) in breadcrumbs" :key="crumb.path">
<ChevronRight v-if="i > 0" :size="12" class="text-muted-foreground/50 shrink-0" />
<ChevronRight v-if="i > 0" :size="12" class="text-muted-foreground/70 shrink-0" />
<button
class="text-xs px-1 py-0.5 rounded transition-colors shrink-0 whitespace-nowrap"
:class="i === breadcrumbs.length - 1 ? 'text-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-muted'"
@@ -132,7 +132,7 @@ function selectCurrent() {
v-model="search"
type="text"
placeholder="Filter folders…"
class="flex-1 bg-transparent text-xs text-foreground placeholder:text-muted-foreground/50 focus:outline-none"
class="flex-1 bg-transparent text-xs text-foreground placeholder:text-muted-foreground/70 focus:outline-none"
/>
<button v-if="search" class="text-muted-foreground hover:text-foreground" @click="search = ''">
<X :size="12" />
@@ -163,7 +163,7 @@ function selectCurrent() {
<Folder :size="15" class="text-primary/70 shrink-0 group-hover:hidden" />
<FolderOpen :size="15" class="text-primary shrink-0 hidden group-hover:block" />
<span class="flex-1 text-sm text-foreground text-left truncate">{{ entry.name }}</span>
<ChevronRight :size="13" class="text-muted-foreground/40 shrink-0" />
<ChevronRight :size="13" class="text-muted-foreground/60 shrink-0" />
</button>
</div>
</div>
@@ -69,7 +69,7 @@ onMounted(loadAccess)
<template>
<div class="px-6 py-6 space-y-5">
<div v-if="!libraryId" class="rounded-lg border border-dashed border-border px-5 py-8 text-center">
<UserPlus :size="22" class="text-muted-foreground/30 mx-auto mb-2" />
<UserPlus :size="22" class="text-muted-foreground/60 mx-auto mb-2" />
<p class="text-sm text-muted-foreground">Access can be configured after the library is created.</p>
</div>
@@ -68,7 +68,7 @@ const selectedIconComponent = computed(() => (props.icon ? (LucideIcons as Recor
:value="name"
placeholder="My Library"
maxlength="255"
class="flex-1 rounded-lg border border-border bg-background px-4 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/40 focus:outline-none focus:ring-2 focus:ring-ring"
class="flex-1 rounded-lg border border-border bg-background px-4 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-2 focus:ring-ring"
@input="emit('update:name', ($event.target as HTMLInputElement).value)"
/>
</div>
@@ -95,7 +95,7 @@ const selectedIconComponent = computed(() => (props.icon ? (LucideIcons as Recor
v-model="search"
type="text"
placeholder="Search icons…"
class="flex-1 bg-transparent text-xs text-foreground placeholder:text-muted-foreground/50 focus:outline-none"
class="flex-1 bg-transparent text-xs text-foreground placeholder:text-muted-foreground/70 focus:outline-none"
/>
<button v-if="search" class="text-muted-foreground hover:text-foreground" @click="search = ''">
<X :size="12" />
@@ -105,7 +105,7 @@ function onManualKeydown(e: KeyboardEvent) {
v-model="manualPath"
type="text"
placeholder="/path/to/books"
class="w-full rounded-md border bg-background px-3 py-2 text-sm text-foreground font-mono placeholder:text-muted-foreground/40 placeholder:font-sans focus:outline-none focus:ring-2 focus:ring-ring transition-colors"
class="w-full rounded-md border bg-background px-3 py-2 text-sm text-foreground font-mono placeholder:text-muted-foreground/60 placeholder:font-sans focus:outline-none focus:ring-2 focus:ring-ring transition-colors"
:class="testResult === 'ok' ? 'border-emerald-500' : testResult === 'error' ? 'border-destructive' : 'border-border'"
@input="onManualInput"
@keydown="onManualKeydown"
@@ -172,7 +172,7 @@ function onManualKeydown(e: KeyboardEvent) {
</div>
<div v-if="folders.length === 0" class="rounded-lg border border-dashed border-border px-4 py-6 text-center">
<FolderOpen :size="22" class="text-muted-foreground/30 mx-auto mb-2" />
<FolderOpen :size="22" class="text-muted-foreground/60 mx-auto mb-2" />
<p class="text-xs text-muted-foreground">No folders added yet</p>
</div>
</div>
@@ -305,7 +305,7 @@ function onSectionEvent(id: SectionId, event: string, value: unknown) {
stepIndex === i
? 'bg-background text-foreground font-medium shadow-sm'
: mode === 'create' && i > visitedUpTo
? 'text-muted-foreground/40 cursor-not-allowed'
? 'text-muted-foreground/60 cursor-not-allowed'
: 'text-muted-foreground hover:text-foreground hover:bg-muted/60 cursor-pointer',
]"
:disabled="mode === 'create' && i > visitedUpTo"
@@ -320,13 +320,13 @@ function onSectionEvent(id: SectionId, event: string, value: unknown) {
? 'bg-primary text-primary-foreground'
: stepIndex === i
? 'bg-primary/15 text-primary ring-1 ring-primary'
: 'bg-muted-foreground/15 text-muted-foreground/40'
: 'bg-muted-foreground/15 text-muted-foreground/60'
"
>
<Check v-if="i < stepIndex" :size="10" />
<template v-else>{{ i + 1 }}</template>
</span>
<component v-else :is="section.icon" :size="14" :class="stepIndex === i ? 'text-primary' : 'text-muted-foreground/70'" />
<component v-else :is="section.icon" :size="14" :class="stepIndex === i ? 'text-primary' : 'text-muted-foreground/85'" />
{{ section.label }}
<div v-if="stepIndex === i" class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 rounded-r bg-primary" />
@@ -114,7 +114,7 @@ function onPatternKeydown(e: KeyboardEvent) {
@drop="metaDrag.onDrop()"
@dragend="metaDrag.onDragEnd()"
>
<GripVertical :size="14" class="text-muted-foreground/40 shrink-0" />
<GripVertical :size="14" class="text-muted-foreground/60 shrink-0" />
<span class="flex items-center justify-center w-5 h-5 rounded-full bg-primary/10 text-primary text-[10px] font-bold shrink-0">
{{ index + 1 }}
</span>
@@ -140,7 +140,7 @@ function onPatternKeydown(e: KeyboardEvent) {
@drop="fmtDrag.onDrop()"
@dragend="fmtDrag.onDragEnd()"
>
<GripVertical :size="14" class="text-muted-foreground/40 shrink-0" />
<GripVertical :size="14" class="text-muted-foreground/60 shrink-0" />
<span class="flex items-center justify-center w-5 h-5 rounded-full bg-primary/10 text-primary text-[10px] font-bold shrink-0">
{{ index + 1 }}
</span>
@@ -187,7 +187,7 @@ function onPatternKeydown(e: KeyboardEvent) {
v-model="newPattern"
type="text"
placeholder="**/node_modules/**"
class="flex-1 rounded-md border border-border bg-background px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-ring"
class="flex-1 rounded-md border border-border bg-background px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground/70 focus:outline-none focus:ring-2 focus:ring-ring"
@keydown="onPatternKeydown"
/>
<button
@@ -107,7 +107,7 @@ function humanReadableCron(cron: string | null): string {
type="text"
:value="autoScanCronExpression ?? ''"
placeholder="0 0 * * *"
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-ring"
class="w-full rounded-md border border-border bg-background px-3 py-2 text-sm font-mono text-foreground placeholder:text-muted-foreground/70 focus:outline-none focus:ring-2 focus:ring-ring"
@input="emit('update:autoScanCronExpression', ($event.target as HTMLInputElement).value || null)"
/>
<p class="mt-1 text-xs text-muted-foreground">Format: minute hour day month weekday</p>
@@ -115,7 +115,7 @@ const missingCount = computed(() => groups.value.flatMap((g) => g.fields).filter
<div v-for="entry in group.fields" :key="entry.field" class="flex items-center justify-between gap-2 text-sm">
<div class="flex items-center gap-1.5 min-w-0">
<Check v-if="entry.filled" class="size-3.5 text-green-500 shrink-0" />
<Minus v-else class="size-3.5 text-muted-foreground/50 shrink-0" />
<Minus v-else class="size-3.5 text-muted-foreground/70 shrink-0" />
<span :class="entry.filled ? 'text-foreground' : 'text-muted-foreground'">{{ entry.label }}</span>
</div>
<span class="text-xs text-muted-foreground shrink-0">{{ entry.weight }}</span>
@@ -93,7 +93,7 @@ function onClear() {
><mark class="bg-yellow-300 text-yellow-900 rounded px-0.5">{{ result.excerpt.match }}</mark
><span class="text-muted-foreground">{{ result.excerpt.post }}</span>
</p>
<p v-if="result.sectionTitle" class="text-xs text-muted-foreground/60 truncate">
<p v-if="result.sectionTitle" class="text-xs text-muted-foreground/80 truncate">
{{ result.sectionTitle }}
</p>
</li>
@@ -48,7 +48,7 @@ defineExpose({
v-model="query"
type="search"
placeholder="Find in document..."
class="flex-1 min-w-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground/50"
class="flex-1 min-w-0 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground/70"
style="max-width: 260px"
@keydown.enter.exact="emit('next')"
@keydown.enter.shift.exact.prevent="emit('prev')"
@@ -173,7 +173,7 @@ function toggleOverlay(key: CardOverlayKey) {
]"
:key="group.label"
>
<p class="text-[10px] font-bold text-muted-foreground/60 uppercase tracking-widest mb-2.5 ml-0.5">{{ group.label }}</p>
<p class="text-[10px] font-bold text-muted-foreground/80 uppercase tracking-widest mb-2.5 ml-0.5">{{ group.label }}</p>
<div class="flex items-center gap-4 flex-wrap">
<Tooltip v-for="opt in BACKGROUND_OPTIONS.filter((o) => group.ids.includes(o.id))" :key="opt.id">
<TooltipTrigger as-child>
@@ -258,7 +258,7 @@ onMounted(async () => {
<code class="text-xs font-mono font-semibold text-primary">{{ '{' + t.token + '}' }}</code>
<p class="text-[11px] text-muted-foreground leading-tight mt-0.5">{{ t.description }}</p>
</div>
<ClipboardCopy :size="13" class="shrink-0 text-muted-foreground/40 group-hover:text-primary/60 transition-colors" />
<ClipboardCopy :size="13" class="shrink-0 text-muted-foreground/60 group-hover:text-primary/60 transition-colors" />
</button>
</TooltipTrigger>
<TooltipContent>Copy {{ '{' + t.token + '}' }}</TooltipContent>
@@ -244,7 +244,7 @@ async function saveSettings() {
<div v-if="devices.length === 0 && !showCreateForm" class="border border-border rounded-lg px-5 py-10 bg-card text-center shadow-sm">
<div class="w-10 h-10 rounded-full bg-muted flex items-center justify-center mx-auto mb-3">
<Tablet :size="18" class="text-muted-foreground/50" />
<Tablet :size="18" class="text-muted-foreground/70" />
</div>
<p class="text-sm font-medium text-foreground">No devices yet</p>
<p class="text-xs text-muted-foreground mt-1 max-w-[240px] mx-auto">
@@ -378,7 +378,7 @@ function coverRefreshLabel(libraryId: number): string {
<!-- Empty state -->
<div v-if="libraries.length === 0" class="rounded-xl border border-dashed border-border bg-card/50 px-8 py-16 text-center">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-muted mx-auto mb-4">
<FolderOpen :size="22" class="text-muted-foreground/60" />
<FolderOpen :size="22" class="text-muted-foreground/80" />
</div>
<p class="text-sm font-medium text-foreground mb-1">No libraries yet</p>
<p class="text-sm text-muted-foreground mb-5">Add a library to start organizing your books.</p>
@@ -25,7 +25,7 @@ const open = ref(true)
<template>
<div class="border-b border-border/60 last:border-0">
<button
class="w-full flex items-center gap-2.5 px-6 py-2.5 text-[10px] font-bold text-muted-foreground/60 uppercase tracking-[0.1em] hover:bg-muted/30 hover:text-muted-foreground transition-all group"
class="w-full flex items-center gap-2.5 px-6 py-2.5 text-[10px] font-bold text-muted-foreground/80 uppercase tracking-[0.1em] hover:bg-muted/30 hover:text-muted-foreground transition-all group"
@click="open = !open"
>
<component :is="open ? ChevronDown : ChevronRight" :size="12" class="transition-transform duration-200 group-hover:scale-110" />
@@ -135,7 +135,7 @@ function onDrop(e: DragEvent) {
>
{{ PROVIDER_SHORT_LABELS[key] ?? key }}
</span>
<span v-if="preference.providers.length === 0 && !preference.enabled" class="text-xs text-muted-foreground/40 italic">Not fetched</span>
<span v-if="preference.providers.length === 0 && !preference.enabled" class="text-xs text-muted-foreground/60 italic">Not fetched</span>
<span v-if="noProviders" class="flex items-center gap-1.5 text-xs text-amber-500 font-bold uppercase tracking-tight">
<AlertTriangle :size="12" />
Empty
@@ -105,7 +105,7 @@ function removeProvider(index: number) {
</button>
</div>
<span v-if="providers.length === 0 && !disabled" class="text-xs text-muted-foreground/40 italic h-6 flex items-center px-1">
<span v-if="providers.length === 0 && !disabled" class="text-xs text-muted-foreground/60 italic h-6 flex items-center px-1">
drag a provider here
</span>
</div>
@@ -144,7 +144,7 @@ function save() {
:type="field.type"
:placeholder="field.label"
:disabled="!draft[row.key].enabled"
class="h-9 w-64 lg:w-80 rounded-md border border-input bg-background px-3 text-xs font-medium placeholder:text-muted-foreground/40 focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-40 transition-all"
class="h-9 w-64 lg:w-80 rounded-md border border-input bg-background px-3 text-xs font-medium placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-40 transition-all"
/>
</div>
</div>
@@ -276,7 +276,7 @@ function targetSummary(file: StagingFile): string {
>
{{ file.confidence }}%
</span>
<span v-else class="text-xs text-muted-foreground/40">-</span>
<span v-else class="text-xs text-muted-foreground/60">-</span>
</div>
<div class="w-7 flex justify-center shrink-0">
@@ -127,7 +127,7 @@ function clearSearch() {
<input
v-model="searchQuery"
placeholder="Search files..."
class="h-full w-32 sm:w-44 bg-transparent text-xs outline-none placeholder:text-muted-foreground/60"
class="h-full w-32 sm:w-44 bg-transparent text-xs outline-none placeholder:text-muted-foreground/80"
@input="onSearchInput"
/>
<button class="text-muted-foreground hover:text-foreground shrink-0" @click="clearSearch">
@@ -39,7 +39,7 @@ defineProps<{
</div>
<div class="flex items-center gap-2">
<slot name="controls" />
<GripVertical class="drag-handle text-muted-foreground/50 hover:text-muted-foreground size-4 cursor-grab active:cursor-grabbing" />
<GripVertical class="drag-handle text-muted-foreground/70 hover:text-muted-foreground size-4 cursor-grab active:cursor-grabbing" />
</div>
</div>
@@ -194,7 +194,7 @@ function openConfig() {
!chart.visible && 'opacity-50',
]"
>
<GripVertical class="text-muted-foreground/60 drawer-drag-handle size-4 shrink-0 cursor-grab active:cursor-grabbing" />
<GripVertical class="text-muted-foreground/80 drawer-drag-handle size-4 shrink-0 cursor-grab active:cursor-grabbing" />
<component :is="CHART_ICONS[chart.id]" :class="['size-4 shrink-0', chart.visible ? 'text-primary' : 'text-muted-foreground']" />
<span :class="['flex-1 text-sm', chart.visible ? 'text-foreground' : 'text-muted-foreground']">
{{ CHART_LABELS[chart.id] }}
@@ -40,7 +40,7 @@ const kpis = computed(() => [
<template>
<div class="relative overflow-hidden rounded-xl border bg-card">
<div class="pointer-events-none absolute inset-0 bg-gradient-to-br from-primary/8 to-transparent" />
<BarChart3 class="pointer-events-none absolute -right-4 -top-2 opacity-[0.04]" :size="160" aria-hidden="true" />
<BarChart3 class="pointer-events-none absolute -right-0 -top-2 opacity-[0.04]" :size="100" aria-hidden="true" />
<div class="relative p-4">
<div class="flex gap-3 overflow-x-auto pb-0.5 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
+1 -1
View File
@@ -214,7 +214,7 @@ watch(
<main class="flex-1 min-h-0">
<div v-if="!loading && books.length === 0" class="flex flex-col items-center justify-center py-24 gap-3 text-center">
<div class="h-16 w-16 rounded-full bg-muted flex items-center justify-center">
<FolderOpen :size="28" class="text-muted-foreground/50" />
<FolderOpen :size="28" class="text-muted-foreground/70" />
</div>
<p class="text-sm font-medium text-foreground">No books in this collection</p>
<p class="text-xs text-muted-foreground">Select books from your library and add them here.</p>
+3 -3
View File
@@ -323,7 +323,7 @@ function handleBookAction(book: BookCard, action: BookActionType) {
<button
v-if="!isDefaultSort"
@click="resetSort"
class="h-8 w-8 flex items-center justify-center rounded-md text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 transition-colors"
class="h-8 w-8 flex items-center justify-center rounded-md text-muted-foreground/70 hover:text-destructive hover:bg-destructive/10 transition-colors"
>
<X :size="13" />
</button>
@@ -406,7 +406,7 @@ function handleBookAction(book: BookCard, action: BookActionType) {
<button
v-if="hasSavedFilter"
@click="forgetSavedFilter"
class="h-6 w-6 flex items-center justify-center rounded text-muted-foreground/50 hover:text-destructive hover:bg-destructive/10 transition-colors"
class="h-6 w-6 flex items-center justify-center rounded text-muted-foreground/70 hover:text-destructive hover:bg-destructive/10 transition-colors"
>
<X :size="11" />
</button>
@@ -428,7 +428,7 @@ function handleBookAction(book: BookCard, action: BookActionType) {
<!-- Empty state: no books in library -->
<div v-else-if="!loading && books.length === 0" class="flex flex-col items-center justify-center py-24 gap-4 text-center">
<div class="h-16 w-16 rounded-full bg-muted flex items-center justify-center">
<BookOpen :size="28" class="text-muted-foreground/50" />
<BookOpen :size="28" class="text-muted-foreground/70" />
</div>
<div class="flex flex-col gap-1">
<p class="text-sm font-medium text-foreground">Your library is empty</p>
+2 -2
View File
@@ -280,7 +280,7 @@ watch(
<!-- Empty state: no rules configured -->
<div v-if="!loading && !lens?.filter && books.length === 0" class="flex flex-col items-center justify-center py-24 gap-4 text-center">
<div class="h-16 w-16 rounded-full bg-muted flex items-center justify-center">
<Settings2 :size="28" class="text-muted-foreground/50" />
<Settings2 :size="28" class="text-muted-foreground/70" />
</div>
<div class="flex flex-col gap-1">
<p class="text-sm font-medium text-foreground">No rules configured</p>
@@ -299,7 +299,7 @@ watch(
<!-- Empty state: rules set but no matches -->
<div v-else-if="!loading && books.length === 0 && lens?.filter" class="flex flex-col items-center justify-center py-24 gap-3 text-center">
<div class="h-16 w-16 rounded-full bg-muted flex items-center justify-center">
<Aperture :size="28" class="text-muted-foreground/50" />
<Aperture :size="28" class="text-muted-foreground/70" />
</div>
<p class="text-sm font-medium text-foreground">No books match this lens</p>
<p class="text-xs text-muted-foreground">Try adjusting the filter rules.</p>
+1 -1
View File
@@ -8,7 +8,7 @@ const maxWidth = computed(() => (route.meta.maxWidth as string | undefined) ?? '
</script>
<template>
<div class="flex flex-col my-2 min-h-[calc(100%-1.25rem)] overflow-hidden rounded-xl border border-border/70 bg-card/30">
<div class="flex flex-col my-2 min-h-[calc(100%-1.25rem)] overflow-hidden rounded-xl border border-border/70 bg-card/40">
<SettingsHeader />
<main class="flex-1 overflow-y-auto overflow-x-hidden">
<div class="px-6 py-6" :class="maxWidth">