@webspire/mcp 0.8.1 → 0.10.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.
package/dist/search.js CHANGED
@@ -1,40 +1,40 @@
1
1
  const STEM_SUFFIXES = [
2
- "tion",
3
- "sion",
4
- "ions",
5
- "ment",
6
- "ness",
7
- "able",
8
- "ible",
9
- "ous",
10
- "ive",
11
- "ing",
12
- "ed",
13
- "es",
14
- "ly",
15
- "s",
2
+ 'tion',
3
+ 'sion',
4
+ 'ions',
5
+ 'ment',
6
+ 'ness',
7
+ 'able',
8
+ 'ible',
9
+ 'ous',
10
+ 'ive',
11
+ 'ing',
12
+ 'ed',
13
+ 'es',
14
+ 'ly',
15
+ 's',
16
16
  ];
17
17
  /** Common synonyms for CSS/UI terms — maps alternative words to canonical terms */
18
18
  const SYNONYMS = {
19
- blur: ["glass", "frosted", "glassmorphism", "backdrop"],
20
- glass: ["blur", "frosted", "glassmorphism", "transparent"],
21
- fade: ["opacity", "appear", "entrance", "reveal"],
22
- slide: ["translate", "move", "enter", "entrance"],
23
- hover: ["mouse", "interaction", "lift", "pointer"],
24
- card: ["panel", "surface", "container", "box"],
25
- animation: ["animate", "motion", "transition", "keyframe"],
26
- glow: ["neon", "light", "shine", "luminous"],
27
- scroll: ["viewport", "intersection", "parallax", "progress"],
28
- text: ["font", "typography", "heading", "title"],
29
- gradient: ["color", "blend", "mesh", "aurora"],
30
- dark: ["darkmode", "theme", "scheme", "night"],
31
- button: ["btn", "cta", "action", "click"],
32
- responsive: ["mobile", "fluid", "adaptive", "clamp"],
33
- focus: ["keyboard", "a11y", "accessibility", "ring"],
34
- stagger: ["cascade", "sequence", "delay", "children"],
35
- reveal: ["show", "appear", "unhide", "visible", "fade"],
36
- shimmer: ["skeleton", "loading", "placeholder", "pulse"],
37
- ripple: ["click", "material", "touch", "feedback"],
19
+ blur: ['glass', 'frosted', 'glassmorphism', 'backdrop'],
20
+ glass: ['blur', 'frosted', 'glassmorphism', 'transparent'],
21
+ fade: ['opacity', 'appear', 'entrance', 'reveal'],
22
+ slide: ['translate', 'move', 'enter', 'entrance'],
23
+ hover: ['mouse', 'interaction', 'lift', 'pointer'],
24
+ card: ['panel', 'surface', 'container', 'box'],
25
+ animation: ['animate', 'motion', 'transition', 'keyframe'],
26
+ glow: ['neon', 'light', 'shine', 'luminous'],
27
+ scroll: ['viewport', 'intersection', 'parallax', 'progress'],
28
+ text: ['font', 'typography', 'heading', 'title'],
29
+ gradient: ['color', 'blend', 'mesh', 'aurora'],
30
+ dark: ['darkmode', 'theme', 'scheme', 'night'],
31
+ button: ['btn', 'cta', 'action', 'click'],
32
+ responsive: ['mobile', 'fluid', 'adaptive', 'clamp'],
33
+ focus: ['keyboard', 'a11y', 'accessibility', 'ring'],
34
+ stagger: ['cascade', 'sequence', 'delay', 'children'],
35
+ reveal: ['show', 'appear', 'unhide', 'visible', 'fade'],
36
+ shimmer: ['skeleton', 'loading', 'placeholder', 'pulse'],
37
+ ripple: ['click', 'material', 'touch', 'feedback'],
38
38
  };
39
39
  export function stem(word) {
40
40
  for (const suffix of STEM_SUFFIXES) {
@@ -88,22 +88,16 @@ export function scoreSnippet(snippet, queryStems, expandedTerms) {
88
88
  const desc = snippet.description.toLowerCase();
89
89
  score += queryStems.filter((w) => desc.includes(w)).length * 2;
90
90
  // Tags
91
- const tagStr = snippet.tags.join(" ").toLowerCase();
91
+ const tagStr = snippet.tags.join(' ').toLowerCase();
92
92
  score += queryStems.filter((w) => tagStr.includes(w)).length * 2;
93
93
  // Classes
94
- const classes = snippet.meta.classes.join(" ").toLowerCase();
94
+ const classes = snippet.meta.classes.join(' ').toLowerCase();
95
95
  score += queryStems.filter((w) => classes.includes(w)).length;
96
96
  // Synonym-expanded terms — lower weight to avoid noise
97
97
  const synonymOnly = expandedTerms.filter((t) => !queryStems.includes(t));
98
98
  if (synonymOnly.length > 0) {
99
- const allText = [
100
- title,
101
- desc,
102
- tagStr,
103
- ...snippet.meta.useCases,
104
- ...snippet.meta.solves,
105
- ]
106
- .join(" ")
99
+ const allText = [title, desc, tagStr, ...snippet.meta.useCases, ...snippet.meta.solves]
100
+ .join(' ')
107
101
  .toLowerCase();
108
102
  score += synonymOnly.filter((w) => allText.includes(w)).length;
109
103
  }
@@ -179,12 +173,10 @@ export function scorePattern(pattern, queryStems, expandedTerms, options) {
179
173
  // Summary + Description
180
174
  const summary = pattern.summary.toLowerCase();
181
175
  score += queryStems.filter((w) => summary.includes(w)).length * 2;
182
- const desc = (pattern.description ?? "").toLowerCase();
176
+ const desc = (pattern.description ?? '').toLowerCase();
183
177
  score += queryStems.filter((w) => desc.includes(w)).length * 2;
184
178
  // Tags + Keywords
185
- const tagStr = [...pattern.tags, ...pattern.search.keywords]
186
- .join(" ")
187
- .toLowerCase();
179
+ const tagStr = [...pattern.tags, ...pattern.search.keywords].join(' ').toLowerCase();
188
180
  score += queryStems.filter((w) => tagStr.includes(w)).length * 2;
189
181
  // Semantic domain match — strong boost
190
182
  if (options.domain && pattern.semantics?.domains.includes(options.domain)) {
@@ -213,7 +205,7 @@ export function scorePattern(pattern, queryStems, expandedTerms, options) {
213
205
  ...pattern.search.intent,
214
206
  ...pattern.search.useCases,
215
207
  ]
216
- .join(" ")
208
+ .join(' ')
217
209
  .toLowerCase();
218
210
  score += synonymOnly.filter((w) => allText.includes(w)).length;
219
211
  }
@@ -268,28 +260,28 @@ const ROLE_LIMITS = {
268
260
  footer: 1,
269
261
  };
270
262
  const ROLE_FAMILY_BONUSES = {
271
- navigation: { navbar: 3, "announcement-bar": 1, search: 1 },
263
+ navigation: { navbar: 3, 'announcement-bar': 1, search: 1 },
272
264
  entry: {
273
265
  hero: 4,
274
- "page-header": 3,
266
+ 'page-header': 3,
275
267
  portfolio: 2,
276
- "product-detail": 2,
277
- "blog-details": 3,
268
+ 'product-detail': 2,
269
+ 'blog-details': 3,
278
270
  },
279
271
  supporting: {
280
272
  features: 3,
281
273
  services: 3,
282
274
  about: 2,
283
275
  blog: 2,
284
- "blog-timeline": 2,
285
- "related-articles": 2,
276
+ 'blog-timeline': 2,
277
+ 'related-articles': 2,
286
278
  legal: 2,
287
279
  careers: 2,
288
280
  portfolio: 2,
289
- "case-study": 2,
281
+ 'case-study': 2,
290
282
  integrations: 2,
291
283
  faq: 2,
292
- "product-grid": 2,
284
+ 'product-grid': 2,
293
285
  resume: 2,
294
286
  },
295
287
  conversion: {
@@ -297,37 +289,37 @@ const ROLE_FAMILY_BONUSES = {
297
289
  contact: 3,
298
290
  cta: 3,
299
291
  appointment: 2,
300
- "cart-progress": 2,
301
- "deal-of-the-day": 2,
292
+ 'cart-progress': 2,
293
+ 'deal-of-the-day': 2,
302
294
  newsletter: 2,
303
295
  },
304
296
  trust: {
305
297
  testimonials: 3,
306
- "logo-cloud": 2,
298
+ 'logo-cloud': 2,
307
299
  team: 2,
308
- "trust-strip": 2,
309
- "awards-list": 2,
310
- "product-reviews": 3,
300
+ 'trust-strip': 2,
301
+ 'awards-list': 2,
302
+ 'product-reviews': 3,
311
303
  about: 1,
312
- "blog-read-next": 2,
304
+ 'blog-read-next': 2,
313
305
  },
314
306
  footer: { footer: 4 },
315
307
  };
316
308
  const DOMAIN_FAMILY_BONUSES = {
317
309
  legal: {
318
310
  hero: 2,
319
- "page-header": 2,
311
+ 'page-header': 2,
320
312
  about: 2,
321
313
  legal: 4,
322
314
  team: 3,
323
315
  testimonials: 3,
324
316
  contact: 3,
325
317
  faq: 2,
326
- "awards-list": 2,
318
+ 'awards-list': 2,
327
319
  },
328
320
  healthcare: {
329
321
  hero: 2,
330
- "page-header": 2,
322
+ 'page-header': 2,
331
323
  about: 2,
332
324
  appointment: 3,
333
325
  team: 3,
@@ -336,10 +328,10 @@ const DOMAIN_FAMILY_BONUSES = {
336
328
  },
337
329
  education: {
338
330
  hero: 2,
339
- "page-header": 2,
331
+ 'page-header': 2,
340
332
  blog: 3,
341
- "blog-details": 3,
342
- "related-articles": 2,
333
+ 'blog-details': 3,
334
+ 'related-articles': 2,
343
335
  newsletter: 2,
344
336
  features: 2,
345
337
  pricing: 2,
@@ -349,7 +341,7 @@ const DOMAIN_FAMILY_BONUSES = {
349
341
  },
350
342
  finance: {
351
343
  hero: 2,
352
- "page-header": 2,
344
+ 'page-header': 2,
353
345
  about: 2,
354
346
  pricing: 3,
355
347
  comparison: 3,
@@ -358,10 +350,10 @@ const DOMAIN_FAMILY_BONUSES = {
358
350
  },
359
351
  saas: {
360
352
  hero: 3,
361
- "page-header": 2,
353
+ 'page-header': 2,
362
354
  blog: 2,
363
- "blog-details": 3,
364
- "related-articles": 2,
355
+ 'blog-details': 3,
356
+ 'related-articles': 2,
365
357
  newsletter: 2,
366
358
  careers: 2,
367
359
  legal: 2,
@@ -371,58 +363,58 @@ const DOMAIN_FAMILY_BONUSES = {
371
363
  integrations: 3,
372
364
  testimonials: 2,
373
365
  faq: 2,
374
- "logo-cloud": 2,
366
+ 'logo-cloud': 2,
375
367
  },
376
368
  ecommerce: {
377
369
  hero: 3,
378
- "product-detail": 5,
379
- "product-reviews": 4,
380
- "product-grid": 4,
381
- "deal-of-the-day": 3,
382
- "cart-progress": 3,
370
+ 'product-detail': 5,
371
+ 'product-reviews': 4,
372
+ 'product-grid': 4,
373
+ 'deal-of-the-day': 3,
374
+ 'cart-progress': 3,
383
375
  testimonials: 2,
384
376
  faq: 2,
385
- "logo-cloud": 1,
377
+ 'logo-cloud': 1,
386
378
  },
387
379
  agency: {
388
380
  hero: 2,
389
- "page-header": 2,
381
+ 'page-header': 2,
390
382
  about: 3,
391
383
  blog: 2,
392
- "blog-details": 2,
393
- "related-articles": 2,
384
+ 'blog-details': 2,
385
+ 'related-articles': 2,
394
386
  newsletter: 1,
395
387
  careers: 2,
396
388
  portfolio: 4,
397
389
  services: 3,
398
- "case-study": 3,
399
- "awards-list": 2,
390
+ 'case-study': 3,
391
+ 'awards-list': 2,
400
392
  team: 2,
401
393
  contact: 2,
402
394
  },
403
395
  consulting: {
404
396
  hero: 2,
405
- "page-header": 2,
397
+ 'page-header': 2,
406
398
  about: 3,
407
399
  blog: 2,
408
- "blog-details": 2,
409
- "related-articles": 2,
400
+ 'blog-details': 2,
401
+ 'related-articles': 2,
410
402
  newsletter: 1,
411
403
  careers: 1,
412
404
  legal: 1,
413
405
  services: 3,
414
- "case-study": 3,
406
+ 'case-study': 3,
415
407
  team: 2,
416
408
  contact: 2,
417
409
  faq: 2,
418
410
  },
419
411
  nonprofit: {
420
412
  hero: 2,
421
- "page-header": 2,
413
+ 'page-header': 2,
422
414
  about: 3,
423
415
  blog: 2,
424
- "blog-details": 2,
425
- "related-articles": 2,
416
+ 'blog-details': 2,
417
+ 'related-articles': 2,
426
418
  newsletter: 3,
427
419
  careers: 1,
428
420
  stats: 3,
@@ -440,7 +432,7 @@ const DOMAIN_FAMILY_BONUSES = {
440
432
  },
441
433
  realestate: {
442
434
  hero: 2,
443
- "page-header": 2,
435
+ 'page-header': 2,
444
436
  about: 2,
445
437
  careers: 1,
446
438
  gallery: 3,
@@ -452,150 +444,141 @@ const DOMAIN_FAMILY_BONUSES = {
452
444
  hero: 2,
453
445
  about: 2,
454
446
  blog: 2,
455
- "blog-details": 3,
456
- "related-articles": 2,
447
+ 'blog-details': 3,
448
+ 'related-articles': 2,
457
449
  newsletter: 1,
458
450
  portfolio: 3,
459
451
  resume: 3,
460
452
  testimonials: 1,
461
453
  contact: 2,
462
- "link-in-bio": 2,
454
+ 'link-in-bio': 2,
463
455
  },
464
456
  };
465
457
  export const DOMAIN_KEYS = [
466
- "legal",
467
- "healthcare",
468
- "education",
469
- "finance",
470
- "saas",
471
- "ecommerce",
472
- "agency",
473
- "consulting",
474
- "nonprofit",
475
- "gastronomy",
476
- "realestate",
477
- "personal",
458
+ 'legal',
459
+ 'healthcare',
460
+ 'education',
461
+ 'finance',
462
+ 'saas',
463
+ 'ecommerce',
464
+ 'agency',
465
+ 'consulting',
466
+ 'nonprofit',
467
+ 'gastronomy',
468
+ 'realestate',
469
+ 'personal',
478
470
  ];
479
- export const DOMAIN_LABEL = DOMAIN_KEYS.join(", ");
471
+ export const DOMAIN_LABEL = DOMAIN_KEYS.join(', ');
480
472
  export const TONE_KEYS = [
481
- "serious",
482
- "premium",
483
- "modern",
484
- "friendly",
485
- "approachable",
486
- "approachable_professional",
487
- "technical",
488
- "editorial",
489
- "playful",
490
- "institutional",
491
- "industrial",
492
- "casual",
473
+ 'serious',
474
+ 'premium',
475
+ 'modern',
476
+ 'friendly',
477
+ 'approachable',
478
+ 'approachable_professional',
479
+ 'technical',
480
+ 'editorial',
481
+ 'playful',
482
+ 'institutional',
483
+ 'industrial',
484
+ 'casual',
493
485
  ];
494
- export const TONE_LABEL = TONE_KEYS.join(", ");
486
+ export const TONE_LABEL = TONE_KEYS.join(', ');
495
487
  export const UX_GOAL_KEYS = [
496
- "build_trust",
497
- "drive_contact",
498
- "drive_signup",
499
- "drive_purchase",
500
- "explain_offer",
501
- "highlight_expertise",
502
- "showcase_work",
503
- "reduce_friction",
504
- "provide_overview",
505
- "structure_learning",
506
- "guide_stepwise",
507
- "tell_story",
508
- "demonstrate_value",
488
+ 'build_trust',
489
+ 'drive_contact',
490
+ 'drive_signup',
491
+ 'drive_purchase',
492
+ 'explain_offer',
493
+ 'highlight_expertise',
494
+ 'showcase_work',
495
+ 'reduce_friction',
496
+ 'provide_overview',
497
+ 'structure_learning',
498
+ 'guide_stepwise',
499
+ 'tell_story',
500
+ 'demonstrate_value',
509
501
  ];
510
- export const UX_GOAL_LABEL = UX_GOAL_KEYS.join(", ");
502
+ export const UX_GOAL_LABEL = UX_GOAL_KEYS.join(', ');
511
503
  export const CONTENT_NEED_KEYS = [
512
- "about",
513
- "article",
514
- "blog",
515
- "careers",
516
- "case_study",
517
- "comparison",
518
- "contact",
519
- "conversion",
520
- "faq",
521
- "gallery",
522
- "integrations",
523
- "legal",
524
- "newsletter",
525
- "portfolio",
526
- "pricing",
527
- "product",
528
- "related_content",
529
- "resume",
530
- "support",
531
- "team",
532
- "testimonials",
533
- "trust",
504
+ 'about',
505
+ 'article',
506
+ 'blog',
507
+ 'careers',
508
+ 'case_study',
509
+ 'comparison',
510
+ 'contact',
511
+ 'conversion',
512
+ 'faq',
513
+ 'gallery',
514
+ 'integrations',
515
+ 'legal',
516
+ 'newsletter',
517
+ 'portfolio',
518
+ 'pricing',
519
+ 'product',
520
+ 'related_content',
521
+ 'resume',
522
+ 'support',
523
+ 'team',
524
+ 'testimonials',
525
+ 'trust',
534
526
  ];
535
- export const CONTENT_NEED_LABEL = CONTENT_NEED_KEYS.join(", ");
527
+ export const CONTENT_NEED_LABEL = CONTENT_NEED_KEYS.join(', ');
536
528
  const CONTENT_NEED_FAMILIES = {
537
- about: ["about"],
538
- article: ["blog-details", "blog-read-next", "related-articles"],
539
- blog: ["blog", "blog-timeline", "related-articles", "newsletter"],
540
- careers: ["careers", "team", "about", "cta"],
541
- case_study: ["case-study", "portfolio"],
542
- comparison: ["comparison", "pricing"],
543
- contact: ["contact", "appointment", "cta"],
544
- conversion: ["pricing", "cta", "contact", "newsletter", "appointment"],
545
- faq: ["faq", "help-center"],
546
- gallery: ["gallery", "portfolio"],
547
- integrations: ["integrations", "faq"],
548
- legal: ["legal", "faq", "contact"],
549
- newsletter: ["newsletter"],
550
- portfolio: ["portfolio", "case-study", "gallery"],
551
- pricing: ["pricing", "comparison"],
552
- product: ["product-detail", "product-reviews", "product-grid"],
553
- related_content: ["related-articles", "blog-read-next", "newsletter"],
554
- resume: ["resume", "about"],
555
- support: ["help-center", "faq", "contact"],
556
- team: ["team", "about"],
557
- testimonials: ["testimonials", "product-reviews"],
558
- trust: [
559
- "testimonials",
560
- "logo-cloud",
561
- "trust-strip",
562
- "awards-list",
563
- "team",
564
- "product-reviews",
565
- ],
529
+ about: ['about'],
530
+ article: ['blog-details', 'blog-read-next', 'related-articles'],
531
+ blog: ['blog', 'blog-timeline', 'related-articles', 'newsletter'],
532
+ careers: ['careers', 'team', 'about', 'cta'],
533
+ case_study: ['case-study', 'portfolio'],
534
+ comparison: ['comparison', 'pricing'],
535
+ contact: ['contact', 'appointment', 'cta'],
536
+ conversion: ['pricing', 'cta', 'contact', 'newsletter', 'appointment'],
537
+ faq: ['faq', 'help-center'],
538
+ gallery: ['gallery', 'portfolio'],
539
+ integrations: ['integrations', 'faq'],
540
+ legal: ['legal', 'faq', 'contact'],
541
+ newsletter: ['newsletter'],
542
+ portfolio: ['portfolio', 'case-study', 'gallery'],
543
+ pricing: ['pricing', 'comparison'],
544
+ product: ['product-detail', 'product-reviews', 'product-grid'],
545
+ related_content: ['related-articles', 'blog-read-next', 'newsletter'],
546
+ resume: ['resume', 'about'],
547
+ support: ['help-center', 'faq', 'contact'],
548
+ team: ['team', 'about'],
549
+ testimonials: ['testimonials', 'product-reviews'],
550
+ trust: ['testimonials', 'logo-cloud', 'trust-strip', 'awards-list', 'team', 'product-reviews'],
566
551
  };
567
552
  const CONTENT_NEED_ALIASES = {
568
- blog_detail: "article",
569
- blog_details: "article",
570
- blog_post: "article",
571
- blog_posts: "blog",
572
- blog_roll: "blog",
573
- company: "about",
574
- company_info: "about",
575
- content: "blog",
576
- cta: "conversion",
577
- faq_section: "faq",
578
- hiring: "careers",
579
- lead_capture: "newsletter",
580
- lead_gen: "conversion",
581
- lead_generation: "conversion",
582
- page_legal: "legal",
583
- read_next: "related_content",
584
- related: "related_content",
585
- related_articles: "related_content",
586
- reviews: "testimonials",
587
- social_proof: "trust",
588
- support_center: "support",
553
+ blog_detail: 'article',
554
+ blog_details: 'article',
555
+ blog_post: 'article',
556
+ blog_posts: 'blog',
557
+ blog_roll: 'blog',
558
+ company: 'about',
559
+ company_info: 'about',
560
+ content: 'blog',
561
+ cta: 'conversion',
562
+ faq_section: 'faq',
563
+ hiring: 'careers',
564
+ lead_capture: 'newsletter',
565
+ lead_gen: 'conversion',
566
+ lead_generation: 'conversion',
567
+ page_legal: 'legal',
568
+ read_next: 'related_content',
569
+ related: 'related_content',
570
+ related_articles: 'related_content',
571
+ reviews: 'testimonials',
572
+ social_proof: 'trust',
573
+ support_center: 'support',
589
574
  };
590
575
  export function normalizeContentNeed(need) {
591
576
  const normalized = need
592
577
  .trim()
593
578
  .toLowerCase()
594
- .replace(/[\s-]+/g, "_");
579
+ .replace(/[\s-]+/g, '_');
595
580
  const canonical = CONTENT_NEED_ALIASES[normalized] ?? normalized;
596
- return CONTENT_NEED_KEYS.includes(canonical)
597
- ? canonical
598
- : null;
581
+ return CONTENT_NEED_KEYS.includes(canonical) ? canonical : null;
599
582
  }
600
583
  const ROLE_SNIPPET_CATEGORY_BONUSES = {
601
584
  navigation: { interactions: 2, text: 1 },
@@ -620,43 +603,43 @@ const TONE_SNIPPET_CATEGORY_BONUSES = {
620
603
  casual: { interactions: 2, decorative: 1, text: 1 },
621
604
  };
622
605
  const FAMILY_SNIPPET_TERMS = {
623
- hero: ["headline", "entrance", "reveal", "background", "hero"],
624
- navbar: ["navigation", "nav", "link", "menu"],
625
- features: ["cards", "feature", "grid", "content hierarchy"],
626
- services: ["reveal", "service", "list"],
627
- pricing: ["pricing", "comparison", "toggle", "cta"],
628
- faq: ["faq", "accordion", "readability"],
629
- testimonials: ["quote", "social proof", "testimonial"],
630
- contact: ["form", "contact", "focus", "cta"],
631
- cta: ["button", "cta", "action", "conversion"],
632
- portfolio: ["portfolio", "gallery", "showcase"],
633
- gallery: ["gallery", "image", "media"],
634
- blog: ["editorial", "article", "metadata", "reading"],
635
- "blog-details": ["article", "reading", "progress", "content"],
636
- newsletter: ["newsletter", "signup", "form"],
637
- team: ["profile", "card", "bio"],
638
- footer: ["footer", "link", "divider"],
606
+ hero: ['headline', 'entrance', 'reveal', 'background', 'hero'],
607
+ navbar: ['navigation', 'nav', 'link', 'menu'],
608
+ features: ['cards', 'feature', 'grid', 'content hierarchy'],
609
+ services: ['reveal', 'service', 'list'],
610
+ pricing: ['pricing', 'comparison', 'toggle', 'cta'],
611
+ faq: ['faq', 'accordion', 'readability'],
612
+ testimonials: ['quote', 'social proof', 'testimonial'],
613
+ contact: ['form', 'contact', 'focus', 'cta'],
614
+ cta: ['button', 'cta', 'action', 'conversion'],
615
+ portfolio: ['portfolio', 'gallery', 'showcase'],
616
+ gallery: ['gallery', 'image', 'media'],
617
+ blog: ['editorial', 'article', 'metadata', 'reading'],
618
+ 'blog-details': ['article', 'reading', 'progress', 'content'],
619
+ newsletter: ['newsletter', 'signup', 'form'],
620
+ team: ['profile', 'card', 'bio'],
621
+ footer: ['footer', 'link', 'divider'],
639
622
  };
640
623
  const UX_GOAL_SNIPPET_TERMS = {
641
- build_trust: ["clarity", "readability", "focus", "social proof"],
642
- drive_contact: ["form", "button", "cta", "focus"],
643
- drive_signup: ["signup", "cta", "button", "conversion"],
644
- drive_purchase: ["pricing", "cta", "comparison", "button"],
645
- explain_offer: ["feature", "comparison", "content hierarchy", "reading"],
646
- highlight_expertise: ["editorial", "case study", "depth", "metadata"],
647
- showcase_work: ["gallery", "portfolio", "image", "showcase"],
648
- reduce_friction: ["form", "focus", "clarity", "progress"],
649
- provide_overview: ["overview", "grid", "hierarchy", "cards"],
650
- structure_learning: ["steps", "progress", "reading", "content"],
651
- guide_stepwise: ["steps", "progress", "sequence", "reveal"],
652
- tell_story: ["story", "editorial", "scroll", "timeline"],
653
- demonstrate_value: ["comparison", "highlight", "cta", "proof"],
624
+ build_trust: ['clarity', 'readability', 'focus', 'social proof'],
625
+ drive_contact: ['form', 'button', 'cta', 'focus'],
626
+ drive_signup: ['signup', 'cta', 'button', 'conversion'],
627
+ drive_purchase: ['pricing', 'cta', 'comparison', 'button'],
628
+ explain_offer: ['feature', 'comparison', 'content hierarchy', 'reading'],
629
+ highlight_expertise: ['editorial', 'case study', 'depth', 'metadata'],
630
+ showcase_work: ['gallery', 'portfolio', 'image', 'showcase'],
631
+ reduce_friction: ['form', 'focus', 'clarity', 'progress'],
632
+ provide_overview: ['overview', 'grid', 'hierarchy', 'cards'],
633
+ structure_learning: ['steps', 'progress', 'reading', 'content'],
634
+ guide_stepwise: ['steps', 'progress', 'sequence', 'reveal'],
635
+ tell_story: ['story', 'editorial', 'scroll', 'timeline'],
636
+ demonstrate_value: ['comparison', 'highlight', 'cta', 'proof'],
654
637
  };
655
638
  function recommendSnippetsForComposition(snippets, patterns, options, maxSnippets = 5) {
656
639
  if (patterns.length === 0)
657
640
  return [];
658
641
  const families = [...new Set(patterns.map((pattern) => pattern.family))];
659
- const roles = patterns.map((pattern) => pattern.ai?.compositionRole ?? "supporting");
642
+ const roles = patterns.map((pattern) => pattern.ai?.compositionRole ?? 'supporting');
660
643
  const queryTerms = [
661
644
  options.domain,
662
645
  options.tone,
@@ -664,9 +647,7 @@ function recommendSnippetsForComposition(snippets, patterns, options, maxSnippet
664
647
  ...families.flatMap((family) => FAMILY_SNIPPET_TERMS[family] ?? [family]),
665
648
  ...options.uxGoals.flatMap((goal) => UX_GOAL_SNIPPET_TERMS[goal] ?? []),
666
649
  ];
667
- const words = [
668
- ...new Set(queryTerms.join(" ").toLowerCase().split(/\s+/).filter(Boolean)),
669
- ];
650
+ const words = [...new Set(queryTerms.join(' ').toLowerCase().split(/\s+/).filter(Boolean))];
670
651
  const stems = words.map(stem);
671
652
  const expanded = expandWithSynonyms(words);
672
653
  const categoryUsage = new Map();
@@ -676,10 +657,9 @@ function recommendSnippetsForComposition(snippets, patterns, options, maxSnippet
676
657
  for (const role of roles) {
677
658
  score += ROLE_SNIPPET_CATEGORY_BONUSES[role]?.[snippet.category] ?? 0;
678
659
  }
679
- score +=
680
- TONE_SNIPPET_CATEGORY_BONUSES[options.tone]?.[snippet.category] ?? 0;
660
+ score += TONE_SNIPPET_CATEGORY_BONUSES[options.tone]?.[snippet.category] ?? 0;
681
661
  if (snippet.meta.accessibility.prefersReducedMotion &&
682
- ["animations", "scroll", "interactions"].includes(snippet.category)) {
662
+ ['animations', 'scroll', 'interactions'].includes(snippet.category)) {
683
663
  score += 1;
684
664
  }
685
665
  if (snippet.responsive)
@@ -690,8 +670,7 @@ function recommendSnippetsForComposition(snippets, patterns, options, maxSnippet
690
670
  const categoryRoleMatch = roles.some((role) => (ROLE_SNIPPET_CATEGORY_BONUSES[role]?.[snippet.category] ?? 0) > 0);
691
671
  if (categoryRoleMatch)
692
672
  reasonBits.push(`${snippet.category} fits the selected section roles`);
693
- if ((TONE_SNIPPET_CATEGORY_BONUSES[options.tone]?.[snippet.category] ?? 0) >
694
- 0) {
673
+ if ((TONE_SNIPPET_CATEGORY_BONUSES[options.tone]?.[snippet.category] ?? 0) > 0) {
695
674
  reasonBits.push(`${snippet.category} suits the ${options.tone} tone`);
696
675
  }
697
676
  const matchedFamilies = families.filter((family) => {
@@ -702,17 +681,17 @@ function recommendSnippetsForComposition(snippets, patterns, options, maxSnippet
702
681
  ...snippet.meta.useCases,
703
682
  ...snippet.meta.solves,
704
683
  ]
705
- .join(" ")
684
+ .join(' ')
706
685
  .toLowerCase();
707
686
  return (FAMILY_SNIPPET_TERMS[family] ?? []).some((term) => haystack.includes(term));
708
687
  });
709
688
  if (matchedFamilies.length > 0) {
710
- reasonBits.push(`supports ${matchedFamilies.slice(0, 2).join(", ")} sections`);
689
+ reasonBits.push(`supports ${matchedFamilies.slice(0, 2).join(', ')} sections`);
711
690
  }
712
691
  return {
713
692
  snippet,
714
693
  score,
715
- reason: reasonBits.join("; "),
694
+ reason: reasonBits.join('; '),
716
695
  };
717
696
  })
718
697
  .filter(({ score }) => score > 0)
@@ -727,8 +706,7 @@ function recommendSnippetsForComposition(snippets, patterns, options, maxSnippet
727
706
  .slice(0, maxSnippets)
728
707
  .map(({ snippet, reason }) => ({
729
708
  snippet,
730
- reason: reason ||
731
- `Matches ${options.domain}, ${options.tone}, and the selected page goals`,
709
+ reason: reason || `Matches ${options.domain}, ${options.tone}, and the selected page goals`,
732
710
  }));
733
711
  }
734
712
  export function composePage(patterns, snippets, options) {
@@ -743,7 +721,7 @@ export function composePage(patterns, snippets, options) {
743
721
  .map((p) => {
744
722
  let score = 0;
745
723
  let matchedSemantics = 0;
746
- const role = p.ai?.compositionRole ?? "supporting";
724
+ const role = p.ai?.compositionRole ?? 'supporting';
747
725
  // Domain match
748
726
  if (p.semantics?.domains.includes(domain)) {
749
727
  score += 5;
@@ -765,10 +743,10 @@ export function composePage(patterns, snippets, options) {
765
743
  }
766
744
  }
767
745
  // Prefer base variants
768
- if (p.tier === "base")
746
+ if (p.tier === 'base')
769
747
  score += 1;
770
748
  // compositionRole bonus for entry patterns
771
- if (p.ai?.compositionRole === "entry")
749
+ if (p.ai?.compositionRole === 'entry')
772
750
  score += 2;
773
751
  score += ROLE_FAMILY_BONUSES[role]?.[p.family] ?? 0;
774
752
  score += DOMAIN_FAMILY_BONUSES[domain]?.[p.family] ?? 0;
@@ -779,9 +757,7 @@ export function composePage(patterns, snippets, options) {
779
757
  }
780
758
  return { pattern: p, score, matchedSemantics };
781
759
  })
782
- .filter(({ score, matchedSemantics, pattern }) => score > 0 &&
783
- matchedSemantics > 0 &&
784
- !pattern.semantics?.avoidForTones.includes(tone))
760
+ .filter(({ score, matchedSemantics, pattern }) => score > 0 && matchedSemantics > 0 && !pattern.semantics?.avoidForTones.includes(tone))
785
761
  .sort((a, b) => b.score - a.score || b.matchedSemantics - a.matchedSemantics);
786
762
  // Step 2: Select best pattern per composition role
787
763
  const selected = new Map();
@@ -789,7 +765,7 @@ export function composePage(patterns, snippets, options) {
789
765
  const roleCounts = new Map();
790
766
  const coveredContentNeeds = new Set();
791
767
  function trySelect(pattern, reasonPrefix, matchedNeed) {
792
- const role = pattern.ai?.compositionRole ?? "supporting";
768
+ const role = pattern.ai?.compositionRole ?? 'supporting';
793
769
  if (usedFamilies.has(pattern.family))
794
770
  return false;
795
771
  const currentRoleCount = roleCounts.get(role) ?? 0;
@@ -808,7 +784,7 @@ export function composePage(patterns, snippets, options) {
808
784
  }
809
785
  }
810
786
  const matchedUxCount = uxGoals.filter((g) => pattern.semantics?.uxGoals.includes(g)).length;
811
- reasoning.push(`${reasonPrefix ?? pattern.id}: ${role} (score: domain=${pattern.semantics?.domains.includes(domain) ? "" : ""}, tone=${pattern.semantics?.tones.includes(tone) ? "" : ""}, ux=${matchedUxCount}/${uxGoals.length}${matchedNeed ? `, need=${matchedNeed}` : ""})`);
787
+ reasoning.push(`${reasonPrefix ?? pattern.id}: ${role} (score: domain=${pattern.semantics?.domains.includes(domain) ? '' : ''}, tone=${pattern.semantics?.tones.includes(tone) ? '' : ''}, ux=${matchedUxCount}/${uxGoals.length}${matchedNeed ? `, need=${matchedNeed}` : ''})`);
812
788
  return true;
813
789
  }
814
790
  // Step 2a: Satisfy explicit content needs first
@@ -831,8 +807,8 @@ export function composePage(patterns, snippets, options) {
831
807
  }
832
808
  // Step 3: Sort by composition role
833
809
  const result = [...selected.values()].sort((a, b) => {
834
- const roleA = a.ai?.compositionRole ?? "supporting";
835
- const roleB = b.ai?.compositionRole ?? "supporting";
810
+ const roleA = a.ai?.compositionRole ?? 'supporting';
811
+ const roleB = b.ai?.compositionRole ?? 'supporting';
836
812
  return (ROLE_ORDER[roleA] ?? 2) - (ROLE_ORDER[roleB] ?? 2);
837
813
  });
838
814
  // Step 4: Check neighbors / warn about conflicts
@@ -868,3 +844,158 @@ export function composePage(patterns, snippets, options) {
868
844
  warnings,
869
845
  };
870
846
  }
847
+ // --- Font Recommendation ---
848
+ export function recommendFonts(fontsData, domain, tone) {
849
+ const key = `${tone}-${domain}`;
850
+ const predefined = fontsData.pairings[key];
851
+ if (predefined) {
852
+ const heading = fontsData.fonts.find((f) => f.id === predefined.heading);
853
+ const body = fontsData.fonts.find((f) => f.id === predefined.body);
854
+ const mono = fontsData.fonts.find((f) => f.id === predefined.mono);
855
+ if (heading && body && mono) {
856
+ const packages = [...new Set([heading.npm, body.npm, mono.npm])];
857
+ return {
858
+ heading: {
859
+ id: heading.id,
860
+ name: heading.name,
861
+ npm: heading.npm,
862
+ css: heading.css,
863
+ reason: `Predefined pairing for ${tone} ${domain}`,
864
+ },
865
+ body: {
866
+ id: body.id,
867
+ name: body.name,
868
+ npm: body.npm,
869
+ css: body.css,
870
+ reason: `Predefined pairing for ${tone} ${domain}`,
871
+ },
872
+ mono: {
873
+ id: mono.id,
874
+ name: mono.name,
875
+ npm: mono.npm,
876
+ css: mono.css,
877
+ reason: `Predefined pairing for ${tone} ${domain}`,
878
+ },
879
+ install: `npm i ${packages.join(' ')}`,
880
+ tailwindTheme: buildTailwindTheme(heading, body, mono),
881
+ isPredefined: true,
882
+ };
883
+ }
884
+ }
885
+ // Dynamic scoring
886
+ function scoreFont(font, role) {
887
+ if (font.avoidForTones.includes(tone))
888
+ return -1;
889
+ let score = 0;
890
+ if (font.tones.includes(tone))
891
+ score += 5;
892
+ if (font.domains.includes(domain))
893
+ score += 3;
894
+ score += font.roles[role] * 4;
895
+ return score;
896
+ }
897
+ function pickBest(role) {
898
+ const candidates = fontsData.fonts
899
+ .map((f) => ({ font: f, score: scoreFont(f, role) }))
900
+ .filter(({ score }) => score > 0)
901
+ .sort((a, b) => b.score - a.score);
902
+ return candidates[0]?.font ?? fontsData.fonts[0];
903
+ }
904
+ const heading = pickBest('heading');
905
+ const body = pickBest('body');
906
+ const mono = pickBest('mono');
907
+ const packages = [...new Set([heading.npm, body.npm, mono.npm])];
908
+ function reason(font, role) {
909
+ const parts = [];
910
+ if (font.tones.includes(tone))
911
+ parts.push(`tone match (${tone})`);
912
+ if (font.domains.includes(domain))
913
+ parts.push(`domain match (${domain})`);
914
+ parts.push(`${role} fitness: ${font.roles[role]}`);
915
+ return parts.join(', ');
916
+ }
917
+ return {
918
+ heading: {
919
+ id: heading.id,
920
+ name: heading.name,
921
+ npm: heading.npm,
922
+ css: heading.css,
923
+ reason: reason(heading, 'heading'),
924
+ },
925
+ body: {
926
+ id: body.id,
927
+ name: body.name,
928
+ npm: body.npm,
929
+ css: body.css,
930
+ reason: reason(body, 'body'),
931
+ },
932
+ mono: {
933
+ id: mono.id,
934
+ name: mono.name,
935
+ npm: mono.npm,
936
+ css: mono.css,
937
+ reason: reason(mono, 'mono'),
938
+ },
939
+ install: `npm i ${packages.join(' ')}`,
940
+ tailwindTheme: buildTailwindTheme(heading, body, mono),
941
+ isPredefined: false,
942
+ };
943
+ }
944
+ function buildTailwindTheme(heading, body, mono) {
945
+ return `@theme {
946
+ --font-heading: ${heading.css};
947
+ --font-body: ${body.css};
948
+ --font-mono: ${mono.css};
949
+ }`;
950
+ }
951
+ export function searchCanvasEffects(effects, options) {
952
+ const { query, category, interactive, animate, limit = 10 } = options;
953
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
954
+ const stems = words.map(stem);
955
+ const expanded = expandWithSynonyms(words);
956
+ if (stems.length === 0)
957
+ return [];
958
+ const scored = effects
959
+ .filter((effect) => {
960
+ if (category && effect.category !== category)
961
+ return false;
962
+ if (interactive !== undefined && effect.interactive !== interactive)
963
+ return false;
964
+ if (animate !== undefined && effect.animate !== animate)
965
+ return false;
966
+ return true;
967
+ })
968
+ .map((effect) => {
969
+ let score = 0;
970
+ const idLower = effect.id.toLowerCase();
971
+ for (const term of stems) {
972
+ if (idLower.includes(term))
973
+ score += 5;
974
+ }
975
+ const title = effect.title.toLowerCase();
976
+ score += stems.filter((w) => title.includes(w)).length * 3;
977
+ const desc = effect.description.toLowerCase();
978
+ score += stems.filter((w) => desc.includes(w)).length * 2;
979
+ const tagStr = effect.tags.join(' ').toLowerCase();
980
+ score += stems.filter((w) => tagStr.includes(w)).length * 2;
981
+ for (const useCase of effect.useCases) {
982
+ const lower = useCase.toLowerCase();
983
+ score += stems.filter((w) => lower.includes(w)).length * 3;
984
+ }
985
+ for (const solve of effect.solves) {
986
+ const lower = solve.toLowerCase();
987
+ score += stems.filter((w) => lower.includes(w)).length * 4;
988
+ }
989
+ const synonymOnly = expanded.filter((t) => !stems.includes(t));
990
+ if (synonymOnly.length > 0) {
991
+ const allText = [title, desc, tagStr, ...effect.useCases, ...effect.solves]
992
+ .join(' ')
993
+ .toLowerCase();
994
+ score += synonymOnly.filter((w) => allText.includes(w)).length;
995
+ }
996
+ return { effect, score };
997
+ })
998
+ .filter(({ score }) => score > 0)
999
+ .sort((a, b) => b.score - a.score);
1000
+ return scored.slice(0, limit).map(({ effect }) => effect);
1001
+ }