mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
e21a92b8a4
Each family is a row with its specimen, a weight grid of the styles readers get and how readers use it; its sheet previews the face on reader pages and inside the reader's own font list. Uploads answer per file in one tray, removing a family confirms with its cost, and a rename is one request. Server: additive GET /server-fonts/usage (counts only), PATCH /server-fonts/families, upload error codes.
136 lines
6.2 KiB
Vue
136 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { BookOpen, BookMarked, EyeOff, FileUp, MoreHorizontal, Pencil, Trash2, TriangleAlert } from '@lucide/vue'
|
|
import type { ServerFontFamilyUsage } from '@bookorbit/types'
|
|
import { Button } from '@/components/ui/button'
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
|
import type { FontFamily } from '@/features/reader/epub/composables/useCustomFonts'
|
|
import { useFontLabels } from '../composables/useFontLabels'
|
|
import FontStyleGrid from './FontStyleGrid.vue'
|
|
|
|
const props = defineProps<{
|
|
family: FontFamily
|
|
usage: ServerFontFamilyUsage
|
|
previewText: string
|
|
previewSize: number
|
|
isNew: boolean
|
|
readonly: boolean
|
|
/** The browser refused the family's files, so readers see a fallback face. */
|
|
loadFailed: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{ open: [name: string]; rename: [name: string]; addFiles: []; remove: [name: string] }>()
|
|
|
|
const { t } = useI18n()
|
|
const { styleSummaryText, fileCountSummary } = useFontLabels()
|
|
|
|
const fontFamily = computed(() => `'${props.family.cssFamilyName}', sans-serif`)
|
|
const summary = computed(() => styleSummaryText(props.family.variants))
|
|
// Scales down with the row on narrow screens, so a 32 px preview does not dwarf a phone.
|
|
const specimenStyle = computed(() => ({ fontFamily: fontFamily.value, fontSize: `min(${props.previewSize}px, 6cqi)`, lineHeight: 1.35 }))
|
|
|
|
function handleOpen() {
|
|
emit('open', props.family.name)
|
|
}
|
|
function handleRename() {
|
|
emit('rename', props.family.name)
|
|
}
|
|
function handleAddFiles() {
|
|
emit('addFiles')
|
|
}
|
|
function handleRemove() {
|
|
emit('remove', props.family.name)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="grid cursor-pointer grid-cols-[minmax(0,1fr)_2rem] gap-x-3 gap-y-2.5 rounded-xl border border-border bg-card px-4 py-3.5 shadow-xs transition-colors hover:border-foreground/15 @min-[60rem]:grid-cols-[15.75rem_minmax(0,1fr)_11rem_9.5rem_2rem] @min-[60rem]:items-center @min-[60rem]:gap-x-7 @min-[60rem]:py-[17px] @min-[60rem]:ps-6 @min-[60rem]:pe-5"
|
|
:class="isNew ? 'bg-[linear-gradient(90deg,color-mix(in_oklch,var(--success)_9%,transparent),transparent_40%)]' : ''"
|
|
data-testid="server-font-row"
|
|
@click="handleOpen"
|
|
>
|
|
<div class="order-1 flex min-w-0 flex-col gap-0.5">
|
|
<button
|
|
type="button"
|
|
class="block max-w-full truncate rounded-sm text-start text-[21px] leading-7 text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
:style="{ fontFamily }"
|
|
>
|
|
{{ family.name }}
|
|
</button>
|
|
<span class="flex items-center gap-2 text-[12.5px] text-muted-foreground">
|
|
<span v-if="isNew" class="rounded-full bg-success/15 px-1.5 text-[11px] leading-[18px] font-medium text-success">{{
|
|
t('settings.admin.serverFonts.newBadge')
|
|
}}</span>
|
|
<span class="tabular-nums">{{ fileCountSummary(family.variants) }}</span>
|
|
</span>
|
|
<span v-if="loadFailed" class="mt-0.5 inline-flex items-center gap-1.5 text-[12.5px] text-warning">
|
|
<TriangleAlert :size="13" class="shrink-0" aria-hidden="true" />
|
|
{{ t('settings.admin.serverFonts.loadFailed') }}
|
|
</span>
|
|
</div>
|
|
|
|
<p
|
|
class="order-3 col-span-2 min-w-0 overflow-hidden whitespace-nowrap text-foreground [mask-image:linear-gradient(to_right,black_calc(100%-3.5rem),transparent)] rtl:[mask-image:linear-gradient(to_left,black_calc(100%-3.5rem),transparent)] @min-[60rem]:order-2 @min-[60rem]:col-span-1"
|
|
:style="specimenStyle"
|
|
>
|
|
{{ previewText }}
|
|
</p>
|
|
|
|
<div
|
|
class="order-4 col-span-2 flex flex-wrap items-center gap-x-2.5 gap-y-1.5 @min-[60rem]:order-3 @min-[60rem]:col-span-1 @min-[60rem]:flex-col @min-[60rem]:items-start"
|
|
>
|
|
<FontStyleGrid :fonts="family.variants" :label="summary" />
|
|
<span class="text-[12.5px] text-muted-foreground">{{ summary }}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="order-5 col-span-2 flex flex-wrap gap-x-3 gap-y-1 empty:hidden @min-[60rem]:order-4 @min-[60rem]:col-span-1 @min-[60rem]:flex-col @min-[60rem]:empty:flex"
|
|
data-testid="server-font-usage"
|
|
>
|
|
<span
|
|
v-if="usage.books > 0"
|
|
class="inline-flex items-center gap-1.5 text-[12.5px] text-foreground"
|
|
:title="t('settings.admin.serverFonts.facts.booksDetail', { books: usage.books, readers: usage.readers })"
|
|
>
|
|
<BookOpen :size="14" class="text-muted-foreground" aria-hidden="true" />
|
|
{{ t('settings.admin.serverFonts.facts.books', { count: usage.books }) }}
|
|
</span>
|
|
<span v-if="usage.defaults > 0" class="inline-flex items-center gap-1.5 text-[12.5px] text-foreground">
|
|
<BookMarked :size="14" class="text-muted-foreground" aria-hidden="true" />
|
|
{{ t('settings.admin.serverFonts.facts.defaults', { count: usage.defaults }) }}
|
|
</span>
|
|
<span v-if="usage.hidden > 0" class="inline-flex items-center gap-1.5 text-[12.5px] text-muted-foreground">
|
|
<EyeOff :size="14" aria-hidden="true" />
|
|
{{ t('settings.admin.serverFonts.facts.hidden', { count: usage.hidden }) }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="order-2 flex justify-end self-start @min-[60rem]:order-5 @min-[60rem]:self-center" @click.stop>
|
|
<DropdownMenu v-if="!readonly">
|
|
<DropdownMenuTrigger as-child>
|
|
<Button variant="outline" size="icon-sm" type="button" :aria-label="t('settings.admin.serverFonts.moreActions', { name: family.name })">
|
|
<MoreHorizontal :size="16" aria-hidden="true" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" class="w-52">
|
|
<DropdownMenuItem @click="handleRename">
|
|
<Pencil />
|
|
{{ t('settings.admin.serverFonts.menu.rename') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem @click="handleAddFiles">
|
|
<FileUp />
|
|
{{ t('settings.admin.serverFonts.menu.addFiles') }}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem variant="destructive" @click="handleRemove">
|
|
<Trash2 />
|
|
{{ t('settings.admin.serverFonts.menu.remove') }}
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</template>
|