@pixerate/editor 0.176.0 → 0.177.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-FKUKJ3XA.cjs → chunk-4XSBOGMK.cjs} +5 -4
- package/dist/{chunk-CJE5MOIS.js → chunk-N4ZXW3WB.js} +2 -1
- package/dist/effects/index.cjs +11 -3
- package/dist/effects/index.js +10 -2
- package/dist/effects/ui.d.cts +4 -1
- package/dist/effects/ui.d.ts +4 -1
- package/dist/typography/index.cjs +185 -40
- package/dist/typography/index.d.cts +1 -0
- package/dist/typography/index.d.ts +1 -0
- package/dist/typography/index.js +149 -4
- package/dist/typography/registry.d.cts +4 -0
- package/dist/typography/registry.d.ts +4 -0
- package/dist/typography/stylesheet.d.cts +86 -0
- package/dist/typography/stylesheet.d.ts +86 -0
- package/package.json +1 -1
- package/skills/pixerate-effect-authoring/SKILL.md +5 -0
- package/skills/pixerate-typography/SKILL.md +7 -3
|
@@ -30,8 +30,9 @@ function fontHasWeight(font, weight) {
|
|
|
30
30
|
}
|
|
31
31
|
function filterFonts(fonts, filter = {}) {
|
|
32
32
|
const q = _nullishCoalesce(_optionalChain([filter, 'access', _ => _.query, 'optionalAccess', _2 => _2.trim, 'call', _3 => _3(), 'access', _4 => _4.toLowerCase, 'call', _5 => _5()]), () => ( ""));
|
|
33
|
+
const wantedTags = (_nullishCoalesce(filter.tags, () => ( []))).map((t) => t.trim().toLowerCase().replace(/\s+/g, "-")).filter(Boolean);
|
|
33
34
|
const kept = fonts.filter(
|
|
34
|
-
(f) => (!filter.category || f.category === filter.category) && (!filter.source || f.source === filter.source) && (filter.weight === void 0 || fontHasWeight(f, filter.weight)) && (!filter.style || f.styles.includes(filter.style)) && (filter.isVariable === void 0 || !!f.isVariable === filter.isVariable) && (!q || f.family.toLowerCase().includes(q))
|
|
35
|
+
(f) => (!filter.category || f.category === filter.category) && (!filter.source || f.source === filter.source) && (filter.weight === void 0 || fontHasWeight(f, filter.weight)) && (!filter.style || f.styles.includes(filter.style)) && (filter.isVariable === void 0 || !!f.isVariable === filter.isVariable) && (!wantedTags.length || wantedTags.every((t) => _optionalChain([f, 'access', _6 => _6.tags, 'optionalAccess', _7 => _7.includes, 'call', _8 => _8(t)]))) && (!q || f.family.toLowerCase().includes(q))
|
|
35
36
|
);
|
|
36
37
|
if (!q) return kept;
|
|
37
38
|
const starts = kept.filter((f) => f.family.toLowerCase().startsWith(q));
|
|
@@ -217,7 +218,7 @@ function createSystemFontProvider(options = {}) {
|
|
|
217
218
|
for (const e of entries) {
|
|
218
219
|
const key = e.family.toLowerCase();
|
|
219
220
|
const known = findFont(SYSTEM_FONT_CATALOG, e.family);
|
|
220
|
-
const entry = _nullishCoalesce(byFamily.get(key), () => ( { family: _nullishCoalesce(_optionalChain([known, 'optionalAccess',
|
|
221
|
+
const entry = _nullishCoalesce(byFamily.get(key), () => ( { family: _nullishCoalesce(_optionalChain([known, 'optionalAccess', _9 => _9.family]), () => ( e.family)), category: _nullishCoalesce(_optionalChain([known, 'optionalAccess', _10 => _10.category]), () => ( guessFontCategory(e.family))), weights: [], styles: [], source: "system" }));
|
|
221
222
|
const w = weightFromStyleName(e.style);
|
|
222
223
|
if (!entry.weights.includes(w)) entry.weights.push(w);
|
|
223
224
|
const st = /italic/i.test(_nullishCoalesce(e.style, () => ( ""))) ? "italic" : /oblique/i.test(_nullishCoalesce(e.style, () => ( ""))) ? "oblique" : "normal";
|
|
@@ -279,7 +280,7 @@ function createWebFontProvider(options) {
|
|
|
279
280
|
const done = /* @__PURE__ */ new Set();
|
|
280
281
|
const g = globalThis;
|
|
281
282
|
const createFace = _nullishCoalesce(options.createFontFace, () => ( (g.FontFace ? (family, source, descriptors) => new g.FontFace(family, source, descriptors) : void 0)));
|
|
282
|
-
const set = () => _nullishCoalesce(options.fontFaceSet, () => ( _optionalChain([g, 'access',
|
|
283
|
+
const set = () => _nullishCoalesce(options.fontFaceSet, () => ( _optionalChain([g, 'access', _11 => _11.document, 'optionalAccess', _12 => _12.fonts])));
|
|
283
284
|
const def = (family) => defs.find((d) => d.family.toLowerCase() === family.trim().toLowerCase());
|
|
284
285
|
const key = (family, weight, style) => `${family.toLowerCase()}|${normalizeFontWeight(weight)}|${_nullishCoalesce(style, () => ( "normal"))}`;
|
|
285
286
|
function pickSource(d, weight, style) {
|
|
@@ -314,7 +315,7 @@ function createWebFontProvider(options) {
|
|
|
314
315
|
...source.unicodeRange ? { unicodeRange: source.unicodeRange } : {}
|
|
315
316
|
});
|
|
316
317
|
const promise = face.load().then(() => {
|
|
317
|
-
_optionalChain([set, 'call',
|
|
318
|
+
_optionalChain([set, 'call', _13 => _13(), 'optionalAccess', _14 => _14.add, 'call', _15 => _15(face)]);
|
|
318
319
|
done.add(k);
|
|
319
320
|
return true;
|
|
320
321
|
}).catch(() => {
|
|
@@ -30,8 +30,9 @@ function fontHasWeight(font, weight) {
|
|
|
30
30
|
}
|
|
31
31
|
function filterFonts(fonts, filter = {}) {
|
|
32
32
|
const q = filter.query?.trim().toLowerCase() ?? "";
|
|
33
|
+
const wantedTags = (filter.tags ?? []).map((t) => t.trim().toLowerCase().replace(/\s+/g, "-")).filter(Boolean);
|
|
33
34
|
const kept = fonts.filter(
|
|
34
|
-
(f) => (!filter.category || f.category === filter.category) && (!filter.source || f.source === filter.source) && (filter.weight === void 0 || fontHasWeight(f, filter.weight)) && (!filter.style || f.styles.includes(filter.style)) && (filter.isVariable === void 0 || !!f.isVariable === filter.isVariable) && (!q || f.family.toLowerCase().includes(q))
|
|
35
|
+
(f) => (!filter.category || f.category === filter.category) && (!filter.source || f.source === filter.source) && (filter.weight === void 0 || fontHasWeight(f, filter.weight)) && (!filter.style || f.styles.includes(filter.style)) && (filter.isVariable === void 0 || !!f.isVariable === filter.isVariable) && (!wantedTags.length || wantedTags.every((t) => f.tags?.includes(t))) && (!q || f.family.toLowerCase().includes(q))
|
|
35
36
|
);
|
|
36
37
|
if (!q) return kept;
|
|
37
38
|
const starts = kept.filter((f) => f.family.toLowerCase().startsWith(q));
|
package/dist/effects/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var _chunk4XSBOGMKcjs = require('../chunk-4XSBOGMK.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
var _chunk6DUEV36Fcjs = require('../chunk-6DUEV36F.cjs');
|
|
@@ -597,6 +597,11 @@ var EFFECT_USE_LABELS = {
|
|
|
597
597
|
};
|
|
598
598
|
var HEX2 = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
|
|
599
599
|
var typeOf = (s) => Array.isArray(s.type) ? s.type.find((t) => t !== "null") : s.type;
|
|
600
|
+
function hexColour(value) {
|
|
601
|
+
if (typeof value === "string") return HEX2.test(value) ? value : void 0;
|
|
602
|
+
if (!Array.isArray(value) || value.length !== 3 && value.length !== 4 || !value.every((n) => typeof n === "number" && Number.isFinite(n))) return void 0;
|
|
603
|
+
return `#${value.map((n) => Math.round(Math.min(1, Math.max(0, n)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
604
|
+
}
|
|
600
605
|
function isEffectFontControl(schema) {
|
|
601
606
|
return typeOf(schema) === "string" && (schema.format === "font" || schema["x-pixerate-control"] === "font");
|
|
602
607
|
}
|
|
@@ -621,7 +626,7 @@ function effectControlField(name, schema, fallback, options = {}) {
|
|
|
621
626
|
}
|
|
622
627
|
if (type === "boolean") return { ...base, type: "boolean", default: def === true };
|
|
623
628
|
if (isEffectFontControl(schema)) {
|
|
624
|
-
const fonts = _nullishCoalesce(options.fonts, () => (
|
|
629
|
+
const fonts = _nullishCoalesce(options.fonts, () => ( _chunk4XSBOGMKcjs.SYSTEM_FONT_CATALOG));
|
|
625
630
|
const opts = fonts.map((f) => ({ value: f.family, label: f.family, description: f.category }));
|
|
626
631
|
const current = typeof def === "string" && def ? def : _nullishCoalesce(_optionalChain([opts, 'access', _23 => _23[0], 'optionalAccess', _24 => _24.value]), () => ( "sans-serif"));
|
|
627
632
|
if (!opts.some((o) => o.value.toLowerCase() === current.toLowerCase())) opts.unshift({ value: current, label: current, description: "current" });
|
|
@@ -649,7 +654,10 @@ function effectFontFamilies(description, values = {}) {
|
|
|
649
654
|
}
|
|
650
655
|
return out;
|
|
651
656
|
}
|
|
652
|
-
var effectFieldValue = (field, value) =>
|
|
657
|
+
var effectFieldValue = (field, value) => {
|
|
658
|
+
if (field.type === "color") return _nullishCoalesce(hexColour(value), () => ( field.default));
|
|
659
|
+
return value === void 0 || value === null || typeof value === "object" && !Array.isArray(value) ? field.default : value;
|
|
660
|
+
};
|
|
653
661
|
function effectSamplePicture(width, height, variant = 0) {
|
|
654
662
|
const p = _chunkA3A5TI6Rcjs.createEffectPixels.call(void 0, width, height);
|
|
655
663
|
for (let y = 0, i = 0; y < height; y++) {
|
package/dist/effects/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SYSTEM_FONT_CATALOG
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-N4ZXW3WB.js";
|
|
4
4
|
import {
|
|
5
5
|
createEffectAuthoringSession
|
|
6
6
|
} from "../chunk-WMVEYSOA.js";
|
|
@@ -597,6 +597,11 @@ var EFFECT_USE_LABELS = {
|
|
|
597
597
|
};
|
|
598
598
|
var HEX2 = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
|
|
599
599
|
var typeOf = (s) => Array.isArray(s.type) ? s.type.find((t) => t !== "null") : s.type;
|
|
600
|
+
function hexColour(value) {
|
|
601
|
+
if (typeof value === "string") return HEX2.test(value) ? value : void 0;
|
|
602
|
+
if (!Array.isArray(value) || value.length !== 3 && value.length !== 4 || !value.every((n) => typeof n === "number" && Number.isFinite(n))) return void 0;
|
|
603
|
+
return `#${value.map((n) => Math.round(Math.min(1, Math.max(0, n)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
604
|
+
}
|
|
600
605
|
function isEffectFontControl(schema) {
|
|
601
606
|
return typeOf(schema) === "string" && (schema.format === "font" || schema["x-pixerate-control"] === "font");
|
|
602
607
|
}
|
|
@@ -649,7 +654,10 @@ function effectFontFamilies(description, values = {}) {
|
|
|
649
654
|
}
|
|
650
655
|
return out;
|
|
651
656
|
}
|
|
652
|
-
var effectFieldValue = (field, value) =>
|
|
657
|
+
var effectFieldValue = (field, value) => {
|
|
658
|
+
if (field.type === "color") return hexColour(value) ?? field.default;
|
|
659
|
+
return value === void 0 || value === null || typeof value === "object" && !Array.isArray(value) ? field.default : value;
|
|
660
|
+
};
|
|
653
661
|
function effectSamplePicture(width, height, variant = 0) {
|
|
654
662
|
const p = createEffectPixels(width, height);
|
|
655
663
|
for (let y = 0, i = 0; y < height; y++) {
|
package/dist/effects/ui.d.cts
CHANGED
|
@@ -27,7 +27,10 @@ export declare function effectParameterFields(description: Pick<EffectDescriptio
|
|
|
27
27
|
* to load with `ensureFontsLoaded` before rendering a title preview.
|
|
28
28
|
*/
|
|
29
29
|
export declare function effectFontFamilies(description: Pick<EffectDescription, 'controls' | 'defaults'>, values?: Readonly<Record<string, unknown>>): string[];
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* A saved value as a field shows it (`ParameterValue`), or the field's default. Colour fields show
|
|
32
|
+
* channel arrays as hex, and the default for values they can't show (`"currentColor"`, `rgb(…)`).
|
|
33
|
+
*/
|
|
31
34
|
export declare const effectFieldValue: (field: ParameterField, value: unknown) => ParameterValue;
|
|
32
35
|
/**
|
|
33
36
|
* A colourful, deterministic picture for previews of filters and transitions (`variant` 0 and 1
|
package/dist/effects/ui.d.ts
CHANGED
|
@@ -27,7 +27,10 @@ export declare function effectParameterFields(description: Pick<EffectDescriptio
|
|
|
27
27
|
* to load with `ensureFontsLoaded` before rendering a title preview.
|
|
28
28
|
*/
|
|
29
29
|
export declare function effectFontFamilies(description: Pick<EffectDescription, 'controls' | 'defaults'>, values?: Readonly<Record<string, unknown>>): string[];
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* A saved value as a field shows it (`ParameterValue`), or the field's default. Colour fields show
|
|
32
|
+
* channel arrays as hex, and the default for values they can't show (`"currentColor"`, `rgb(…)`).
|
|
33
|
+
*/
|
|
31
34
|
export declare const effectFieldValue: (field: ParameterField, value: unknown) => ParameterValue;
|
|
32
35
|
/**
|
|
33
36
|
* A colourful, deterministic picture for previews of filters and transitions (`variant` 0 and 1
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
var
|
|
18
|
+
var _chunk4XSBOGMKcjs = require('../chunk-4XSBOGMK.cjs');
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
var _chunkTFF2GTGNcjs = require('../chunk-TFF2GTGN.cjs');
|
|
@@ -312,6 +312,143 @@ function auditTextReadability(text, font = {}) {
|
|
|
312
312
|
};
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// src/typography/stylesheet.ts
|
|
316
|
+
function linkStylesheet(url) {
|
|
317
|
+
const doc = globalThis.document;
|
|
318
|
+
if (!_optionalChain([doc, 'optionalAccess', _4 => _4.head])) return Promise.reject(new Error("No document to add a stylesheet to."));
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
const link = doc.createElement("link");
|
|
321
|
+
link.rel = "stylesheet";
|
|
322
|
+
link.href = url;
|
|
323
|
+
link.onload = () => resolve();
|
|
324
|
+
link.onerror = () => {
|
|
325
|
+
link.remove();
|
|
326
|
+
reject(new Error(`Stylesheet failed to load: ${url}`));
|
|
327
|
+
};
|
|
328
|
+
doc.head.appendChild(link);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
var SAMPLE_TEXT = "AaBbZz09";
|
|
332
|
+
var quoteFamily = (family) => `"${family.replace(/["\\]/g, "\\$&")}"`;
|
|
333
|
+
function createStylesheetFontProvider(options) {
|
|
334
|
+
let catalog = null;
|
|
335
|
+
const fonts = () => {
|
|
336
|
+
catalog ??= Promise.resolve().then(options.listFonts).catch((error) => {
|
|
337
|
+
catalog = null;
|
|
338
|
+
throw error;
|
|
339
|
+
});
|
|
340
|
+
return catalog;
|
|
341
|
+
};
|
|
342
|
+
const addStylesheet = _nullishCoalesce(options.addStylesheet, () => ( linkStylesheet));
|
|
343
|
+
const fontSet = () => _nullishCoalesce(options.fontFaceSet, () => ( _optionalChain([globalThis, 'access', _5 => _5.document, 'optionalAccess', _6 => _6.fonts])));
|
|
344
|
+
const sheets = /* @__PURE__ */ new Map();
|
|
345
|
+
const faces = /* @__PURE__ */ new Map();
|
|
346
|
+
const done = /* @__PURE__ */ new Set();
|
|
347
|
+
const key = (family, weight, style) => `${family.toLowerCase()}|${_chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, weight)}|${_nullishCoalesce(style, () => ( "normal"))}`;
|
|
348
|
+
const sheet = (url) => {
|
|
349
|
+
let pending = sheets.get(url);
|
|
350
|
+
if (!pending) {
|
|
351
|
+
pending = addStylesheet(url).catch((error) => {
|
|
352
|
+
sheets.delete(url);
|
|
353
|
+
throw error;
|
|
354
|
+
});
|
|
355
|
+
sheets.set(url, pending);
|
|
356
|
+
}
|
|
357
|
+
return pending;
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
id: _nullishCoalesce(options.id, () => ( "stylesheet")),
|
|
361
|
+
listFonts: async () => (await fonts()).map((f) => ({ ...f })),
|
|
362
|
+
hasFont: async (family) => !!_chunk4XSBOGMKcjs.findFont.call(void 0, await fonts(), family),
|
|
363
|
+
loaded: (family, weight, style) => done.has(key(family, weight, style)),
|
|
364
|
+
async loadFont(family, weight, style) {
|
|
365
|
+
const k = key(family, weight, style);
|
|
366
|
+
const cached = faces.get(k);
|
|
367
|
+
if (cached) return cached;
|
|
368
|
+
const promise = (async () => {
|
|
369
|
+
const listed = _chunk4XSBOGMKcjs.findFont.call(void 0, await fonts(), family);
|
|
370
|
+
const set = fontSet();
|
|
371
|
+
if (!listed || !set) return false;
|
|
372
|
+
const wanted = _chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, weight);
|
|
373
|
+
const w = listed.isVariable || !listed.weights.length ? wanted : _chunk4XSBOGMKcjs.nearestFontWeight.call(void 0, wanted, listed.weights);
|
|
374
|
+
const st = !listed.styles.length || listed.styles.includes(_nullishCoalesce(style, () => ( "normal"))) ? _nullishCoalesce(style, () => ( "normal")) : "normal";
|
|
375
|
+
await sheet(options.stylesheetUrl(listed.family, w, st));
|
|
376
|
+
const matched = await set.load(`${st === "normal" ? "" : `${st} `}${w} 1em ${quoteFamily(listed.family)}`, _nullishCoalesce(options.sampleText, () => ( SAMPLE_TEXT)));
|
|
377
|
+
if (!matched.length) throw new Error("No face matched.");
|
|
378
|
+
done.add(k);
|
|
379
|
+
return true;
|
|
380
|
+
})().catch(() => {
|
|
381
|
+
faces.delete(k);
|
|
382
|
+
return false;
|
|
383
|
+
});
|
|
384
|
+
faces.set(k, promise);
|
|
385
|
+
return promise;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
var GOOGLE_CATEGORIES = {
|
|
390
|
+
"sans serif": "sans-serif",
|
|
391
|
+
serif: "serif",
|
|
392
|
+
display: "display",
|
|
393
|
+
handwriting: "handwriting",
|
|
394
|
+
monospace: "monospace"
|
|
395
|
+
};
|
|
396
|
+
var tagOf = (value) => value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
397
|
+
function googleFontsCatalog(metadata) {
|
|
398
|
+
let data = metadata;
|
|
399
|
+
if (typeof data === "string") data = JSON.parse(data.replace(/^\)\]\}'\s*/, ""));
|
|
400
|
+
const list = Array.isArray(data) ? data : _optionalChain([data, 'optionalAccess', _7 => _7.familyMetadataList]);
|
|
401
|
+
if (!Array.isArray(list)) throw new Error("Not Google Fonts metadata: expected familyMetadataList.");
|
|
402
|
+
const families = list.filter((f) => typeof _optionalChain([f, 'optionalAccess', _8 => _8.family]) === "string" && f.family);
|
|
403
|
+
return families.map((f, index) => ({ f, index })).sort((a, b) => (_nullishCoalesce(a.f.popularity, () => ( Infinity))) - (_nullishCoalesce(b.f.popularity, () => ( Infinity))) || a.index - b.index).map(({ f }) => {
|
|
404
|
+
const classes = (_nullishCoalesce(f.classifications, () => ( []))).map(tagOf);
|
|
405
|
+
const filed = _nullishCoalesce(GOOGLE_CATEGORIES[(_nullishCoalesce(f.category, () => ( ""))).toLowerCase()], () => ( "sans-serif"));
|
|
406
|
+
const category = _nullishCoalesce(["monospace", "handwriting", "display"].find((c) => classes.includes(c)), () => ( filed));
|
|
407
|
+
const weights = /* @__PURE__ */ new Set();
|
|
408
|
+
const styles = /* @__PURE__ */ new Set();
|
|
409
|
+
for (const face of Object.keys(_nullishCoalesce(f.fonts, () => ( {})))) {
|
|
410
|
+
const m = /^(\d{3})(i?)$/.exec(face);
|
|
411
|
+
if (!m) continue;
|
|
412
|
+
weights.add(_chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, Number(m[1])));
|
|
413
|
+
styles.add(m[2] ? "italic" : "normal");
|
|
414
|
+
}
|
|
415
|
+
const wght = _optionalChain([f, 'access', _9 => _9.axes, 'optionalAccess', _10 => _10.find, 'call', _11 => _11((a) => a.tag === "wght")]);
|
|
416
|
+
if (wght) {
|
|
417
|
+
for (const w of FONT_WEIGHTS) if (w >= wght.min && w <= wght.max) weights.add(w);
|
|
418
|
+
}
|
|
419
|
+
if (!weights.size) weights.add(400);
|
|
420
|
+
if (!styles.size) styles.add("normal");
|
|
421
|
+
const tags = [.../* @__PURE__ */ new Set([...f.category ? [tagOf(f.category)] : [], ...classes, ...f.stroke ? [tagOf(f.stroke)] : [], ...(_nullishCoalesce(f.axes, () => ( []))).map((a) => `variable-${a.tag.toLowerCase()}`)])];
|
|
422
|
+
return {
|
|
423
|
+
family: f.family,
|
|
424
|
+
category,
|
|
425
|
+
weights: [...weights].sort((a, b) => a - b),
|
|
426
|
+
styles: [...styles],
|
|
427
|
+
...wght ? { isVariable: true } : {},
|
|
428
|
+
source: "web",
|
|
429
|
+
...tags.length ? { tags } : {}
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function googleFontsStylesheetUrl(family, weight = 400, style = "normal", options = {}) {
|
|
434
|
+
const base = _nullishCoalesce(options.baseUrl, () => ( "https://fonts.googleapis.com/css2"));
|
|
435
|
+
const name = encodeURIComponent(family.trim()).replace(/%20/g, "+");
|
|
436
|
+
return `${base}?family=${name}:ital,wght@${style === "normal" ? 0 : 1},${_chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, weight)}&display=${_nullishCoalesce(options.display, () => ( "swap"))}`;
|
|
437
|
+
}
|
|
438
|
+
function createGoogleFontsProvider(options) {
|
|
439
|
+
const { catalog, baseUrl, ...rest } = options;
|
|
440
|
+
return createStylesheetFontProvider({
|
|
441
|
+
id: "google-fonts",
|
|
442
|
+
...rest,
|
|
443
|
+
listFonts: async () => {
|
|
444
|
+
const data = await catalog();
|
|
445
|
+
const built = Array.isArray(data) && data.every((f) => typeof _optionalChain([f, 'optionalAccess', _12 => _12.family]) === "string" && typeof _optionalChain([f, 'optionalAccess', _13 => _13.category]) === "string" && Array.isArray(_optionalChain([f, 'optionalAccess', _14 => _14.weights])));
|
|
446
|
+
return built ? data : googleFontsCatalog(data);
|
|
447
|
+
},
|
|
448
|
+
stylesheetUrl: (family, weight, style) => googleFontsStylesheetUrl(family, weight, style, { baseUrl })
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
315
452
|
// src/typography/stack.ts
|
|
316
453
|
var GENERIC_FONT_FAMILIES = [
|
|
317
454
|
"serif",
|
|
@@ -406,7 +543,7 @@ function resolveFontFallback(family, category) {
|
|
|
406
543
|
const families = parseFontFamily(family);
|
|
407
544
|
if (!families.length) throw new Error("family must name at least one font.");
|
|
408
545
|
if (GENERIC.has(families[families.length - 1])) return formatFontFamily(families);
|
|
409
|
-
const cat = _nullishCoalesce(category, () => (
|
|
546
|
+
const cat = _nullishCoalesce(category, () => ( _chunk4XSBOGMKcjs.guessFontCategory.call(void 0, families[0])));
|
|
410
547
|
return formatFontFamily([...families, ...parseFontFamily(FONT_STACKS[CATEGORY_FALLBACK_STACK[cat]])]);
|
|
411
548
|
}
|
|
412
549
|
|
|
@@ -414,7 +551,7 @@ function resolveFontFallback(family, category) {
|
|
|
414
551
|
var LARGE_TEXT_PX = 24;
|
|
415
552
|
var LARGE_BOLD_TEXT_PX = 14 * 4 / 3;
|
|
416
553
|
function isLargeText(font) {
|
|
417
|
-
const bold =
|
|
554
|
+
const bold = _chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, font.weight) >= 700;
|
|
418
555
|
return font.size >= LARGE_TEXT_PX || bold && font.size >= LARGE_BOLD_TEXT_PX - 0.01;
|
|
419
556
|
}
|
|
420
557
|
function checkTypographyAccessibility(options) {
|
|
@@ -428,12 +565,12 @@ function checkTypographyAccessibility(options) {
|
|
|
428
565
|
const ratio = Math.round(_chunk5UEFBM2Mcjs.contrastRatio.call(void 0, fg, bg) * 100) / 100;
|
|
429
566
|
const level = _chunk5UEFBM2Mcjs.contrastLevel.call(void 0, ratio, { large });
|
|
430
567
|
const passesAA = level !== "fail";
|
|
431
|
-
const bold =
|
|
568
|
+
const bold = _chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, font.weight) >= 700;
|
|
432
569
|
const issues = [];
|
|
433
570
|
const required = large ? { AA: 3, AAA: 4.5 } : { AA: 4.5, AAA: 7 };
|
|
434
571
|
if (!passesAA) issues.push(`Contrast ${ratio}:1 is below the ${required.AA}:1 AA minimum for ${large ? "large" : "normal"} text.`);
|
|
435
572
|
if (font.size < 12) issues.push(`${font.size}px text is hard to read at any contrast; use 12px or more (16px for body text).`);
|
|
436
|
-
const weight =
|
|
573
|
+
const weight = _chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, font.weight);
|
|
437
574
|
if (weight <= 300 && font.size < 24) issues.push(`Weight ${weight} at ${font.size}px has thin strokes that lower effective contrast; use 400 or heavier below 24px.`);
|
|
438
575
|
const suggestion = passesAA ? void 0 : _chunkTFF2GTGNcjs.suggestAccessibleForeground.call(void 0, fg, bg, { large });
|
|
439
576
|
return {
|
|
@@ -453,7 +590,7 @@ function checkTypographyAccessibility(options) {
|
|
|
453
590
|
var ALL_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
|
454
591
|
function offeredWeights(font) {
|
|
455
592
|
if (!font || !font.weights.length) return [...ALL_WEIGHTS];
|
|
456
|
-
const ws = font.weights.map((w) =>
|
|
593
|
+
const ws = font.weights.map((w) => _chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, w));
|
|
457
594
|
if (font.isVariable) return ALL_WEIGHTS.filter((w) => w >= Math.min(...ws) && w <= Math.max(...ws));
|
|
458
595
|
return [...new Set(ws)].sort((a, b) => a - b);
|
|
459
596
|
}
|
|
@@ -477,17 +614,17 @@ var FontPickerModel = (_class = class {
|
|
|
477
614
|
this.recentFamilies = [..._nullishCoalesce(options.recent, () => ( []))];
|
|
478
615
|
const fonts = [..._nullishCoalesce(options.fonts, () => ( []))];
|
|
479
616
|
const v = _nullishCoalesce(options.value, () => ( {}));
|
|
480
|
-
const family = _nullishCoalesce(_nullishCoalesce(v.family, () => ( _optionalChain([fonts, 'access',
|
|
481
|
-
const known =
|
|
617
|
+
const family = _nullishCoalesce(_nullishCoalesce(v.family, () => ( _optionalChain([fonts, 'access', _15 => _15[0], 'optionalAccess', _16 => _16.family]))), () => ( "system-ui"));
|
|
618
|
+
const known = _chunk4XSBOGMKcjs.findFont.call(void 0, fonts, family);
|
|
482
619
|
this.state = this.derive({
|
|
483
620
|
value: {
|
|
484
|
-
family: _nullishCoalesce(_optionalChain([known, 'optionalAccess',
|
|
485
|
-
weight:
|
|
621
|
+
family: _nullishCoalesce(_optionalChain([known, 'optionalAccess', _17 => _17.family]), () => ( family)),
|
|
622
|
+
weight: _chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, v.weight),
|
|
486
623
|
style: _nullishCoalesce(v.style, () => ( "normal")),
|
|
487
624
|
size: _nullishCoalesce(v.size, () => ( 16)),
|
|
488
625
|
lineHeight: _nullishCoalesce(v.lineHeight, () => ( 1.5)),
|
|
489
626
|
letterSpacing: _nullishCoalesce(v.letterSpacing, () => ( 0)),
|
|
490
|
-
..._nullishCoalesce(v.category, () => ( _optionalChain([known, 'optionalAccess',
|
|
627
|
+
..._nullishCoalesce(v.category, () => ( _optionalChain([known, 'optionalAccess', _18 => _18.category]))) ? { category: _nullishCoalesce(v.category, () => ( _optionalChain([known, 'optionalAccess', _19 => _19.category]))) } : {}
|
|
491
628
|
},
|
|
492
629
|
category: "all",
|
|
493
630
|
query: "",
|
|
@@ -522,7 +659,7 @@ var FontPickerModel = (_class = class {
|
|
|
522
659
|
}
|
|
523
660
|
/** The selected family's summary, if it is in `fonts`. */
|
|
524
661
|
get selectedFont() {
|
|
525
|
-
return
|
|
662
|
+
return _chunk4XSBOGMKcjs.findFont.call(void 0, this.state.fonts, this.state.value.family);
|
|
526
663
|
}
|
|
527
664
|
/** Weights the selected family offers (all nine when unknown or variable). */
|
|
528
665
|
get availableWeights() {
|
|
@@ -530,13 +667,13 @@ var FontPickerModel = (_class = class {
|
|
|
530
667
|
}
|
|
531
668
|
/** Styles the selected family offers (`normal` when unknown). */
|
|
532
669
|
get availableStyles() {
|
|
533
|
-
return _optionalChain([this, 'access',
|
|
670
|
+
return _optionalChain([this, 'access', _20 => _20.selectedFont, 'optionalAccess', _21 => _21.styles, 'access', _22 => _22.length]) ? [...this.selectedFont.styles] : ["normal", "italic"];
|
|
534
671
|
}
|
|
535
672
|
/** CSS for the selection: a fallback-resolved `font-family` plus the other properties. */
|
|
536
673
|
get css() {
|
|
537
674
|
const v = this.state.value;
|
|
538
675
|
return {
|
|
539
|
-
fontFamily: resolveFontFallback(v.family, _nullishCoalesce(v.category, () => ( _optionalChain([this, 'access',
|
|
676
|
+
fontFamily: resolveFontFallback(v.family, _nullishCoalesce(v.category, () => ( _optionalChain([this, 'access', _23 => _23.selectedFont, 'optionalAccess', _24 => _24.category])))),
|
|
540
677
|
fontWeight: String(v.weight),
|
|
541
678
|
fontStyle: v.style,
|
|
542
679
|
fontSize: `${v.size}px`,
|
|
@@ -555,7 +692,7 @@ var FontPickerModel = (_class = class {
|
|
|
555
692
|
try {
|
|
556
693
|
const fonts = await this.provider.listFonts();
|
|
557
694
|
if (this.destroyed) return fonts;
|
|
558
|
-
const known =
|
|
695
|
+
const known = _chunk4XSBOGMKcjs.findFont.call(void 0, fonts, this.state.value.family);
|
|
559
696
|
this.set({ fonts, loading: false, value: known ? { ...this.state.value, family: known.family, category: known.category } : this.state.value });
|
|
560
697
|
return fonts;
|
|
561
698
|
} catch (e) {
|
|
@@ -580,11 +717,11 @@ var FontPickerModel = (_class = class {
|
|
|
580
717
|
* the family has it, else picks the nearest one; keeps the style if offered, else `normal`.
|
|
581
718
|
*/
|
|
582
719
|
selectFamily(family) {
|
|
583
|
-
const font =
|
|
720
|
+
const font = _chunk4XSBOGMKcjs.findFont.call(void 0, this.state.fonts, family);
|
|
584
721
|
if (!font && this.state.fonts.length) throw new Error(`Font "${family}" is not available; pick one of the listed families (search with a query).`);
|
|
585
|
-
const name = _nullishCoalesce(_optionalChain([font, 'optionalAccess',
|
|
722
|
+
const name = _nullishCoalesce(_optionalChain([font, 'optionalAccess', _25 => _25.family]), () => ( family.trim()));
|
|
586
723
|
if (!name) throw new Error("family must not be empty.");
|
|
587
|
-
const weight = font ?
|
|
724
|
+
const weight = font ? _chunk4XSBOGMKcjs.nearestFontWeight.call(void 0, this.state.value.weight, offeredWeights(font)) : this.state.value.weight;
|
|
588
725
|
const style = !font || font.styles.includes(this.state.value.style) ? this.state.value.style : "normal";
|
|
589
726
|
this.recentFamilies = [name, ...this.recentFamilies.filter((f) => f.toLowerCase() !== name.toLowerCase())].slice(0, this.maxRecent);
|
|
590
727
|
const { category: _old, ...rest } = this.state.value;
|
|
@@ -593,7 +730,7 @@ var FontPickerModel = (_class = class {
|
|
|
593
730
|
}
|
|
594
731
|
/** Sets the weight; snaps to the nearest weight the family offers. */
|
|
595
732
|
setWeight(weight) {
|
|
596
|
-
this.commit({ ...this.state.value, weight:
|
|
733
|
+
this.commit({ ...this.state.value, weight: _chunk4XSBOGMKcjs.nearestFontWeight.call(void 0, _chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, weight), this.availableWeights) });
|
|
597
734
|
}
|
|
598
735
|
setStyle(style) {
|
|
599
736
|
if (!["normal", "italic", "oblique"].includes(style)) throw new Error(`Unknown style "${style}"; use normal, italic or oblique.`);
|
|
@@ -618,15 +755,15 @@ var FontPickerModel = (_class = class {
|
|
|
618
755
|
setValue(value) {
|
|
619
756
|
const v = this.state.value;
|
|
620
757
|
const family = _nullishCoalesce(value.family, () => ( v.family));
|
|
621
|
-
const known =
|
|
758
|
+
const known = _chunk4XSBOGMKcjs.findFont.call(void 0, this.state.fonts, family);
|
|
622
759
|
const next = {
|
|
623
|
-
family: _nullishCoalesce(_optionalChain([known, 'optionalAccess',
|
|
624
|
-
weight: value.weight !== void 0 ?
|
|
760
|
+
family: _nullishCoalesce(_optionalChain([known, 'optionalAccess', _26 => _26.family]), () => ( family)),
|
|
761
|
+
weight: value.weight !== void 0 ? _chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, value.weight) : v.weight,
|
|
625
762
|
style: _nullishCoalesce(value.style, () => ( v.style)),
|
|
626
763
|
size: _nullishCoalesce(value.size, () => ( v.size)),
|
|
627
764
|
lineHeight: _nullishCoalesce(value.lineHeight, () => ( v.lineHeight)),
|
|
628
765
|
letterSpacing: _nullishCoalesce(value.letterSpacing, () => ( v.letterSpacing)),
|
|
629
|
-
..._nullishCoalesce(_nullishCoalesce(value.category, () => ( _optionalChain([known, 'optionalAccess',
|
|
766
|
+
..._nullishCoalesce(_nullishCoalesce(value.category, () => ( _optionalChain([known, 'optionalAccess', _27 => _27.category]))), () => ( (family === v.family ? v.category : void 0))) ? { category: _nullishCoalesce(_nullishCoalesce(value.category, () => ( _optionalChain([known, 'optionalAccess', _28 => _28.category]))), () => ( v.category)) } : {}
|
|
630
767
|
};
|
|
631
768
|
if (JSON.stringify(next) === JSON.stringify(v)) return;
|
|
632
769
|
this.set({ value: next });
|
|
@@ -682,7 +819,7 @@ var FontPickerModel = (_class = class {
|
|
|
682
819
|
/** Loads a family's face through the provider (for previews) and tracks its status. */
|
|
683
820
|
async loadFamily(family, weight, style) {
|
|
684
821
|
const key = family.toLowerCase();
|
|
685
|
-
if (!_optionalChain([this, 'access',
|
|
822
|
+
if (!_optionalChain([this, 'access', _29 => _29.provider, 'optionalAccess', _30 => _30.loadFont])) return false;
|
|
686
823
|
if (this.state.loadStatus[key] === "loaded") return true;
|
|
687
824
|
this.set({ loadStatus: { ...this.state.loadStatus, [key]: "loading" } });
|
|
688
825
|
const ok = await this.provider.loadFont(family, _nullishCoalesce(weight, () => ( this.state.value.weight)), _nullishCoalesce(style, () => ( this.state.value.style))).catch(() => false);
|
|
@@ -701,12 +838,12 @@ var FontPickerModel = (_class = class {
|
|
|
701
838
|
}
|
|
702
839
|
this.set({ value });
|
|
703
840
|
const spec = this.value;
|
|
704
|
-
_optionalChain([this, 'access',
|
|
841
|
+
_optionalChain([this, 'access', _31 => _31.onChange, 'optionalCall', _32 => _32(spec)]);
|
|
705
842
|
for (const l of [...this.valueListeners]) l(spec);
|
|
706
843
|
}
|
|
707
844
|
derive(state) {
|
|
708
|
-
const results =
|
|
709
|
-
const recent = this.recentFamilies.map((f) =>
|
|
845
|
+
const results = _chunk4XSBOGMKcjs.filterFonts.call(void 0, state.fonts, { ...state.category !== "all" ? { category: state.category } : {}, query: state.query });
|
|
846
|
+
const recent = this.recentFamilies.map((f) => _chunk4XSBOGMKcjs.findFont.call(void 0, state.fonts, f)).filter((f) => !!f);
|
|
710
847
|
return { ...state, results, recent, highlighted: Math.min(state.highlighted, results.length - 1) };
|
|
711
848
|
}
|
|
712
849
|
set(patch) {
|
|
@@ -744,11 +881,11 @@ var rejectUnknownRatio = (ratio) => {
|
|
|
744
881
|
if (typeof ratio === "string" || typeof ratio === "number") resolveScaleRatio(ratio);
|
|
745
882
|
};
|
|
746
883
|
function createTypographyToolProvider(options = {}) {
|
|
747
|
-
const fonts = _nullishCoalesce(options.provider, () => (
|
|
884
|
+
const fonts = _nullishCoalesce(options.provider, () => ( _chunk4XSBOGMKcjs.createSystemFontProvider.call(void 0, )));
|
|
748
885
|
const listFonts = {
|
|
749
886
|
name: "list_fonts",
|
|
750
887
|
title: "List fonts",
|
|
751
|
-
description: "Lists the fonts the host offers, optionally filtered by category, source, weight, style or a name query (families starting with the query first). Returns { fonts: [{ family, category, weights, styles, isVariable?, source }], total, truncated }. Use a family from here with resolve_font_stack, load_font or set_font_selection.",
|
|
888
|
+
description: "Lists the fonts the host offers, optionally filtered by category, source, weight, style, tags or a name query (families starting with the query first), in the provider's order (web services list their most popular first). Returns { fonts: [{ family, category, weights, styles, isVariable?, source, tags? }], total, truncated, tags: [{ tag, count }] } where tags counts the classes of every font matched (display, slab-serif, variable-wdth\u2026), so you can narrow by them. Use a family from here with resolve_font_stack, load_font or set_font_selection.",
|
|
752
889
|
inputSchema: {
|
|
753
890
|
type: "object",
|
|
754
891
|
properties: {
|
|
@@ -757,6 +894,7 @@ function createTypographyToolProvider(options = {}) {
|
|
|
757
894
|
weight: WEIGHT_SCHEMA,
|
|
758
895
|
style: STYLE_SCHEMA,
|
|
759
896
|
query: { type: "string", description: "Case-insensitive part of the family name." },
|
|
897
|
+
tags: { type: "array", items: { type: "string", minLength: 1 }, maxItems: 8, description: "Only fonts carrying every one of these tags (from a previous result's tags)." },
|
|
760
898
|
limit: { type: "integer", minimum: 1, maximum: 500, description: "Most fonts to return (default 50, max 500)." }
|
|
761
899
|
},
|
|
762
900
|
additionalProperties: false
|
|
@@ -764,9 +902,12 @@ function createTypographyToolProvider(options = {}) {
|
|
|
764
902
|
annotations: { title: "List fonts", ...READ },
|
|
765
903
|
handler: async (args) => {
|
|
766
904
|
const all = await fonts.listFonts();
|
|
767
|
-
const kept =
|
|
905
|
+
const kept = _chunk4XSBOGMKcjs.filterFonts.call(void 0, all, { category: args.category, source: args.source, weight: args.weight, style: args.style, query: args.query, tags: args.tags });
|
|
768
906
|
const limit = _nullishCoalesce(args.limit, () => ( 50));
|
|
769
|
-
|
|
907
|
+
const counts = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const f of kept) for (const t of _nullishCoalesce(f.tags, () => ( []))) counts.set(t, (_nullishCoalesce(counts.get(t), () => ( 0))) + 1);
|
|
909
|
+
const tags = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([tag, count]) => ({ tag, count }));
|
|
910
|
+
return { fonts: kept.slice(0, limit), total: kept.length, truncated: kept.length > limit, tags };
|
|
770
911
|
}
|
|
771
912
|
};
|
|
772
913
|
const resolveFontStack = {
|
|
@@ -787,8 +928,8 @@ function createTypographyToolProvider(options = {}) {
|
|
|
787
928
|
const families = parseFontFamily(String(_nullishCoalesce(args.family, () => ( ""))));
|
|
788
929
|
if (!families.length) throw new Error('family must name at least one font, e.g. "Inter".');
|
|
789
930
|
const first = families[0];
|
|
790
|
-
const listed =
|
|
791
|
-
const category = _nullishCoalesce(_nullishCoalesce(args.category, () => ( _optionalChain([listed, 'optionalAccess',
|
|
931
|
+
const listed = _chunk4XSBOGMKcjs.findFont.call(void 0, await fonts.listFonts(), first);
|
|
932
|
+
const category = _nullishCoalesce(_nullishCoalesce(args.category, () => ( _optionalChain([listed, 'optionalAccess', _33 => _33.category]))), () => ( _chunk4XSBOGMKcjs.guessFontCategory.call(void 0, first)));
|
|
792
933
|
const stack = resolveFontFallback(args.family, category);
|
|
793
934
|
return { stack, families: parseFontFamily(stack), category, available: !!listed || !!await fonts.hasFont(first) };
|
|
794
935
|
}
|
|
@@ -943,9 +1084,9 @@ function createTypographyToolProvider(options = {}) {
|
|
|
943
1084
|
},
|
|
944
1085
|
annotations: { title: "Load font", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
945
1086
|
handler: async (args) => {
|
|
946
|
-
const listed =
|
|
1087
|
+
const listed = _chunk4XSBOGMKcjs.findFont.call(void 0, await fonts.listFonts(), String(args.family));
|
|
947
1088
|
if (!listed) throw new Error(`Font "${args.family}" is not offered by this host; call list_fonts to see the families you can load.`);
|
|
948
|
-
const weight =
|
|
1089
|
+
const weight = _chunk4XSBOGMKcjs.normalizeFontWeight.call(void 0, args.weight);
|
|
949
1090
|
const style = _nullishCoalesce(args.style, () => ( "normal"));
|
|
950
1091
|
const loaded = await fonts.loadFont(listed.family, weight, style);
|
|
951
1092
|
return { family: listed.family, weight, style, loaded };
|
|
@@ -988,15 +1129,15 @@ function createFontPickerToolProvider(model, options = {}) {
|
|
|
988
1129
|
},
|
|
989
1130
|
annotations: { title: "Set font selection", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
990
1131
|
handler: (args) => {
|
|
991
|
-
const reason = _optionalChain([options, 'access',
|
|
1132
|
+
const reason = _optionalChain([options, 'access', _34 => _34.readOnly, 'optionalCall', _35 => _35()]);
|
|
992
1133
|
if (reason) throw new Error(`The font picker is read-only: ${reason}`);
|
|
993
1134
|
if (!Object.keys(args).length) throw new Error("Pass at least one of family, weight, style, size, lineHeight or letterSpacing.");
|
|
994
1135
|
const { fonts } = model.getState();
|
|
995
|
-
if (args.family !== void 0 && fonts.length && !
|
|
1136
|
+
if (args.family !== void 0 && fonts.length && !_chunk4XSBOGMKcjs.findFont.call(void 0, fonts, args.family)) {
|
|
996
1137
|
throw new Error(`Font "${args.family}" is not in the picker; call list_fonts and pass one of its families.`);
|
|
997
1138
|
}
|
|
998
1139
|
if (args.style !== void 0) {
|
|
999
|
-
const font = args.family !== void 0 ?
|
|
1140
|
+
const font = args.family !== void 0 ? _chunk4XSBOGMKcjs.findFont.call(void 0, fonts, args.family) : model.selectedFont;
|
|
1000
1141
|
if (font && font.styles.length && !font.styles.includes(args.style)) throw new Error(`"${font.family}" has no ${args.style} style; it offers ${font.styles.join(", ")}.`);
|
|
1001
1142
|
}
|
|
1002
1143
|
if (args.family !== void 0) model.selectFamily(args.family);
|
|
@@ -1024,7 +1165,7 @@ function createFontPickerToolProvider(model, options = {}) {
|
|
|
1024
1165
|
annotations: { title: "List picker fonts", ...READ },
|
|
1025
1166
|
handler: (args) => {
|
|
1026
1167
|
const state = model.getState();
|
|
1027
|
-
const kept =
|
|
1168
|
+
const kept = _chunk4XSBOGMKcjs.filterFonts.call(void 0, state.fonts, { category: args.category, query: args.query });
|
|
1028
1169
|
const limit = _nullishCoalesce(args.limit, () => ( 50));
|
|
1029
1170
|
return { fonts: kept.slice(0, limit), recent: state.recent.map((f) => f.family), total: kept.length, truncated: kept.length > limit };
|
|
1030
1171
|
}
|
|
@@ -1098,4 +1239,8 @@ function createFontPickerToolProvider(model, options = {}) {
|
|
|
1098
1239
|
|
|
1099
1240
|
|
|
1100
1241
|
|
|
1101
|
-
|
|
1242
|
+
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
|
|
1246
|
+
exports.ASSUMED_NORMAL_LINE_HEIGHT = ASSUMED_NORMAL_LINE_HEIGHT; exports.CATEGORY_FALLBACK_STACK = CATEGORY_FALLBACK_STACK; exports.DEFAULT_PREVIEW_TEXT = DEFAULT_PREVIEW_TEXT; exports.FONT_CATEGORIES = FONT_CATEGORIES; exports.FONT_CATEGORY_LABELS = FONT_CATEGORY_LABELS; exports.FONT_PLATFORMS = _chunk4XSBOGMKcjs.FONT_PLATFORMS; exports.FONT_STACKS = FONT_STACKS; exports.FONT_STACK_NAMES = FONT_STACK_NAMES; exports.FONT_STYLES = FONT_STYLES; exports.FONT_WEIGHTS = FONT_WEIGHTS; exports.FontPickerModel = FontPickerModel; exports.GENERIC_FONT_FAMILIES = GENERIC_FONT_FAMILIES; exports.LARGE_BOLD_TEXT_PX = LARGE_BOLD_TEXT_PX; exports.LARGE_TEXT_PX = LARGE_TEXT_PX; exports.MODULAR_SCALE_RATIOS = MODULAR_SCALE_RATIOS; exports.MODULAR_SCALE_RATIO_NAMES = MODULAR_SCALE_RATIO_NAMES; exports.OPTIMAL_MEASURE = OPTIMAL_MEASURE; exports.READING_WORDS_PER_MINUTE = READING_WORDS_PER_MINUTE; exports.RECOMMENDED_LINE_HEIGHT = RECOMMENDED_LINE_HEIGHT; exports.SYSTEM_FONT_CATALOG = _chunk4XSBOGMKcjs.SYSTEM_FONT_CATALOG; exports.TYPE_HIERARCHY_STEPS = TYPE_HIERARCHY_STEPS; exports.auditTextReadability = auditTextReadability; exports.checkTypographyAccessibility = checkTypographyAccessibility; exports.combineFontProviders = _chunk4XSBOGMKcjs.combineFontProviders; exports.computeFluidSize = computeFluidSize; exports.computeTypeScale = computeTypeScale; exports.countGraphemes = countGraphemes; exports.createCatalogFontProvider = _chunk4XSBOGMKcjs.createCatalogFontProvider; exports.createFontPicker = createFontPicker; exports.createFontPickerToolProvider = createFontPickerToolProvider; exports.createGoogleFontsProvider = createGoogleFontsProvider; exports.createStylesheetFontProvider = createStylesheetFontProvider; exports.createSystemFontProvider = _chunk4XSBOGMKcjs.createSystemFontProvider; exports.createTypographyToolProvider = createTypographyToolProvider; exports.createWebFontProvider = _chunk4XSBOGMKcjs.createWebFontProvider; exports.ensureFontsLoaded = _chunk4XSBOGMKcjs.ensureFontsLoaded; exports.estimateCharactersPerLine = estimateCharactersPerLine; exports.filterFonts = _chunk4XSBOGMKcjs.filterFonts; exports.findFont = _chunk4XSBOGMKcjs.findFont; exports.fontFaceCss = _chunk4XSBOGMKcjs.fontFaceCss; exports.fontHasWeight = _chunk4XSBOGMKcjs.fontHasWeight; exports.formatFontFamily = formatFontFamily; exports.generateTypeHierarchy = generateTypeHierarchy; exports.getScaleRatio = getScaleRatio; exports.googleFontsCatalog = googleFontsCatalog; exports.googleFontsStylesheetUrl = googleFontsStylesheetUrl; exports.graphemes = graphemes; exports.guessFontCategory = _chunk4XSBOGMKcjs.guessFontCategory; exports.isFontWeight = _chunk4XSBOGMKcjs.isFontWeight; exports.isLargeText = isLargeText; exports.nearestFontWeight = _chunk4XSBOGMKcjs.nearestFontWeight; exports.normalizeFontWeight = _chunk4XSBOGMKcjs.normalizeFontWeight; exports.offeredWeights = offeredWeights; exports.parseFontFamily = parseFontFamily; exports.quoteFontFamily = quoteFontFamily; exports.recommendedLetterSpacing = recommendedLetterSpacing; exports.recommendedLineHeight = recommendedLineHeight; exports.resolveFontFallback = resolveFontFallback; exports.resolveScaleRatio = resolveScaleRatio; exports.segmentText = segmentText; exports.typeHierarchyToCss = typeHierarchyToCss; exports.typeScaleStep = typeScaleStep; exports.webFontSummary = _chunk4XSBOGMKcjs.webFontSummary;
|
|
@@ -10,6 +10,7 @@ export * from './segment.cjs';
|
|
|
10
10
|
export * from './metrics.cjs';
|
|
11
11
|
export * from './registry.cjs';
|
|
12
12
|
export * from './catalog.cjs';
|
|
13
|
+
export * from './stylesheet.cjs';
|
|
13
14
|
export * from './stack.cjs';
|
|
14
15
|
export * from './accessibility.cjs';
|
|
15
16
|
export * from './picker.cjs';
|
|
@@ -10,6 +10,7 @@ export * from './segment.js';
|
|
|
10
10
|
export * from './metrics.js';
|
|
11
11
|
export * from './registry.js';
|
|
12
12
|
export * from './catalog.js';
|
|
13
|
+
export * from './stylesheet.js';
|
|
13
14
|
export * from './stack.js';
|
|
14
15
|
export * from './accessibility.js';
|
|
15
16
|
export * from './picker.js';
|
package/dist/typography/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
nearestFontWeight,
|
|
16
16
|
normalizeFontWeight,
|
|
17
17
|
webFontSummary
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-N4ZXW3WB.js";
|
|
19
19
|
import {
|
|
20
20
|
suggestAccessibleForeground
|
|
21
21
|
} from "../chunk-YBAPT7NL.js";
|
|
@@ -312,6 +312,143 @@ function auditTextReadability(text, font = {}) {
|
|
|
312
312
|
};
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// src/typography/stylesheet.ts
|
|
316
|
+
function linkStylesheet(url) {
|
|
317
|
+
const doc = globalThis.document;
|
|
318
|
+
if (!doc?.head) return Promise.reject(new Error("No document to add a stylesheet to."));
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
const link = doc.createElement("link");
|
|
321
|
+
link.rel = "stylesheet";
|
|
322
|
+
link.href = url;
|
|
323
|
+
link.onload = () => resolve();
|
|
324
|
+
link.onerror = () => {
|
|
325
|
+
link.remove();
|
|
326
|
+
reject(new Error(`Stylesheet failed to load: ${url}`));
|
|
327
|
+
};
|
|
328
|
+
doc.head.appendChild(link);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
var SAMPLE_TEXT = "AaBbZz09";
|
|
332
|
+
var quoteFamily = (family) => `"${family.replace(/["\\]/g, "\\$&")}"`;
|
|
333
|
+
function createStylesheetFontProvider(options) {
|
|
334
|
+
let catalog = null;
|
|
335
|
+
const fonts = () => {
|
|
336
|
+
catalog ??= Promise.resolve().then(options.listFonts).catch((error) => {
|
|
337
|
+
catalog = null;
|
|
338
|
+
throw error;
|
|
339
|
+
});
|
|
340
|
+
return catalog;
|
|
341
|
+
};
|
|
342
|
+
const addStylesheet = options.addStylesheet ?? linkStylesheet;
|
|
343
|
+
const fontSet = () => options.fontFaceSet ?? globalThis.document?.fonts;
|
|
344
|
+
const sheets = /* @__PURE__ */ new Map();
|
|
345
|
+
const faces = /* @__PURE__ */ new Map();
|
|
346
|
+
const done = /* @__PURE__ */ new Set();
|
|
347
|
+
const key = (family, weight, style) => `${family.toLowerCase()}|${normalizeFontWeight(weight)}|${style ?? "normal"}`;
|
|
348
|
+
const sheet = (url) => {
|
|
349
|
+
let pending = sheets.get(url);
|
|
350
|
+
if (!pending) {
|
|
351
|
+
pending = addStylesheet(url).catch((error) => {
|
|
352
|
+
sheets.delete(url);
|
|
353
|
+
throw error;
|
|
354
|
+
});
|
|
355
|
+
sheets.set(url, pending);
|
|
356
|
+
}
|
|
357
|
+
return pending;
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
id: options.id ?? "stylesheet",
|
|
361
|
+
listFonts: async () => (await fonts()).map((f) => ({ ...f })),
|
|
362
|
+
hasFont: async (family) => !!findFont(await fonts(), family),
|
|
363
|
+
loaded: (family, weight, style) => done.has(key(family, weight, style)),
|
|
364
|
+
async loadFont(family, weight, style) {
|
|
365
|
+
const k = key(family, weight, style);
|
|
366
|
+
const cached = faces.get(k);
|
|
367
|
+
if (cached) return cached;
|
|
368
|
+
const promise = (async () => {
|
|
369
|
+
const listed = findFont(await fonts(), family);
|
|
370
|
+
const set = fontSet();
|
|
371
|
+
if (!listed || !set) return false;
|
|
372
|
+
const wanted = normalizeFontWeight(weight);
|
|
373
|
+
const w = listed.isVariable || !listed.weights.length ? wanted : nearestFontWeight(wanted, listed.weights);
|
|
374
|
+
const st = !listed.styles.length || listed.styles.includes(style ?? "normal") ? style ?? "normal" : "normal";
|
|
375
|
+
await sheet(options.stylesheetUrl(listed.family, w, st));
|
|
376
|
+
const matched = await set.load(`${st === "normal" ? "" : `${st} `}${w} 1em ${quoteFamily(listed.family)}`, options.sampleText ?? SAMPLE_TEXT);
|
|
377
|
+
if (!matched.length) throw new Error("No face matched.");
|
|
378
|
+
done.add(k);
|
|
379
|
+
return true;
|
|
380
|
+
})().catch(() => {
|
|
381
|
+
faces.delete(k);
|
|
382
|
+
return false;
|
|
383
|
+
});
|
|
384
|
+
faces.set(k, promise);
|
|
385
|
+
return promise;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
var GOOGLE_CATEGORIES = {
|
|
390
|
+
"sans serif": "sans-serif",
|
|
391
|
+
serif: "serif",
|
|
392
|
+
display: "display",
|
|
393
|
+
handwriting: "handwriting",
|
|
394
|
+
monospace: "monospace"
|
|
395
|
+
};
|
|
396
|
+
var tagOf = (value) => value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
397
|
+
function googleFontsCatalog(metadata) {
|
|
398
|
+
let data = metadata;
|
|
399
|
+
if (typeof data === "string") data = JSON.parse(data.replace(/^\)\]\}'\s*/, ""));
|
|
400
|
+
const list = Array.isArray(data) ? data : data?.familyMetadataList;
|
|
401
|
+
if (!Array.isArray(list)) throw new Error("Not Google Fonts metadata: expected familyMetadataList.");
|
|
402
|
+
const families = list.filter((f) => typeof f?.family === "string" && f.family);
|
|
403
|
+
return families.map((f, index) => ({ f, index })).sort((a, b) => (a.f.popularity ?? Infinity) - (b.f.popularity ?? Infinity) || a.index - b.index).map(({ f }) => {
|
|
404
|
+
const classes = (f.classifications ?? []).map(tagOf);
|
|
405
|
+
const filed = GOOGLE_CATEGORIES[(f.category ?? "").toLowerCase()] ?? "sans-serif";
|
|
406
|
+
const category = ["monospace", "handwriting", "display"].find((c) => classes.includes(c)) ?? filed;
|
|
407
|
+
const weights = /* @__PURE__ */ new Set();
|
|
408
|
+
const styles = /* @__PURE__ */ new Set();
|
|
409
|
+
for (const face of Object.keys(f.fonts ?? {})) {
|
|
410
|
+
const m = /^(\d{3})(i?)$/.exec(face);
|
|
411
|
+
if (!m) continue;
|
|
412
|
+
weights.add(normalizeFontWeight(Number(m[1])));
|
|
413
|
+
styles.add(m[2] ? "italic" : "normal");
|
|
414
|
+
}
|
|
415
|
+
const wght = f.axes?.find((a) => a.tag === "wght");
|
|
416
|
+
if (wght) {
|
|
417
|
+
for (const w of FONT_WEIGHTS) if (w >= wght.min && w <= wght.max) weights.add(w);
|
|
418
|
+
}
|
|
419
|
+
if (!weights.size) weights.add(400);
|
|
420
|
+
if (!styles.size) styles.add("normal");
|
|
421
|
+
const tags = [.../* @__PURE__ */ new Set([...f.category ? [tagOf(f.category)] : [], ...classes, ...f.stroke ? [tagOf(f.stroke)] : [], ...(f.axes ?? []).map((a) => `variable-${a.tag.toLowerCase()}`)])];
|
|
422
|
+
return {
|
|
423
|
+
family: f.family,
|
|
424
|
+
category,
|
|
425
|
+
weights: [...weights].sort((a, b) => a - b),
|
|
426
|
+
styles: [...styles],
|
|
427
|
+
...wght ? { isVariable: true } : {},
|
|
428
|
+
source: "web",
|
|
429
|
+
...tags.length ? { tags } : {}
|
|
430
|
+
};
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function googleFontsStylesheetUrl(family, weight = 400, style = "normal", options = {}) {
|
|
434
|
+
const base = options.baseUrl ?? "https://fonts.googleapis.com/css2";
|
|
435
|
+
const name = encodeURIComponent(family.trim()).replace(/%20/g, "+");
|
|
436
|
+
return `${base}?family=${name}:ital,wght@${style === "normal" ? 0 : 1},${normalizeFontWeight(weight)}&display=${options.display ?? "swap"}`;
|
|
437
|
+
}
|
|
438
|
+
function createGoogleFontsProvider(options) {
|
|
439
|
+
const { catalog, baseUrl, ...rest } = options;
|
|
440
|
+
return createStylesheetFontProvider({
|
|
441
|
+
id: "google-fonts",
|
|
442
|
+
...rest,
|
|
443
|
+
listFonts: async () => {
|
|
444
|
+
const data = await catalog();
|
|
445
|
+
const built = Array.isArray(data) && data.every((f) => typeof f?.family === "string" && typeof f?.category === "string" && Array.isArray(f?.weights));
|
|
446
|
+
return built ? data : googleFontsCatalog(data);
|
|
447
|
+
},
|
|
448
|
+
stylesheetUrl: (family, weight, style) => googleFontsStylesheetUrl(family, weight, style, { baseUrl })
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
315
452
|
// src/typography/stack.ts
|
|
316
453
|
var GENERIC_FONT_FAMILIES = [
|
|
317
454
|
"serif",
|
|
@@ -748,7 +885,7 @@ function createTypographyToolProvider(options = {}) {
|
|
|
748
885
|
const listFonts = {
|
|
749
886
|
name: "list_fonts",
|
|
750
887
|
title: "List fonts",
|
|
751
|
-
description: "Lists the fonts the host offers, optionally filtered by category, source, weight, style or a name query (families starting with the query first). Returns { fonts: [{ family, category, weights, styles, isVariable?, source }], total, truncated }. Use a family from here with resolve_font_stack, load_font or set_font_selection.",
|
|
888
|
+
description: "Lists the fonts the host offers, optionally filtered by category, source, weight, style, tags or a name query (families starting with the query first), in the provider's order (web services list their most popular first). Returns { fonts: [{ family, category, weights, styles, isVariable?, source, tags? }], total, truncated, tags: [{ tag, count }] } where tags counts the classes of every font matched (display, slab-serif, variable-wdth\u2026), so you can narrow by them. Use a family from here with resolve_font_stack, load_font or set_font_selection.",
|
|
752
889
|
inputSchema: {
|
|
753
890
|
type: "object",
|
|
754
891
|
properties: {
|
|
@@ -757,6 +894,7 @@ function createTypographyToolProvider(options = {}) {
|
|
|
757
894
|
weight: WEIGHT_SCHEMA,
|
|
758
895
|
style: STYLE_SCHEMA,
|
|
759
896
|
query: { type: "string", description: "Case-insensitive part of the family name." },
|
|
897
|
+
tags: { type: "array", items: { type: "string", minLength: 1 }, maxItems: 8, description: "Only fonts carrying every one of these tags (from a previous result's tags)." },
|
|
760
898
|
limit: { type: "integer", minimum: 1, maximum: 500, description: "Most fonts to return (default 50, max 500)." }
|
|
761
899
|
},
|
|
762
900
|
additionalProperties: false
|
|
@@ -764,9 +902,12 @@ function createTypographyToolProvider(options = {}) {
|
|
|
764
902
|
annotations: { title: "List fonts", ...READ },
|
|
765
903
|
handler: async (args) => {
|
|
766
904
|
const all = await fonts.listFonts();
|
|
767
|
-
const kept = filterFonts(all, { category: args.category, source: args.source, weight: args.weight, style: args.style, query: args.query });
|
|
905
|
+
const kept = filterFonts(all, { category: args.category, source: args.source, weight: args.weight, style: args.style, query: args.query, tags: args.tags });
|
|
768
906
|
const limit = args.limit ?? 50;
|
|
769
|
-
|
|
907
|
+
const counts = /* @__PURE__ */ new Map();
|
|
908
|
+
for (const f of kept) for (const t of f.tags ?? []) counts.set(t, (counts.get(t) ?? 0) + 1);
|
|
909
|
+
const tags = [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([tag, count]) => ({ tag, count }));
|
|
910
|
+
return { fonts: kept.slice(0, limit), total: kept.length, truncated: kept.length > limit, tags };
|
|
770
911
|
}
|
|
771
912
|
};
|
|
772
913
|
const resolveFontStack = {
|
|
@@ -1069,6 +1210,8 @@ export {
|
|
|
1069
1210
|
createCatalogFontProvider,
|
|
1070
1211
|
createFontPicker,
|
|
1071
1212
|
createFontPickerToolProvider,
|
|
1213
|
+
createGoogleFontsProvider,
|
|
1214
|
+
createStylesheetFontProvider,
|
|
1072
1215
|
createSystemFontProvider,
|
|
1073
1216
|
createTypographyToolProvider,
|
|
1074
1217
|
createWebFontProvider,
|
|
@@ -1081,6 +1224,8 @@ export {
|
|
|
1081
1224
|
formatFontFamily,
|
|
1082
1225
|
generateTypeHierarchy,
|
|
1083
1226
|
getScaleRatio,
|
|
1227
|
+
googleFontsCatalog,
|
|
1228
|
+
googleFontsStylesheetUrl,
|
|
1084
1229
|
graphemes,
|
|
1085
1230
|
guessFontCategory,
|
|
1086
1231
|
isFontWeight,
|
|
@@ -14,6 +14,8 @@ export interface FontSummary {
|
|
|
14
14
|
source: FontSource;
|
|
15
15
|
/** Platforms that ship it, for system fonts. */
|
|
16
16
|
platforms?: FontPlatform[];
|
|
17
|
+
/** Lowercase, hyphenated classes from the provider, e.g. `display`, `slab-serif`, `variable-wdth`. */
|
|
18
|
+
tags?: string[];
|
|
17
19
|
}
|
|
18
20
|
export interface FontProvider {
|
|
19
21
|
readonly id: string;
|
|
@@ -33,6 +35,8 @@ export interface FontFilter {
|
|
|
33
35
|
/** Case-insensitive substring of the family name. */
|
|
34
36
|
query?: string;
|
|
35
37
|
isVariable?: boolean;
|
|
38
|
+
/** Keep fonts carrying every one of these tags (any case, spaces as hyphens). */
|
|
39
|
+
tags?: readonly string[];
|
|
36
40
|
}
|
|
37
41
|
/** Whether a font offers a weight (variable fonts offer every weight in their range). */
|
|
38
42
|
export declare function fontHasWeight(font: FontSummary, weight: FontWeight): boolean;
|
|
@@ -14,6 +14,8 @@ export interface FontSummary {
|
|
|
14
14
|
source: FontSource;
|
|
15
15
|
/** Platforms that ship it, for system fonts. */
|
|
16
16
|
platforms?: FontPlatform[];
|
|
17
|
+
/** Lowercase, hyphenated classes from the provider, e.g. `display`, `slab-serif`, `variable-wdth`. */
|
|
18
|
+
tags?: string[];
|
|
17
19
|
}
|
|
18
20
|
export interface FontProvider {
|
|
19
21
|
readonly id: string;
|
|
@@ -33,6 +35,8 @@ export interface FontFilter {
|
|
|
33
35
|
/** Case-insensitive substring of the family name. */
|
|
34
36
|
query?: string;
|
|
35
37
|
isVariable?: boolean;
|
|
38
|
+
/** Keep fonts carrying every one of these tags (any case, spaces as hyphens). */
|
|
39
|
+
tags?: readonly string[];
|
|
36
40
|
}
|
|
37
41
|
/** Whether a font offers a weight (variable fonts offer every weight in their range). */
|
|
38
42
|
export declare function fontHasWeight(font: FontSummary, weight: FontWeight): boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font services that serve CSS (Google Fonts, Bunny Fonts, a self-hosted css2-compatible proxy):
|
|
3
|
+
* a provider that loads a family by adding the service's stylesheet and waiting for the face,
|
|
4
|
+
* and an adapter for the Google Fonts catalog and stylesheet URLs.
|
|
5
|
+
*/
|
|
6
|
+
import { type FontProvider, type FontSummary } from './registry.cjs';
|
|
7
|
+
import { type FontStyle, type FontWeight } from './types.cjs';
|
|
8
|
+
/** The parts of `document.fonts` the stylesheet provider uses. */
|
|
9
|
+
export interface StylesheetFontSetLike {
|
|
10
|
+
load(font: string, text?: string): Promise<readonly unknown[]>;
|
|
11
|
+
}
|
|
12
|
+
export interface StylesheetFontProviderOptions {
|
|
13
|
+
id?: string;
|
|
14
|
+
/** The service's families; called once, on first use. */
|
|
15
|
+
listFonts: () => FontSummary[] | Promise<FontSummary[]>;
|
|
16
|
+
/** The stylesheet URL that declares one face. */
|
|
17
|
+
stylesheetUrl: (family: string, weight: FontWeight, style: FontStyle) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Adds a stylesheet and resolves when it has loaded (default: a `<link rel="stylesheet">` in
|
|
20
|
+
* `document.head`). Inject a fake in tests: nothing then hits the network.
|
|
21
|
+
*/
|
|
22
|
+
addStylesheet?: (url: string) => Promise<void>;
|
|
23
|
+
/** Where faces are waited for (default `document.fonts`). */
|
|
24
|
+
fontFaceSet?: StylesheetFontSetLike;
|
|
25
|
+
/**
|
|
26
|
+
* Characters the face must cover once loaded (default Latin letters and digits). Services split
|
|
27
|
+
* a family into `unicode-range` subsets, and `document.fonts.load` without text fetches none of
|
|
28
|
+
* the files a page needs for words.
|
|
29
|
+
*/
|
|
30
|
+
sampleText?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fonts from a service that serves CSS. `loadFont` asks for the nearest weight and style the
|
|
34
|
+
* family offers, adds that face's stylesheet once, then resolves `true` when the browser has the face (`document.fonts.load` matched it), `false` when the family
|
|
35
|
+
* is not listed, the stylesheet fails or no face matches. Loads are cached per face; failures can
|
|
36
|
+
* be retried.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createStylesheetFontProvider(options: StylesheetFontProviderOptions): FontProvider & {
|
|
39
|
+
loaded(family: string, weight?: FontWeight, style?: FontStyle): boolean;
|
|
40
|
+
};
|
|
41
|
+
/** One family in the Google Fonts metadata (`https://fonts.google.com/metadata/fonts`), as far as this module reads it. */
|
|
42
|
+
export interface GoogleFontsFamilyMetadata {
|
|
43
|
+
family: string;
|
|
44
|
+
/** `Sans Serif`, `Serif`, `Display`, `Handwriting` or `Monospace`. */
|
|
45
|
+
category?: string;
|
|
46
|
+
/** Further classes, e.g. `Display`, `Handwriting`, `Monospace`, `Symbols`. */
|
|
47
|
+
classifications?: readonly string[];
|
|
48
|
+
/** Stroke class, e.g. `Sans Serif`, `Serif`, `Slab Serif`. */
|
|
49
|
+
stroke?: string | null;
|
|
50
|
+
/** Faces by weight, `i` for italic: `{ "400": …, "700i": … }`. */
|
|
51
|
+
fonts?: Record<string, unknown>;
|
|
52
|
+
axes?: ReadonlyArray<{
|
|
53
|
+
tag: string;
|
|
54
|
+
min: number;
|
|
55
|
+
max: number;
|
|
56
|
+
}>;
|
|
57
|
+
/** Rank, 1 = most popular. */
|
|
58
|
+
popularity?: number;
|
|
59
|
+
subsets?: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The families in a Google Fonts metadata response (the object, its `familyMetadataList`, or the
|
|
63
|
+
* raw text with its `)]}'` prefix), most popular first. A family classified as display,
|
|
64
|
+
* handwriting or monospace takes that category even when Google files it under sans serif or
|
|
65
|
+
* serif (Anton is a sans serif classified as display); its classes, stroke and axes become
|
|
66
|
+
* `tags` (`display`, `slab-serif`, `variable-wdth`…).
|
|
67
|
+
*/
|
|
68
|
+
export declare function googleFontsCatalog(metadata: unknown): FontSummary[];
|
|
69
|
+
/** The Google Fonts css2 URL for one face, e.g. `…/css2?family=Space+Grotesk:ital,wght@0,700&display=swap`. */
|
|
70
|
+
export declare function googleFontsStylesheetUrl(family: string, weight?: FontWeight, style?: FontStyle, options?: {
|
|
71
|
+
baseUrl?: string;
|
|
72
|
+
display?: string;
|
|
73
|
+
}): string;
|
|
74
|
+
export interface GoogleFontsProviderOptions extends Omit<StylesheetFontProviderOptions, 'listFonts' | 'stylesheetUrl'> {
|
|
75
|
+
/**
|
|
76
|
+
* The Google Fonts metadata, or the catalog already built from it. fonts.google.com does not
|
|
77
|
+
* allow cross-origin reads, so a browser host fetches it through its own server.
|
|
78
|
+
*/
|
|
79
|
+
catalog: () => Promise<unknown>;
|
|
80
|
+
/** css2-compatible stylesheet endpoint (default `https://fonts.googleapis.com/css2`). */
|
|
81
|
+
baseUrl?: string;
|
|
82
|
+
}
|
|
83
|
+
/** Google Fonts (or a css2-compatible mirror): its catalog, loaded face by face through its stylesheets. */
|
|
84
|
+
export declare function createGoogleFontsProvider(options: GoogleFontsProviderOptions): FontProvider & {
|
|
85
|
+
loaded(family: string, weight?: FontWeight, style?: FontStyle): boolean;
|
|
86
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font services that serve CSS (Google Fonts, Bunny Fonts, a self-hosted css2-compatible proxy):
|
|
3
|
+
* a provider that loads a family by adding the service's stylesheet and waiting for the face,
|
|
4
|
+
* and an adapter for the Google Fonts catalog and stylesheet URLs.
|
|
5
|
+
*/
|
|
6
|
+
import { type FontProvider, type FontSummary } from './registry.js';
|
|
7
|
+
import { type FontStyle, type FontWeight } from './types.js';
|
|
8
|
+
/** The parts of `document.fonts` the stylesheet provider uses. */
|
|
9
|
+
export interface StylesheetFontSetLike {
|
|
10
|
+
load(font: string, text?: string): Promise<readonly unknown[]>;
|
|
11
|
+
}
|
|
12
|
+
export interface StylesheetFontProviderOptions {
|
|
13
|
+
id?: string;
|
|
14
|
+
/** The service's families; called once, on first use. */
|
|
15
|
+
listFonts: () => FontSummary[] | Promise<FontSummary[]>;
|
|
16
|
+
/** The stylesheet URL that declares one face. */
|
|
17
|
+
stylesheetUrl: (family: string, weight: FontWeight, style: FontStyle) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Adds a stylesheet and resolves when it has loaded (default: a `<link rel="stylesheet">` in
|
|
20
|
+
* `document.head`). Inject a fake in tests: nothing then hits the network.
|
|
21
|
+
*/
|
|
22
|
+
addStylesheet?: (url: string) => Promise<void>;
|
|
23
|
+
/** Where faces are waited for (default `document.fonts`). */
|
|
24
|
+
fontFaceSet?: StylesheetFontSetLike;
|
|
25
|
+
/**
|
|
26
|
+
* Characters the face must cover once loaded (default Latin letters and digits). Services split
|
|
27
|
+
* a family into `unicode-range` subsets, and `document.fonts.load` without text fetches none of
|
|
28
|
+
* the files a page needs for words.
|
|
29
|
+
*/
|
|
30
|
+
sampleText?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fonts from a service that serves CSS. `loadFont` asks for the nearest weight and style the
|
|
34
|
+
* family offers, adds that face's stylesheet once, then resolves `true` when the browser has the face (`document.fonts.load` matched it), `false` when the family
|
|
35
|
+
* is not listed, the stylesheet fails or no face matches. Loads are cached per face; failures can
|
|
36
|
+
* be retried.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createStylesheetFontProvider(options: StylesheetFontProviderOptions): FontProvider & {
|
|
39
|
+
loaded(family: string, weight?: FontWeight, style?: FontStyle): boolean;
|
|
40
|
+
};
|
|
41
|
+
/** One family in the Google Fonts metadata (`https://fonts.google.com/metadata/fonts`), as far as this module reads it. */
|
|
42
|
+
export interface GoogleFontsFamilyMetadata {
|
|
43
|
+
family: string;
|
|
44
|
+
/** `Sans Serif`, `Serif`, `Display`, `Handwriting` or `Monospace`. */
|
|
45
|
+
category?: string;
|
|
46
|
+
/** Further classes, e.g. `Display`, `Handwriting`, `Monospace`, `Symbols`. */
|
|
47
|
+
classifications?: readonly string[];
|
|
48
|
+
/** Stroke class, e.g. `Sans Serif`, `Serif`, `Slab Serif`. */
|
|
49
|
+
stroke?: string | null;
|
|
50
|
+
/** Faces by weight, `i` for italic: `{ "400": …, "700i": … }`. */
|
|
51
|
+
fonts?: Record<string, unknown>;
|
|
52
|
+
axes?: ReadonlyArray<{
|
|
53
|
+
tag: string;
|
|
54
|
+
min: number;
|
|
55
|
+
max: number;
|
|
56
|
+
}>;
|
|
57
|
+
/** Rank, 1 = most popular. */
|
|
58
|
+
popularity?: number;
|
|
59
|
+
subsets?: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The families in a Google Fonts metadata response (the object, its `familyMetadataList`, or the
|
|
63
|
+
* raw text with its `)]}'` prefix), most popular first. A family classified as display,
|
|
64
|
+
* handwriting or monospace takes that category even when Google files it under sans serif or
|
|
65
|
+
* serif (Anton is a sans serif classified as display); its classes, stroke and axes become
|
|
66
|
+
* `tags` (`display`, `slab-serif`, `variable-wdth`…).
|
|
67
|
+
*/
|
|
68
|
+
export declare function googleFontsCatalog(metadata: unknown): FontSummary[];
|
|
69
|
+
/** The Google Fonts css2 URL for one face, e.g. `…/css2?family=Space+Grotesk:ital,wght@0,700&display=swap`. */
|
|
70
|
+
export declare function googleFontsStylesheetUrl(family: string, weight?: FontWeight, style?: FontStyle, options?: {
|
|
71
|
+
baseUrl?: string;
|
|
72
|
+
display?: string;
|
|
73
|
+
}): string;
|
|
74
|
+
export interface GoogleFontsProviderOptions extends Omit<StylesheetFontProviderOptions, 'listFonts' | 'stylesheetUrl'> {
|
|
75
|
+
/**
|
|
76
|
+
* The Google Fonts metadata, or the catalog already built from it. fonts.google.com does not
|
|
77
|
+
* allow cross-origin reads, so a browser host fetches it through its own server.
|
|
78
|
+
*/
|
|
79
|
+
catalog: () => Promise<unknown>;
|
|
80
|
+
/** css2-compatible stylesheet endpoint (default `https://fonts.googleapis.com/css2`). */
|
|
81
|
+
baseUrl?: string;
|
|
82
|
+
}
|
|
83
|
+
/** Google Fonts (or a css2-compatible mirror): its catalog, loaded face by face through its stylesheets. */
|
|
84
|
+
export declare function createGoogleFontsProvider(options: GoogleFontsProviderOptions): FontProvider & {
|
|
85
|
+
loaded(family: string, weight?: FontWeight, style?: FontStyle): boolean;
|
|
86
|
+
};
|
package/package.json
CHANGED
|
@@ -49,6 +49,11 @@ For general error reporting and undo conventions, see `pixerate-agent-tools`.
|
|
|
49
49
|
|
|
50
50
|
- **Batch parameter changes.** Set all related control values in a single `effect_authoring_set_values`
|
|
51
51
|
call so they apply cleanly in a single undo step.
|
|
52
|
+
- **Write colours as hex.** A control with `type: "color"` in `effect_authoring_describe` takes
|
|
53
|
+
`#rrggbb` (or `#rrggbbaa` when `alpha` is true), even when the provider's schema also accepts 0–1
|
|
54
|
+
channel arrays or CSS colour strings (Flair's colours do). Hex keyframes blend smoothly. A control
|
|
55
|
+
that is in `values` but has no entry in `controls` has a shape no field can edit. Set it only with a
|
|
56
|
+
value that matches the provider's schema.
|
|
52
57
|
- **Check compile issues.** When editing source code via `effect_authoring_set_source`, always verify
|
|
53
58
|
the returned `issues` array. Fix errors before attempting to preview or set controls.
|
|
54
59
|
- **Keyframe animation tracks.** Controls with active keyframes calculate their values dynamically
|
|
@@ -23,8 +23,12 @@ what this machine has.
|
|
|
23
23
|
|
|
24
24
|
## Working method
|
|
25
25
|
|
|
26
|
-
1. **Find fonts.** `list_fonts` with `category`, `weight`, `style` or `query`. Only use
|
|
27
|
-
returns (or that the user names), and always ship them inside a stack from
|
|
26
|
+
1. **Find fonts.** `list_fonts` with `category`, `weight`, `style`, `tags` or `query`. Only use
|
|
27
|
+
families it returns (or that the user names), and always ship them inside a stack from
|
|
28
|
+
`resolve_font_stack`. When the host offers a web font service (`source: 'web'`, e.g. Google
|
|
29
|
+
Fonts), its catalog is far larger than the installed fonts and listed most popular first:
|
|
30
|
+
narrow it by `category` and by the `tags` the result counts (`display`, `slab-serif`,
|
|
31
|
+
`variable-wdth`…) to find faces with the character you want, rather than taking the top few.
|
|
28
32
|
2. **Load before rendering.** `load_font` before drawing or capturing text in a web font, so the
|
|
29
33
|
first frame does not use the fallback. `loaded: false` means the fallback will show.
|
|
30
34
|
3. **Set up the scale.** `generate_type_hierarchy` for named tokens and ready CSS custom
|
|
@@ -62,7 +66,7 @@ what this machine has.
|
|
|
62
66
|
|
|
63
67
|
| Tool | When to use | Returns |
|
|
64
68
|
| --- | --- | --- |
|
|
65
|
-
| `list_fonts` | Discover families the host offers; filter by `category`, `source`, `weight`, `style`, `query`; `limit` default 50 | `{ fonts: [{ family, category, weights, styles, isVariable?, source }], total, truncated }` |
|
|
69
|
+
| `list_fonts` | Discover families the host offers; filter by `category`, `source`, `weight`, `style`, `tags`, `query`; `limit` default 50 | `{ fonts: [{ family, category, weights, styles, isVariable?, source, tags? }], total, truncated, tags: [{ tag, count }] }` |
|
|
66
70
|
| `resolve_font_stack` | Turn a family (or stack) into a resilient `font-family` value for its category; also says if the provider lists it | `{ stack, families, category, available }` |
|
|
67
71
|
| `suggest_type_scale` | Sizes for arbitrary integer `steps` (-12…12) from `baseSize` (px, default 16) and `ratio` (name or number, default major-third) | `{ ratio, baseSize, steps: [{ name, step, size, lineHeight, letterSpacing }] }` |
|
|
68
72
|
| `generate_type_hierarchy` | Named tokens caption…display on one scale, with CSS custom properties (`prefix`, `selector`) | `{ ratio, ratioName?, baseSize, steps: { body, h1, … }, css }` |
|