mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(client): add glass sidebar/header and surface brightness control
This commit is contained in:
+14
-13
@@ -86,26 +86,27 @@
|
||||
|
||||
.dark {
|
||||
--tint-c: 0.01;
|
||||
--bg-lift: 0;
|
||||
|
||||
--background: oklch(0.145 var(--tint-c) var(--tint-h));
|
||||
--background: oklch(calc(0.145 + var(--bg-lift)) var(--tint-c) var(--tint-h));
|
||||
--foreground: oklch(0.985 0.004 var(--tint-h));
|
||||
|
||||
--card: oklch(0.205 0.012 var(--tint-h));
|
||||
--card: oklch(calc(0.205 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--card-foreground: oklch(0.985 0.004 var(--tint-h));
|
||||
|
||||
--popover: oklch(0.205 0.012 var(--tint-h));
|
||||
--popover: oklch(calc(0.205 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--popover-foreground: oklch(0.985 0.004 var(--tint-h));
|
||||
|
||||
--primary: oklch(0.91 var(--tint-c) var(--tint-h));
|
||||
--primary-foreground: oklch(0.205 0.012 var(--tint-h));
|
||||
|
||||
--secondary: oklch(0.265 0.012 var(--tint-h));
|
||||
--secondary: oklch(calc(0.265 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--secondary-foreground: oklch(0.985 0.004 var(--tint-h));
|
||||
|
||||
--muted: oklch(0.265 0.012 var(--tint-h));
|
||||
--muted: oklch(calc(0.265 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--muted-foreground: oklch(0.68 0.008 var(--tint-h));
|
||||
|
||||
--accent: oklch(0.265 0.012 var(--tint-h));
|
||||
--accent: oklch(calc(0.265 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--accent-foreground: oklch(0.985 0.004 var(--tint-h));
|
||||
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
@@ -115,11 +116,11 @@
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.52 0.008 var(--tint-h));
|
||||
|
||||
--sidebar: oklch(0.205 0.012 var(--tint-h));
|
||||
--sidebar: oklch(calc(0.205 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--sidebar-foreground: oklch(0.985 0.004 var(--tint-h));
|
||||
--sidebar-primary: oklch(0.91 var(--tint-c) var(--tint-h));
|
||||
--sidebar-primary-foreground: oklch(0.205 0.012 var(--tint-h));
|
||||
--sidebar-accent: oklch(0.265 0.012 var(--tint-h));
|
||||
--sidebar-accent: oklch(calc(0.265 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--sidebar-accent-foreground: oklch(0.985 0.004 var(--tint-h));
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.52 0.008 var(--tint-h));
|
||||
@@ -131,10 +132,10 @@
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
|
||||
/* Elevation surfaces */
|
||||
--surface-1: oklch(0.205 0.012 var(--tint-h));
|
||||
--surface-2: oklch(0.235 0.012 var(--tint-h));
|
||||
--surface-3: oklch(0.265 0.012 var(--tint-h));
|
||||
--surface-4: oklch(0.295 0.012 var(--tint-h));
|
||||
--surface-1: oklch(calc(0.205 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--surface-2: oklch(calc(0.235 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--surface-3: oklch(calc(0.265 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
--surface-4: oklch(calc(0.295 + var(--bg-lift)) 0.012 var(--tint-h));
|
||||
|
||||
/* Shadows */
|
||||
--elevation-xs: 0 1px 2px oklch(0 0 0 / 0.4);
|
||||
@@ -144,7 +145,7 @@
|
||||
--elevation-xl: 0 20px 30px -5px oklch(0 0 0 / 0.6), 0 8px 12px -6px oklch(0 0 0 / 0.4);
|
||||
--elevation-2xl: 0 25px 50px -12px oklch(0 0 0 / 0.7);
|
||||
|
||||
--dot-color: oklch(0.3 0.01 var(--tint-h));
|
||||
--dot-color: oklch(calc(0.3 + var(--bg-lift)) 0.01 var(--tint-h));
|
||||
}
|
||||
|
||||
/* ── Accent presets ──────────────────────────────────────────
|
||||
|
||||
@@ -65,7 +65,9 @@ function navigateToResult(result: GlobalSearchResult) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="flex h-14 shrink-0 items-center gap-2 border-b border-primary/20 bg-background/90 backdrop-blur-md px-3 shadow-sm sticky top-0 z-10">
|
||||
<header
|
||||
class="flex h-14 shrink-0 items-center gap-2 border-b border-border/50 bg-background/70 backdrop-blur-xl backdrop-saturate-150 px-3 shadow-sm sticky top-0 z-10"
|
||||
>
|
||||
<!-- Mobile: search active overlay -->
|
||||
<template v-if="mobileSearchOpen">
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8 shrink-0" @click="closeMobileSearch()">
|
||||
|
||||
@@ -117,7 +117,7 @@ onMounted(async () => {
|
||||
style="--sidebar-border: color-mix(in oklch, var(--primary) 18%, transparent)"
|
||||
class="[&>div:last-child]:shadow-[4px_0_12px_-2px_oklch(0_0_0/0.08)]"
|
||||
>
|
||||
<SidebarHeader class="border-b border-sidebar-border">
|
||||
<SidebarHeader class="border-b border-sidebar-border bg-gradient-to-b from-primary/6 to-transparent">
|
||||
<div class="flex items-center gap-2.5 px-1 py-1">
|
||||
<!-- Logo mark -->
|
||||
<div
|
||||
@@ -148,7 +148,9 @@ onMounted(async () => {
|
||||
<SidebarContent>
|
||||
<!-- Libraries -->
|
||||
<SidebarGroup class="pt-2">
|
||||
<SidebarGroupLabel class="text-[10px] uppercase tracking-widest text-sidebar-foreground/35 font-medium group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel
|
||||
class="text-[10px] uppercase tracking-widest text-sidebar-foreground/75 font-semibold group-data-[collapsible=icon]:hidden"
|
||||
>
|
||||
Libraries
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupAction v-if="hasPermission('manage_libraries')" tooltip="New Library" @click="createLibraryOpen = true">
|
||||
@@ -182,7 +184,8 @@ onMounted(async () => {
|
||||
</span>
|
||||
<span
|
||||
v-else-if="lib.bookCount !== undefined"
|
||||
class="ml-auto shrink-0 rounded-full bg-sidebar-foreground/10 px-2 py-0.5 text-xs tabular-nums text-sidebar-foreground/70 group-data-[collapsible=icon]:hidden"
|
||||
class="ml-auto shrink-0 rounded-full px-2 py-0.5 text-xs tabular-nums group-data-[collapsible=icon]:hidden transition-colors"
|
||||
:class="activeLibraryId === lib.id ? 'bg-primary/15 text-primary' : 'bg-sidebar-foreground/10 text-sidebar-foreground/70'"
|
||||
>
|
||||
{{ lib.bookCount.toLocaleString() }}
|
||||
</span>
|
||||
@@ -212,7 +215,9 @@ onMounted(async () => {
|
||||
|
||||
<!-- Lenses -->
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel class="text-[10px] uppercase tracking-widest text-sidebar-foreground/35 font-medium group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel
|
||||
class="text-[10px] uppercase tracking-widest text-sidebar-foreground/75 font-semibold group-data-[collapsible=icon]:hidden"
|
||||
>
|
||||
Lenses
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupAction tooltip="New Lens" @click="createLensOpen = true">
|
||||
@@ -237,7 +242,8 @@ onMounted(async () => {
|
||||
</span>
|
||||
<span
|
||||
v-if="lens.bookCount !== undefined"
|
||||
class="ml-auto shrink-0 rounded-full bg-sidebar-foreground/10 px-2 py-0.5 text-xs tabular-nums text-sidebar-foreground/70 group-data-[collapsible=icon]:hidden"
|
||||
class="ml-auto shrink-0 rounded-full px-2 py-0.5 text-xs tabular-nums group-data-[collapsible=icon]:hidden transition-colors"
|
||||
:class="activeLensId === lens.id ? 'bg-primary/15 text-primary' : 'bg-sidebar-foreground/10 text-sidebar-foreground/70'"
|
||||
>
|
||||
{{ lens.bookCount.toLocaleString() }}
|
||||
</span>
|
||||
@@ -254,7 +260,9 @@ onMounted(async () => {
|
||||
|
||||
<!-- Collections -->
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel class="text-[10px] uppercase tracking-widest text-sidebar-foreground/35 font-medium group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel
|
||||
class="text-[10px] uppercase tracking-widest text-sidebar-foreground/75 font-semibold group-data-[collapsible=icon]:hidden"
|
||||
>
|
||||
Collections
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupAction tooltip="New Collection" @click="createCollectionOpen = true">
|
||||
@@ -278,7 +286,8 @@ onMounted(async () => {
|
||||
{{ collection.name }}
|
||||
</span>
|
||||
<span
|
||||
class="ml-auto shrink-0 rounded-full bg-sidebar-foreground/10 px-2 py-0.5 text-xs tabular-nums text-sidebar-foreground/70 group-data-[collapsible=icon]:hidden"
|
||||
class="ml-auto shrink-0 rounded-full px-2 py-0.5 text-xs tabular-nums group-data-[collapsible=icon]:hidden transition-colors"
|
||||
:class="activeCollectionId === collection.id ? 'bg-primary/15 text-primary' : 'bg-sidebar-foreground/10 text-sidebar-foreground/70'"
|
||||
>
|
||||
{{ collection.bookCount.toLocaleString() }}
|
||||
</span>
|
||||
|
||||
@@ -24,7 +24,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
<div
|
||||
v-if="collapsible === 'none'"
|
||||
data-slot="sidebar"
|
||||
:class="cn('bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', props.class)"
|
||||
:class="cn('bg-sidebar/80 backdrop-blur-xl backdrop-saturate-150 text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col', props.class)"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<slot />
|
||||
@@ -36,7 +36,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
:side="side"
|
||||
class="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
class="bg-sidebar/90 backdrop-blur-xl backdrop-saturate-150 text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
:style="{
|
||||
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||
}"
|
||||
@@ -60,19 +60,8 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
:data-variant="variant"
|
||||
:data-side="side"
|
||||
>
|
||||
<!-- This is what handles the sidebar gap on desktop -->
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear',
|
||||
'group-data-[collapsible=offcanvas]:w-0',
|
||||
'group-data-[side=right]:rotate-180',
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<!-- Zero-width sentinel — keeps the peer element in the DOM for CSS selectors -->
|
||||
<div aria-hidden="true" class="w-0 shrink-0" />
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
@@ -83,7 +72,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === 'floating' || variant === 'inset'
|
||||
? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
|
||||
: 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
@@ -91,7 +80,7 @@ const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
class="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
class="bg-sidebar/50 backdrop-blur-xl backdrop-saturate-150 border-r border-sidebar-border/60 shadow-[1px_0_0_0_oklch(1_0_0/0.04)] group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,12 @@ const props = defineProps<{
|
||||
:class="
|
||||
cn(
|
||||
'bg-background relative flex w-full flex-1 flex-col',
|
||||
// Sidebar variant: padding-left replaces the old spacer div, letting the background extend under the sidebar for glass effect
|
||||
'md:peer-data-[variant=sidebar]:pl-(--sidebar-width)',
|
||||
'md:peer-data-[variant=sidebar]:peer-data-[collapsible=offcanvas]:pl-0',
|
||||
'md:peer-data-[variant=sidebar]:peer-data-[collapsible=icon]:pl-(--sidebar-width-icon)',
|
||||
'md:peer-data-[variant=sidebar]:transition-[padding-left] md:peer-data-[variant=sidebar]:duration-200 md:peer-data-[variant=sidebar]:ease-linear',
|
||||
// Inset variant: existing margin-based layout unchanged
|
||||
'md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
|
||||
props.class,
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ export { default as SidebarTrigger } from './SidebarTrigger.vue'
|
||||
export { useSidebar } from './utils'
|
||||
|
||||
export const sidebarMenuButtonVariants = cva(
|
||||
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding,box-shadow] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-primary/10 data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[active=true]:shadow-[inset_2px_0_0_var(--primary)] data-[active=true]:rounded-l-none data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! group-data-[collapsible=icon]:data-[active=true]:shadow-none group-data-[collapsible=icon]:data-[active=true]:rounded-md [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -104,6 +104,32 @@ const { coverSize, gridGap } = useDisplaySettings()
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dark mode brightness -->
|
||||
<div v-if="themeStore.theme === 'dark'" 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">Surface brightness</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Lighten dark mode surfaces</p>
|
||||
</div>
|
||||
<button
|
||||
v-if="themeStore.brightness > 0"
|
||||
class="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
@click="themeStore.setBrightness(0)"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
:value="themeStore.brightness"
|
||||
@input="themeStore.setBrightness(Number(($event.target as HTMLInputElement).value))"
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
step="5"
|
||||
class="w-full accent-primary cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -110,6 +110,8 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
const storedBackground = storage.get<Background>('background', 'dots')
|
||||
const background = ref<Background>(BACKGROUND_IDS.includes(storedBackground) ? storedBackground : 'dots')
|
||||
|
||||
const brightness = ref<number>(storage.get<number>('brightness', 0))
|
||||
|
||||
function applyTheme(t: Theme) {
|
||||
document.documentElement.classList.toggle('dark', t === 'dark')
|
||||
}
|
||||
@@ -124,6 +126,11 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
if (r !== 'default') document.documentElement.classList.add(`radius-${r}`)
|
||||
}
|
||||
|
||||
function applyBrightness(b: number) {
|
||||
const lift = (b / 100) * 0.12
|
||||
document.documentElement.style.setProperty('--bg-lift', lift.toFixed(4))
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
theme.value = theme.value === 'dark' ? 'light' : 'dark'
|
||||
}
|
||||
@@ -136,10 +143,23 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
radius.value = r
|
||||
}
|
||||
|
||||
function applyBackground(b: Background) {
|
||||
BACKGROUND_IDS.forEach((id) => {
|
||||
const cssClass = BACKGROUND_OPTIONS.find((o) => o.id === id)?.cssClass
|
||||
if (cssClass) document.body.classList.remove(cssClass)
|
||||
})
|
||||
const cssClass = BACKGROUND_OPTIONS.find((o) => o.id === b)?.cssClass
|
||||
if (cssClass) document.body.classList.add(cssClass)
|
||||
}
|
||||
|
||||
function setBackground(b: Background) {
|
||||
background.value = b
|
||||
}
|
||||
|
||||
function setBrightness(b: number) {
|
||||
brightness.value = Math.min(100, Math.max(0, b))
|
||||
}
|
||||
|
||||
watch(
|
||||
theme,
|
||||
(t) => {
|
||||
@@ -167,10 +187,19 @@ export const useThemeStore = defineStore('theme', () => {
|
||||
watch(
|
||||
background,
|
||||
(b) => {
|
||||
applyBackground(b)
|
||||
storage.set('background', b)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
watch(
|
||||
brightness,
|
||||
(b) => {
|
||||
applyBrightness(b)
|
||||
storage.set('brightness', b)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
return { theme, accent, radius, background, toggleTheme, setAccent, setRadius, setBackground }
|
||||
return { theme, accent, radius, background, brightness, toggleTheme, setAccent, setRadius, setBackground, setBrightness }
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1771394367208,
|
||||
"when": 1771401479472,
|
||||
"tag": "0000_baseline",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user