skillui 1.1.2 → 1.1.4

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 (62) hide show
  1. package/README.md +20 -15
  2. package/dist/cli.js +105073 -194
  3. package/package.json +15 -6
  4. package/dist/cli.d.ts +0 -3
  5. package/dist/extractors/components.d.ts +0 -11
  6. package/dist/extractors/components.js +0 -455
  7. package/dist/extractors/framework.d.ts +0 -4
  8. package/dist/extractors/framework.js +0 -126
  9. package/dist/extractors/tokens/computed.d.ts +0 -7
  10. package/dist/extractors/tokens/computed.js +0 -249
  11. package/dist/extractors/tokens/css.d.ts +0 -3
  12. package/dist/extractors/tokens/css.js +0 -510
  13. package/dist/extractors/tokens/http-css.d.ts +0 -14
  14. package/dist/extractors/tokens/http-css.js +0 -1689
  15. package/dist/extractors/tokens/tailwind.d.ts +0 -3
  16. package/dist/extractors/tokens/tailwind.js +0 -353
  17. package/dist/extractors/tokens/tokens-file.d.ts +0 -3
  18. package/dist/extractors/tokens/tokens-file.js +0 -229
  19. package/dist/extractors/ultra/animations.d.ts +0 -21
  20. package/dist/extractors/ultra/animations.js +0 -527
  21. package/dist/extractors/ultra/components-dom.d.ts +0 -13
  22. package/dist/extractors/ultra/components-dom.js +0 -149
  23. package/dist/extractors/ultra/interactions.d.ts +0 -14
  24. package/dist/extractors/ultra/interactions.js +0 -222
  25. package/dist/extractors/ultra/layout.d.ts +0 -14
  26. package/dist/extractors/ultra/layout.js +0 -123
  27. package/dist/extractors/ultra/pages.d.ts +0 -16
  28. package/dist/extractors/ultra/pages.js +0 -228
  29. package/dist/font-resolver.d.ts +0 -10
  30. package/dist/font-resolver.js +0 -280
  31. package/dist/modes/dir.d.ts +0 -6
  32. package/dist/modes/dir.js +0 -213
  33. package/dist/modes/repo.d.ts +0 -6
  34. package/dist/modes/repo.js +0 -76
  35. package/dist/modes/ultra.d.ts +0 -22
  36. package/dist/modes/ultra.js +0 -281
  37. package/dist/modes/url.d.ts +0 -14
  38. package/dist/modes/url.js +0 -161
  39. package/dist/normalizer.d.ts +0 -11
  40. package/dist/normalizer.js +0 -867
  41. package/dist/playwright-loader.d.ts +0 -10
  42. package/dist/playwright-loader.js +0 -71
  43. package/dist/screenshot.d.ts +0 -9
  44. package/dist/screenshot.js +0 -94
  45. package/dist/types-ultra.d.ts +0 -157
  46. package/dist/types-ultra.js +0 -4
  47. package/dist/types.d.ts +0 -182
  48. package/dist/types.js +0 -4
  49. package/dist/writers/animations-md.d.ts +0 -17
  50. package/dist/writers/animations-md.js +0 -313
  51. package/dist/writers/components-md.d.ts +0 -8
  52. package/dist/writers/components-md.js +0 -151
  53. package/dist/writers/design-md.d.ts +0 -7
  54. package/dist/writers/design-md.js +0 -704
  55. package/dist/writers/interactions-md.d.ts +0 -8
  56. package/dist/writers/interactions-md.js +0 -146
  57. package/dist/writers/layout-md.d.ts +0 -8
  58. package/dist/writers/layout-md.js +0 -120
  59. package/dist/writers/skill.d.ts +0 -12
  60. package/dist/writers/skill.js +0 -1006
  61. package/dist/writers/tokens-json.d.ts +0 -11
  62. package/dist/writers/tokens-json.js +0 -164
@@ -1,1006 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.generateSkill = generateSkill;
37
- const fs = __importStar(require("fs"));
38
- const path = __importStar(require("path"));
39
- const archiver = __importStar(require("archiver"));
40
- const font_resolver_1 = require("../font-resolver");
41
- const design_md_1 = require("./design-md");
42
- /**
43
- * Generate SKILL.md + references/DESIGN.md and package as .skill zip file.
44
- * @param screenshotPath - optional local path to homepage screenshot (relative to skillDir)
45
- * @param ultraResult - optional full animation/ultra data for embedding scroll journey
46
- */
47
- async function generateSkill(profile, designMdContent, outputDir, screenshotPath, ultraResult) {
48
- const safeName = profile.projectName.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
49
- const skillDirName = `${safeName}-design`;
50
- const skillDir = path.join(outputDir, skillDirName);
51
- const refsDir = path.join(skillDir, 'references');
52
- fs.mkdirSync(refsDir, { recursive: true });
53
- // Bundle font files from Google Fonts API into the skill package
54
- let bundledFontCount = 0;
55
- const fontFamilies = profile.typography.map(t => t.fontFamily).filter(Boolean);
56
- if (fontFamilies.length > 0 || profile.fontSources.length > 0) {
57
- const usedWeights = new Set(profile.typography.map(t => String(t.fontWeight || '400')));
58
- const result = await (0, font_resolver_1.bundleFonts)(profile.fontSources, fontFamilies, skillDir, usedWeights);
59
- profile = { ...profile, fontSources: result.updatedSources };
60
- bundledFontCount = result.bundledCount;
61
- }
62
- const skillMd = generateSkillMd(profile, screenshotPath, ultraResult);
63
- fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillMd, 'utf-8');
64
- // If fonts were bundled, regenerate DESIGN.md with local font paths
65
- const finalDesignMd = bundledFontCount > 0 ? (0, design_md_1.generateDesignMd)(profile, screenshotPath) : designMdContent;
66
- fs.writeFileSync(path.join(refsDir, 'DESIGN.md'), finalDesignMd, 'utf-8');
67
- const skillFile = path.join(outputDir, `${skillDirName}.skill`);
68
- await createZip(skillDir, skillFile);
69
- return { skillDir, skillFile };
70
- }
71
- function generateSkillMd(profile, screenshotPath, ultraResult) {
72
- const bg = profile.colors.find(c => c.role === 'background');
73
- const surface = profile.colors.find(c => c.role === 'surface');
74
- const accent = profile.colors.find(c => c.role === 'accent');
75
- const textPrimary = profile.colors.find(c => c.role === 'text-primary');
76
- const textMuted = profile.colors.find(c => c.role === 'text-muted');
77
- const border = profile.colors.find(c => c.role === 'border');
78
- const danger = profile.colors.find(c => c.role === 'danger');
79
- const success = profile.colors.find(c => c.role === 'success');
80
- const warning = profile.colors.find(c => c.role === 'warning');
81
- const allFonts = [...new Set(profile.typography.map(t => t.fontFamily))].filter(f => f);
82
- const displayFonts = allFonts.filter(f => !/mono|consolas|courier|fira code|jetbrains|sf mono|menlo/i.test(f));
83
- const fonts = allFonts; // Keep for backward compat
84
- const primaryFont = displayFonts[0] || allFonts[0] || 'sans-serif';
85
- const borderRadius = profile.borderRadius.filter(r => !r.includes('9999'));
86
- const commonRadius = borderRadius[Math.floor(borderRadius.length / 2)] || '8px';
87
- const traits = profile.designTraits;
88
- const theme = traits.isDark ? 'dark' : 'light';
89
- let md = '';
90
- const isUltra = !!(ultraResult && (ultraResult.scrollFrames.length > 0 || ultraResult.keyframes.length > 0));
91
- // ── Frontmatter ──────────────────────────────────────────────────────
92
- const ultraDesc = isUltra
93
- ? ` Includes ultra-mode visual journey: read references/ANIMATIONS.md, references/LAYOUT.md, references/COMPONENTS.md, and references/INTERACTIONS.md for full motion and layout details.`
94
- : '';
95
- md += `---
96
- name: ${profile.projectName}-design
97
- description: Design system skill for ${profile.projectName}. Activate when building UI components, pages, or any visual elements. Provides exact color tokens, typography scale, spacing grid, component patterns, and craft rules. Read references/DESIGN.md before writing any CSS or JSX.${ultraDesc}
98
- version: 1.0.0
99
- allowed-tools: [Read, Write, Edit, Glob, Grep]
100
- ---
101
-
102
- `;
103
- // ── Header ───────────────────────────────────────────────────────────
104
- md += `# ${profile.projectName} Design System\n\n`;
105
- md += `You are building UI for **${profile.projectName}**. ${getAestheticSummary(profile)}\n\n`;
106
- // ── Visual Reference ─────────────────────────────────────────────────
107
- if (screenshotPath || isUltra) {
108
- md += `## Visual Reference\n\n`;
109
- md += `**IMPORTANT**: Study ALL screenshots below before writing any UI. Match colors, typography, spacing, layout, and motion exactly as shown.\n\n`;
110
- if (screenshotPath) {
111
- md += `### Homepage\n\n`;
112
- md += `![${profile.projectName} Homepage](${screenshotPath})\n\n`;
113
- }
114
- // Embed scroll journey frames
115
- if (isUltra && ultraResult.scrollFrames.length > 0) {
116
- md += `### Scroll Journey (Cinematic Visual States)\n\n`;
117
- md += `> These screenshots capture the website at different scroll depths. The design changes dramatically as you scroll — each frame shows a different cinematic state. Replicate these exact visual transitions.\n\n`;
118
- for (const frame of ultraResult.scrollFrames) {
119
- // Convert absolute path to relative path from skillDir
120
- const relPath = frame.filePath.replace(/\\/g, '/');
121
- const label = frame.scrollPercent === 0 ? 'Hero / Above the fold'
122
- : frame.scrollPercent === 100 ? 'Footer / End of page'
123
- : `Mid-page at ${frame.scrollPercent}% scroll`;
124
- md += `#### ${frame.scrollPercent}% — ${label}\n\n`;
125
- md += `![Scroll ${frame.scrollPercent}%](${relPath})\n\n`;
126
- }
127
- }
128
- // Embed video first frames
129
- if (isUltra && ultraResult.videos.some(v => v.firstFramePath)) {
130
- md += `### Video Backgrounds (First Frames)\n\n`;
131
- for (const v of ultraResult.videos.filter(vv => vv.firstFramePath)) {
132
- const relPath = v.firstFramePath.replace(/\\/g, '/');
133
- md += `![Video ${v.index} (${v.role})](${relPath})\n\n`;
134
- }
135
- }
136
- md += `> Read \`references/DESIGN.md\` for full token details.`;
137
- if (isUltra) {
138
- md += ` Read \`references/ANIMATIONS.md\` for motion specs. Read \`references/LAYOUT.md\` for layout structure. Read \`references/COMPONENTS.md\` for component patterns.`;
139
- }
140
- md += `\n\n`;
141
- }
142
- // ── Ultra Reference Files Index ──────────────────────────────────────
143
- if (isUltra) {
144
- md += `## Ultra Reference Files\n\n`;
145
- md += `This package includes extended documentation. **Read these files before implementing:**\n\n`;
146
- md += `| File | Contents |\n`;
147
- md += `|------|----------|\n`;
148
- md += `| \`references/DESIGN.md\` | Full design system tokens, colors, typography, spacing |\n`;
149
- md += `| \`references/VISUAL_GUIDE.md\` | **START HERE** — Master visual guide with all screenshots embedded |\n`;
150
- md += `| \`references/ANIMATIONS.md\` | CSS keyframes, scroll triggers, motion library stack, video specs |\n`;
151
- md += `| \`references/LAYOUT.md\` | Flex/grid containers, page structure, spacing relationships |\n`;
152
- md += `| \`references/COMPONENTS.md\` | DOM component patterns, HTML structure, class fingerprints |\n`;
153
- md += `| \`references/INTERACTIONS.md\` | Hover/focus states with before/after style diffs |\n`;
154
- if (ultraResult.scrollFrames.length > 0) {
155
- md += `| \`screens/scroll/\` | ${ultraResult.scrollFrames.length} scroll journey screenshots showing cinematic states |\n`;
156
- }
157
- md += `\n`;
158
- // Animation stack summary
159
- if (ultraResult.libraries.length > 0) {
160
- md += `### Animation Stack Detected\n\n`;
161
- for (const lib of ultraResult.libraries) {
162
- md += `- **${lib.name}**${lib.version ? ` v${lib.version}` : ''} — ${lib.type}${lib.cdn ? ` ([CDN](${lib.cdn}))` : ''}\n`;
163
- }
164
- md += `\n`;
165
- }
166
- }
167
- // ── Section 1: Design Philosophy ─────────────────────────────────────
168
- md += `## Design Philosophy\n\n`;
169
- md += generateDesignPhilosophy(profile, fonts, primaryFont, traits);
170
- // ── Section 2: Color System ──────────────────────────────────────────
171
- md += `## Color System\n\n`;
172
- md += generateColorSystem(profile, bg, surface, accent, textPrimary, textMuted, border, danger, success, warning);
173
- // ── Section 3: Typography ────────────────────────────────────────────
174
- md += `## Typography\n\n`;
175
- md += generateTypographySection(profile, fonts, primaryFont);
176
- // ── Section 4: Spacing & Layout ──────────────────────────────────────
177
- md += `## Spacing & Layout\n\n`;
178
- md += generateSpacingSection(profile, commonRadius);
179
- // ── Section 5: Component Patterns ────────────────────────────────────
180
- md += `## Component Patterns\n\n`;
181
- md += generateComponentPatterns(profile, bg, surface, accent, textPrimary, textMuted, border, commonRadius);
182
- // ── Section 5b: Page Sections (URL mode) ─────────────────────────────
183
- if (profile.pageSections.length > 0) {
184
- md += `## Page Structure\n\n`;
185
- md += `The following page sections were detected:\n\n`;
186
- for (const section of profile.pageSections) {
187
- md += `- **${section.type.charAt(0).toUpperCase() + section.type.slice(1)}** — ${section.description}`;
188
- if (section.childCount > 0)
189
- md += ` (${section.childCount} items)`;
190
- md += `\n`;
191
- }
192
- md += `\nWhen building pages, follow this section order and structure.\n\n`;
193
- }
194
- // ── Section 6: Animation & Motion ────────────────────────────────────
195
- // Always output motion section — use extracted tokens or defaults
196
- md += `## Animation & Motion\n\n`;
197
- md += generateAnimationSection(profile);
198
- // ── Section 7: Dark Mode ─────────────────────────────────────────────
199
- if (traits.hasDarkMode && profile.darkModeVars.length > 0) {
200
- md += `## Dark Mode\n\n`;
201
- md += generateDarkModeSection(profile);
202
- }
203
- // ── Section 8: Depth & Elevation ─────────────────────────────────────
204
- md += `## Depth & Elevation\n\n`;
205
- md += generateElevationSection(profile, traits, surface, bg);
206
- // ── Section 9: Anti-Patterns ─────────────────────────────────────────
207
- md += `## Anti-Patterns (Never Do)\n\n`;
208
- md += generateAntiPatterns(profile, fonts, traits);
209
- // ── Section 10: Workflow ─────────────────────────────────────────────
210
- md += `## Workflow\n\n`;
211
- md += generateWorkflow(profile, fonts);
212
- // ── Section 10b: Brand Spec ───────────────────────────────────────────
213
- md += `## Brand Spec\n\n`;
214
- if (profile.favicon) {
215
- md += `- **Favicon:** \`${profile.favicon}\`\n`;
216
- }
217
- if (profile.siteUrl) {
218
- md += `- **Site URL:** \`${profile.siteUrl}\`\n`;
219
- }
220
- const brandColor = accent;
221
- if (brandColor?.hex) {
222
- md += `- **Brand color:** \`${brandColor.hex}\`\n`;
223
- }
224
- const primaryFontForBrand = profile.typography.find(t => t.fontFamily && !/(mono|dm mono|consolas|courier)/i.test(t.fontFamily));
225
- if (primaryFontForBrand?.fontFamily) {
226
- md += `- **Brand typeface:** ${primaryFontForBrand.fontFamily}\n`;
227
- }
228
- md += `\n`;
229
- // ── Section 11: Quick Reference Table ────────────────────────────────
230
- md += `## Quick Reference\n\n`;
231
- md += generateQuickReference(profile, bg, surface, accent, textPrimary, textMuted, border, primaryFont, commonRadius);
232
- // ── Section 12: When to Trigger ──────────────────────────────────────
233
- md += `## When to Trigger\n\n`;
234
- md += `Activate this skill when:\n`;
235
- md += `- Creating new components, pages, or visual elements for ${profile.projectName}\n`;
236
- md += `- Writing CSS, Tailwind classes, styled-components, or inline styles\n`;
237
- md += `- Building page layouts, templates, or responsive designs\n`;
238
- md += `- Reviewing UI code for design consistency\n`;
239
- md += `- The user mentions "${profile.projectName}" design, style, UI, or theme\n`;
240
- md += `- Generating mockups, wireframes, or visual prototypes\n`;
241
- return md;
242
- }
243
- // ── Section Generators ─────────────────────────────────────────────────
244
- function generateDesignPhilosophy(profile, fonts, primaryFont, traits) {
245
- let s = '';
246
- // Core principles
247
- if (traits.isDark && !traits.hasShadows) {
248
- s += `- **Flat elevation** — depth through color shifts and borders, never shadows. Surfaces get progressively lighter to indicate elevation.\n`;
249
- }
250
- else if (traits.hasShadows) {
251
- s += `- **Layered depth** — use shadow tokens to create a sense of physical layering. Each elevation level has a specific shadow.\n`;
252
- }
253
- if (!traits.hasGradients) {
254
- s += `- **Solid colors only** — no gradients anywhere. Every surface is a single flat color.\n`;
255
- }
256
- else {
257
- s += `- **Gradient accents** — gradients are used thoughtfully for emphasis, not decoration.\n`;
258
- }
259
- const displayFonts = fonts.filter(f => !/mono|consolas|courier|fira code|jetbrains|sf mono|menlo/i.test(f));
260
- if (displayFonts.length === 1) {
261
- s += `- **Single typeface** — ${primaryFont} carries all text. Hierarchy comes from size, weight, and color — never font mixing.\n`;
262
- }
263
- else if (displayFonts.length >= 2) {
264
- s += `- **Type pairing** — ${displayFonts[0]} for body/UI text, ${displayFonts[1]} for headings/display. Never introduce a third typeface.\n`;
265
- }
266
- s += `- **${traits.density} density** — ${profile.spacing.base}px base grid. Every dimension is a multiple of ${profile.spacing.base}.\n`;
267
- s += `- **${traits.primaryColorTemp} palette** — the color temperature runs ${traits.primaryColorTemp}, matching the ${traits.fontStyle} typography.\n`;
268
- const accentColor = profile.colors.find(c => c.role === 'accent');
269
- if (accentColor) {
270
- s += `- **Restrained accent** — \`${accentColor.hex}\` is the only pop of color. Used exclusively for CTAs, links, focus rings, and active states.\n`;
271
- }
272
- if (traits.motionStyle === 'expressive') {
273
- s += `- **Expressive motion** — animations are an integral part of the experience. Use spring physics and layout animations.\n`;
274
- }
275
- else if (traits.motionStyle === 'subtle') {
276
- s += `- **Subtle motion** — transitions smooth state changes. Keep durations under 300ms, use ease-out curves.\n`;
277
- }
278
- else {
279
- s += `- **Minimal motion** — prefer instant state changes. Only use transitions for loading and page transitions.\n`;
280
- }
281
- if (profile.iconLibrary) {
282
- s += `- **${profile.iconLibrary} icons** — use ${profile.iconLibrary} for all iconography. Do not mix icon libraries.\n`;
283
- }
284
- s += '\n';
285
- return s;
286
- }
287
- function generateColorSystem(profile, bg, surface, accent, textPrimary, textMuted, border, danger, success, warning) {
288
- let s = '';
289
- s += `### Core Palette\n\n`;
290
- s += `| Role | Token | Hex | Use |\n`;
291
- s += `|------|-------|-----|-----|\n`;
292
- if (bg)
293
- s += `| Background | \`--background\` | \`${bg.hex}\` | Page/app background |\n`;
294
- if (surface)
295
- s += `| Surface | \`--surface\` | \`${surface.hex}\` | Cards, panels, modals |\n`;
296
- if (textPrimary)
297
- s += `| Text Primary | \`--text-primary\` | \`${textPrimary.hex}\` | Headings, body text |\n`;
298
- if (textMuted)
299
- s += `| Text Muted | \`--text-muted\` | \`${textMuted.hex}\` | Captions, placeholders |\n`;
300
- if (accent)
301
- s += `| Accent | \`--accent\` | \`${accent.hex}\` | CTAs, links, focus rings |\n`;
302
- if (border)
303
- s += `| Border | \`--border\` | \`${border.hex}\` | Dividers, card borders |\n`;
304
- s += '\n';
305
- // Status colors
306
- if (danger || success || warning) {
307
- s += `### Status Colors\n\n`;
308
- s += `| Status | Hex | Use |\n`;
309
- s += `|--------|-----|-----|\n`;
310
- if (success)
311
- s += `| Success | \`${success.hex}\` | Confirmations, positive trends |\n`;
312
- if (warning)
313
- s += `| Warning | \`${warning.hex}\` | Caution states, pending items |\n`;
314
- if (danger)
315
- s += `| Danger | \`${danger.hex}\` | Errors, destructive actions |\n`;
316
- s += '\n';
317
- }
318
- // Additional palette colors — with usage context for named tokens
319
- const extraColors = profile.colors.filter(c => c.role === 'unknown' || c.role === 'info').slice(0, 8);
320
- if (extraColors.length > 0) {
321
- s += `### Extended Palette\n\n`;
322
- for (const c of extraColors) {
323
- const usage = guessExtendedColorUsage(c.name || '', c.hex);
324
- s += `- ${c.name ? `**${c.name}:** ` : ''}\`${c.hex}\`${usage ? ` — ${usage}` : ''}\n`;
325
- }
326
- s += '\n';
327
- }
328
- // CSS variable tokens subsection — filter out Tailwind internal/utility vars
329
- const colorVars = profile.cssVariables.filter(v => /foreground|background|primary|secondary|muted|accent|destructive|border|card|popover/i.test(v.name) &&
330
- v.value.length < 40 &&
331
- !/^--tw-|^--vt-|^--un-/i.test(v.name) // exclude Tailwind, UnoCSS, Vite internals
332
- );
333
- if (colorVars.length > 0) {
334
- s += `### CSS Variable Tokens\n\n`;
335
- s += `\`\`\`css\n`;
336
- for (const v of colorVars.slice(0, 20)) {
337
- s += `${v.name}: ${v.value};\n`;
338
- }
339
- s += `\`\`\`\n\n`;
340
- }
341
- return s;
342
- }
343
- function generateTypographySection(profile, fonts, primaryFont) {
344
- let s = '';
345
- s += `### Font Stack\n\n`;
346
- for (const font of fonts) {
347
- const roles = profile.typography.filter(t => t.fontFamily === font).map(t => formatRole(t.role));
348
- s += `- **${font}** — ${roles.join(', ')}\n`;
349
- }
350
- s += '\n';
351
- // Font sources (@font-face declarations)
352
- if (profile.fontSources.length > 0) {
353
- s += `### Font Sources\n\n`;
354
- s += `\`\`\`css\n`;
355
- // Group by family, pick best format (prefer woff2)
356
- const families = [...new Set(profile.fontSources.map(fs => fs.family))];
357
- // Collect weights actually used in the type scale
358
- const usedWeights = new Set(profile.typography.map(t => t.fontWeight || '400'));
359
- usedWeights.add('400'); // always include regular
360
- usedWeights.add('700'); // always include bold
361
- for (const family of families) {
362
- const sources = profile.fontSources.filter(fs => fs.family === family);
363
- // Pick best source per weight, but limit to key weights
364
- const byWeight = new Map();
365
- // Check for variable fonts first — they cover all weights in one file
366
- const variableSource = sources.find(s => s.weight === 'variable');
367
- if (variableSource) {
368
- s += `@font-face {\n`;
369
- s += ` font-family: "${family}";\n`;
370
- s += ` src: url("${variableSource.src}")${variableSource.format ? ` format("${variableSource.format}")` : ''};\n`;
371
- s += ` font-weight: 100 900;\n`;
372
- s += `}\n`;
373
- }
374
- else {
375
- for (const src of sources) {
376
- const w = src.weight || '400';
377
- // Only include weights used in type scale, or standard 400/700
378
- if (!usedWeights.has(w))
379
- continue;
380
- const existing = byWeight.get(w);
381
- if (!existing || (src.format === 'woff2' && existing.format !== 'woff2')) {
382
- byWeight.set(w, src);
383
- }
384
- }
385
- // If no weights matched (all non-standard), fall back to first available
386
- if (byWeight.size === 0 && sources.length > 0) {
387
- byWeight.set(sources[0].weight || '400', sources[0]);
388
- }
389
- for (const [weight, src] of byWeight) {
390
- s += `@font-face {\n`;
391
- s += ` font-family: "${family}";\n`;
392
- s += ` src: url("${src.src}")${src.format ? ` format("${src.format}")` : ''};\n`;
393
- s += ` font-weight: ${weight};\n`;
394
- s += `}\n`;
395
- }
396
- }
397
- }
398
- s += `\`\`\`\n\n`;
399
- }
400
- s += `### Type Scale\n\n`;
401
- s += `| Role | Family | Size | Weight |\n`;
402
- s += `|------|--------|------|--------|\n`;
403
- for (const t of profile.typography) {
404
- s += `| ${formatRole(t.role)} | ${t.fontFamily} | ${t.fontSize || 'inherit'} | ${t.fontWeight || 'inherit'} |\n`;
405
- }
406
- s += '\n';
407
- s += `### Typography Rules\n\n`;
408
- const dispFonts = fonts.filter(f => !/mono|consolas|courier|fira code|jetbrains|sf mono|menlo/i.test(f));
409
- if (dispFonts.length <= 1) {
410
- s += `- All text uses **${primaryFont}** — never add another font family\n`;
411
- }
412
- else {
413
- s += `- Body/UI: **${dispFonts[0]}**, Headings: **${dispFonts[1]}** — these are the only display fonts\n`;
414
- }
415
- s += `- Max 3-4 font sizes per screen\n`;
416
- s += `- Headings: weight 600-700, body: weight 400\n`;
417
- s += `- Use color and opacity for text hierarchy, not additional font sizes\n`;
418
- s += `- Line height: 1.5 for body, 1.2 for headings\n\n`;
419
- return s;
420
- }
421
- function generateSpacingSection(profile, commonRadius) {
422
- const sp = profile.spacing;
423
- let s = '';
424
- s += `### Base Grid: ${sp.base}px\n\n`;
425
- s += `Every dimension (margin, padding, gap, width, height) must be a multiple of **${sp.base}px**.\n\n`;
426
- s += `### Spacing Scale\n\n`;
427
- s += `\`${sp.values.slice(0, 12).join(', ')}\` px\n\n`;
428
- s += `### Spacing as Meaning\n\n`;
429
- s += `| Spacing | Use |\n`;
430
- s += `|---------|-----|\n`;
431
- if (sp.base <= 4) {
432
- s += `| ${sp.base}-${sp.base * 2}px | Tight: related items (icon + label, avatar + name) |\n`;
433
- s += `| ${sp.base * 3}-${sp.base * 4}px | Medium: between groups within a section |\n`;
434
- s += `| ${sp.base * 6}-${sp.base * 8}px | Wide: between distinct sections |\n`;
435
- s += `| ${sp.base * 12}px+ | Vast: major page section breaks |\n`;
436
- }
437
- else {
438
- s += `| ${sp.base / 2}-${sp.base}px | Tight: related items within a group |\n`;
439
- s += `| ${sp.base * 2}px | Medium: between groups |\n`;
440
- s += `| ${sp.base * 3}-${sp.base * 4}px | Wide: between sections |\n`;
441
- s += `| ${sp.base * 6}px+ | Vast: major section breaks |\n`;
442
- }
443
- s += '\n';
444
- // Border radius
445
- s += `### Border Radius\n\n`;
446
- const radii = profile.borderRadius.filter(r => !r.includes('9999'));
447
- if (radii.length > 0) {
448
- s += `Scale: \`${radii.join(', ')}\`\n`;
449
- s += `Default: \`${commonRadius}\`\n\n`;
450
- }
451
- else {
452
- s += `Default: \`${commonRadius}\`\n\n`;
453
- }
454
- // Container
455
- if (profile.containerMaxWidth) {
456
- s += `### Container\n\n`;
457
- s += `Max-width: \`${profile.containerMaxWidth}\`, centered with auto margins.\n\n`;
458
- }
459
- // Breakpoints
460
- if (profile.breakpoints.length > 0) {
461
- s += `### Breakpoints\n\n`;
462
- s += `| Name | Value |\n`;
463
- s += `|------|-------|\n`;
464
- for (const bp of profile.breakpoints) {
465
- s += `| ${bp.name} | ${bp.value} |\n`;
466
- }
467
- s += '\nMobile-first: design for small screens, layer on responsive overrides.\n\n';
468
- }
469
- return s;
470
- }
471
- function generateComponentPatterns(profile, bg, surface, accent, textPrimary, textMuted, border, commonRadius) {
472
- let s = '';
473
- const sp = profile.spacing;
474
- const hasColors = profile.colors.length > 0;
475
- // Helper: output a real hex color. When token is missing, derive a sensible
476
- // fallback from the actual extracted palette rather than an unresolved var() ref.
477
- const col = (token, role) => {
478
- if (token?.hex)
479
- return token.hex;
480
- if (role === 'text-muted') {
481
- // Derive muted text from text-primary dimmed to ~55%
482
- if (textPrimary?.hex) {
483
- const r = parseInt(textPrimary.hex.slice(1, 3), 16);
484
- const g = parseInt(textPrimary.hex.slice(3, 5), 16);
485
- const b = parseInt(textPrimary.hex.slice(5, 7), 16);
486
- const dim = (v) => Math.round(v * 0.55).toString(16).padStart(2, '0');
487
- return `#${dim(r)}${dim(g)}${dim(b)}`;
488
- }
489
- return profile.designTraits.isDark ? '#888888' : '#6b7280';
490
- }
491
- if (role === 'surface')
492
- return bg?.hex || (profile.designTraits.isDark ? '#1a1a1a' : '#f9fafb');
493
- return profile.designTraits.isDark ? '#444444' : '#cccccc';
494
- };
495
- // Card
496
- s += `### Card\n\n`;
497
- s += `\`\`\`css\n`;
498
- s += `.card {\n`;
499
- s += ` background: ${col(surface || bg, 'surface')};\n`;
500
- if (border)
501
- s += ` border: 1px solid ${border.hex};\n`;
502
- s += ` border-radius: ${commonRadius};\n`;
503
- s += ` padding: ${pickSpacing(sp, 4)}px;\n`;
504
- if (profile.designTraits.hasShadows && profile.shadows.length > 0) {
505
- const cardShadow = profile.shadows.find(s => s.level === 'raised') || profile.shadows[0];
506
- s += ` box-shadow: ${cardShadow.value};\n`;
507
- }
508
- s += `}\n`;
509
- s += `\`\`\`\n\n`;
510
- s += `\`\`\`html\n`;
511
- s += `<div class="card">\n`;
512
- s += ` <h3>Card Title</h3>\n`;
513
- s += ` <p>Card content goes here.</p>\n`;
514
- s += `</div>\n`;
515
- s += `\`\`\`\n\n`;
516
- // Button
517
- s += `### Button\n\n`;
518
- s += `\`\`\`css\n`;
519
- s += `/* Primary */\n`;
520
- s += `.btn-primary {\n`;
521
- s += ` background: ${col(accent, 'accent')};\n`;
522
- s += ` color: ${col(textPrimary, 'text-on-accent')};\n`;
523
- s += ` border-radius: ${commonRadius};\n`;
524
- s += ` padding: ${pickSpacing(sp, 2)}px ${pickSpacing(sp, 4)}px;\n`;
525
- s += ` font-weight: 500;\n`;
526
- s += ` transition: opacity 150ms ease;\n`;
527
- s += `}\n`;
528
- s += `.btn-primary:hover { opacity: 0.9; }\n\n`;
529
- s += `/* Ghost */\n`;
530
- s += `.btn-ghost {\n`;
531
- s += ` background: transparent;\n`;
532
- if (border)
533
- s += ` border: 1px solid ${border.hex};\n`;
534
- else
535
- s += ` border: 1px solid ${col(null, 'border')};\n`;
536
- s += ` color: ${col(textPrimary, 'text-primary')};\n`;
537
- s += ` border-radius: ${commonRadius};\n`;
538
- s += ` padding: ${pickSpacing(sp, 2)}px ${pickSpacing(sp, 4)}px;\n`;
539
- s += `}\n`;
540
- s += `\`\`\`\n\n`;
541
- s += `\`\`\`html\n`;
542
- s += `<button class="btn-primary">Get Started</button>\n`;
543
- s += `<button class="btn-ghost">Learn More</button>\n`;
544
- s += `\`\`\`\n\n`;
545
- // Input
546
- s += `### Input\n\n`;
547
- s += `\`\`\`css\n`;
548
- s += `.input {\n`;
549
- s += ` background: ${col(bg, 'background')};\n`;
550
- if (border)
551
- s += ` border: 1px solid ${border.hex};\n`;
552
- else
553
- s += ` border: 1px solid ${col(null, 'border')};\n`;
554
- s += ` border-radius: ${commonRadius};\n`;
555
- s += ` padding: ${pickSpacing(sp, 2)}px ${pickSpacing(sp, 3)}px;\n`;
556
- s += ` color: ${col(textPrimary, 'text-primary')};\n`;
557
- s += ` font-size: 14px;\n`;
558
- s += `}\n`;
559
- if (accent)
560
- s += `.input:focus { border-color: ${accent.hex}; outline: none; }\n`;
561
- else
562
- s += `.input:focus { border-color: var(--accent); outline: none; }\n`;
563
- s += `\`\`\`\n\n`;
564
- s += `\`\`\`html\n`;
565
- s += `<input class="input" type="text" placeholder="Search..." />\n`;
566
- s += `\`\`\`\n\n`;
567
- // Badge
568
- s += `### Badge / Chip\n\n`;
569
- s += `\`\`\`css\n`;
570
- s += `.badge {\n`;
571
- s += ` display: inline-flex;\n`;
572
- s += ` align-items: center;\n`;
573
- s += ` padding: ${pickSpacing(sp, 1)}px ${pickSpacing(sp, 2)}px;\n`;
574
- s += ` border-radius: 9999px;\n`;
575
- s += ` font-size: 12px;\n`;
576
- s += ` font-weight: 500;\n`;
577
- s += ` background: ${col(surface, 'surface')};\n`;
578
- s += ` color: ${col(textMuted, 'text-muted')};\n`;
579
- s += `}\n`;
580
- s += `\`\`\`\n\n`;
581
- s += `\`\`\`html\n`;
582
- s += `<span class="badge">New</span>\n`;
583
- s += `<span class="badge">Beta</span>\n`;
584
- s += `\`\`\`\n\n`;
585
- // Modal
586
- s += `### Modal / Dialog\n\n`;
587
- s += `\`\`\`css\n`;
588
- s += `.modal-backdrop { background: rgba(0, 0, 0, 0.6); }\n`;
589
- s += `.modal {\n`;
590
- s += ` background: ${col(surface || bg, 'surface')};\n`;
591
- if (border)
592
- s += ` border: 1px solid ${border.hex};\n`;
593
- s += ` border-radius: ${getRadius(profile, 'xl')};\n`;
594
- s += ` padding: ${pickSpacing(sp, 6)}px;\n`;
595
- s += ` max-width: 480px;\n`;
596
- s += ` width: 90vw;\n`;
597
- if (profile.designTraits.hasShadows && profile.shadows.length > 0) {
598
- const modalShadow = profile.shadows.find(s => s.level === 'overlay' || s.level === 'floating') || profile.shadows[profile.shadows.length - 1];
599
- s += ` box-shadow: ${modalShadow.value};\n`;
600
- }
601
- s += `}\n`;
602
- s += `\`\`\`\n\n`;
603
- s += `\`\`\`html\n`;
604
- s += `<div class="modal-backdrop">\n`;
605
- s += ` <div class="modal">\n`;
606
- s += ` <h2>Dialog Title</h2>\n`;
607
- s += ` <p>Dialog content.</p>\n`;
608
- s += ` <button class="btn-primary">Confirm</button>\n`;
609
- s += ` <button class="btn-ghost">Cancel</button>\n`;
610
- s += ` </div>\n`;
611
- s += `</div>\n`;
612
- s += `\`\`\`\n\n`;
613
- // Table
614
- s += `### Table\n\n`;
615
- s += `\`\`\`css\n`;
616
- s += `.table { width: 100%; border-collapse: collapse; }\n`;
617
- s += `.table th {\n`;
618
- s += ` text-align: left;\n`;
619
- s += ` padding: ${pickSpacing(sp, 2)}px ${pickSpacing(sp, 3)}px;\n`;
620
- s += ` font-weight: 500;\n`;
621
- s += ` font-size: 12px;\n`;
622
- s += ` color: ${col(textMuted, 'text-muted')};\n`;
623
- s += ` text-transform: uppercase;\n`;
624
- s += ` letter-spacing: 0.05em;\n`;
625
- if (border)
626
- s += ` border-bottom: 1px solid ${border.hex};\n`;
627
- else
628
- s += ` border-bottom: 1px solid ${col(null, 'border')};\n`;
629
- s += `}\n`;
630
- s += `.table td {\n`;
631
- s += ` padding: ${pickSpacing(sp, 3)}px;\n`;
632
- if (border)
633
- s += ` border-bottom: 1px solid ${border.hex};\n`;
634
- else
635
- s += ` border-bottom: 1px solid ${col(null, 'border')};\n`;
636
- s += `}\n`;
637
- s += `\`\`\`\n\n`;
638
- s += `\`\`\`html\n`;
639
- s += `<table class="table">\n`;
640
- s += ` <thead><tr><th>Name</th><th>Status</th><th>Date</th></tr></thead>\n`;
641
- s += ` <tbody>\n`;
642
- s += ` <tr><td>Item One</td><td>Active</td><td>Jan 1</td></tr>\n`;
643
- s += ` <tr><td>Item Two</td><td>Pending</td><td>Jan 2</td></tr>\n`;
644
- s += ` </tbody>\n`;
645
- s += `</table>\n`;
646
- s += `\`\`\`\n\n`;
647
- // Navigation
648
- s += `### Navigation\n\n`;
649
- s += `\`\`\`css\n`;
650
- s += `.nav {\n`;
651
- s += ` display: flex;\n`;
652
- s += ` align-items: center;\n`;
653
- s += ` gap: ${pickSpacing(sp, 2)}px;\n`;
654
- s += ` padding: ${pickSpacing(sp, 3)}px ${pickSpacing(sp, 4)}px;\n`;
655
- if (border)
656
- s += ` border-bottom: 1px solid ${border.hex};\n`;
657
- s += `}\n`;
658
- s += `.nav-link {\n`;
659
- s += ` color: ${col(textMuted, 'text-muted')};\n`;
660
- s += ` padding: ${pickSpacing(sp, 2)}px ${pickSpacing(sp, 3)}px;\n`;
661
- s += ` border-radius: ${commonRadius};\n`;
662
- s += ` transition: color 150ms;\n`;
663
- s += `}\n`;
664
- if (textPrimary)
665
- s += `.nav-link:hover { color: ${textPrimary.hex}; }\n`;
666
- if (accent)
667
- s += `.nav-link.active { color: ${accent.hex}; }\n`;
668
- s += `\`\`\`\n\n`;
669
- s += `\`\`\`html\n`;
670
- s += `<nav class="nav">\n`;
671
- s += ` <a href="/" class="nav-link active">Home</a>\n`;
672
- s += ` <a href="/about" class="nav-link">About</a>\n`;
673
- s += ` <a href="/pricing" class="nav-link">Pricing</a>\n`;
674
- s += ` <button class="btn-primary" style="margin-left: auto">Get Started</button>\n`;
675
- s += `</nav>\n`;
676
- s += `\`\`\`\n\n`;
677
- // If we have actual extracted components, show the top ones
678
- const significantComponents = profile.components
679
- .filter(c => c.variants.length > 0 || c.props.length > 3 || c.cssClasses.length > 5)
680
- .slice(0, 10);
681
- if (significantComponents.length > 0) {
682
- s += `### Extracted Components\n\n`;
683
- s += `These components were found in the codebase:\n\n`;
684
- for (const comp of significantComponents) {
685
- s += `**${comp.name}** (\`${comp.filePath}\`)\n`;
686
- if (comp.variants.length > 0)
687
- s += `- Variants: ${comp.variants.map(v => `\`${v}\``).join(', ')}\n`;
688
- if (comp.props.length > 0)
689
- s += `- Props: ${comp.props.slice(0, 6).map(p => `\`${p}\``).join(', ')}\n`;
690
- const keyStyles = getKeyStyles(comp);
691
- if (keyStyles.length > 0)
692
- s += `- Styles: ${keyStyles.join(', ')}\n`;
693
- s += '\n';
694
- }
695
- }
696
- return s;
697
- }
698
- function generateAnimationSection(profile) {
699
- let s = '';
700
- if (profile.designTraits.motionStyle === 'expressive') {
701
- s += `This project uses **expressive motion**. Animations are part of the design language.\n\n`;
702
- }
703
- else {
704
- s += `This project uses **subtle motion**. Transitions smooth state changes without calling attention.\n\n`;
705
- }
706
- // Framer Motion patterns
707
- const hasFramerMotion = profile.animations.some(a => a.type === 'framer-motion');
708
- if (hasFramerMotion) {
709
- s += `### Framer Motion\n\n`;
710
- s += `\`\`\`tsx\n`;
711
- s += `// Standard enter animation\n`;
712
- s += `<motion.div\n`;
713
- s += ` initial={{ opacity: 0, y: 8 }}\n`;
714
- s += ` animate={{ opacity: 1, y: 0 }}\n`;
715
- s += ` transition={{ duration: 0.3, ease: "easeOut" }}\n`;
716
- s += `/>\n\n`;
717
- s += `// List stagger\n`;
718
- s += `const container = { hidden: {}, show: { transition: { staggerChildren: 0.05 } } }\n`;
719
- s += `const item = { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0 } }\n`;
720
- s += `\`\`\`\n\n`;
721
- // Spring configs if detected
722
- const springs = profile.animations.filter(a => a.type === 'spring');
723
- if (springs.length > 0) {
724
- s += `### Spring Configs\n\n`;
725
- for (const sp of springs.slice(0, 3)) {
726
- s += `\`\`\`\n${sp.value}\n\`\`\`\n`;
727
- }
728
- s += '\n';
729
- }
730
- }
731
- // CSS keyframes
732
- const keyframes = profile.animations.filter(a => a.type === 'css-keyframe');
733
- if (keyframes.length > 0) {
734
- s += `### CSS Animations\n\n`;
735
- for (const kf of keyframes.slice(0, 5)) {
736
- s += `- \`${kf.name}\`\n`;
737
- }
738
- s += '\n';
739
- }
740
- // Motion tokens — real extracted values
741
- const mt = profile.motionTokens;
742
- if (mt.durations.length > 0 || mt.easings.length > 0) {
743
- s += `### Motion Tokens\n\n`;
744
- if (mt.durations.length > 0) {
745
- s += `- **Duration scale:** ${mt.durations.map(d => `\`${d}\``).join(', ')}\n`;
746
- }
747
- if (mt.easings.length > 0) {
748
- s += `- **Easing functions:** ${mt.easings.map(e => `\`${e}\``).join(', ')}\n`;
749
- }
750
- if (mt.properties.length > 0) {
751
- s += `- **Animated properties:** ${mt.properties.map(p => `\`${p}\``).join(', ')}\n`;
752
- }
753
- s += '\n';
754
- }
755
- s += `### Motion Guidelines\n\n`;
756
- if (mt.durations.length > 0) {
757
- s += `- **Duration:** Use values from the duration scale above. Short (${mt.durations[0]}) for micro-interactions, long (${mt.durations[mt.durations.length - 1]}) for page transitions\n`;
758
- }
759
- else {
760
- s += `- **Duration:** 150-300ms for micro-interactions, 300-500ms for page transitions\n`;
761
- }
762
- if (mt.easings.length > 0) {
763
- s += `- **Easing:** Use \`${mt.easings[0]}\` as the default easing curve\n`;
764
- }
765
- else {
766
- s += `- **Easing:** \`ease-out\` for enters, \`ease-in\` for exits\n`;
767
- }
768
- s += `- **Direction:** Elements enter from bottom/right, exit to top/left\n`;
769
- s += `- **Reduced motion:** Always respect \`prefers-reduced-motion\` — disable animations when set\n\n`;
770
- return s;
771
- }
772
- function generateDarkModeSection(profile) {
773
- let s = '';
774
- s += `This project supports **light and dark mode** via CSS variables.\n\n`;
775
- s += `### Token Mapping\n\n`;
776
- s += `| Variable | Light | Dark |\n`;
777
- s += `|----------|-------|------|\n`;
778
- for (const dmv of profile.darkModeVars.slice(0, 15)) {
779
- s += `| \`${dmv.variable}\` | \`${dmv.lightValue}\` | \`${dmv.darkValue}\` |\n`;
780
- }
781
- s += `\n### Implementation\n\n`;
782
- s += `- Toggle via \`.dark\` class on \`<html>\` or \`[data-theme="dark"]\`\n`;
783
- s += `- Always use CSS variables for colors — never hardcode hex values\n`;
784
- s += `- Test both modes for contrast and readability\n\n`;
785
- return s;
786
- }
787
- function generateElevationSection(profile, traits, surface, bg) {
788
- let s = '';
789
- if (!traits.hasShadows) {
790
- s += `This design uses **flat elevation** — no box-shadows anywhere.\n\n`;
791
- s += `### Elevation Strategy\n\n`;
792
- s += `| Level | Technique | Use |\n`;
793
- s += `|-------|-----------|-----|\n`;
794
- s += `| 0 — Base | Background color | Page background |\n`;
795
- s += `| 1 — Raised | Lighter surface + subtle border | Cards, panels |\n`;
796
- s += `| 2 — Floating | Even lighter surface + stronger border | Dropdowns, popovers |\n`;
797
- s += `| 3 — Overlay | Backdrop + modal surface | Modals, dialogs |\n\n`;
798
- }
799
- else {
800
- s += `### Shadow Tokens\n\n`;
801
- const levelNames = {
802
- flat: 'Subtle',
803
- raised: 'Raised (cards, buttons)',
804
- floating: 'Floating (dropdowns, popovers)',
805
- overlay: 'Overlay (modals, dialogs)',
806
- };
807
- for (const shadow of profile.shadows.slice(0, 6)) {
808
- const label = shadow.name ? `**${shadow.name}** (${levelNames[shadow.level] || shadow.level})` : levelNames[shadow.level] || shadow.level;
809
- s += `- ${label}: \`${shadow.value}\`\n`;
810
- }
811
- s += '\n';
812
- }
813
- // z-index scale
814
- if (profile.zIndexScale.length > 0) {
815
- s += `### Z-Index Scale\n\n`;
816
- s += `\`${profile.zIndexScale.join(', ')}\`\n\n`;
817
- s += `Use these exact values — never invent z-index values.\n\n`;
818
- }
819
- return s;
820
- }
821
- function generateAntiPatterns(profile, fonts, traits) {
822
- let s = '';
823
- // Core anti-patterns from traits
824
- if (!traits.hasShadows)
825
- s += `- **No box-shadow** on any element — use borders and surface colors for depth\n`;
826
- if (!traits.hasGradients)
827
- s += `- **No gradients** — solid colors only, everywhere\n`;
828
- if (profile.antiPatterns.includes('no-blur'))
829
- s += `- **No blur effects** — no backdrop-blur, no filter: blur()\n`;
830
- if (profile.antiPatterns.includes('no-zebra-striping'))
831
- s += `- **No zebra striping** — tables and lists use borders for separation\n`;
832
- // Color discipline
833
- s += `- **No invented colors** — every hex value must come from the palette above\n`;
834
- s += `- **No arbitrary spacing** — every dimension is a multiple of ${profile.spacing.base}px\n`;
835
- // Font discipline
836
- if (fonts.length > 0) {
837
- const monoFont = fonts.find(f => /mono|consolas|courier|fira code|jetbrains|sf mono|menlo/i.test(f));
838
- const dFonts = fonts.filter(f => !/mono|consolas|courier|fira code|jetbrains|sf mono|menlo/i.test(f));
839
- const fontList = [...dFonts, ...(monoFont ? [monoFont] : [])].join(' and ');
840
- s += `- **No extra fonts** — only ${fontList} are allowed\n`;
841
- }
842
- // Border radius discipline — only show px/rem values, cap at 10 entries
843
- const cleanRadii = profile.borderRadius
844
- .filter(r => !r.includes('9999') && !r.includes('%') && !/^(inherit|initial|unset|revert)$/.test(r))
845
- .filter(r => /^[\d.]+(px|rem|em)?$/.test(r.trim()))
846
- .slice(0, 10);
847
- if (cleanRadii.length > 0) {
848
- s += `- **No arbitrary border-radius** — use the scale: ${cleanRadii.join(', ')}\n`;
849
- }
850
- // Opacity anti-pattern
851
- s += `- **No opacity for disabled states** — use muted colors instead\n`;
852
- // If project has specific patterns
853
- if (!traits.hasRoundedFull) {
854
- s += `- **No pill shapes** — this design doesn't use rounded-full / 9999px radius\n`;
855
- }
856
- s += '\n';
857
- return s;
858
- }
859
- function generateWorkflow(profile, fonts) {
860
- let s = '';
861
- s += `1. **Read** \`references/DESIGN.md\` before writing any UI code\n`;
862
- s += `2. **Pick colors** from the Color System section — never invent new ones\n`;
863
- s += `3. **Set typography** — ${fonts.length > 0 ? fonts.join(', ') : 'project font'} only, using the type scale\n`;
864
- s += `4. **Build layout** on the ${profile.spacing.base}px grid — check every margin, padding, gap\n`;
865
- s += `5. **Match components** to patterns above before creating new ones\n`;
866
- s += `6. **Apply elevation** — ${profile.designTraits.hasShadows ? 'use shadow tokens' : 'flat, surface color shifts only'}\n`;
867
- s += `7. **Validate** — every value traces back to a design token. No magic numbers.\n\n`;
868
- return s;
869
- }
870
- function generateQuickReference(profile, bg, surface, accent, textPrimary, textMuted, border, primaryFont, commonRadius) {
871
- let s = '';
872
- s += `\`\`\`\n`;
873
- s += `Background: ${bg?.hex || '(not extracted)'}\n`;
874
- s += `Surface: ${surface?.hex || '(not extracted)'}\n`;
875
- s += `Text: ${textPrimary?.hex || '(not extracted)'} / ${textMuted?.hex || '(not extracted)'}\n`;
876
- s += `Accent: ${accent?.hex || '(not extracted)'}\n`;
877
- s += `Border: ${border?.hex || '(not extracted)'}\n`;
878
- s += `Font: ${primaryFont}\n`;
879
- s += `Spacing: ${profile.spacing.base}px grid\n`;
880
- s += `Radius: ${commonRadius}\n`;
881
- const frameworkList = profile.frameworks.map(f => f.name).join(', ');
882
- if (frameworkList)
883
- s += `Frameworks: ${frameworkList}\n`;
884
- if (profile.iconLibrary)
885
- s += `Icons: ${profile.iconLibrary}\n`;
886
- if (profile.stateLibrary)
887
- s += `State: ${profile.stateLibrary}\n`;
888
- s += `Components: ${profile.components.length} detected\n`;
889
- s += `\`\`\`\n\n`;
890
- return s;
891
- }
892
- // ── Helpers ────────────────────────────────────────────────────────────
893
- function getAestheticSummary(profile) {
894
- const traits = profile.designTraits;
895
- const fonts = [...new Set(profile.typography.map(t => t.fontFamily))].filter(f => f);
896
- const primaryFont = fonts[0] || 'sans-serif';
897
- const theme = traits.isDark ? 'Dark' : 'Light';
898
- const parts = [];
899
- parts.push(`${theme}-themed`);
900
- parts.push(`${traits.primaryColorTemp} palette`);
901
- parts.push(`${traits.fontStyle} typography (${primaryFont})`);
902
- parts.push(`${traits.density} density on a ${profile.spacing.base}px grid`);
903
- if (!traits.hasShadows)
904
- parts.push('flat elevation (no shadows)');
905
- if (traits.motionStyle === 'expressive')
906
- parts.push('expressive motion');
907
- return parts.join(', ') + '.';
908
- }
909
- function formatRole(role) {
910
- return role.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
911
- }
912
- function guessExtendedColorUsage(name, hex) {
913
- const n = name.toLowerCase();
914
- // Glow / ambient light effects
915
- if (/glow/.test(n))
916
- return 'Radial gradient depth layer for background glow effect';
917
- // Brand colors
918
- if (/brand/.test(n) && /orange|red|coral|primary/.test(n))
919
- return 'Brand color for logo, CTAs, and primary emphasis';
920
- if (/brand/.test(n))
921
- return 'Core brand color';
922
- // Gradient stops
923
- if (/gradient/.test(n))
924
- return 'Gradient stop for decorative background or accent';
925
- // Danger / destructive
926
- if (/danger|destructive|error/.test(n))
927
- return 'Destructive actions, error states';
928
- // Warning
929
- if (/warn/.test(n))
930
- return 'Warning banners, caution states';
931
- // Success / positive
932
- if (/success|positive|confirm/.test(n))
933
- return 'Confirmations, positive trend indicators';
934
- // Muted / secondary text
935
- if (/muted|secondary|subtle/.test(n))
936
- return 'Secondary text, placeholder text';
937
- // Overlay / scrim
938
- if (/overlay|scrim|backdrop/.test(n))
939
- return 'Modal/dialog backdrop overlay';
940
- // Analyze hex value if name gives no clue
941
- const r = parseInt(hex.slice(1, 3), 16);
942
- const g = parseInt(hex.slice(3, 5), 16);
943
- const b = parseInt(hex.slice(5, 7), 16);
944
- if (isNaN(r))
945
- return '';
946
- const max = Math.max(r, g, b) / 255;
947
- const min = Math.min(r, g, b) / 255;
948
- const lightness = (max + min) / 2;
949
- // Very dark colors — likely backgrounds or shadow layers
950
- if (lightness < 0.12)
951
- return 'Deep background layer or shadow color';
952
- // Very light colors — likely surface or highlight
953
- if (lightness > 0.88)
954
- return 'Light surface or highlight color';
955
- // Warm-red / orange tones
956
- if (r > 180 && g < 100 && b < 100)
957
- return 'Warm accent — hover glow or decorative highlight';
958
- return '';
959
- }
960
- function pickSpacing(spacing, multiplier) {
961
- const target = spacing.base * multiplier;
962
- const clamped = Math.min(target, 32);
963
- if (spacing.values.length > 0) {
964
- return spacing.values.reduce((prev, curr) => Math.abs(curr - clamped) < Math.abs(prev - clamped) ? curr : prev);
965
- }
966
- return clamped;
967
- }
968
- function getRadius(profile, size) {
969
- const radii = profile.borderRadius.filter(r => !r.includes('9999'));
970
- if (radii.length === 0)
971
- return '8px';
972
- switch (size) {
973
- case 'sm': return radii[0] || '4px';
974
- case 'md': return radii[Math.floor(radii.length / 3)] || '6px';
975
- case 'lg': return radii[Math.floor(radii.length * 2 / 3)] || '8px';
976
- case 'xl': return radii[radii.length - 1] || '12px';
977
- default: return radii[Math.floor(radii.length / 2)] || '8px';
978
- }
979
- }
980
- function getKeyStyles(comp) {
981
- const styles = [];
982
- const tp = comp.tailwindPatterns;
983
- if (tp.backgrounds.length > 0)
984
- styles.push(tp.backgrounds[0]);
985
- if (tp.borders.length > 0)
986
- styles.push(tp.borders[0]);
987
- if (tp.spacing.length > 0)
988
- styles.push(tp.spacing[0]);
989
- if (tp.typography.length > 0)
990
- styles.push(tp.typography[0]);
991
- if (tp.effects.length > 0)
992
- styles.push(tp.effects[0]);
993
- return styles.map(s => `\`${s}\``);
994
- }
995
- function createZip(sourceDir, outputPath) {
996
- return new Promise((resolve, reject) => {
997
- const output = fs.createWriteStream(outputPath);
998
- const archive = archiver.default('zip', { zlib: { level: 9 } });
999
- output.on('close', resolve);
1000
- archive.on('error', reject);
1001
- archive.pipe(output);
1002
- archive.directory(sourceDir, path.basename(sourceDir));
1003
- archive.finalize();
1004
- });
1005
- }
1006
- //# sourceMappingURL=skill.js.map