implement font-family module

This commit is contained in:
Julie Blanc 2026-03-05 16:29:42 +01:00
parent 6f5efb6fbc
commit cb9fd93e51
137 changed files with 1177 additions and 21 deletions

View file

@ -57,10 +57,12 @@ export function usePreviewRenderer({
}
// Render to the hidden frame
// Font-face CSS is injected as a separate style block so PagedJS doesn't try to parse its URLs
hiddenFrame.srcdoc = `
<!DOCTYPE html>
<html>
<head>
<style id="font-faces">${stylesheetStore.fontFaceCss}</style>
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
<style id="dynamic-styles">${stylesheetStore.content}</style>
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
@ -136,6 +138,15 @@ export function usePreviewRenderer({
}
);
// Watch for font-face CSS changes (new font loaded) and re-render
watch(
() => stylesheetStore.fontFaceCss,
() => {
if (!initialized.value) return;
renderPreview();
}
);
// Re-render when narrative data changes
watch(
() => narrativeStore.data,

View file

@ -0,0 +1,153 @@
import { reactive, computed } from 'vue';
import { useStylesheetStore } from '../stores/stylesheet';
const PROJECT_FONTS = [
{
name: 'Alegreya',
category: 'serif',
files: {
variable: '/assets/fonts/project/Alegreya/Alegreya-VariableFont_wght.ttf',
variableItalic: '/assets/fonts/project/Alegreya/Alegreya-Italic-VariableFont_wght.ttf',
},
},
{
name: 'EB Garamond',
category: 'serif',
files: {
variable: '/assets/fonts/project/EB_Garamond/EBGaramond-VariableFont_wght.ttf',
variableItalic: '/assets/fonts/project/EB_Garamond/EBGaramond-Italic-VariableFont_wght.ttf',
},
},
{
name: 'Fira Sans',
category: 'sans-serif',
files: {
regular: '/assets/fonts/project/Fira_Sans/FiraSans-Regular.ttf',
bold: '/assets/fonts/project/Fira_Sans/FiraSans-Bold.ttf',
italic: '/assets/fonts/project/Fira_Sans/FiraSans-Italic.ttf',
boldItalic: '/assets/fonts/project/Fira_Sans/FiraSans-BoldItalic.ttf',
},
},
{
name: 'Montserrat',
category: 'sans-serif',
files: {
variable: '/assets/fonts/project/Montserrat/Montserrat-VariableFont_wght.ttf',
variableItalic: '/assets/fonts/project/Montserrat/Montserrat-Italic-VariableFont_wght.ttf',
},
},
{
name: 'Open Sans',
category: 'sans-serif',
files: {
variable: '/assets/fonts/project/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf',
variableItalic: '/assets/fonts/project/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf',
},
},
{
name: 'Source Code Pro',
category: 'monospace',
files: {
variable: '/assets/fonts/project/Source_Code_Pro/SourceCodePro-VariableFont_wght.ttf',
variableItalic: '/assets/fonts/project/Source_Code_Pro/SourceCodePro-Italic-VariableFont_wght.ttf',
},
},
];
// Singleton state
const fonts = reactive(
PROJECT_FONTS.map((f) => ({ ...f, loaded: false }))
);
const loadedSet = new Set();
function absoluteUrl(path) {
return window.location.origin + path.replace(/,/g, '%2C');
}
function generateFontFaceCss(font) {
const rules = [];
const { name, files } = font;
if (files.variable) {
rules.push(
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.variable)}") format("truetype");\n font-weight: 100 900;\n font-style: normal;\n}`
);
}
if (files.variableItalic) {
rules.push(
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.variableItalic)}") format("truetype");\n font-weight: 100 900;\n font-style: italic;\n}`
);
}
if (files.regular) {
rules.push(
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.regular)}") format("truetype");\n font-weight: 400;\n font-style: normal;\n}`
);
}
if (files.bold) {
rules.push(
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.bold)}") format("truetype");\n font-weight: 700;\n font-style: normal;\n}`
);
}
if (files.italic) {
rules.push(
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.italic)}") format("truetype");\n font-weight: 400;\n font-style: italic;\n}`
);
}
if (files.boldItalic) {
rules.push(
`@font-face {\n font-family: "${name}";\n src: url("${absoluteUrl(files.boldItalic)}") format("truetype");\n font-weight: 700;\n font-style: italic;\n}`
);
}
return rules.join('\n\n');
}
async function loadFontInDocument(font) {
const file = font.files.variable || font.files.regular;
if (!file) return;
const face = new FontFace(font.name, `url("${file}")`);
await face.load();
document.fonts.add(face);
}
const loadedFontsCss = computed(() => {
return fonts
.filter((f) => f.loaded)
.map((f) => generateFontFaceCss(f))
.join('\n\n');
});
async function loadFont(fontName) {
if (!fontName || fontName === 'sans-serif' || loadedSet.has(fontName)) return;
const font = fonts.find((f) => f.name === fontName);
if (!font) return;
await loadFontInDocument(font);
font.loaded = true;
loadedSet.add(fontName);
// Update stylesheet store font-face CSS (injected separately in iframe)
const stylesheetStore = useStylesheetStore();
stylesheetStore.setFontFaceCss(loadedFontsCss.value);
}
async function loadAllFontPreviews() {
// Only loads fonts into document.fonts for select preview — does NOT update the stylesheet store
const promises = fonts
.filter((f) => !loadedSet.has(f.name))
.map((f) => loadFontInDocument(f).catch(() => {}));
await Promise.all(promises);
}
export function useProjectFonts() {
return {
fonts,
loadFont,
loadAllFontPreviews,
loadedFontsCss,
};
}

View file

@ -4,7 +4,7 @@ import { reactive } from 'vue';
const defaults = reactive({
fontSize: { value: 16, unit: 'px' },
lineHeight: { value: 20, unit: 'px' },
fontFamily: 'Alegreya Sans',
fontFamily: 'sans-serif',
color: 'rgb(0, 0, 0)',
});