@webspire/mcp 0.12.0 → 0.13.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.
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { loadFonts, searchSnippets } from './registry.js';
3
- import { CONTENT_NEED_KEYS, CONTENT_NEED_LABEL, composePage, DOMAIN_KEYS, DOMAIN_LABEL, normalizeContentNeed, recommendFonts, searchCanvasEffects, searchMotionRecipes, searchPatterns, TONE_KEYS, TONE_LABEL, UX_GOAL_KEYS, UX_GOAL_LABEL, } from './search.js';
3
+ import { augmentPage, CONTENT_NEED_KEYS, CONTENT_NEED_LABEL, composePage, DOMAIN_KEYS, DOMAIN_LABEL, normalizeContentNeed, recommendFonts, searchCanvasEffects, searchMotionRecipes, searchPatterns, TONE_KEYS, TONE_LABEL, UX_GOAL_KEYS, UX_GOAL_LABEL, } from './search.js';
4
4
  function formatSnippetBrief(s) {
5
5
  return [
6
6
  `**${s.title}** (${s.id})`,
@@ -388,9 +388,36 @@ export function registerToolsWithProvider(server, getRegistry) {
388
388
  const sections = result.patterns
389
389
  .map((p, i) => {
390
390
  const role = p.ai?.compositionRole ?? 'supporting';
391
- return `${i + 1}. **${p.title}** (${p.id}) — role: ${role}\n ${p.summary}`;
391
+ return `${i + 1}. **${p.title}** (\`${p.id}\`) — role: ${role}\n ${p.summary}`;
392
392
  })
393
393
  .join('\n\n');
394
+ // Alternatives per section
395
+ const alternativesText = Object.keys(result.alternatives).length > 0
396
+ ? `\n\n## Alternatives (swap any section)\n${Object.entries(result.alternatives)
397
+ .map(([primaryId, alts]) => {
398
+ const altList = alts.map((a) => `\`${a.id}\``).join(', ');
399
+ return `- For **${primaryId}**: ${altList}`;
400
+ })
401
+ .join('\n')}`
402
+ : '';
403
+ // Bold picks
404
+ const boldPicksText = result.boldPicks.length > 0
405
+ ? `\n\n## Bold Picks 💡\nCreative alternatives that weren't selected but fit the mood:\n${result.boldPicks
406
+ .map((p) => {
407
+ const flags = [
408
+ p.tier === 'enhanced' ? 'enhanced' : null,
409
+ p.capabilities?.animated ? 'animated' : null,
410
+ ]
411
+ .filter(Boolean)
412
+ .join(', ');
413
+ return `- **${p.title}** (\`${p.id}\`)${flags ? ` [${flags}]` : ''} — ${p.summary}`;
414
+ })
415
+ .join('\n')}`
416
+ : '';
417
+ // Missing from library
418
+ const missingText = result.missingFromLibrary.length > 0
419
+ ? `\n\n## Ideas Not Yet in Library\nContent elements that would fit but don't exist as patterns yet:\n${result.missingFromLibrary.map((idea) => `- ${idea}`).join('\n')}`
420
+ : '';
394
421
  const reasoningText = result.reasoning.length > 0
395
422
  ? `\n\n## Selection Reasoning\n${result.reasoning.join('\n')}`
396
423
  : '';
@@ -412,7 +439,107 @@ export function registerToolsWithProvider(server, getRegistry) {
412
439
  content: [
413
440
  {
414
441
  type: 'text',
415
- text: `# Composed Page: ${domain} / ${tone}\n\nUX Goals: ${ux_goals.join(', ')}${content_needs?.length ? `\nContent Needs: ${content_needs.join(', ')}` : ''}\n\n## Recommended Sections\n\n${sections}${snippetsText}${fontsText}${warningsText}${reasoningText}\n\nUse \`get_pattern(id)\` to get full HTML for each section.`,
442
+ text: `# Composed Page: ${domain} / ${tone}\n\nUX Goals: ${ux_goals.join(', ')}${content_needs?.length ? `\nContent Needs: ${content_needs.join(', ')}` : ''}\n\n## Recommended Sections\n\n${sections}${alternativesText}${boldPicksText}${missingText}${snippetsText}${fontsText}${warningsText}${reasoningText}\n\nUse \`get_pattern(id)\` to get full HTML for each section.`,
443
+ },
444
+ ],
445
+ };
446
+ });
447
+ server.tool('augment_page', 'Suggest additions for a page that already has some sections. Unlike compose_page (greenfield), augment_page works out which composition roles are already covered and only fills the gaps. Use this when you have an existing page and want to improve or complete it.', {
448
+ existing_sections: z
449
+ .array(z.string())
450
+ .describe('Family names of sections already present on the page, e.g. ["navbar", "hero", "services"]. Use the family name, not the pattern ID.'),
451
+ domain: z.enum(DOMAIN_KEYS).describe(`Business domain: ${DOMAIN_LABEL}`),
452
+ tone: z.enum(TONE_KEYS).describe(`Desired tone: ${TONE_LABEL}`),
453
+ ux_goals: z.array(z.enum(UX_GOAL_KEYS)).describe(`UX goals: ${UX_GOAL_LABEL}`),
454
+ content_needs: z
455
+ .array(z.enum(CONTENT_NEED_KEYS))
456
+ .optional()
457
+ .describe(`Optional explicit content requirements: ${CONTENT_NEED_LABEL}`),
458
+ max_sections: z
459
+ .number()
460
+ .optional()
461
+ .default(6)
462
+ .describe('Maximum number of additional sections to suggest (default: 6)'),
463
+ }, async ({ existing_sections, domain, tone, ux_goals, content_needs, max_sections }) => {
464
+ const registry = await getRegistry();
465
+ const allPatterns = registry.patterns ?? [];
466
+ const result = augmentPage(allPatterns, registry.snippets, {
467
+ existingSections: existing_sections,
468
+ domain,
469
+ tone,
470
+ uxGoals: ux_goals,
471
+ contentNeeds: content_needs
472
+ ?.map(normalizeContentNeed)
473
+ .filter((need) => need !== null),
474
+ maxSections: max_sections,
475
+ });
476
+ // Already covered
477
+ const coveredText = result.alreadyCovered.length > 0
478
+ ? `## Already Covered\n${result.alreadyCovered
479
+ .map((f) => `- \`${f}\``)
480
+ .join('\n')}\nCovered roles: ${result.coveredRoles.length > 0 ? result.coveredRoles.join(', ') : 'none detected'}`
481
+ : '## Already Covered\n(none specified)';
482
+ const uncoveredText = result.uncoveredRoles.length > 0
483
+ ? `\n\n## Gaps Found\nMissing roles: **${result.uncoveredRoles.join(', ')}**`
484
+ : '\n\n## Gaps Found\nAll composition roles appear covered — the suggestions below are enhancements.';
485
+ // Suggestions
486
+ const suggestions = result.suggestions;
487
+ if (suggestions.patterns.length === 0) {
488
+ return {
489
+ content: [
490
+ {
491
+ type: 'text',
492
+ text: `# Page Augmentation: ${domain} / ${tone}\n\n${coveredText}${uncoveredText}\n\nNo additional patterns found. The page appears complete for these goals.`,
493
+ },
494
+ ],
495
+ };
496
+ }
497
+ const suggestedSections = suggestions.patterns
498
+ .map((p, i) => {
499
+ const role = p.ai?.compositionRole ?? 'supporting';
500
+ return `${i + 1}. **${p.title}** (\`${p.id}\`) — role: ${role}\n ${p.summary}`;
501
+ })
502
+ .join('\n\n');
503
+ const altText = Object.keys(suggestions.alternatives).length > 0
504
+ ? `\n\n## Alternatives\n${Object.entries(suggestions.alternatives)
505
+ .map(([id, alts]) => `- For **${id}**: ${alts.map((a) => `\`${a.id}\``).join(', ')}`)
506
+ .join('\n')}`
507
+ : '';
508
+ const boldText = suggestions.boldPicks.length > 0
509
+ ? `\n\n## Bold Picks 💡\n${suggestions.boldPicks
510
+ .map((p) => {
511
+ const flags = [
512
+ p.tier === 'enhanced' ? 'enhanced' : null,
513
+ p.capabilities?.animated ? 'animated' : null,
514
+ ]
515
+ .filter(Boolean)
516
+ .join(', ');
517
+ return `- **${p.title}** (\`${p.id}\`)${flags ? ` [${flags}]` : ''} — ${p.summary}`;
518
+ })
519
+ .join('\n')}`
520
+ : '';
521
+ const missingText = suggestions.missingFromLibrary.length > 0
522
+ ? `\n\n## Ideas Not Yet in Library\n${suggestions.missingFromLibrary.map((idea) => `- ${idea}`).join('\n')}`
523
+ : '';
524
+ const snippetsText = suggestions.snippets.length > 0
525
+ ? `\n\n## Recommended Snippets\n${suggestions.snippets
526
+ .map(({ snippet, reason }, i) => `${i + 1}. **${snippet.title}** (${snippet.id}) — ${reason}`)
527
+ .join('\n')}\n\nUse \`get_snippet(id)\` to retrieve the CSS.`
528
+ : '';
529
+ let fontsText = '';
530
+ const fontsDataLoaded = await loadFonts();
531
+ if (fontsDataLoaded) {
532
+ const fontRec = recommendFonts(fontsDataLoaded, domain, tone);
533
+ fontsText = `\n\n## Recommended Fonts\n- **Heading:** ${fontRec.heading.name} (\`${fontRec.heading.npm}\`)\n- **Body:** ${fontRec.body.name} (\`${fontRec.body.npm}\`)`;
534
+ }
535
+ const warningsText = suggestions.warnings.length > 0
536
+ ? `\n\n## Warnings\n${suggestions.warnings.join('\n')}`
537
+ : '';
538
+ return {
539
+ content: [
540
+ {
541
+ type: 'text',
542
+ text: `# Page Augmentation: ${domain} / ${tone}\n\nUX Goals: ${ux_goals.join(', ')}\n\n${coveredText}${uncoveredText}\n\n## Suggested Additions\n\n${suggestedSections}${altText}${boldText}${missingText}${snippetsText}${fontsText}${warningsText}\n\nUse \`get_pattern(id)\` to get full HTML for any section.`,
416
543
  },
417
544
  ],
418
545
  };
@@ -1013,6 +1140,159 @@ mountCanvas(document.getElementById('ws-canvas'), effect, { animate: ${effect.an
1013
1140
  content: [{ type: 'text', text }],
1014
1141
  };
1015
1142
  });
1143
+ // --- Design Skill Tools ---
1144
+ function formatSkillBrief(s) {
1145
+ return [
1146
+ `**${s.title}** (${s.id})`,
1147
+ ` ${s.summary}`,
1148
+ ` Tone: ${s.tone} | Domains: ${s.domains.join(', ')}`,
1149
+ s.tags.length > 0 ? ` Tags: ${s.tags.join(', ')}` : '',
1150
+ ]
1151
+ .filter(Boolean)
1152
+ .join('\n');
1153
+ }
1154
+ function formatSkillFull(s) {
1155
+ const colors = Object.entries(s.colors)
1156
+ .map(([k, v]) => ` ${k}: ${v}`)
1157
+ .join('\n');
1158
+ return [
1159
+ `# Design Skill: ${s.title}`,
1160
+ '',
1161
+ s.summary,
1162
+ '',
1163
+ '## Identity',
1164
+ `ID: ${s.id}`,
1165
+ `Tone: ${s.tone}`,
1166
+ `Domains: ${s.domains.join(', ')}`,
1167
+ s.uxGoals.length > 0 ? `UX Goals: ${s.uxGoals.join(', ')}` : '',
1168
+ '',
1169
+ `## Color Palette\n${colors}`,
1170
+ '',
1171
+ `## Typography`,
1172
+ `Heading: ${s.typography.heading} (weight ${s.typography.weight.heading})`,
1173
+ `Body: ${s.typography.body} (weight ${s.typography.weight.body})`,
1174
+ `Mono: ${s.typography.mono}`,
1175
+ '',
1176
+ s.relatedPatterns.length > 0 ? `## Related Patterns\n${s.relatedPatterns.join(', ')}` : '',
1177
+ s.suggestedTemplate ? `## Suggested Template\n${s.suggestedTemplate}` : '',
1178
+ '',
1179
+ '## Design Rules',
1180
+ s.body,
1181
+ ]
1182
+ .filter((line) => line !== undefined)
1183
+ .join('\n');
1184
+ }
1185
+ server.tool('list_design_skills', 'List all available Design Skills (persistent design system rules for AI agents). Each skill defines color palette, typography, spacing, and component rules for a specific visual style.', {}, async () => {
1186
+ const registry = await getRegistry();
1187
+ const skills = registry.skills ?? [];
1188
+ if (skills.length === 0) {
1189
+ return {
1190
+ content: [
1191
+ {
1192
+ type: 'text',
1193
+ text: 'No design skills available in registry.',
1194
+ },
1195
+ ],
1196
+ };
1197
+ }
1198
+ return {
1199
+ content: [
1200
+ {
1201
+ type: 'text',
1202
+ text: `${skills.length} design skills available:\n\n${skills.map(formatSkillBrief).join('\n\n')}\n\nUse \`get_design_skill(id)\` for full design rules.`,
1203
+ },
1204
+ ],
1205
+ };
1206
+ });
1207
+ server.tool('get_design_skill', "Get the full design rules for a specific Design Skill. Returns color palette, typography, spacing rules, component guidelines, and do/don't rules in markdown format.", {
1208
+ id: z.string().describe('Skill ID, e.g. "modern", "corporate", "glassmorphism", "minimal"'),
1209
+ }, async ({ id }) => {
1210
+ const registry = await getRegistry();
1211
+ const skills = registry.skills ?? [];
1212
+ const skill = skills.find((s) => s.id === id);
1213
+ if (!skill) {
1214
+ const available = skills.map((s) => s.id).join(', ');
1215
+ return {
1216
+ content: [
1217
+ {
1218
+ type: 'text',
1219
+ text: `Design skill "${id}" not found. Available: ${available}`,
1220
+ },
1221
+ ],
1222
+ };
1223
+ }
1224
+ return {
1225
+ content: [{ type: 'text', text: formatSkillFull(skill) }],
1226
+ };
1227
+ });
1228
+ server.tool('recommend_design_skill', 'Recommend a Design Skill based on business domain, desired tone, or UX goals. Returns the best matching skill with its design rules.', {
1229
+ domain: z
1230
+ .string()
1231
+ .optional()
1232
+ .describe('Business domain, e.g. "saas", "agency", "legal", "healthcare", "finance", "ecommerce"'),
1233
+ tone: z
1234
+ .string()
1235
+ .optional()
1236
+ .describe('Desired visual tone, e.g. "modern", "serious", "premium", "playful", "editorial", "minimal"'),
1237
+ ux_goal: z
1238
+ .string()
1239
+ .optional()
1240
+ .describe('Primary UX goal, e.g. "build_trust", "drive_signup", "showcase_work", "drive_contact"'),
1241
+ }, async ({ domain, tone, ux_goal }) => {
1242
+ const registry = await getRegistry();
1243
+ const skills = registry.skills ?? [];
1244
+ if (skills.length === 0) {
1245
+ return {
1246
+ content: [
1247
+ {
1248
+ type: 'text',
1249
+ text: 'No design skills available in registry.',
1250
+ },
1251
+ ],
1252
+ };
1253
+ }
1254
+ // Score each skill by match quality
1255
+ const scored = skills.map((s) => {
1256
+ let score = 0;
1257
+ if (tone && s.tone === tone)
1258
+ score += 10;
1259
+ if (tone && s.tags.includes(tone))
1260
+ score += 5;
1261
+ if (domain && s.domains.includes(domain))
1262
+ score += 8;
1263
+ if (ux_goal && s.uxGoals.includes(ux_goal))
1264
+ score += 6;
1265
+ return { skill: s, score };
1266
+ });
1267
+ scored.sort((a, b) => b.score - a.score);
1268
+ const best = scored[0];
1269
+ if (best.score === 0) {
1270
+ // No match — return all skills as suggestions
1271
+ return {
1272
+ content: [
1273
+ {
1274
+ type: 'text',
1275
+ text: `No exact match found. Available skills:\n\n${skills.map(formatSkillBrief).join('\n\n')}\n\nUse \`get_design_skill(id)\` for full rules.`,
1276
+ },
1277
+ ],
1278
+ };
1279
+ }
1280
+ const reason = [];
1281
+ if (tone && best.skill.tone === tone)
1282
+ reason.push(`tone matches "${tone}"`);
1283
+ if (domain && best.skill.domains.includes(domain))
1284
+ reason.push(`domain "${domain}" supported`);
1285
+ if (ux_goal && best.skill.uxGoals.includes(ux_goal))
1286
+ reason.push(`UX goal "${ux_goal}" supported`);
1287
+ return {
1288
+ content: [
1289
+ {
1290
+ type: 'text',
1291
+ text: `## Recommended: ${best.skill.title} (${best.skill.id})\n\n**Why:** ${reason.join(', ')}\n\n${formatSkillFull(best.skill)}`,
1292
+ },
1293
+ ],
1294
+ };
1295
+ });
1016
1296
  }
1017
1297
  export function registerResourcesWithProvider(server, getRegistry) {
1018
1298
  server.resource('categories', 'webspire://categories', {
package/dist/search.d.ts CHANGED
@@ -47,11 +47,37 @@ export interface RecommendedSnippet {
47
47
  }
48
48
  export interface ComposedPage {
49
49
  patterns: PatternEntry[];
50
+ /** Per primary pattern: top alternatives with same role, different family */
51
+ alternatives: Record<string, PatternEntry[]>;
52
+ /** Creative wildcards — high-scoring patterns not selected because role was full */
53
+ boldPicks: PatternEntry[];
50
54
  snippets: RecommendedSnippet[];
51
55
  reasoning: string[];
52
56
  warnings: string[];
57
+ /** Content ideas that would fit but don't yet exist as patterns in the library */
58
+ missingFromLibrary: string[];
59
+ }
60
+ export interface AugmentPageOptions extends ComposePageOptions {
61
+ /** Family names of sections already present on the page, e.g. ["navbar", "hero"] */
62
+ existingSections: string[];
63
+ }
64
+ export interface AugmentedPage {
65
+ /** The family names that were passed in as already present */
66
+ alreadyCovered: string[];
67
+ /** Which compositionRoles those families cover */
68
+ coveredRoles: string[];
69
+ /** Which compositionRoles still have open slots */
70
+ uncoveredRoles: string[];
71
+ /** Pattern/snippet suggestions for the gaps */
72
+ suggestions: ComposedPage;
53
73
  }
54
74
  export declare function composePage(patterns: PatternEntry[], snippets: SnippetEntry[], options: ComposePageOptions): ComposedPage;
75
+ /**
76
+ * Suggests additions for a page that already has some sections.
77
+ * Unlike composePage (greenfield), augmentPage works out which roles
78
+ * are already covered and only fills the gaps.
79
+ */
80
+ export declare function augmentPage(patterns: PatternEntry[], snippets: SnippetEntry[], options: AugmentPageOptions): AugmentedPage;
55
81
  export declare function recommendFonts(fontsData: FontsData, domain: string, tone: string): FontRecommendation;
56
82
  export interface CanvasSearchOptions {
57
83
  query: string;
package/dist/search.js CHANGED
@@ -225,10 +225,12 @@ export function searchPatterns(patterns, options) {
225
225
  if (tier)
226
226
  filtered = filtered.filter((p) => p.tier === tier);
227
227
  const scored = filtered
228
- .map((pattern) => ({
229
- pattern,
230
- score: scorePattern(pattern, stems, expanded, options),
231
- }))
228
+ .map((pattern) => {
229
+ let score = scorePattern(pattern, stems, expanded, options);
230
+ if (pattern.qualityTier === 'recommended')
231
+ score += 15;
232
+ return { pattern, score };
233
+ })
232
234
  .filter(({ pattern, score }) => {
233
235
  if (score <= 0)
234
236
  return false;
@@ -837,11 +839,222 @@ export function composePage(patterns, snippets, options) {
837
839
  }
838
840
  }
839
841
  const recommendedSnippets = recommendSnippetsForComposition(snippets, result, options);
842
+ // Step 6: Compute alternatives (per selected pattern: top-3 same-role, different family)
843
+ const selectedFamilies = new Set(result.map((p) => p.family));
844
+ const alternatives = {};
845
+ for (const selectedPattern of result) {
846
+ const role = selectedPattern.ai?.compositionRole ?? 'supporting';
847
+ const alts = scored
848
+ .filter(({ pattern }) => (pattern.ai?.compositionRole ?? 'supporting') === role &&
849
+ pattern.family !== selectedPattern.family &&
850
+ !selectedFamilies.has(pattern.family))
851
+ .slice(0, 3)
852
+ .map(({ pattern }) => pattern);
853
+ if (alts.length > 0) {
854
+ alternatives[selectedPattern.id] = alts;
855
+ }
856
+ }
857
+ // Step 7: Bold picks — high-scoring patterns that weren't selected, preferring animated or enhanced
858
+ const boldPicks = scored
859
+ .filter(({ pattern, score }) => !selectedFamilies.has(pattern.family) &&
860
+ score >= 5 &&
861
+ (pattern.capabilities?.animated === true || pattern.tier === 'enhanced'))
862
+ .slice(0, 2)
863
+ .map(({ pattern }) => pattern);
864
+ // Step 8: Missing from library — content ideas that don't yet exist as patterns
865
+ const missingFromLibrary = computeMissingFromLibrary(domain, tone, uxGoals, result);
840
866
  return {
841
867
  patterns: result,
868
+ alternatives,
869
+ boldPicks,
842
870
  snippets: recommendedSnippets,
843
871
  reasoning,
844
872
  warnings,
873
+ missingFromLibrary,
874
+ };
875
+ }
876
+ // --- Missing From Library ---
877
+ /**
878
+ * Domain-specific content ideas that would make sense on this type of site
879
+ * but don't yet exist as patterns in the WebSpire library.
880
+ * These are intentionally opinionated — the goal is to inspire, not be exhaustive.
881
+ */
882
+ const DOMAIN_CONTENT_SUGGESTIONS = {
883
+ agency: [
884
+ 'Awards & Recognition strip (Awwwards, FWA, Cannes badges)',
885
+ 'Client project counter with animated numbers',
886
+ 'Scroll-driven process / methodology diagram',
887
+ '"Work in progress" sneak-peek teaser section',
888
+ 'Behind-the-scenes studio culture section',
889
+ ],
890
+ saas: [
891
+ 'Live product demo with real-time preview embed',
892
+ 'GitHub stars + contributor avatar stack',
893
+ 'API code snippet preview with language tabs',
894
+ 'Customer ROI / time-saved calculator widget',
895
+ 'System status / uptime badge row',
896
+ ],
897
+ ecommerce: [
898
+ 'Recently-viewed products strip',
899
+ 'Bundle & save suggestion row',
900
+ 'Loyalty points explainer section',
901
+ 'Real-time low-stock indicator',
902
+ 'Size guide / fit advisor modal trigger',
903
+ ],
904
+ consulting: [
905
+ 'Client portfolio outcome timeline',
906
+ 'Methodology / engagement model visual',
907
+ 'Thought leadership article highlight strip',
908
+ 'Industry specialization selector',
909
+ ],
910
+ healthcare: [
911
+ 'Symptom checker / self-assessment widget',
912
+ 'Insurance accepted / coverage explainer',
913
+ 'Staff credentials strip with specialty badges',
914
+ 'Emergency contact / triage priority bar',
915
+ ],
916
+ education: [
917
+ 'Course prerequisite map / learning path',
918
+ 'Student outcome stats (placement rate, salary)',
919
+ 'Enrollment deadline countdown banner',
920
+ 'Cohort community / alumni network section',
921
+ ],
922
+ finance: [
923
+ 'Interest rate / mortgage calculator widget',
924
+ 'Risk tolerance questionnaire flow',
925
+ 'Regulatory disclaimer bar (compact, collapsible)',
926
+ 'Portfolio allocation visual explainer',
927
+ ],
928
+ legal: [
929
+ 'Practice area decision tree',
930
+ 'Case outcome / win-rate stats bar',
931
+ 'Attorney matching questionnaire',
932
+ 'Jurisdiction / location selector',
933
+ ],
934
+ nonprofit: [
935
+ 'Live impact counter (meals served, trees planted)',
936
+ 'Volunteer availability / shift calendar',
937
+ 'Recurring vs one-time donation toggle',
938
+ 'Partner organization acknowledgement wall',
939
+ ],
940
+ gastronomy: [
941
+ 'Menu item of the day / featured dish widget',
942
+ 'Real-time table availability checker',
943
+ 'Chef profile with signature dish spotlight',
944
+ 'Dietary / allergy filter strip',
945
+ ],
946
+ realestate: [
947
+ 'Mortgage / affordability calculator embed',
948
+ 'Neighborhood walkability / transit score widget',
949
+ 'Virtual tour launcher',
950
+ 'Price history sparkline chart',
951
+ ],
952
+ personal: [
953
+ 'Reading list / bookshelf section',
954
+ 'Life stats / fun-facts counter strip',
955
+ "Now page (what I'm working on / listening to)",
956
+ 'Newsletter with personalized "why subscribe" hook',
957
+ ],
958
+ };
959
+ const UX_GOAL_CONTENT_SUGGESTIONS = {
960
+ build_trust: 'Third-party review aggregate (G2, Trustpilot rating block)',
961
+ drive_contact: 'Smart contact form with department routing',
962
+ drive_signup: 'Progressive disclosure signup — minimal first step, details later',
963
+ drive_purchase: 'Social proof at checkout: "X people bought this today"',
964
+ explain_offer: 'Interactive feature comparison with toggle/filter',
965
+ highlight_expertise: 'Case study timeline with measurable outcomes',
966
+ showcase_work: 'Full-bleed project filmstrip with scroll-scrubbing',
967
+ reduce_friction: 'Inline FAQ that answers objections near the CTA',
968
+ tell_story: 'Scroll-driven narrative with chapter indicators',
969
+ demonstrate_value: 'Before/after slider or split-screen comparison',
970
+ };
971
+ function computeMissingFromLibrary(domain, tone, uxGoals, selectedPatterns) {
972
+ const ideas = [];
973
+ // 2 domain-specific ideas
974
+ const domainIdeas = DOMAIN_CONTENT_SUGGESTIONS[domain] ?? [];
975
+ ideas.push(...domainIdeas.slice(0, 2));
976
+ // 1 idea per uncovered UX goal (max 2)
977
+ const coveredGoals = new Set();
978
+ for (const p of selectedPatterns) {
979
+ for (const g of p.semantics?.uxGoals ?? [])
980
+ coveredGoals.add(g);
981
+ }
982
+ let goalIdeasAdded = 0;
983
+ for (const goal of uxGoals) {
984
+ if (!coveredGoals.has(goal) && UX_GOAL_CONTENT_SUGGESTIONS[goal] && goalIdeasAdded < 2) {
985
+ ideas.push(UX_GOAL_CONTENT_SUGGESTIONS[goal]);
986
+ goalIdeasAdded++;
987
+ }
988
+ }
989
+ // Tone-specific wildcard
990
+ if (tone === 'editorial' && !ideas.some((i) => i.includes('scroll'))) {
991
+ ideas.push('Scroll-progress reading indicator for long-form content');
992
+ }
993
+ else if (tone === 'premium' && !ideas.some((i) => i.includes('3D'))) {
994
+ ideas.push('3D product viewer / depth-of-field showcase element');
995
+ }
996
+ else if (tone === 'playful') {
997
+ ideas.push('Easter egg / hidden interaction for delighted discovery');
998
+ }
999
+ return ideas.slice(0, 4);
1000
+ }
1001
+ // --- Augment Existing Page ---
1002
+ /**
1003
+ * Suggests additions for a page that already has some sections.
1004
+ * Unlike composePage (greenfield), augmentPage works out which roles
1005
+ * are already covered and only fills the gaps.
1006
+ */
1007
+ export function augmentPage(patterns, snippets, options) {
1008
+ const { existingSections, ...composeOptions } = options;
1009
+ // Determine which compositionRoles are already covered
1010
+ const coveredRoleCounts = new Map();
1011
+ for (const family of existingSections) {
1012
+ // Check ROLE_FAMILY_BONUSES for authoritative role lookup
1013
+ let found = false;
1014
+ for (const [role, families] of Object.entries(ROLE_FAMILY_BONUSES)) {
1015
+ if (Object.hasOwn(families, family)) {
1016
+ coveredRoleCounts.set(role, (coveredRoleCounts.get(role) ?? 0) + 1);
1017
+ found = true;
1018
+ break;
1019
+ }
1020
+ }
1021
+ // Fallback: look up a matching pattern's compositionRole
1022
+ if (!found) {
1023
+ const match = patterns.find((p) => p.family === family);
1024
+ if (match?.ai?.compositionRole) {
1025
+ const role = match.ai.compositionRole;
1026
+ coveredRoleCounts.set(role, (coveredRoleCounts.get(role) ?? 0) + 1);
1027
+ }
1028
+ }
1029
+ }
1030
+ // Compute remaining slots per role
1031
+ const remainingSlots = {};
1032
+ for (const [role, limit] of Object.entries(ROLE_LIMITS)) {
1033
+ const used = coveredRoleCounts.get(role) ?? 0;
1034
+ remainingSlots[role] = Math.max(0, limit - used);
1035
+ }
1036
+ const coveredRoles = [...coveredRoleCounts.keys()].filter((r) => (coveredRoleCounts.get(r) ?? 0) >= (ROLE_LIMITS[r] ?? 1));
1037
+ const allRoles = Object.keys(ROLE_ORDER);
1038
+ const uncoveredRoles = allRoles.filter((r) => (remainingSlots[r] ?? 0) > 0);
1039
+ // Filter patterns: exclude already-present families and fully-covered roles
1040
+ const availablePatterns = patterns.filter((p) => {
1041
+ if (existingSections.includes(p.family))
1042
+ return false;
1043
+ const role = p.ai?.compositionRole ?? 'supporting';
1044
+ return (remainingSlots[role] ?? 0) > 0;
1045
+ });
1046
+ // Adjust maxSections to remaining open slots total
1047
+ const totalRemainingSlots = Object.values(remainingSlots).reduce((a, b) => a + b, 0);
1048
+ const adjustedMax = Math.min(composeOptions.maxSections ?? 8, totalRemainingSlots);
1049
+ const suggestions = composePage(availablePatterns, snippets, {
1050
+ ...composeOptions,
1051
+ maxSections: adjustedMax,
1052
+ });
1053
+ return {
1054
+ alreadyCovered: existingSections,
1055
+ coveredRoles,
1056
+ uncoveredRoles,
1057
+ suggestions,
845
1058
  };
846
1059
  }
847
1060
  // --- Font Recommendation ---
@@ -0,0 +1 @@
1
+ export {};