From c378354dac1090c55cd1e15b79470b922a4cb833 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:54:38 +0000 Subject: [PATCH] feat(settings): rebuild reader fonts on the shared font ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reader › Fonts now uses the Server Fonts ledger, family sheet and upload tray for a reader's own fonts, and lists the server's fonts with a show/hide switch and a preview sheet. The shared pieces move to features/settings/fonts and the ledger columns narrow to give the specimen room. Server: additive GET /fonts/usage and PATCH /fonts/families, upload error codes, and a 409 instead of a 500 when a reader uploads a second file for the same face. --- .../features/settings/FontLibraryPanel.vue | 461 ---------------- .../src/features/settings/FontsSettings.vue | 363 ++++++++++++- .../settings/ServerFontVisibility.vue | 96 ---- .../features/settings/ServerFontsSettings.vue | 308 +++-------- .../settings/__tests__/FontsSettings.spec.ts | 496 +++++++----------- .../__tests__/ServerFontVisibility.spec.ts | 190 ------- .../__tests__/ServerFontsSettings.spec.ts | 32 +- .../components/FontDropOverlay.vue | 4 +- .../fonts/components/FontFamilyMenu.vue | 48 ++ .../fonts/components/FontFamilyRow.vue | 96 ++++ .../components/FontFamilySheet.vue} | 138 +++-- .../fonts/components/FontLedgerHeader.vue | 20 + .../fonts/components/FontLedgerToolbar.vue | 113 ++++ .../components/FontPagePreview.vue | 0 .../components/FontStyleGrid.vue | 0 .../components/FontUploadTray.vue | 9 +- .../fonts/components/FontUseFacts.vue | 40 ++ .../components/ReaderFontPickerPreview.vue | 38 +- .../__tests__/useFailedFontFaces.spec.ts | 5 +- .../__tests__/useFontLibrary.spec.ts} | 60 ++- .../composables/useFailedFontFaces.ts | 4 +- .../fonts/composables/useFamilySheet.ts | 78 +++ .../fonts/composables/useFontDropTarget.ts | 53 ++ .../composables/useFontLabels.ts | 0 .../composables/useFontLibrary.ts} | 60 ++- .../fonts/composables/useInjectedFontFaces.ts | 29 + .../lib/__tests__/font-style-grid.spec.ts | 0 .../lib/__tests__/upload-outcome.spec.ts | 0 .../lib/font-style-grid.ts | 0 .../settings/fonts/lib/preview-sizes.ts | 3 + .../lib/upload-outcome.ts | 8 +- .../server-fonts/components/ServerFontRow.vue | 135 ----- client/src/locales/en.json | 20 +- client/src/router/index.ts | 2 +- packages/types/src/font.ts | 20 +- .../dto/rename-font-family.dto.ts} | 4 +- server/src/modules/font/font-errors.ts | 41 ++ .../src/modules/font/font.controller.test.ts | 34 ++ server/src/modules/font/font.controller.ts | 13 + server/src/modules/font/font.repository.ts | 44 +- server/src/modules/font/font.service.test.ts | 113 +++- server/src/modules/font/font.service.ts | 69 ++- .../server-font/server-font.controller.ts | 4 +- .../server-font/server-font.service.ts | 33 +- .../route-inventory/uncovered-routes.json | 2 + 45 files changed, 1699 insertions(+), 1587 deletions(-) delete mode 100644 client/src/features/settings/FontLibraryPanel.vue delete mode 100644 client/src/features/settings/ServerFontVisibility.vue delete mode 100644 client/src/features/settings/__tests__/ServerFontVisibility.spec.ts rename client/src/features/settings/{server-fonts => fonts}/components/FontDropOverlay.vue (85%) create mode 100644 client/src/features/settings/fonts/components/FontFamilyMenu.vue create mode 100644 client/src/features/settings/fonts/components/FontFamilyRow.vue rename client/src/features/settings/{server-fonts/components/ServerFontFamilySheet.vue => fonts/components/FontFamilySheet.vue} (72%) create mode 100644 client/src/features/settings/fonts/components/FontLedgerHeader.vue create mode 100644 client/src/features/settings/fonts/components/FontLedgerToolbar.vue rename client/src/features/settings/{server-fonts => fonts}/components/FontPagePreview.vue (100%) rename client/src/features/settings/{server-fonts => fonts}/components/FontStyleGrid.vue (100%) rename client/src/features/settings/{server-fonts => fonts}/components/FontUploadTray.vue (94%) create mode 100644 client/src/features/settings/fonts/components/FontUseFacts.vue rename client/src/features/settings/{server-fonts => fonts}/components/ReaderFontPickerPreview.vue (75%) rename client/src/features/settings/{server-fonts => fonts}/composables/__tests__/useFailedFontFaces.spec.ts (93%) rename client/src/features/settings/{server-fonts/composables/__tests__/useServerFontLibrary.spec.ts => fonts/composables/__tests__/useFontLibrary.spec.ts} (79%) rename client/src/features/settings/{server-fonts => fonts}/composables/useFailedFontFaces.ts (81%) create mode 100644 client/src/features/settings/fonts/composables/useFamilySheet.ts create mode 100644 client/src/features/settings/fonts/composables/useFontDropTarget.ts rename client/src/features/settings/{server-fonts => fonts}/composables/useFontLabels.ts (100%) rename client/src/features/settings/{server-fonts/composables/useServerFontLibrary.ts => fonts/composables/useFontLibrary.ts} (74%) create mode 100644 client/src/features/settings/fonts/composables/useInjectedFontFaces.ts rename client/src/features/settings/{server-fonts => fonts}/lib/__tests__/font-style-grid.spec.ts (100%) rename client/src/features/settings/{server-fonts => fonts}/lib/__tests__/upload-outcome.spec.ts (100%) rename client/src/features/settings/{server-fonts => fonts}/lib/font-style-grid.ts (100%) create mode 100644 client/src/features/settings/fonts/lib/preview-sizes.ts rename client/src/features/settings/{server-fonts => fonts}/lib/upload-outcome.ts (87%) delete mode 100644 client/src/features/settings/server-fonts/components/ServerFontRow.vue rename server/src/modules/{server-font/dto/rename-server-font-family.dto.ts => font/dto/rename-font-family.dto.ts} (59%) create mode 100644 server/src/modules/font/font-errors.ts diff --git a/client/src/features/settings/FontLibraryPanel.vue b/client/src/features/settings/FontLibraryPanel.vue deleted file mode 100644 index 01c9a64e9..000000000 --- a/client/src/features/settings/FontLibraryPanel.vue +++ /dev/null @@ -1,461 +0,0 @@ - - - diff --git a/client/src/features/settings/FontsSettings.vue b/client/src/features/settings/FontsSettings.vue index e27b8d3b2..1fd0e63ce 100644 --- a/client/src/features/settings/FontsSettings.vue +++ b/client/src/features/settings/FontsSettings.vue @@ -1,24 +1,361 @@ diff --git a/client/src/features/settings/ServerFontVisibility.vue b/client/src/features/settings/ServerFontVisibility.vue deleted file mode 100644 index 0703981d9..000000000 --- a/client/src/features/settings/ServerFontVisibility.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - diff --git a/client/src/features/settings/ServerFontsSettings.vue b/client/src/features/settings/ServerFontsSettings.vue index 5add27501..d81bd9d30 100644 --- a/client/src/features/settings/ServerFontsSettings.vue +++ b/client/src/features/settings/ServerFontsSettings.vue @@ -1,30 +1,35 @@ - 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 @@ + + + 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 @@ + + + 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 @@ + + + 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' > -