@pixerate/editor 0.175.0 → 0.177.0

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.
Files changed (39) hide show
  1. package/dist/{chunk-FKUKJ3XA.cjs → chunk-4XSBOGMK.cjs} +5 -4
  2. package/dist/{chunk-CJE5MOIS.js → chunk-N4ZXW3WB.js} +2 -1
  3. package/dist/{chunk-OVYQAYGH.cjs → chunk-UHMSMUHK.cjs} +75 -66
  4. package/dist/{chunk-BDIM6NVW.js → chunk-Y46OMERN.js} +10 -1
  5. package/dist/effects/fake.d.cts +1 -1
  6. package/dist/effects/fake.d.ts +1 -1
  7. package/dist/effects/index.cjs +38 -24
  8. package/dist/effects/index.js +20 -6
  9. package/dist/effects/types.d.cts +6 -0
  10. package/dist/effects/types.d.ts +6 -0
  11. package/dist/effects/ui.d.cts +0 -6
  12. package/dist/effects/ui.d.ts +0 -6
  13. package/dist/image-editor/controller.d.cts +28 -5
  14. package/dist/image-editor/controller.d.ts +28 -5
  15. package/dist/image-editor/effect-layers.d.cts +40 -0
  16. package/dist/image-editor/effect-layers.d.ts +40 -0
  17. package/dist/image-editor/index.cjs +931 -812
  18. package/dist/image-editor/index.d.cts +1 -0
  19. package/dist/image-editor/index.d.ts +1 -0
  20. package/dist/image-editor/index.js +152 -33
  21. package/dist/image-editor/layers/model.d.cts +15 -1
  22. package/dist/image-editor/layers/model.d.ts +15 -1
  23. package/dist/image-editor/layers/panel.d.cts +6 -2
  24. package/dist/image-editor/layers/panel.d.ts +6 -2
  25. package/dist/image-editor/provider-effect-tools.d.cts +2 -1
  26. package/dist/image-editor/provider-effect-tools.d.ts +2 -1
  27. package/dist/paint-editor/index.cjs +6 -6
  28. package/dist/paint-editor/index.js +1 -1
  29. package/dist/typography/index.cjs +185 -40
  30. package/dist/typography/index.d.cts +1 -0
  31. package/dist/typography/index.d.ts +1 -0
  32. package/dist/typography/index.js +149 -4
  33. package/dist/typography/registry.d.cts +4 -0
  34. package/dist/typography/registry.d.ts +4 -0
  35. package/dist/typography/stylesheet.d.cts +86 -0
  36. package/dist/typography/stylesheet.d.ts +86 -0
  37. package/package.json +1 -1
  38. package/skills/pixerate-image-editor/SKILL.md +2 -1
  39. package/skills/pixerate-typography/SKILL.md +7 -3
@@ -15,7 +15,7 @@
15
15
 
16
16
 
17
17
 
18
- var _chunkFKUKJ3XAcjs = require('../chunk-FKUKJ3XA.cjs');
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, () => ( _chunkFKUKJ3XAcjs.guessFontCategory.call(void 0, families[0])));
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 = _chunkFKUKJ3XAcjs.normalizeFontWeight.call(void 0, font.weight) >= 700;
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 = _chunkFKUKJ3XAcjs.normalizeFontWeight.call(void 0, font.weight) >= 700;
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 = _chunkFKUKJ3XAcjs.normalizeFontWeight.call(void 0, font.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) => _chunkFKUKJ3XAcjs.normalizeFontWeight.call(void 0, 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', _4 => _4[0], 'optionalAccess', _5 => _5.family]))), () => ( "system-ui"));
481
- const known = _chunkFKUKJ3XAcjs.findFont.call(void 0, fonts, family);
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', _6 => _6.family]), () => ( family)),
485
- weight: _chunkFKUKJ3XAcjs.normalizeFontWeight.call(void 0, v.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', _7 => _7.category]))) ? { category: _nullishCoalesce(v.category, () => ( _optionalChain([known, 'optionalAccess', _8 => _8.category]))) } : {}
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 _chunkFKUKJ3XAcjs.findFont.call(void 0, this.state.fonts, this.state.value.family);
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', _9 => _9.selectedFont, 'optionalAccess', _10 => _10.styles, 'access', _11 => _11.length]) ? [...this.selectedFont.styles] : ["normal", "italic"];
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', _12 => _12.selectedFont, 'optionalAccess', _13 => _13.category])))),
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 = _chunkFKUKJ3XAcjs.findFont.call(void 0, fonts, this.state.value.family);
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 = _chunkFKUKJ3XAcjs.findFont.call(void 0, this.state.fonts, family);
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', _14 => _14.family]), () => ( family.trim()));
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 ? _chunkFKUKJ3XAcjs.nearestFontWeight.call(void 0, this.state.value.weight, offeredWeights(font)) : this.state.value.weight;
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: _chunkFKUKJ3XAcjs.nearestFontWeight.call(void 0, _chunkFKUKJ3XAcjs.normalizeFontWeight.call(void 0, weight), this.availableWeights) });
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 = _chunkFKUKJ3XAcjs.findFont.call(void 0, this.state.fonts, family);
758
+ const known = _chunk4XSBOGMKcjs.findFont.call(void 0, this.state.fonts, family);
622
759
  const next = {
623
- family: _nullishCoalesce(_optionalChain([known, 'optionalAccess', _15 => _15.family]), () => ( family)),
624
- weight: value.weight !== void 0 ? _chunkFKUKJ3XAcjs.normalizeFontWeight.call(void 0, value.weight) : v.weight,
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', _16 => _16.category]))), () => ( (family === v.family ? v.category : void 0))) ? { category: _nullishCoalesce(_nullishCoalesce(value.category, () => ( _optionalChain([known, 'optionalAccess', _17 => _17.category]))), () => ( v.category)) } : {}
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', _18 => _18.provider, 'optionalAccess', _19 => _19.loadFont])) return false;
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', _20 => _20.onChange, 'optionalCall', _21 => _21(spec)]);
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 = _chunkFKUKJ3XAcjs.filterFonts.call(void 0, state.fonts, { ...state.category !== "all" ? { category: state.category } : {}, query: state.query });
709
- const recent = this.recentFamilies.map((f) => _chunkFKUKJ3XAcjs.findFont.call(void 0, state.fonts, f)).filter((f) => !!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, () => ( _chunkFKUKJ3XAcjs.createSystemFontProvider.call(void 0, )));
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 = _chunkFKUKJ3XAcjs.filterFonts.call(void 0, all, { category: args.category, source: args.source, weight: args.weight, style: args.style, query: args.query });
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
- return { fonts: kept.slice(0, limit), total: kept.length, truncated: kept.length > limit };
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 = _chunkFKUKJ3XAcjs.findFont.call(void 0, await fonts.listFonts(), first);
791
- const category = _nullishCoalesce(_nullishCoalesce(args.category, () => ( _optionalChain([listed, 'optionalAccess', _22 => _22.category]))), () => ( _chunkFKUKJ3XAcjs.guessFontCategory.call(void 0, first)));
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 = _chunkFKUKJ3XAcjs.findFont.call(void 0, await fonts.listFonts(), String(args.family));
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 = _chunkFKUKJ3XAcjs.normalizeFontWeight.call(void 0, args.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', _23 => _23.readOnly, 'optionalCall', _24 => _24()]);
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 && !_chunkFKUKJ3XAcjs.findFont.call(void 0, fonts, args.family)) {
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 ? _chunkFKUKJ3XAcjs.findFont.call(void 0, fonts, args.family) : model.selectedFont;
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 = _chunkFKUKJ3XAcjs.filterFonts.call(void 0, state.fonts, { category: args.category, query: args.query });
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
- 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 = _chunkFKUKJ3XAcjs.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 = _chunkFKUKJ3XAcjs.SYSTEM_FONT_CATALOG; exports.TYPE_HIERARCHY_STEPS = TYPE_HIERARCHY_STEPS; exports.auditTextReadability = auditTextReadability; exports.checkTypographyAccessibility = checkTypographyAccessibility; exports.combineFontProviders = _chunkFKUKJ3XAcjs.combineFontProviders; exports.computeFluidSize = computeFluidSize; exports.computeTypeScale = computeTypeScale; exports.countGraphemes = countGraphemes; exports.createCatalogFontProvider = _chunkFKUKJ3XAcjs.createCatalogFontProvider; exports.createFontPicker = createFontPicker; exports.createFontPickerToolProvider = createFontPickerToolProvider; exports.createSystemFontProvider = _chunkFKUKJ3XAcjs.createSystemFontProvider; exports.createTypographyToolProvider = createTypographyToolProvider; exports.createWebFontProvider = _chunkFKUKJ3XAcjs.createWebFontProvider; exports.ensureFontsLoaded = _chunkFKUKJ3XAcjs.ensureFontsLoaded; exports.estimateCharactersPerLine = estimateCharactersPerLine; exports.filterFonts = _chunkFKUKJ3XAcjs.filterFonts; exports.findFont = _chunkFKUKJ3XAcjs.findFont; exports.fontFaceCss = _chunkFKUKJ3XAcjs.fontFaceCss; exports.fontHasWeight = _chunkFKUKJ3XAcjs.fontHasWeight; exports.formatFontFamily = formatFontFamily; exports.generateTypeHierarchy = generateTypeHierarchy; exports.getScaleRatio = getScaleRatio; exports.graphemes = graphemes; exports.guessFontCategory = _chunkFKUKJ3XAcjs.guessFontCategory; exports.isFontWeight = _chunkFKUKJ3XAcjs.isFontWeight; exports.isLargeText = isLargeText; exports.nearestFontWeight = _chunkFKUKJ3XAcjs.nearestFontWeight; exports.normalizeFontWeight = _chunkFKUKJ3XAcjs.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 = _chunkFKUKJ3XAcjs.webFontSummary;
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';
@@ -15,7 +15,7 @@ import {
15
15
  nearestFontWeight,
16
16
  normalizeFontWeight,
17
17
  webFontSummary
18
- } from "../chunk-CJE5MOIS.js";
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
- return { fonts: kept.slice(0, limit), total: kept.length, truncated: kept.length > limit };
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,