@@ -327,11 +375,27 @@ function confirmRemoveFile() {
-
+
+
+
+
+
+
+
+
+
diff --git a/client/src/features/settings/fonts/components/FontLedgerHeader.vue b/client/src/features/settings/fonts/components/FontLedgerHeader.vue
new file mode 100644
index 000000000..648dcd5c3
--- /dev/null
+++ b/client/src/features/settings/fonts/components/FontLedgerHeader.vue
@@ -0,0 +1,20 @@
+
+
+
+
+ {{ t('settings.admin.serverFonts.columns.family') }}
+ {{ t('settings.admin.serverFonts.columns.specimen') }}
+ {{ t('settings.admin.serverFonts.columns.styles') }}
+ {{ useLabel }}
+
+
diff --git a/client/src/features/settings/fonts/components/FontLedgerToolbar.vue b/client/src/features/settings/fonts/components/FontLedgerToolbar.vue
new file mode 100644
index 000000000..d47e928dd
--- /dev/null
+++ b/client/src/features/settings/fonts/components/FontLedgerToolbar.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatNumber(families) }}
+ {{ t('settings.admin.serverFonts.familiesUnit', { count: families }) }}
+ ·
+
+ {{ formatNumber(files) }}
+ {{ formatNumber(maxFiles) }}
+
+ ·
+
+ {{ formatBytes(bytes) }}
+
+
+
+
diff --git a/client/src/features/settings/server-fonts/components/FontPagePreview.vue b/client/src/features/settings/fonts/components/FontPagePreview.vue
similarity index 100%
rename from client/src/features/settings/server-fonts/components/FontPagePreview.vue
rename to client/src/features/settings/fonts/components/FontPagePreview.vue
diff --git a/client/src/features/settings/server-fonts/components/FontStyleGrid.vue b/client/src/features/settings/fonts/components/FontStyleGrid.vue
similarity index 100%
rename from client/src/features/settings/server-fonts/components/FontStyleGrid.vue
rename to client/src/features/settings/fonts/components/FontStyleGrid.vue
diff --git a/client/src/features/settings/server-fonts/components/FontUploadTray.vue b/client/src/features/settings/fonts/components/FontUploadTray.vue
similarity index 94%
rename from client/src/features/settings/server-fonts/components/FontUploadTray.vue
rename to client/src/features/settings/fonts/components/FontUploadTray.vue
index 1a0f725a3..393cc8a27 100644
--- a/client/src/features/settings/server-fonts/components/FontUploadTray.vue
+++ b/client/src/features/settings/fonts/components/FontUploadTray.vue
@@ -2,11 +2,12 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { ChevronRight, CircleCheck, CircleDashed, CircleX, Clock, LoaderCircle, Upload } from '@lucide/vue'
+import type { FontScope } from '@bookorbit/types'
import { fontCssFamilyGroupName } from '@bookorbit/types'
import { Button } from '@/components/ui/button'
import { fontVariantLabel } from '@/features/reader/shared/lib/font-variants'
import { formatBytes } from '@/lib/formatting'
-import type { UploadEntry } from '../composables/useServerFontLibrary'
+import type { UploadEntry } from '../composables/useFontLibrary'
import { refusalMessageKey } from '../lib/upload-outcome'
/**
@@ -14,7 +15,7 @@ import { refusalMessageKey } from '../lib/upload-outcome'
* already on the server, or why it was refused. Replaces a toast per file, which scrolled away before
* a ten-file family had finished.
*/
-const props = defineProps<{ entries: UploadEntry[] }>()
+const props = withDefaults(defineProps<{ entries: UploadEntry[]; scope?: FontScope }>(), { scope: 'server' })
const emit = defineEmits<{ dismiss: [] }>()
const { t } = useI18n()
@@ -40,7 +41,7 @@ function iconClass(entry: UploadEntry): string {
function faceStyle(entry: UploadEntry) {
if (!entry.face) return {}
return {
- fontFamily: `'${fontCssFamilyGroupName(entry.face.familyName, 'server')}', sans-serif`,
+ fontFamily: `'${fontCssFamilyGroupName(entry.face.familyName, props.scope)}', sans-serif`,
fontWeight: entry.face.weight,
fontStyle: entry.face.style,
}
@@ -53,7 +54,7 @@ function faceLabel(entry: UploadEntry): string {
}
function reason(entry: UploadEntry): string {
- return t(refusalMessageKey(entry.code), entry.meta)
+ return t(refusalMessageKey(entry.code, props.scope), entry.meta)
}
function handleDismiss() {
diff --git a/client/src/features/settings/fonts/components/FontUseFacts.vue b/client/src/features/settings/fonts/components/FontUseFacts.vue
new file mode 100644
index 000000000..8668941bc
--- /dev/null
+++ b/client/src/features/settings/fonts/components/FontUseFacts.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+ {{ t('settings.admin.serverFonts.facts.books', { count: use.books }) }}
+
+
+
+ {{
+ perspective === 'server'
+ ? t('settings.admin.serverFonts.facts.defaults', { count: use.defaults })
+ : t('settings.reader.fonts.library.yourDefault')
+ }}
+
+
+
+ {{ t('settings.admin.serverFonts.facts.hidden', { count: use.hidden }) }}
+
+
diff --git a/client/src/features/settings/server-fonts/components/ReaderFontPickerPreview.vue b/client/src/features/settings/fonts/components/ReaderFontPickerPreview.vue
similarity index 75%
rename from client/src/features/settings/server-fonts/components/ReaderFontPickerPreview.vue
rename to client/src/features/settings/fonts/components/ReaderFontPickerPreview.vue
index ed5609ac5..e3fcbf5c5 100644
--- a/client/src/features/settings/server-fonts/components/ReaderFontPickerPreview.vue
+++ b/client/src/features/settings/fonts/components/ReaderFontPickerPreview.vue
@@ -6,18 +6,30 @@ import { BUILTIN_READER_FONT_OPTIONS } from '@/features/reader/shared/constants/
import { formatFontFamilyLabel } from '@/features/reader/shared/lib/font-display'
import { closestVariant, familyVariants, fontVariantLabel, isSameVariant } from '@/features/reader/shared/lib/font-variants'
-/**
- * The Font section of the reader's settings panel, drawn with this server's families and one of them
- * chosen, so an administrator sees exactly what readers will be offered. It mirrors
- * ReaderSettingsPanel's markup and is display-only.
- */
-const props = defineProps<{
- families: FontFamily[]
- selected: FontFamily | null
-}>()
-
const { t } = useI18n()
+/**
+ * The Font section of the reader's settings panel, drawn with real families and one of them chosen,
+ * so whoever manages fonts sees exactly what a reader's list offers. It mirrors ReaderSettingsPanel's
+ * markup and is display-only.
+ */
+const props = withDefaults(
+ defineProps<{
+ serverFamilies: FontFamily[]
+ userFamilies?: FontFamily[]
+ selected: FontFamily | null
+ }>(),
+ { userFamilies: () => [] },
+)
+
+/** In the reader's own order: server fonts, then the reader's own. */
+const sections = computed(() =>
+ [
+ { key: 'server', label: t('reader.settings.fontServer'), families: props.serverFamilies },
+ { key: 'user', label: t('reader.settings.fontYours'), families: props.userFamilies },
+ ].filter((section) => section.families.length > 0),
+)
+
const chips = computed(() => (props.selected ? familyVariants(props.selected.variants) : []))
/** A reader arriving with default settings lands on the style nearest Regular, as the reader picks it. */
const chosenChip = computed(() => closestVariant(chips.value, { weight: 400, style: 'normal' }))
@@ -41,11 +53,11 @@ const tile = 'flex h-10 items-center truncate rounded-lg border px-2 text-sm'
>
-