mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(client): keep book detail layouts responsive
Size book detail layouts from usable pane width so sidebar width and browser zoom do not force inappropriate compact layouts. Keep metadata save actions reachable while secondary controls overflow safely. Fixes #1192
This commit is contained in:
@@ -1181,22 +1181,24 @@ watch(
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Direction A. Below xl the page is a single scrolling column. From xl the pane owns the
|
||||
height: three columns fill row one, the discovery shelf takes row two, and any column
|
||||
that runs long scrolls inside itself so the page itself never does.
|
||||
Direction A. Below 46rem of pane width the page is a single scrolling column. Above it
|
||||
the pane owns the height: three columns fill row one, the discovery shelf takes row two,
|
||||
and any column that runs long scrolls inside itself so the page itself never does.
|
||||
-->
|
||||
<div
|
||||
class="flex flex-col gap-5 xl:grid xl:h-full xl:min-h-0 xl:grid-cols-[17rem_minmax(0,1fr)_19.25rem] xl:grid-rows-[minmax(0,1fr)_clamp(11.25rem,29%,17.5rem)] xl:gap-x-6 xl:gap-y-5"
|
||||
class="flex flex-col gap-5 @min-[46rem]/book-detail:grid @min-[46rem]/book-detail:h-full @min-[46rem]/book-detail:min-h-0 @min-[46rem]/book-detail:grid-cols-[clamp(12rem,23cqi,17rem)_minmax(16rem,1fr)_clamp(15rem,26cqi,19.25rem)] @min-[46rem]/book-detail:grid-rows-[minmax(0,1fr)_clamp(11.25rem,29%,17.5rem)] @min-[46rem]/book-detail:gap-x-6 @min-[46rem]/book-detail:gap-y-5"
|
||||
>
|
||||
<!-- Cover column -->
|
||||
<div class="flex min-w-0 flex-col gap-3 xl:min-h-0 xl:col-start-1 xl:row-start-1">
|
||||
<div class="flex items-start gap-4 sm:gap-5 xl:block xl:min-h-0 xl:flex-1">
|
||||
<div
|
||||
class="flex min-w-0 flex-col gap-3 @min-[46rem]/book-detail:col-start-1 @min-[46rem]/book-detail:row-start-1 @min-[46rem]/book-detail:min-h-0"
|
||||
>
|
||||
<div class="flex items-start gap-4 sm:gap-5 @min-[46rem]/book-detail:block @min-[46rem]/book-detail:min-h-0 @min-[46rem]/book-detail:flex-1">
|
||||
<div
|
||||
ref="coverSlotEl"
|
||||
class="w-28 shrink-0 sm:w-36 xl:flex xl:h-full xl:w-full xl:items-center xl:justify-center"
|
||||
class="w-28 shrink-0 sm:w-36 @min-[46rem]/book-detail:flex @min-[46rem]/book-detail:h-full @min-[46rem]/book-detail:w-full @min-[46rem]/book-detail:items-center @min-[46rem]/book-detail:justify-center"
|
||||
:class="hasCover && coverLoaded && !coverFailed ? 'cursor-zoom-in' : ''"
|
||||
>
|
||||
<div class="w-full xl:max-h-full" :style="{ maxWidth: coverMaxWidth }">
|
||||
<div class="w-full @min-[46rem]/book-detail:max-h-full" :style="{ maxWidth: coverMaxWidth }">
|
||||
<BookCoverSurface
|
||||
class="book-cover-surface--spine-fitted group relative w-full overflow-hidden rounded-lg shadow-lg shadow-black/40"
|
||||
:disable-spine="isPrimaryAudio"
|
||||
@@ -1236,8 +1238,8 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compact identity, replaced by the full block in column two from xl -->
|
||||
<div class="min-w-0 flex-1 xl:hidden">
|
||||
<!-- Compact identity, replaced by the full block in column two from 46rem -->
|
||||
<div class="min-w-0 flex-1 @min-[46rem]/book-detail:hidden">
|
||||
<h1 class="text-lg font-bold leading-snug break-words sm:text-xl">{{ book.title ?? t('book.detail.details.untitled') }}</h1>
|
||||
<p v-if="book.subtitle" class="mt-1 text-sm leading-snug text-muted-foreground break-words">{{ book.subtitle }}</p>
|
||||
<p v-if="authorLinks.length" class="mt-2 text-[13px] break-words">
|
||||
@@ -1457,8 +1459,10 @@ watch(
|
||||
</div>
|
||||
|
||||
<!-- Main column -->
|
||||
<div class="flex min-w-0 flex-col gap-2.5 xl:col-start-2 xl:row-start-1 xl:min-h-0 xl:overflow-y-auto">
|
||||
<div class="hidden xl:block">
|
||||
<div
|
||||
class="flex min-w-0 flex-col gap-2.5 @min-[46rem]/book-detail:col-start-2 @min-[46rem]/book-detail:row-start-1 @min-[46rem]/book-detail:min-h-0 @min-[46rem]/book-detail:overflow-y-auto"
|
||||
>
|
||||
<div class="hidden @min-[46rem]/book-detail:block">
|
||||
<!-- Identity block -->
|
||||
<div class="flex items-center flex-wrap gap-x-3 gap-y-2 -mt-1">
|
||||
<h1 class="text-2xl font-bold leading-tight">{{ book.title ?? t('book.detail.details.untitled') }}</h1>
|
||||
@@ -1856,13 +1860,20 @@ watch(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<BookReadingActivityCard class="xl:min-h-0 xl:flex-1" :stats="readingStats" :sessions="readingSessions" :loading="readingLogLoading" />
|
||||
<BookReadingActivityCard
|
||||
class="@min-[46rem]/book-detail:min-h-0 @min-[46rem]/book-detail:flex-1"
|
||||
:stats="readingStats"
|
||||
:sessions="readingSessions"
|
||||
:loading="readingLogLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Detail rail -->
|
||||
<div class="flex min-w-0 flex-col gap-3 xl:col-start-3 xl:row-start-1 xl:min-h-0 xl:overflow-y-auto">
|
||||
<div
|
||||
class="flex min-w-0 flex-col gap-3 @min-[46rem]/book-detail:col-start-3 @min-[46rem]/book-detail:row-start-1 @min-[46rem]/book-detail:min-h-0 @min-[46rem]/book-detail:overflow-y-auto"
|
||||
>
|
||||
<section
|
||||
class="flex min-h-0 flex-col rounded-xl border border-border bg-card px-4 pb-2 pt-3 xl:flex-1"
|
||||
class="flex min-h-0 flex-col rounded-xl border border-border bg-card px-4 pt-3 pb-2 @min-[46rem]/book-detail:flex-1"
|
||||
:aria-label="t('book.detail.details.detailsHeading')"
|
||||
>
|
||||
<h3 class="mb-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
@@ -2101,7 +2112,7 @@ watch(
|
||||
</div>
|
||||
|
||||
<!-- Discovery shelf -->
|
||||
<div class="min-w-0 xl:col-span-3 xl:row-start-2 xl:min-h-0">
|
||||
<div class="min-w-0 @min-[46rem]/book-detail:col-span-3 @min-[46rem]/book-detail:row-start-2 @min-[46rem]/book-detail:min-h-0">
|
||||
<DiscoverRow class="h-full" :book-id="book.id" :series-name="book.seriesName" :author-count="book.authors.length" size="lg" flush />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -838,139 +838,141 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
<div class="@container/edit flex min-h-full min-w-0 flex-col">
|
||||
<div class="flex flex-1 flex-col gap-3">
|
||||
<!-- Command strip -->
|
||||
<div class="flex flex-none items-center gap-2 overflow-x-auto no-scrollbar -mx-4 px-4 py-0.5 sm:mx-0 sm:px-0 @3xl/edit:overflow-visible">
|
||||
<div
|
||||
v-if="metadataScore !== null"
|
||||
class="flex h-9 flex-none items-center gap-2 rounded-lg border border-border bg-card px-2.5 sm:h-8"
|
||||
:title="t('book.detail.editMetadata.scoreTooltip', { score: metadataScore })"
|
||||
>
|
||||
<span class="text-[10px] font-bold tracking-[0.11em] text-muted-foreground uppercase">{{ t('book.detail.editMetadata.score') }}</span>
|
||||
<span class="text-sm font-bold tabular-nums" :style="{ color: metadataScoreColour ?? undefined }">{{ metadataScore }}</span>
|
||||
<span class="h-1 w-12 overflow-hidden rounded-full bg-muted" aria-hidden="true">
|
||||
<span class="block h-full rounded-full" :style="{ width: `${metadataScore}%`, backgroundColor: metadataScoreColour ?? undefined }" />
|
||||
</span>
|
||||
<div class="sticky top-0 z-30 -mx-4 flex flex-none items-center gap-2 bg-card/95 px-4 py-0.5 backdrop-blur-sm sm:mx-0 sm:px-0">
|
||||
<div class="no-scrollbar flex min-w-0 flex-1 items-center gap-2 overflow-x-auto">
|
||||
<div
|
||||
v-if="metadataScore !== null"
|
||||
class="flex h-9 flex-none items-center gap-2 rounded-lg border border-border bg-card px-2.5 sm:h-8"
|
||||
:title="t('book.detail.editMetadata.scoreTooltip', { score: metadataScore })"
|
||||
>
|
||||
<span class="text-[10px] font-bold tracking-[0.11em] text-muted-foreground uppercase">{{ t('book.detail.editMetadata.score') }}</span>
|
||||
<span class="text-sm font-bold tabular-nums" :style="{ color: metadataScoreColour ?? undefined }">{{ metadataScore }}</span>
|
||||
<span class="h-1 w-12 overflow-hidden rounded-full bg-muted" aria-hidden="true">
|
||||
<span class="block h-full rounded-full" :style="{ width: `${metadataScore}%`, backgroundColor: metadataScoreColour ?? undefined }" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="emptyFields.length > 0"
|
||||
type="button"
|
||||
class="flex h-9 flex-none items-center gap-1.5 rounded-lg border border-dashed border-amber-500/40 bg-amber-500/10 px-2.5 text-xs font-semibold text-amber-600 transition-colors hover:bg-amber-500/15 sm:h-8 dark:text-amber-400"
|
||||
:title="emptyFieldsTitle"
|
||||
@click="handleOpenSearch"
|
||||
>
|
||||
<TriangleAlert class="size-3.5 shrink-0" aria-hidden="true" />
|
||||
<span>{{ emptyFields.length }}</span>
|
||||
<span class="hidden @3xl/edit:inline">{{ t('book.detail.editMetadata.emptyFields', { count: emptyFields.length }) }}</span>
|
||||
</button>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="flex h-9 flex-none items-center gap-1.5 rounded-lg border border-input bg-background px-2.5 text-sm transition-colors hover:bg-muted disabled:opacity-40 sm:h-8 sm:px-3"
|
||||
:disabled="formDisabled || loadingFromFile || !primaryFile"
|
||||
:aria-label="t('book.detail.editMetadata.loadFromFile')"
|
||||
@click="handleLoadFromFile"
|
||||
>
|
||||
<Loader2 v-if="loadingFromFile" class="size-3.5 animate-spin" aria-hidden="true" />
|
||||
<HardDriveUpload v-else class="size-3.5" aria-hidden="true" />
|
||||
<span class="hidden @3xl/edit:inline">{{ t('book.detail.editMetadata.loadFromFile') }}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{
|
||||
loadingFromFile
|
||||
? t('common.loading')
|
||||
: !primaryFile
|
||||
? t('book.detail.editMetadata.noPrimaryFile')
|
||||
: t('book.detail.editMetadata.loadFromFileTooltip')
|
||||
}}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="flex h-9 flex-none items-center gap-1.5 rounded-lg border border-input bg-background px-2.5 text-sm transition-colors hover:bg-muted disabled:opacity-40 sm:h-8 sm:px-3"
|
||||
:disabled="writingAndRenaming || saving || fileWriteManualDisabledReasonLabel !== null"
|
||||
:aria-label="t('book.detail.editMetadata.writeAndRename')"
|
||||
@click="handleWriteAndRename"
|
||||
>
|
||||
<Loader2 v-if="writingAndRenaming" class="size-3.5 animate-spin" aria-hidden="true" />
|
||||
<HardDriveDownload v-else class="size-3.5" aria-hidden="true" />
|
||||
<span class="hidden @3xl/edit:inline">{{ t('book.detail.editMetadata.writeAndRename') }}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ fileWriteManualTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div class="mx-0.5 h-4 w-px flex-none bg-border" aria-hidden="true" />
|
||||
|
||||
<button
|
||||
class="search-online-btn flex h-9 flex-none items-center gap-1.5 rounded-lg px-3 text-sm font-medium text-primary-foreground transition-all sm:h-8"
|
||||
:disabled="formDisabled"
|
||||
:aria-label="t('common.search')"
|
||||
@click="handleOpenSearch"
|
||||
>
|
||||
<Sparkles class="size-3.5" aria-hidden="true" />
|
||||
<span class="hidden @3xl/edit:inline">{{ t('common.search') }}</span>
|
||||
</button>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="auto-fill-btn flex h-9 flex-none items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-all disabled:opacity-40 sm:h-8 sm:px-3"
|
||||
:disabled="formDisabled || autoFilling || areAllLocked"
|
||||
:aria-label="t('book.detail.editMetadata.autoFill')"
|
||||
@click="autoFill"
|
||||
>
|
||||
<Loader2 v-if="autoFilling" class="size-3.5 animate-spin" aria-hidden="true" />
|
||||
<RefreshCw v-else class="size-3.5" aria-hidden="true" />
|
||||
<span class="hidden @3xl/edit:inline">{{ t('book.detail.editMetadata.autoFill') }}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{
|
||||
autoFilling
|
||||
? t('book.detail.editMetadata.fetchingMetadata')
|
||||
: areAllLocked
|
||||
? t('book.detail.editMetadata.allFieldsLocked')
|
||||
: t('book.detail.editMetadata.autoFillTooltip')
|
||||
}}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div class="mx-0.5 h-4 w-px flex-none bg-border" aria-hidden="true" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="flex size-9 flex-none items-center justify-center rounded-lg border border-input bg-background transition-colors hover:bg-muted disabled:opacity-40 sm:size-8"
|
||||
:disabled="formDisabled || updatingLocks || areAllLocked"
|
||||
:aria-label="t('book.detail.editMetadata.lockAll')"
|
||||
@click="handleLockAll"
|
||||
>
|
||||
<Lock class="size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t('book.detail.editMetadata.lockAllTooltip') }}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="flex size-9 flex-none items-center justify-center rounded-lg border border-input bg-background transition-colors hover:bg-muted disabled:opacity-40 sm:size-8"
|
||||
:disabled="formDisabled || updatingLocks || !hasLockedFields"
|
||||
:aria-label="t('book.detail.editMetadata.unlockAll')"
|
||||
@click="handleUnlockAll"
|
||||
>
|
||||
<LockOpen class="size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t('book.detail.editMetadata.unlockAllTooltip') }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="emptyFields.length > 0"
|
||||
type="button"
|
||||
class="flex h-9 flex-none items-center gap-1.5 rounded-lg border border-dashed border-amber-500/40 bg-amber-500/10 px-2.5 text-xs font-semibold text-amber-600 transition-colors hover:bg-amber-500/15 sm:h-8 dark:text-amber-400"
|
||||
:title="emptyFieldsTitle"
|
||||
@click="handleOpenSearch"
|
||||
>
|
||||
<TriangleAlert class="size-3.5 shrink-0" aria-hidden="true" />
|
||||
<span>{{ emptyFields.length }}</span>
|
||||
<span class="hidden @3xl/edit:inline">{{ t('book.detail.editMetadata.emptyFields', { count: emptyFields.length }) }}</span>
|
||||
</button>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="flex h-9 flex-none items-center gap-1.5 rounded-lg border border-input bg-background px-2.5 text-sm transition-colors hover:bg-muted disabled:opacity-40 sm:h-8 sm:px-3"
|
||||
:disabled="formDisabled || loadingFromFile || !primaryFile"
|
||||
:aria-label="t('book.detail.editMetadata.loadFromFile')"
|
||||
@click="handleLoadFromFile"
|
||||
>
|
||||
<Loader2 v-if="loadingFromFile" class="size-3.5 animate-spin" aria-hidden="true" />
|
||||
<HardDriveUpload v-else class="size-3.5" aria-hidden="true" />
|
||||
<span class="hidden @3xl/edit:inline">{{ t('book.detail.editMetadata.loadFromFile') }}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{
|
||||
loadingFromFile
|
||||
? t('common.loading')
|
||||
: !primaryFile
|
||||
? t('book.detail.editMetadata.noPrimaryFile')
|
||||
: t('book.detail.editMetadata.loadFromFileTooltip')
|
||||
}}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="flex h-9 flex-none items-center gap-1.5 rounded-lg border border-input bg-background px-2.5 text-sm transition-colors hover:bg-muted disabled:opacity-40 sm:h-8 sm:px-3"
|
||||
:disabled="writingAndRenaming || saving || fileWriteManualDisabledReasonLabel !== null"
|
||||
:aria-label="t('book.detail.editMetadata.writeAndRename')"
|
||||
@click="handleWriteAndRename"
|
||||
>
|
||||
<Loader2 v-if="writingAndRenaming" class="size-3.5 animate-spin" aria-hidden="true" />
|
||||
<HardDriveDownload v-else class="size-3.5" aria-hidden="true" />
|
||||
<span class="hidden @3xl/edit:inline">{{ t('book.detail.editMetadata.writeAndRename') }}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ fileWriteManualTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div class="mx-0.5 h-4 w-px flex-none bg-border" aria-hidden="true" />
|
||||
|
||||
<button
|
||||
class="search-online-btn flex h-9 flex-none items-center gap-1.5 rounded-lg px-3 text-sm font-medium text-primary-foreground transition-all sm:h-8"
|
||||
:disabled="formDisabled"
|
||||
:aria-label="t('common.search')"
|
||||
@click="handleOpenSearch"
|
||||
>
|
||||
<Sparkles class="size-3.5" aria-hidden="true" />
|
||||
<span class="hidden @3xl/edit:inline">{{ t('common.search') }}</span>
|
||||
</button>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="auto-fill-btn flex h-9 flex-none items-center gap-1.5 rounded-lg px-2.5 text-sm font-medium transition-all disabled:opacity-40 sm:h-8 sm:px-3"
|
||||
:disabled="formDisabled || autoFilling || areAllLocked"
|
||||
:aria-label="t('book.detail.editMetadata.autoFill')"
|
||||
@click="autoFill"
|
||||
>
|
||||
<Loader2 v-if="autoFilling" class="size-3.5 animate-spin" aria-hidden="true" />
|
||||
<RefreshCw v-else class="size-3.5" aria-hidden="true" />
|
||||
<span class="hidden @3xl/edit:inline">{{ t('book.detail.editMetadata.autoFill') }}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{
|
||||
autoFilling
|
||||
? t('book.detail.editMetadata.fetchingMetadata')
|
||||
: areAllLocked
|
||||
? t('book.detail.editMetadata.allFieldsLocked')
|
||||
: t('book.detail.editMetadata.autoFillTooltip')
|
||||
}}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div class="mx-0.5 h-4 w-px flex-none bg-border" aria-hidden="true" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="flex size-9 flex-none items-center justify-center rounded-lg border border-input bg-background transition-colors hover:bg-muted disabled:opacity-40 sm:size-8"
|
||||
:disabled="formDisabled || updatingLocks || areAllLocked"
|
||||
:aria-label="t('book.detail.editMetadata.lockAll')"
|
||||
@click="handleLockAll"
|
||||
>
|
||||
<Lock class="size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t('book.detail.editMetadata.lockAllTooltip') }}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="flex size-9 flex-none items-center justify-center rounded-lg border border-input bg-background transition-colors hover:bg-muted disabled:opacity-40 sm:size-8"
|
||||
:disabled="formDisabled || updatingLocks || !hasLockedFields"
|
||||
:aria-label="t('book.detail.editMetadata.unlockAll')"
|
||||
@click="handleUnlockAll"
|
||||
>
|
||||
<LockOpen class="size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t('book.detail.editMetadata.unlockAllTooltip') }}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div class="mx-0.5 hidden h-4 w-px flex-none bg-border lg:block" aria-hidden="true" />
|
||||
|
||||
<button
|
||||
class="hidden size-9 flex-none items-center justify-center rounded-lg border border-input bg-background transition-colors hover:bg-muted disabled:opacity-40 sm:size-8 lg:flex"
|
||||
class="flex size-11 flex-none items-center justify-center rounded-lg border border-input bg-background transition-colors hover:bg-muted disabled:opacity-40 sm:size-8"
|
||||
:title="t('common.cancel')"
|
||||
:aria-label="t('common.cancel')"
|
||||
:disabled="submitDisabled"
|
||||
@@ -979,7 +981,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
<X class="size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
class="hidden h-9 flex-none grid-cols-1 grid-rows-1 items-center justify-items-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-40 sm:h-8 lg:inline-grid"
|
||||
class="inline-grid h-11 flex-none grid-cols-1 grid-rows-1 items-center justify-items-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-40 sm:h-8"
|
||||
:disabled="submitDisabled"
|
||||
@click="submit"
|
||||
>
|
||||
@@ -1010,10 +1012,10 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
@dismiss="dismissWriteAndRenameResult"
|
||||
/>
|
||||
|
||||
<!-- Columns. 1 up on phones, 2 from @xl, 3 from @3xl, 4 (and a fixed-height, non-scrolling
|
||||
page) from @5xl. Only the @5xl layout promises the whole record without page scroll. -->
|
||||
<!-- Columns. 1 up on phones, 2 from @xl, 3 from @3xl, and 4 from 60rem. Only the
|
||||
60rem layout promises the whole record without page scroll. -->
|
||||
<div
|
||||
class="grid grid-cols-1 gap-3 @5xl/edit:flex-1 @xl/edit:grid-cols-[minmax(0,10rem)_minmax(0,1fr)] @3xl/edit:grid-cols-[minmax(0,10rem)_minmax(0,1fr)_minmax(0,1.1fr)] @5xl/edit:grid-cols-[12.75rem_minmax(0,1.3fr)_minmax(0,0.88fr)_minmax(0,1.25fr)]"
|
||||
class="grid grid-cols-1 gap-3 @xl/edit:grid-cols-[minmax(0,10rem)_minmax(0,1fr)] @3xl/edit:grid-cols-[minmax(0,10rem)_minmax(0,1fr)_minmax(0,1.1fr)] @min-[60rem]/edit:flex-1 @min-[60rem]/edit:grid-cols-[12.75rem_minmax(0,1.3fr)_minmax(0,0.88fr)_minmax(0,1.25fr)]"
|
||||
>
|
||||
<fieldset :disabled="formDisabled" class="contents">
|
||||
<!-- Artwork and source -->
|
||||
@@ -1026,7 +1028,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
@cover-changed="handleCoverChanged"
|
||||
@toggle-lock="handleCoverLockToggle"
|
||||
/>
|
||||
<MetadataSourceCard :book="props.book" class="hidden @5xl/edit:flex @5xl/edit:flex-1" />
|
||||
<MetadataSourceCard :book="props.book" class="hidden @min-[60rem]/edit:flex @min-[60rem]/edit:flex-1" />
|
||||
</div>
|
||||
|
||||
<!-- Identity -->
|
||||
@@ -1428,7 +1430,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
</section>
|
||||
|
||||
<!-- Description, plus the comic sheet when the primary file is a comic -->
|
||||
<div class="flex flex-col gap-2.5 @xl/edit:col-span-2 @3xl/edit:col-span-3 @5xl/edit:col-span-1 @5xl/edit:self-stretch">
|
||||
<div class="flex flex-col gap-2.5 @xl/edit:col-span-2 @3xl/edit:col-span-3 @min-[60rem]/edit:col-span-1 @min-[60rem]/edit:self-stretch">
|
||||
<section class="flex flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card">
|
||||
<header class="flex h-7 flex-none items-center gap-2 border-b border-border px-2.5">
|
||||
<h3 class="text-[10px] font-bold tracking-[0.12em] text-muted-foreground uppercase">
|
||||
@@ -1553,38 +1555,10 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<MetadataSourceCard :book="props.book" class="@5xl/edit:hidden" />
|
||||
<MetadataSourceCard :book="props.book" class="@min-[60rem]/edit:hidden" />
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Phone and tablet: the toolbar's save pair is hidden above, so it lives here instead -->
|
||||
<div
|
||||
class="fixed inset-x-0 bottom-0 z-40 flex gap-2 border-t border-border bg-background/95 px-4 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] backdrop-blur-sm sm:px-6 lg:hidden"
|
||||
>
|
||||
<button
|
||||
class="flex h-11 items-center gap-1.5 rounded-lg border border-input bg-background px-4 text-sm transition-colors hover:bg-muted disabled:opacity-40"
|
||||
:disabled="submitDisabled"
|
||||
@click="handleReset"
|
||||
>
|
||||
<X class="size-3.5" aria-hidden="true" />
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
class="inline-grid h-11 flex-1 grid-cols-1 grid-rows-1 items-center justify-items-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-40"
|
||||
:disabled="submitDisabled"
|
||||
@click="submit"
|
||||
>
|
||||
<span class="col-start-1 row-start-1 flex items-center gap-1.5" :class="{ invisible: saving }">
|
||||
<Check class="size-3.5" aria-hidden="true" />
|
||||
{{ t('common.save') }}
|
||||
</span>
|
||||
<span class="col-start-1 row-start-1 flex items-center gap-1.5" :class="{ invisible: !saving }">
|
||||
<Loader2 class="size-3.5 animate-spin" aria-hidden="true" />
|
||||
{{ t('book.detail.editMetadata.saving') }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ function onCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
<template>
|
||||
<BookDetailLayout :book-id="bookId" :cover-tint="coverTint">
|
||||
<Transition name="content" mode="out-in">
|
||||
<div v-if="detail" key="detail" class="h-full">
|
||||
<div v-if="detail" key="detail" class="@container/book-detail h-full">
|
||||
<!--
|
||||
Only the two lazy tabs are cached. Both fetch on mount, so without this every return
|
||||
replays blank -> skeleton -> content in under 120ms, which reads as a flash rather than
|
||||
|
||||
Reference in New Issue
Block a user