feat: library background patterns with settings picker

This commit is contained in:
neon
2026-01-08 11:29:28 -06:00
parent c6224bd6fb
commit 015028240c
5 changed files with 103 additions and 6 deletions
+34 -1
View File
@@ -331,11 +331,44 @@ html.radius-rounded {
--shadow-2xl: var(--elevation-2xl);
}
.dot-grid {
.pattern-dots {
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
background-size: 24px 24px;
}
.pattern-grid {
background-image: linear-gradient(var(--dot-color) 1px, transparent 1px), linear-gradient(90deg, var(--dot-color) 1px, transparent 1px);
background-size: 32px 32px;
}
.pattern-cross {
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px), radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
background-size: 24px 24px;
background-position:
0 0,
12px 12px;
}
.pattern-gradient {
background-image: radial-gradient(ellipse 70% 55% at 0% 0%, color-mix(in oklch, var(--primary) 20%, transparent), transparent 65%);
}
.pattern-aurora {
background-image:
radial-gradient(ellipse 60% 45% at 0% 50%, color-mix(in oklch, var(--primary) 15%, transparent), transparent 100%),
radial-gradient(ellipse 50% 40% at 100% 50%, color-mix(in oklch, var(--primary) 10%, transparent), transparent 100%),
radial-gradient(ellipse 40% 35% at 50% 0%, color-mix(in oklch, var(--primary) 8%, transparent), transparent 100%);
}
.pattern-rings {
background-image: repeating-radial-gradient(circle at 50% 50%, transparent 0, transparent 28px, var(--dot-color) 29px, transparent 30px);
}
.pattern-noise {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/></filter><rect width='200' height='200' filter='url(%23n)' opacity='0.05'/></svg>");
background-size: 200px 200px;
}
.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%);
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ACCENT_OPTIONS, RADIUS_OPTIONS, useThemeStore } from '@/stores/theme'
import { ACCENT_OPTIONS, BACKGROUND_OPTIONS, RADIUS_OPTIONS, useThemeStore } from '@/stores/theme'
import { Moon, Sun } from 'lucide-vue-next'
import { useDisplaySettings } from '@/composables/useDisplaySettings'
@@ -95,6 +95,40 @@ const { coverSize, gridGap } = useDisplaySettings()
<!-- Library view -->
<div>
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Library View</p>
<!-- Background pattern -->
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border mb-4">
<div class="px-5 py-4 bg-card">
<div class="flex items-center justify-between mb-3">
<div>
<p class="text-sm font-medium text-foreground">Background pattern</p>
<p class="text-xs text-muted-foreground mt-0.5">Pattern shown behind the book grid</p>
</div>
</div>
<div class="flex items-center gap-3 flex-wrap">
<button
v-for="opt in BACKGROUND_OPTIONS"
:key="opt.id"
:title="opt.label"
type="button"
class="flex flex-col items-center gap-1.5 cursor-pointer"
@click="themeStore.setBackground(opt.id)"
>
<div
class="w-14 h-10 rounded-md overflow-hidden transition-all ring-2"
:class="themeStore.background === opt.id ? 'ring-primary shadow-sm shadow-primary/20' : 'ring-border hover:ring-muted-foreground/40'"
>
<div class="w-full h-full bg-background" :class="opt.cssClass" />
</div>
<span
class="text-[10px] font-medium transition-colors"
:class="themeStore.background === opt.id ? 'text-primary' : 'text-muted-foreground'"
>{{ opt.label }}</span
>
</button>
</div>
</div>
</div>
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
<!-- Cover size -->
<div class="px-5 py-4 bg-card">
+28 -1
View File
@@ -5,6 +5,7 @@ import { storage } from '@/services/storage'
type Theme = 'light' | 'dark'
export type Accent = 'neutral' | 'violet' | 'blue' | 'cyan' | 'green' | 'amber' | 'orange' | 'rose'
export type Radius = 'sharp' | 'default' | 'rounded'
export type Background = 'none' | 'dots' | 'grid' | 'cross' | 'gradient' | 'aurora' | 'rings' | 'noise'
export const ACCENT_OPTIONS: { id: Accent; label: string; color: string }[] = [
{ id: 'neutral', label: 'Neutral', color: '#a8956e' },
@@ -23,8 +24,20 @@ export const RADIUS_OPTIONS: { id: Radius; label: string }[] = [
{ id: 'rounded', label: 'Rounded' },
]
export const BACKGROUND_OPTIONS: { id: Background; label: string; cssClass: string }[] = [
{ id: 'none', label: 'None', cssClass: '' },
{ id: 'dots', label: 'Dots', cssClass: 'pattern-dots' },
{ id: 'grid', label: 'Grid', cssClass: 'pattern-grid' },
{ id: 'cross', label: 'Cross', cssClass: 'pattern-cross' },
{ id: 'gradient', label: 'Gradient', cssClass: 'pattern-gradient' },
{ id: 'aurora', label: 'Aurora', cssClass: 'pattern-aurora' },
{ id: 'rings', label: 'Rings', cssClass: 'pattern-rings' },
{ id: 'noise', label: 'Noise', cssClass: 'pattern-noise' },
]
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)
export const useThemeStore = defineStore('theme', () => {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
@@ -36,6 +49,9 @@ export const useThemeStore = defineStore('theme', () => {
const storedRadius = storage.get<Radius>('radius', 'default')
const radius = ref<Radius>(RADIUS_IDS.includes(storedRadius) ? storedRadius : 'default')
const storedBackground = storage.get<Background>('background', 'dots')
const background = ref<Background>(BACKGROUND_IDS.includes(storedBackground) ? storedBackground : 'dots')
function applyTheme(t: Theme) {
document.documentElement.classList.toggle('dark', t === 'dark')
}
@@ -62,6 +78,10 @@ export const useThemeStore = defineStore('theme', () => {
radius.value = r
}
function setBackground(b: Background) {
background.value = b
}
watch(
theme,
(t) => {
@@ -86,6 +106,13 @@ export const useThemeStore = defineStore('theme', () => {
},
{ immediate: true },
)
watch(
background,
(b) => {
storage.set('background', b)
},
{ immediate: true },
)
return { theme, accent, radius, toggleTheme, setAccent, setRadius }
return { theme, accent, radius, background, toggleTheme, setAccent, setRadius, setBackground }
})
+5 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import BookCoverCard from '@/features/book/components/BookCoverCard.vue'
import AppHeader from '@/components/AppHeader.vue'
import AppSidebar from '@/components/AppSidebar.vue'
@@ -7,7 +7,10 @@ import SettingsDrawer from '@/features/settings/SettingsDrawer.vue'
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
import { useBooks } from '@/features/book/composables/useBooks'
import { useDisplaySettings } from '@/composables/useDisplaySettings'
import { BACKGROUND_OPTIONS, useThemeStore } from '@/stores/theme'
const themeStore = useThemeStore()
const backgroundClass = computed(() => BACKGROUND_OPTIONS.find((b) => b.id === themeStore.background)?.cssClass ?? '')
const { coverSize, gridGap, viewMode } = useDisplaySettings()
const libraryId = ref<number | null>(null)
@@ -60,7 +63,7 @@ watch(search, () => {
v-model:viewMode="viewMode"
/>
<main class="flex-1 overflow-y-auto px-4 py-4 dot-grid">
<main class="flex-1 overflow-y-auto px-4 py-4" :class="backgroundClass">
<div v-if="error" class="text-sm text-destructive mb-4">{{ error }}</div>
<!-- Grid view -->
+1 -1
View File
@@ -5,7 +5,7 @@
{
"idx": 0,
"version": "7",
"when": 1767927442636,
"when": 1767939980131,
"tag": "0000_init_schema",
"breakpoints": true
}