The backend from #303 could store an operator's font but nothing could choose one: no list endpoint outside the Site-Admin routes, no @font-face rules for a family that only exists at runtime, and no management UI. Found while wiring it up — a real defect in #303, invisible to its tests: `fontStack` cannot tell an uploaded family from a deleted one, so the PDF exporter embedded the face and then never named it. Every export of a pond using an operator font rendered in the system font while the job reported success. Both `fontStack` call sites now take the uploaded families (`buildPdfHtml`, `pondFontVariables`); `pdf-html.test.ts` pins the regression from both sides. Verified against a real Gotenberg: with the families the PDF embeds PlayfairDisplay-Bold, without them NotoSans-Bold — that was the whole bug, in one diff of two PDFs. - `GET /fonts/custom` is readable by any signed-in user, not Site Admins only: the pickers, the licence page and the injected `@font-face` rules all need it, and gating it would have forced a second, admin-only UI. - Bundled and uploaded families are told apart by their `<optgroup>`, not by a badge — the grouping is then part of the control's semantics, so a screen reader announces it and the native mobile select keeps it. Within each source the catalog's category grouping is preserved. - The delete confirmation names how many ponds use the family and what happens to them; focus moves to it and back on cancel. Deletion stays unblocked (the api's decision, #303) — the ponds degrade, they do not break. - The licence page grew a second table. That is what makes an attribution obligation satisfiable: a commercial licence that requires naming the foundry needs a page to name it on. Verified in the browser end to end (upload two weights → listed and rendered in its own font → chosen in a pond → page renders in it → deleted → pond falls back): api suite for fonts/export 77 passed, a11y pack 11/11 locally in both schemes, lint/typecheck/i18n:check green.
40 lines
1.5 KiB
TypeScript
40 lines
1.5 KiB
TypeScript
import { CustomFontView, FontCatalogEntry, customFontEntries } from '@dorfteich/shared';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useMemo } from 'react';
|
|
|
|
import { useAuth } from '../auth/auth-context';
|
|
import { apiGet } from '../lib/api';
|
|
|
|
export const CUSTOM_FONTS_KEY = ['fonts', 'custom'];
|
|
|
|
/**
|
|
* The instance's operator-uploaded font families (issues #303/#304).
|
|
*
|
|
* Every font-aware surface needs them: the pickers offer them, the licence
|
|
* page attributes them, `fontStack` needs them to NAME the family instead of
|
|
* falling through to the system stack, and `CustomFontFaces` turns them into
|
|
* `@font-face` rules. One query key, so they are fetched once per session and
|
|
* shared.
|
|
*
|
|
* Only fetched while signed in — the endpoint requires a session, and asking
|
|
* on the login screen would produce a 401 for nothing.
|
|
*/
|
|
export function useCustomFonts(): CustomFontView[] {
|
|
const { user } = useAuth();
|
|
const query = useQuery({
|
|
queryKey: CUSTOM_FONTS_KEY,
|
|
queryFn: () => apiGet<CustomFontView[]>('/fonts/custom'),
|
|
enabled: Boolean(user),
|
|
// Uploading a font is a rare Site-Admin act; the manager invalidates the
|
|
// key itself, so a long life here costs nothing.
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
return query.data ?? [];
|
|
}
|
|
|
|
/** The same families in the shape `fontEntry`/`fontStack` accept. */
|
|
export function useCustomFontEntries(): FontCatalogEntry[] {
|
|
const fonts = useCustomFonts();
|
|
return useMemo(() => customFontEntries(fonts), [fonts]);
|
|
}
|