mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat: library background patterns with settings picker
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1767927442636,
|
||||
"when": 1767939980131,
|
||||
"tag": "0000_init_schema",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user