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.
- package/README.md +20 -15
- package/dist/cli.js +105073 -194
- package/package.json +15 -6
- package/dist/cli.d.ts +0 -3
- package/dist/extractors/components.d.ts +0 -11
- package/dist/extractors/components.js +0 -455
- package/dist/extractors/framework.d.ts +0 -4
- package/dist/extractors/framework.js +0 -126
- package/dist/extractors/tokens/computed.d.ts +0 -7
- package/dist/extractors/tokens/computed.js +0 -249
- package/dist/extractors/tokens/css.d.ts +0 -3
- package/dist/extractors/tokens/css.js +0 -510
- package/dist/extractors/tokens/http-css.d.ts +0 -14
- package/dist/extractors/tokens/http-css.js +0 -1689
- package/dist/extractors/tokens/tailwind.d.ts +0 -3
- package/dist/extractors/tokens/tailwind.js +0 -353
- package/dist/extractors/tokens/tokens-file.d.ts +0 -3
- package/dist/extractors/tokens/tokens-file.js +0 -229
- package/dist/extractors/ultra/animations.d.ts +0 -21
- package/dist/extractors/ultra/animations.js +0 -527
- package/dist/extractors/ultra/components-dom.d.ts +0 -13
- package/dist/extractors/ultra/components-dom.js +0 -149
- package/dist/extractors/ultra/interactions.d.ts +0 -14
- package/dist/extractors/ultra/interactions.js +0 -222
- package/dist/extractors/ultra/layout.d.ts +0 -14
- package/dist/extractors/ultra/layout.js +0 -123
- package/dist/extractors/ultra/pages.d.ts +0 -16
- package/dist/extractors/ultra/pages.js +0 -228
- package/dist/font-resolver.d.ts +0 -10
- package/dist/font-resolver.js +0 -280
- package/dist/modes/dir.d.ts +0 -6
- package/dist/modes/dir.js +0 -213
- package/dist/modes/repo.d.ts +0 -6
- package/dist/modes/repo.js +0 -76
- package/dist/modes/ultra.d.ts +0 -22
- package/dist/modes/ultra.js +0 -281
- package/dist/modes/url.d.ts +0 -14
- package/dist/modes/url.js +0 -161
- package/dist/normalizer.d.ts +0 -11
- package/dist/normalizer.js +0 -867
- package/dist/playwright-loader.d.ts +0 -10
- package/dist/playwright-loader.js +0 -71
- package/dist/screenshot.d.ts +0 -9
- package/dist/screenshot.js +0 -94
- package/dist/types-ultra.d.ts +0 -157
- package/dist/types-ultra.js +0 -4
- package/dist/types.d.ts +0 -182
- package/dist/types.js +0 -4
- package/dist/writers/animations-md.d.ts +0 -17
- package/dist/writers/animations-md.js +0 -313
- package/dist/writers/components-md.d.ts +0 -8
- package/dist/writers/components-md.js +0 -151
- package/dist/writers/design-md.d.ts +0 -7
- package/dist/writers/design-md.js +0 -704
- package/dist/writers/interactions-md.d.ts +0 -8
- package/dist/writers/interactions-md.js +0 -146
- package/dist/writers/layout-md.d.ts +0 -8
- package/dist/writers/layout-md.js +0 -120
- package/dist/writers/skill.d.ts +0 -12
- package/dist/writers/skill.js +0 -1006
- package/dist/writers/tokens-json.d.ts +0 -11
- package/dist/writers/tokens-json.js +0 -164
package/dist/writers/skill.js
DELETED
|
@@ -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 += `\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 += `\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 += `\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
|