mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(client): improve shell responsive gutters and cover hover transforms
Add responsive shell content gutters to tokens CSS and fix cover stack hover transform calculations for scaled square covers in SeriesDetailView.
This commit is contained in:
@@ -70,9 +70,10 @@
|
||||
/* Shape */
|
||||
--radius: 0.625rem;
|
||||
|
||||
/* App shell (sidebar + header) — one gutter, one material.
|
||||
/* App shell (sidebar + header): one card gap, one material.
|
||||
--shell-surface-opacity is user-controlled and overwritten on <html>. */
|
||||
--shell-gap: 0.75rem;
|
||||
--shell-content-gutter: 0.75rem;
|
||||
--shell-surface-opacity: 92%;
|
||||
--shell-surface: color-mix(in oklch, var(--sidebar) var(--shell-surface-opacity), transparent);
|
||||
/* Accent ladder: four steps, used for every tinted fill and border in the shell. */
|
||||
@@ -108,6 +109,18 @@
|
||||
--audit-settings: oklch(0.55 0.15 25);
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
:root {
|
||||
--shell-content-gutter: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 64rem) {
|
||||
:root {
|
||||
--shell-content-gutter: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
--tint-c: 0.0175;
|
||||
|
||||
@@ -46,7 +46,9 @@ const viewKey = computed(() => {
|
||||
<AppHeader />
|
||||
|
||||
<!-- 2. Independent View Area: Everything below the header scrolls here -->
|
||||
<div class="app-shell-scroll px-(--shell-gap) pt-(--shell-gap) flex-1 overflow-y-auto overflow-x-hidden relative scroll-smooth bg-transparent">
|
||||
<div
|
||||
class="app-shell-scroll px-(--shell-content-gutter) pt-(--shell-gap) flex-1 overflow-y-auto overflow-x-hidden relative scroll-smooth bg-transparent"
|
||||
>
|
||||
<router-view v-slot="{ Component }">
|
||||
<Transition name="page" mode="out-in">
|
||||
<KeepAlive :include="GRID_VIEW_NAMES" :max="10">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
centeredBottomScaleTransform,
|
||||
centeredScaleShiftPercent,
|
||||
isSquareCoverRatio,
|
||||
resolveCoverStackAspectRatio,
|
||||
resolveCoverStackDisplayMode,
|
||||
@@ -40,6 +41,9 @@ describe('cover-scale utilities', () => {
|
||||
})
|
||||
|
||||
it('returns centered bottom transform only when scale exceeds 1', () => {
|
||||
expect(centeredScaleShiftPercent(1)).toBe(0)
|
||||
expect(centeredScaleShiftPercent(Number.NaN)).toBe(0)
|
||||
expect(centeredScaleShiftPercent(1.25)).toBe(12.5)
|
||||
expect(centeredBottomScaleTransform(1)).toBeNull()
|
||||
expect(centeredBottomScaleTransform(Number.NaN)).toBeNull()
|
||||
expect(centeredBottomScaleTransform(1.25)).toEqual({
|
||||
|
||||
@@ -40,9 +40,14 @@ export function resolveCoverStackDisplayMode(ratio: number | null | undefined):
|
||||
return isSquareCoverRatio(ratio) ? 'natural-bottom' : undefined
|
||||
}
|
||||
|
||||
export function centeredScaleShiftPercent(scale: number): number {
|
||||
if (!Number.isFinite(scale) || scale <= 1) return 0
|
||||
return (scale - 1) * 50
|
||||
}
|
||||
|
||||
export function centeredBottomScaleTransform(scale: number): { transformOrigin: string; transform: string } | null {
|
||||
if (!Number.isFinite(scale) || scale <= 1) return null
|
||||
const centerShiftPercent = (scale - 1) * 50
|
||||
const centerShiftPercent = centeredScaleShiftPercent(scale)
|
||||
return {
|
||||
transformOrigin: 'center bottom',
|
||||
transform: `translateY(-${centerShiftPercent}%) scale(${scale})`,
|
||||
|
||||
@@ -447,6 +447,8 @@ describe('SeriesDetailView', () => {
|
||||
expect(style).toContain('transform-origin: center bottom')
|
||||
expect(style).toContain('translateY(-12.5%)')
|
||||
expect(style).toContain('aspect-ratio: 1 / 1')
|
||||
expect(style).toContain('--lead-cover-hover-translate-y: calc(-12px - 12.5%)')
|
||||
expect(style).toContain('--lead-cover-hover-scale: 1.2875')
|
||||
expect(wrapper.get('[data-testid="lead-cover-artwork"]').attributes('data-mode')).toBe('natural-bottom')
|
||||
expect(wrapper.get('[data-testid="lead-cover-artwork"]').attributes('data-frame-aspect-ratio')).toBe('1/1')
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import { useCoverStack, MAX_VISIBLE as MAX_STACK_VISIBLE } from '../composables/
|
||||
import {
|
||||
PORTRAIT_STACK_FRAME_ASPECT_RATIO,
|
||||
centeredBottomScaleTransform,
|
||||
centeredScaleShiftPercent,
|
||||
resolveCoverStackAspectRatio,
|
||||
resolveCoverStackDisplayMode,
|
||||
resolveCoverStackFrameAspectRatio,
|
||||
@@ -251,9 +252,12 @@ const scaledLeadCoverStyles = computed(() =>
|
||||
const ratio = leadCoverRatioAt(index)
|
||||
const squareScale = resolveSquareCoverScale(ratio, SERIES_SQUARE_COVER_SCALE)
|
||||
const squareTransform = centeredBottomScaleTransform(squareScale)
|
||||
const centerShiftPercent = centeredScaleShiftPercent(squareScale)
|
||||
const baseForRatio = {
|
||||
...base,
|
||||
aspectRatio: resolveCoverStackAspectRatio(ratio),
|
||||
'--lead-cover-hover-translate-y': `calc(-12px - ${centerShiftPercent}%)`,
|
||||
'--lead-cover-hover-scale': String(1.03 * squareScale),
|
||||
}
|
||||
if (!squareTransform) return baseForRatio
|
||||
return {
|
||||
@@ -747,7 +751,8 @@ defineOptions({ name: 'SeriesDetailView' })
|
||||
}
|
||||
|
||||
.series-cover-stack-item:hover {
|
||||
transform: perspective(1000px) rotateY(0deg) translateY(-12px) translateZ(40px) scale(1.03) !important;
|
||||
transform: perspective(1000px) rotateY(0deg) translateY(var(--lead-cover-hover-translate-y, -12px)) translateZ(40px)
|
||||
scale(var(--lead-cover-hover-scale, 1.03)) !important;
|
||||
z-index: 50 !important;
|
||||
box-shadow:
|
||||
0 20px 25px -5px rgba(0, 0, 0, 0.4),
|
||||
|
||||
Reference in New Issue
Block a user