mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat: settings drawer — mobile-responsive two-step nav
This commit is contained in:
@@ -11,7 +11,7 @@ const stack = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="px-5 py-6 sm:px-10 sm:py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">About</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Information about this application.</p>
|
||||
|
||||
@@ -8,7 +8,7 @@ const { coverSize, gridGap } = useDisplaySettings()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="px-5 py-6 sm:px-10 sm:py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">Appearance</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Customize how the app looks and feels.</p>
|
||||
|
||||
@@ -16,7 +16,7 @@ const backgrounds = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="px-5 py-6 sm:px-10 sm:py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">Comics Reader</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Default settings applied when opening CBZ, CBR, and CB7 files.</p>
|
||||
|
||||
@@ -17,7 +17,7 @@ const themes = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="px-5 py-6 sm:px-10 sm:py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">eBook Reader</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Default settings applied when opening EPUB and MOBI files.</p>
|
||||
|
||||
@@ -37,7 +37,7 @@ async function scanAll() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="px-5 py-6 sm:px-10 sm:py-8 max-w-3xl">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
|
||||
@@ -18,7 +18,7 @@ const zoomOptions = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="px-5 py-6 sm:px-10 sm:py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">PDF Reader</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Default settings applied when opening PDF files.</p>
|
||||
|
||||
@@ -18,7 +18,7 @@ const fileTypes = ref([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="px-5 py-6 sm:px-10 sm:py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">Scanner</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Configure when and how your libraries are scanned for new content.</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { X, Library, ScanLine, Paintbrush, BookOpen, FileText, BookImage, Info, ChevronLeft } from 'lucide-vue-next'
|
||||
import { X, ChevronLeft, ChevronRight, Library, ScanLine, Paintbrush, BookOpen, FileText, BookImage, Info } from 'lucide-vue-next'
|
||||
import { useSettingsDrawer } from '@/composables/useSettingsDrawer'
|
||||
import LibrariesSettings from './LibrariesSettings.vue'
|
||||
import ScannerSettings from './ScannerSettings.vue'
|
||||
@@ -39,22 +39,32 @@ const navGroups = [
|
||||
|
||||
const allItems = navGroups.flatMap((g) => g.items)
|
||||
const activeId = ref<SectionId>('libraries')
|
||||
const activeLabel = ref('Libraries')
|
||||
const ActiveComponent = shallowRef(LibrariesSettings)
|
||||
const mobileView = ref<'nav' | 'content'>('nav')
|
||||
|
||||
function navigate(id: SectionId) {
|
||||
const item = allItems.find((i) => i.id === id)!
|
||||
activeId.value = id
|
||||
ActiveComponent.value = allItems.find((i) => i.id === id)!.component
|
||||
activeLabel.value = item.label
|
||||
ActiveComponent.value = item.component
|
||||
mobileView.value = 'content'
|
||||
}
|
||||
|
||||
watch(isOpen, (v) => {
|
||||
if (v) navigate('libraries')
|
||||
if (v) {
|
||||
activeId.value = 'libraries'
|
||||
activeLabel.value = 'Libraries'
|
||||
ActiveComponent.value = LibrariesSettings
|
||||
mobileView.value = 'nav'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="drawer-fade">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex justify-end" @click.self="close">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex justify-end">
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/40 backdrop-blur-[2px]" @click="close" />
|
||||
|
||||
@@ -62,11 +72,65 @@ watch(isOpen, (v) => {
|
||||
<Transition name="drawer-slide">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="relative flex h-full w-full max-w-[900px] shadow-2xl overflow-hidden"
|
||||
class="relative flex h-full w-full md:max-w-[900px] shadow-2xl overflow-hidden bg-background"
|
||||
style="border-left: 1px solid var(--border)"
|
||||
>
|
||||
<!-- Left nav -->
|
||||
<nav class="flex flex-col w-52 shrink-0 bg-muted/40 border-r border-border h-full">
|
||||
<!-- ── MOBILE: Nav list ─────────────────────────── -->
|
||||
<Transition name="mobile-nav">
|
||||
<div v-if="mobileView === 'nav'" class="md:hidden absolute inset-0 flex flex-col bg-background z-10">
|
||||
<div class="flex items-center justify-between px-4 h-14 border-b border-border shrink-0">
|
||||
<span class="text-base font-semibold text-foreground font-serif">Settings</span>
|
||||
<button
|
||||
class="flex items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
@click="close"
|
||||
>
|
||||
<X :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div v-for="group in navGroups" :key="group.label" class="mt-6 mb-1">
|
||||
<p class="px-4 pb-1 text-[11px] font-semibold text-muted-foreground/60 uppercase tracking-widest">
|
||||
{{ group.label }}
|
||||
</p>
|
||||
<button
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
class="w-full flex items-center gap-3 px-4 py-3.5 border-b border-border/60 hover:bg-muted/50 active:bg-muted transition-colors"
|
||||
@click="navigate(item.id)"
|
||||
>
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 shrink-0">
|
||||
<component :is="item.icon" :size="15" class="text-primary" />
|
||||
</div>
|
||||
<span class="flex-1 text-sm font-medium text-foreground text-left">{{ item.label }}</span>
|
||||
<ChevronRight :size="15" class="text-muted-foreground/40" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="px-4 py-6 text-[11px] text-muted-foreground/40">projectx · v0.1.0</p>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- ── MOBILE: Content view ─────────────────────── -->
|
||||
<Transition name="mobile-content">
|
||||
<div v-if="mobileView === 'content'" class="md:hidden absolute inset-0 flex flex-col bg-background z-10">
|
||||
<div class="flex items-center gap-1 px-2 h-14 border-b border-border shrink-0">
|
||||
<button
|
||||
class="flex items-center gap-1 px-2 py-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
@click="mobileView = 'nav'"
|
||||
>
|
||||
<ChevronLeft :size="18" />
|
||||
<span class="text-sm">Settings</span>
|
||||
</button>
|
||||
<span class="flex-1 text-sm font-medium text-foreground text-center pr-16">{{ activeLabel }}</span>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<component :is="ActiveComponent" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- ── DESKTOP: Two-column ─────────────────────── -->
|
||||
<nav class="hidden md:flex flex-col w-52 shrink-0 bg-muted/40 border-r border-border h-full">
|
||||
<div class="px-4 pt-5 pb-4 border-b border-border flex items-center justify-between">
|
||||
<span class="text-sm font-semibold text-foreground font-serif">Settings</span>
|
||||
<button
|
||||
@@ -76,7 +140,6 @@ watch(isOpen, (v) => {
|
||||
<X :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto py-3 px-2">
|
||||
<div v-for="group in navGroups" :key="group.label" class="mb-4">
|
||||
<p class="px-2 mb-1 text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-widest">
|
||||
@@ -99,14 +162,12 @@ watch(isOpen, (v) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 border-t border-border">
|
||||
<p class="text-[10px] text-muted-foreground/40">projectx · v0.1.0</p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="flex-1 overflow-y-auto bg-background">
|
||||
<main class="hidden md:block flex-1 overflow-y-auto">
|
||||
<component :is="ActiveComponent" />
|
||||
</main>
|
||||
</div>
|
||||
@@ -134,4 +195,28 @@ watch(isOpen, (v) => {
|
||||
.drawer-slide-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* Mobile nav → content: nav slides left, content enters from right */
|
||||
.mobile-nav-leave-active,
|
||||
.mobile-content-enter-active {
|
||||
transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.mobile-nav-leave-to {
|
||||
transform: translateX(-30%);
|
||||
}
|
||||
.mobile-content-enter-from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* Mobile content → nav: content slides right, nav enters from left */
|
||||
.mobile-content-leave-active,
|
||||
.mobile-nav-enter-active {
|
||||
transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.mobile-content-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.mobile-nav-enter-from {
|
||||
transform: translateX(-30%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,28 +5,28 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1767748212922,
|
||||
"when": 1767806438944,
|
||||
"tag": "0000_init_schema",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1767748212922,
|
||||
"when": 1767806438944,
|
||||
"tag": "0001_lethal_pixie",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1767748212922,
|
||||
"when": 1767806438944,
|
||||
"tag": "0002_powerful_thundra",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1767748212922,
|
||||
"when": 1767806438944,
|
||||
"tag": "0003_reading_progress_by_file",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user