Files
TREK/client/src/components/Studio/fontPicker.ts
T
Maurice fabefed39f feat(studio): 34 typefaces with real weights and italics, a categorised font picker
The book catalogue grows from 7 families to 34, all self-hosted through
@fontsource under the SIL OFL 1.1 or the Apache License 2.0: condensed and
poster faces, heavy retro display, elegant display serifs, Montserrat, twelve
scripts and DM Mono. Weights run from 100 to 900, each family offering only
the weights it ships, and the serifs gain real italics.

The catalogue lives in @trek/shared (book-fonts.data.ts) with each family's
roles (display, heading, body, script, mono), weights, italics and letter
width, so the Studio, the findings, the design engine and the assistant read
one list. Page operations and the design engine set a weight a family lacks
at its nearest one, describe_book_page names the role and the weight, and
font_mix no longer counts scripts and mono faces, warning above three.

The Studio gets StudioFontPicker: the current family set in itself, a
searchable list grouped by kind with every family in its own face and the
book's own families first, and a weight slider over the weights the family
has. Faces load only when used; the assistant's page snapshot embeds only the
faces a page draws, the service worker installs only the latin files of the
original seven families, and the faces of an opened book are cached for
offline use. BOOK_SCHEMA_REV goes to 2.
2026-10-23 21:01:29 +02:00

48 lines
2.1 KiB
TypeScript

import { BOOK_FONT_CATEGORIES, type BookFontCategory, type BookWeight } from '@trek/shared';
import { BOOK_FONTS, fontsIn, hasItalic, nearestWeight, type BookFontId } from './bookFonts';
/**
* The type controls' decisions, apart from their markup: which families a
* search finds, the weight a newly picked family is set at, and what the row
* and the italic button say. StudioFontPicker.tsx and StudioTypeControls.tsx
* draw them.
*/
export type FontT = (key: string, params?: Record<string, string | number>) => string;
export interface FontPick {
font: BookFontId;
weight: BookWeight;
}
/** The family, its weight moved onto one the new family has. */
export function pickFont(id: BookFontId, weight: number): FontPick {
// A family that does not ship this weight would render a synthesised bold,
// a smeared regular in print, so the weight moves to the nearest one it has.
return { font: id, weight: nearestWeight(id, weight) as BookWeight };
}
/** The families that match a search, by name, id or group, in picker order. */
export function matchFonts(query: string, groupName: (c: BookFontCategory) => string): BookFontId[] {
const q = query.trim().toLowerCase();
return BOOK_FONT_CATEGORIES.flatMap((category) => {
const group = groupName(category).toLowerCase();
return fontsIn(category).filter(
(id) => !q || BOOK_FONTS[id].name.toLowerCase().includes(q) || id.includes(q) || group.includes(q)
);
});
}
/** The weights a family has, as its row says them: one number, or the first and the last. */
export function weightSpan(id: BookFontId, t: FontT): string {
const ws = BOOK_FONTS[id].weights;
const first = ws[0] ?? 400;
const last = ws[ws.length - 1] ?? first;
return first === last ? String(first) : t('journey.studio.fonts.range', { from: first, to: last });
}
/** The italic button's words: plain where the family has a real italic, the reason where the browser slants it. */
export function italicTitle(font: string, weight: number, t: FontT): string {
return hasItalic(font, nearestWeight(font, weight)) ? t('journey.studio.italic') : t('journey.studio.italicSlanted');
}