design-brain-memory 0.9.2 → 0.9.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 +273 -0
- package/dist/agentBrowser.js +1 -1
- package/dist/agentBrowser.js.map +1 -1
- package/dist/aggregate.d.ts +9 -0
- package/dist/aggregate.js +53 -0
- package/dist/aggregate.js.map +1 -0
- package/dist/batch.d.ts +16 -0
- package/dist/batch.js +44 -0
- package/dist/batch.js.map +1 -0
- package/dist/cli.js +250 -216
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +60 -1
- package/dist/commands.js +323 -10
- package/dist/commands.js.map +1 -1
- package/dist/compare.d.ts +33 -0
- package/dist/compare.js +83 -0
- package/dist/compare.js.map +1 -0
- package/dist/componentGraph.d.ts +22 -0
- package/dist/componentGraph.js +106 -0
- package/dist/componentGraph.js.map +1 -0
- package/dist/contextLayer.d.ts +12 -0
- package/dist/contextLayer.js +263 -0
- package/dist/contextLayer.js.map +1 -0
- package/dist/cssInJs.d.ts +9 -0
- package/dist/cssInJs.js +124 -0
- package/dist/cssInJs.js.map +1 -0
- package/dist/extractFromUrl.d.ts +8 -0
- package/dist/extractFromUrl.js +104 -355
- package/dist/extractFromUrl.js.map +1 -1
- package/dist/graphView.d.ts +21 -0
- package/dist/graphView.js +492 -0
- package/dist/graphView.js.map +1 -0
- package/dist/index.d.ts +26 -11
- package/dist/index.js +17 -10
- package/dist/index.js.map +1 -1
- package/dist/knowledge.d.ts +20 -0
- package/dist/knowledge.js +208 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/liveView.d.ts +15 -0
- package/dist/liveView.js +475 -0
- package/dist/liveView.js.map +1 -0
- package/dist/llm.js +1 -9
- package/dist/llm.js.map +1 -1
- package/dist/moodboard.d.ts +3 -0
- package/dist/moodboard.js +152 -0
- package/dist/moodboard.js.map +1 -0
- package/dist/persona.d.ts +2 -2
- package/dist/persona.js +82 -210
- package/dist/persona.js.map +1 -1
- package/dist/query.js +1 -6
- package/dist/query.js.map +1 -1
- package/dist/render.d.ts +2 -10
- package/dist/render.js +80 -175
- package/dist/render.js.map +1 -1
- package/dist/reviewChecklist.d.ts +17 -0
- package/dist/reviewChecklist.js +126 -0
- package/dist/reviewChecklist.js.map +1 -0
- package/dist/scan.d.ts +19 -7
- package/dist/scan.js +132 -374
- package/dist/scan.js.map +1 -1
- package/dist/scorecard.d.ts +53 -0
- package/dist/scorecard.js +325 -0
- package/dist/scorecard.js.map +1 -0
- package/dist/skillPrompt.d.ts +1 -3
- package/dist/skillPrompt.js +22 -148
- package/dist/skillPrompt.js.map +1 -1
- package/dist/store.d.ts +2 -2
- package/dist/store.js +7 -9
- package/dist/store.js.map +1 -1
- package/dist/styleDictionary.d.ts +16 -0
- package/dist/styleDictionary.js +89 -0
- package/dist/styleDictionary.js.map +1 -0
- package/dist/svg.d.ts +5 -0
- package/dist/svg.js +162 -0
- package/dist/svg.js.map +1 -0
- package/dist/systemDiff.d.ts +28 -0
- package/dist/systemDiff.js +107 -0
- package/dist/systemDiff.js.map +1 -0
- package/dist/tailwind.d.ts +2 -0
- package/dist/tailwind.js +122 -0
- package/dist/tailwind.js.map +1 -0
- package/dist/taste.d.ts +3 -2
- package/dist/taste.js +349 -536
- package/dist/taste.js.map +1 -1
- package/dist/tasteRenderer.d.ts +2 -3
- package/dist/tasteRenderer.js +123 -119
- package/dist/tasteRenderer.js.map +1 -1
- package/dist/tokenNaming.d.ts +5 -0
- package/dist/tokenNaming.js +229 -0
- package/dist/tokenNaming.js.map +1 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.js +44 -0
- package/dist/tokens.js.map +1 -0
- package/dist/trends.d.ts +12 -0
- package/dist/trends.js +178 -0
- package/dist/trends.js.map +1 -0
- package/dist/types.d.ts +47 -101
- package/dist/wiki.d.ts +10 -0
- package/dist/wiki.js +346 -0
- package/dist/wiki.js.map +1 -0
- package/dist/writingStyle.d.ts +38 -0
- package/dist/writingStyle.js +224 -0
- package/dist/writingStyle.js.map +1 -0
- package/package.json +5 -4
- package/dist/classify.d.ts +0 -21
- package/dist/classify.js +0 -205
- package/dist/classify.js.map +0 -1
- package/dist/scanRenderer.d.ts +0 -2
- package/dist/scanRenderer.js +0 -155
- package/dist/scanRenderer.js.map +0 -1
- package/dist/tasteDiff.d.ts +0 -19
- package/dist/tasteDiff.js +0 -340
- package/dist/tasteDiff.js.map +0 -1
- package/dist/tasteGenerate.d.ts +0 -12
- package/dist/tasteGenerate.js +0 -140
- package/dist/tasteGenerate.js.map +0 -1
- package/dist/tasteRefine.d.ts +0 -13
- package/dist/tasteRefine.js +0 -351
- package/dist/tasteRefine.js.map +0 -1
- package/dist/theatrical.d.ts +0 -5
- package/dist/theatrical.js +0 -258
- package/dist/theatrical.js.map +0 -1
- package/skills/SKILL.md +0 -36
- package/skills/design-brain/SKILL.md +0 -77
package/dist/taste.js
CHANGED
|
@@ -1,598 +1,411 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { ingestInspiration } from './commands.js';
|
|
4
|
-
import { captureDesignFromUrl } from './extractFromUrl.js';
|
|
5
|
-
import { runDesignLlm } from './llm.js';
|
|
1
|
+
import { aggregateColors, aggregateTypography, aggregateComponents, aggregateMotion } from './aggregate.js';
|
|
2
|
+
import { designAnalysisToScanTokens, computeScore, looksLikeUrl, normalizeToUrl, hueFromHex, saturationFromHex, lightnessFromHex } from './scan.js';
|
|
6
3
|
import { assignPersona } from './persona.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import { nowIso, unique } from './util.js';
|
|
13
|
-
function asAnimationToken(token) {
|
|
14
|
-
return 'library' in token && 'motionIntent' in token;
|
|
15
|
-
}
|
|
16
|
-
function parseDurationMs(value) {
|
|
17
|
-
const matches = value.match(/(\d+(?:\.\d+)?)\s*(ms|s)\b/gi) ?? [];
|
|
18
|
-
return matches
|
|
19
|
-
.map((match) => {
|
|
20
|
-
const parsed = match.match(/(\d+(?:\.\d+)?)\s*(ms|s)\b/i);
|
|
21
|
-
if (!parsed) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
const numeric = Number.parseFloat(parsed[1]);
|
|
25
|
-
if (!Number.isFinite(numeric)) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
return parsed[2].toLowerCase() === 's' ? numeric * 1000 : numeric;
|
|
29
|
-
})
|
|
30
|
-
.filter((value) => value !== null);
|
|
31
|
-
}
|
|
32
|
-
function parseNumericValues(value) {
|
|
33
|
-
if (!value) {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
36
|
-
return (value.match(/-?\d+(?:\.\d+)?/g) ?? [])
|
|
37
|
-
.map((entry) => Number.parseFloat(entry))
|
|
38
|
-
.filter((entry) => Number.isFinite(entry));
|
|
39
|
-
}
|
|
40
|
-
function parseFontSizeToPx(value) {
|
|
41
|
-
const text = value.trim().toLowerCase();
|
|
42
|
-
const match = text.match(/^(\d+(?:\.\d+)?)(px|rem|em)$/);
|
|
43
|
-
if (!match) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
const numeric = Number.parseFloat(match[1]);
|
|
47
|
-
if (!Number.isFinite(numeric)) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
if (match[2] === 'px') {
|
|
51
|
-
return numeric;
|
|
52
|
-
}
|
|
53
|
-
return numeric * 16;
|
|
54
|
-
}
|
|
55
|
-
function parseWeightValue(weight) {
|
|
56
|
-
const normalized = weight.trim().toLowerCase();
|
|
57
|
-
if (/^\d+$/.test(normalized)) {
|
|
58
|
-
return Number.parseInt(normalized, 10);
|
|
59
|
-
}
|
|
60
|
-
const map = {
|
|
61
|
-
normal: 400,
|
|
62
|
-
medium: 500,
|
|
63
|
-
semibold: 600,
|
|
64
|
-
bold: 700,
|
|
65
|
-
};
|
|
66
|
-
return map[normalized] ?? null;
|
|
67
|
-
}
|
|
68
|
-
function formatDuration(value) {
|
|
69
|
-
return `${Math.round(value)}ms`;
|
|
70
|
-
}
|
|
71
|
-
function resolveColorRole(index) {
|
|
72
|
-
const roles = ['primary', 'background', 'accent', 'text', 'surface'];
|
|
73
|
-
return roles[index] ?? `color-${index + 1}`;
|
|
74
|
-
}
|
|
75
|
-
function sourceForColor(hex, inspirations) {
|
|
76
|
-
const normalized = hex.toUpperCase();
|
|
77
|
-
let bestSource = 'aggregate';
|
|
78
|
-
let bestCount = -1;
|
|
79
|
-
for (const inspiration of inspirations) {
|
|
80
|
-
const match = inspiration.analysis.colors.find((color) => color.hex.toUpperCase() === normalized);
|
|
81
|
-
if (match && match.count > bestCount) {
|
|
82
|
-
bestCount = match.count;
|
|
83
|
-
bestSource = inspiration.url
|
|
84
|
-
? (() => {
|
|
85
|
-
try {
|
|
86
|
-
return new URL(inspiration.url).hostname;
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return inspiration.url;
|
|
90
|
-
}
|
|
91
|
-
})()
|
|
92
|
-
: inspiration.id;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return bestSource;
|
|
96
|
-
}
|
|
97
|
-
function hexToRgb(hex) {
|
|
98
|
-
const clean = hex.replace('#', '').trim();
|
|
99
|
-
if (!/^[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test(clean)) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
const full = clean.length === 3
|
|
103
|
-
? `${clean[0]}${clean[0]}${clean[1]}${clean[1]}${clean[2]}${clean[2]}`
|
|
104
|
-
: clean;
|
|
105
|
-
return {
|
|
106
|
-
r: Number.parseInt(full.slice(0, 2), 16),
|
|
107
|
-
g: Number.parseInt(full.slice(2, 4), 16),
|
|
108
|
-
b: Number.parseInt(full.slice(4, 6), 16),
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function rgbToHsl(rgb) {
|
|
112
|
-
const r = rgb.r / 255;
|
|
113
|
-
const g = rgb.g / 255;
|
|
114
|
-
const b = rgb.b / 255;
|
|
115
|
-
const max = Math.max(r, g, b);
|
|
116
|
-
const min = Math.min(r, g, b);
|
|
117
|
-
const l = (max + min) / 2;
|
|
118
|
-
if (max === min) {
|
|
119
|
-
return { h: 0, s: 0, l };
|
|
120
|
-
}
|
|
121
|
-
const d = max - min;
|
|
122
|
-
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
123
|
-
let h = 0;
|
|
124
|
-
if (max === r) {
|
|
125
|
-
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
126
|
-
}
|
|
127
|
-
else if (max === g) {
|
|
128
|
-
h = ((b - r) / d + 2) / 6;
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
h = ((r - g) / d + 4) / 6;
|
|
132
|
-
}
|
|
133
|
-
return { h: h * 360, s, l };
|
|
134
|
-
}
|
|
135
|
-
function hueDelta(a, b) {
|
|
136
|
-
const direct = Math.abs(a - b);
|
|
137
|
-
return Math.min(direct, 360 - direct);
|
|
138
|
-
}
|
|
4
|
+
import { loadDatabase, ensureProject, saveTasteProfile, loadTasteProfile } from './store.js';
|
|
5
|
+
import { enrichWithLlm } from './llm.js';
|
|
6
|
+
import { ingestInspiration } from './commands.js';
|
|
7
|
+
import { nowIso } from './util.js';
|
|
8
|
+
// ── Derive functions ───────────────────────────────────────
|
|
139
9
|
function classifyHarmony(hues) {
|
|
140
|
-
if (hues.length <= 1)
|
|
10
|
+
if (hues.length <= 1)
|
|
141
11
|
return 'monochromatic';
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
for (let i =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (averageDelta <= 45) {
|
|
12
|
+
const sorted = [...hues].sort((a, b) => a - b);
|
|
13
|
+
const gaps = [];
|
|
14
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
15
|
+
gaps.push(sorted[i] - sorted[i - 1]);
|
|
16
|
+
}
|
|
17
|
+
gaps.push(360 - sorted[sorted.length - 1] + sorted[0]);
|
|
18
|
+
const maxGap = Math.max(...gaps);
|
|
19
|
+
const minGap = Math.min(...gaps);
|
|
20
|
+
const avgGap = 360 / hues.length;
|
|
21
|
+
const spread = maxGap - minGap;
|
|
22
|
+
if (spread < 40)
|
|
154
23
|
return 'analogous';
|
|
155
|
-
|
|
156
|
-
|
|
24
|
+
if (hues.length === 2 && Math.abs(gaps[0] - 180) < 30)
|
|
25
|
+
return 'complementary';
|
|
26
|
+
if (hues.length === 3 && Math.abs(avgGap - 120) < 25)
|
|
27
|
+
return 'triadic';
|
|
28
|
+
if (maxGap > 200)
|
|
29
|
+
return 'split-complementary';
|
|
30
|
+
return 'analogous';
|
|
157
31
|
}
|
|
158
32
|
function deriveColorPreference(colors, inspirations) {
|
|
159
|
-
const palette =
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
33
|
+
const palette = [];
|
|
34
|
+
const roles = ['primary', 'secondary', 'accent', 'background', 'text', 'surface'];
|
|
35
|
+
for (let i = 0; i < Math.min(colors.length, roles.length); i++) {
|
|
36
|
+
const color = colors[i];
|
|
37
|
+
const source = inspirations.find((ins) => ins.analysis.colors.some((c) => c.hex.toUpperCase() === color.hex.toUpperCase()));
|
|
38
|
+
palette.push({
|
|
39
|
+
hex: color.hex,
|
|
40
|
+
role: roles[i],
|
|
41
|
+
source: source?.url ?? source?.name ?? 'unknown',
|
|
42
|
+
});
|
|
168
43
|
}
|
|
169
|
-
const
|
|
170
|
-
.
|
|
171
|
-
.
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
44
|
+
const validHexes = colors
|
|
45
|
+
.filter((c) => c.hex.length === 7)
|
|
46
|
+
.slice(0, 12);
|
|
47
|
+
const hues = [...new Set(validHexes.map((c) => Math.round(hueFromHex(c.hex) / 30) * 30))];
|
|
48
|
+
const saturations = validHexes.map((c) => saturationFromHex(c.hex));
|
|
49
|
+
const lightnesses = validHexes.map((c) => lightnessFromHex(c.hex));
|
|
50
|
+
const avgSat = saturations.length > 0
|
|
51
|
+
? saturations.reduce((a, b) => a + b, 0) / saturations.length
|
|
52
|
+
: 0.5;
|
|
53
|
+
const avgLight = lightnesses.length > 0
|
|
54
|
+
? lightnesses.reduce((a, b) => a + b, 0) / lightnesses.length
|
|
55
|
+
: 0.5;
|
|
56
|
+
const allHues = validHexes.map((c) => hueFromHex(c.hex));
|
|
57
|
+
const minHue = allHues.length > 0 ? Math.min(...allHues) : 0;
|
|
58
|
+
const maxHue = allHues.length > 0 ? Math.max(...allHues) : 360;
|
|
182
59
|
return {
|
|
183
60
|
palette,
|
|
184
61
|
harmony: classifyHarmony(hues),
|
|
185
|
-
hueRange,
|
|
186
|
-
saturationBias: avgSat
|
|
187
|
-
lightnessBias: avgLight
|
|
62
|
+
hueRange: { min: Math.round(minHue), max: Math.round(maxHue) },
|
|
63
|
+
saturationBias: avgSat > 0.6 ? 'vibrant' : avgSat < 0.3 ? 'muted' : 'neutral',
|
|
64
|
+
lightnessBias: avgLight > 0.65 ? 'light' : avgLight < 0.35 ? 'dark' : 'balanced',
|
|
188
65
|
};
|
|
189
66
|
}
|
|
190
67
|
function deriveTypographyPreference(typography) {
|
|
191
|
-
const
|
|
192
|
-
for (const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
.map((
|
|
202
|
-
.filter((
|
|
203
|
-
const sizes = sizeSet.sort((a, b) => {
|
|
204
|
-
const pa = parseFontSizeToPx(a);
|
|
205
|
-
const pb = parseFontSizeToPx(b);
|
|
206
|
-
if (pa === null && pb === null) {
|
|
207
|
-
return a.localeCompare(b);
|
|
208
|
-
}
|
|
209
|
-
if (pa === null) {
|
|
210
|
-
return 1;
|
|
211
|
-
}
|
|
212
|
-
if (pb === null) {
|
|
213
|
-
return -1;
|
|
214
|
-
}
|
|
215
|
-
return pa - pb;
|
|
216
|
-
});
|
|
217
|
-
const pxSizes = sizes
|
|
218
|
-
.map(parseFontSizeToPx)
|
|
219
|
-
.filter((value) => value !== null)
|
|
68
|
+
const familyCount = new Map();
|
|
69
|
+
for (const t of typography) {
|
|
70
|
+
const fam = t.fontFamily.toLowerCase();
|
|
71
|
+
familyCount.set(fam, (familyCount.get(fam) ?? 0) + t.count);
|
|
72
|
+
}
|
|
73
|
+
const sorted = [...familyCount.entries()].sort((a, b) => b[1] - a[1]);
|
|
74
|
+
const primaryFont = sorted[0]?.[0] ?? 'system-ui';
|
|
75
|
+
const secondaryFont = sorted[1]?.[0] ?? null;
|
|
76
|
+
const sizes = [...new Set(typography.map((t) => t.fontSize))];
|
|
77
|
+
const numericSizes = sizes
|
|
78
|
+
.map((s) => parseFloat(s))
|
|
79
|
+
.filter((n) => !isNaN(n))
|
|
220
80
|
.sort((a, b) => a - b);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
81
|
+
let scaleType = 'custom';
|
|
82
|
+
if (numericSizes.length >= 3) {
|
|
83
|
+
const ratios = [];
|
|
84
|
+
for (let i = 1; i < numericSizes.length; i++) {
|
|
85
|
+
ratios.push(numericSizes[i] / numericSizes[i - 1]);
|
|
225
86
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const minWeight = numericWeights[0]?.toString() ?? weightValues[0] ?? '400';
|
|
241
|
-
const maxWeight = numericWeights[numericWeights.length - 1]?.toString() ?? weightValues.at(-1) ?? '700';
|
|
87
|
+
const avgRatio = ratios.reduce((a, b) => a + b, 0) / ratios.length;
|
|
88
|
+
if (Math.abs(avgRatio - 1.25) < 0.1)
|
|
89
|
+
scaleType = 'major-third';
|
|
90
|
+
else if (Math.abs(avgRatio - 1.333) < 0.1)
|
|
91
|
+
scaleType = 'perfect-fourth';
|
|
92
|
+
else if (Math.abs(avgRatio - 1.5) < 0.1)
|
|
93
|
+
scaleType = 'perfect-fifth';
|
|
94
|
+
else if (Math.abs(avgRatio - 1.618) < 0.15)
|
|
95
|
+
scaleType = 'golden-ratio';
|
|
96
|
+
else
|
|
97
|
+
scaleType = 'linear';
|
|
98
|
+
}
|
|
99
|
+
const weights = typography.map((t) => t.fontWeight).filter((w) => w !== 'unknown');
|
|
100
|
+
const numericWeights = weights.map((w) => parseInt(w, 10)).filter((n) => !isNaN(n)).sort((a, b) => a - b);
|
|
242
101
|
return {
|
|
243
102
|
primaryFont,
|
|
244
103
|
secondaryFont,
|
|
245
|
-
scaleType
|
|
246
|
-
sizes,
|
|
104
|
+
scaleType,
|
|
105
|
+
sizes: sizes.slice(0, 8),
|
|
247
106
|
weightRange: {
|
|
248
|
-
min:
|
|
249
|
-
max:
|
|
107
|
+
min: numericWeights[0]?.toString() ?? '400',
|
|
108
|
+
max: numericWeights[numericWeights.length - 1]?.toString() ?? '700',
|
|
250
109
|
},
|
|
251
110
|
};
|
|
252
111
|
}
|
|
253
112
|
function deriveSpacingPreference(components) {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
113
|
+
const spacingProps = [
|
|
114
|
+
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
|
115
|
+
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
|
116
|
+
'gap', 'row-gap', 'column-gap',
|
|
117
|
+
];
|
|
118
|
+
const values = [];
|
|
119
|
+
for (const comp of components) {
|
|
120
|
+
for (const prop of spacingProps) {
|
|
121
|
+
const val = comp.styles[prop];
|
|
122
|
+
if (!val)
|
|
123
|
+
continue;
|
|
124
|
+
const nums = val.match(/(\d+(?:\.\d+)?)/g);
|
|
125
|
+
if (nums) {
|
|
126
|
+
for (const n of nums) {
|
|
127
|
+
const parsed = parseFloat(n);
|
|
128
|
+
if (parsed > 0 && parsed < 200)
|
|
129
|
+
values.push(parsed);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (values.length === 0) {
|
|
135
|
+
return { baseUnit: 8, scale: [4, 8, 12, 16, 24, 32, 48, 64], gridAlignmentRatio: 0 };
|
|
136
|
+
}
|
|
137
|
+
const mod4 = values.filter((v) => v % 4 === 0).length;
|
|
138
|
+
const mod8 = values.filter((v) => v % 8 === 0).length;
|
|
139
|
+
const baseUnit = mod8 / values.length > 0.6 ? 8 : 4;
|
|
140
|
+
const uniqueValues = [...new Set(values)].sort((a, b) => a - b);
|
|
141
|
+
const scale = uniqueValues
|
|
142
|
+
.filter((v) => v % baseUnit === 0)
|
|
143
|
+
.slice(0, 10);
|
|
267
144
|
const gridAlignmentRatio = values.length > 0
|
|
268
|
-
?
|
|
269
|
-
:
|
|
270
|
-
const scale = unique(values.map((value) => Math.round(value)))
|
|
271
|
-
.sort((a, b) => a - b)
|
|
272
|
-
.slice(0, 12);
|
|
273
|
-
const fallbackScale = baseUnit === 8
|
|
274
|
-
? [4, 8, 12, 16, 24, 32, 48, 64]
|
|
275
|
-
: [4, 8, 12, 16, 20, 24, 32, 40];
|
|
145
|
+
? Math.round((mod4 / values.length) * 100) / 100
|
|
146
|
+
: 0;
|
|
276
147
|
return {
|
|
277
148
|
baseUnit,
|
|
278
|
-
scale: scale.length > 0 ? scale :
|
|
279
|
-
gridAlignmentRatio
|
|
149
|
+
scale: scale.length > 0 ? scale : [4, 8, 12, 16, 24, 32, 48, 64],
|
|
150
|
+
gridAlignmentRatio,
|
|
280
151
|
};
|
|
281
152
|
}
|
|
282
153
|
function deriveMotionPreference(motion) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
154
|
+
if (motion.length === 0) {
|
|
155
|
+
return { easing: 'ease', durations: [], intensity: 'none' };
|
|
156
|
+
}
|
|
157
|
+
const easingCount = new Map();
|
|
158
|
+
const durationCount = new Map();
|
|
159
|
+
for (const m of motion) {
|
|
160
|
+
// Extract easing from raw string first (structured fields may have split cubic-bezier on commas)
|
|
161
|
+
let easingFromRaw = null;
|
|
162
|
+
if (m.transition && m.transition !== 'none' && m.transition !== 'all') {
|
|
163
|
+
const cubicMatch = m.transition.match(/cubic-bezier\([^)]+\)/);
|
|
164
|
+
if (cubicMatch) {
|
|
165
|
+
easingFromRaw = cubicMatch[0];
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const keyword = m.transition.match(/\b(ease-in-out|ease-in|ease-out|ease|linear)\b/);
|
|
169
|
+
if (keyword)
|
|
170
|
+
easingFromRaw = keyword[0];
|
|
293
171
|
}
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
for (const duration of parseDurationMs(token.transition)) {
|
|
297
|
-
durations.push(duration);
|
|
298
172
|
}
|
|
299
|
-
|
|
300
|
-
|
|
173
|
+
if (easingFromRaw) {
|
|
174
|
+
easingCount.set(easingFromRaw, (easingCount.get(easingFromRaw) ?? 0) + m.count);
|
|
301
175
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
176
|
+
else {
|
|
177
|
+
// Fall back to structured fields only if raw string didn't yield a result
|
|
178
|
+
for (const t of m.transitions ?? []) {
|
|
179
|
+
if (t.timingFunction && t.timingFunction.length > 3) {
|
|
180
|
+
easingCount.set(t.timingFunction, (easingCount.get(t.timingFunction) ?? 0) + m.count);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const a of m.animations ?? []) {
|
|
184
|
+
if (a.timingFunction && a.timingFunction.length > 3) {
|
|
185
|
+
easingCount.set(a.timingFunction, (easingCount.get(a.timingFunction) ?? 0) + m.count);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
305
188
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
? 'none'
|
|
313
|
-
: count <= 12
|
|
314
|
-
? 'subtle'
|
|
315
|
-
: count <= 40
|
|
316
|
-
? 'moderate'
|
|
317
|
-
: 'expressive';
|
|
318
|
-
return {
|
|
319
|
-
easing: dominantEasing,
|
|
320
|
-
durations: durationLabels.length > 0 ? durationLabels : ['200ms'],
|
|
321
|
-
intensity,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
function deriveComponentPreference(components) {
|
|
325
|
-
const radiusValues = components
|
|
326
|
-
.flatMap((component) => parseNumericValues(component.styles.borderRadius))
|
|
327
|
-
.filter((value) => value >= 0)
|
|
328
|
-
.sort((a, b) => a - b);
|
|
329
|
-
const medianRadius = radiusValues.length > 0
|
|
330
|
-
? radiusValues[Math.floor(radiusValues.length / 2)]
|
|
331
|
-
: 8;
|
|
332
|
-
const shadows = components
|
|
333
|
-
.map((component) => component.styles.boxShadow?.trim())
|
|
334
|
-
.filter((shadow) => Boolean(shadow && shadow !== 'none'));
|
|
335
|
-
const noneCount = components.length - shadows.length;
|
|
336
|
-
const shadowStyle = shadows.length === 0
|
|
337
|
-
? 'none'
|
|
338
|
-
: shadows.length <= noneCount
|
|
339
|
-
? 'subtle'
|
|
340
|
-
: shadows.some((shadow) => /(?:\b2\dpx\b|\b3\dpx\b|\b4\dpx\b|rgba?\([^)]*,\s*0\.[45-9])/i.test(shadow))
|
|
341
|
-
? 'dramatic'
|
|
342
|
-
: 'elevated';
|
|
343
|
-
return {
|
|
344
|
-
cherryPicks: [],
|
|
345
|
-
borderRadius: `${Math.round(medianRadius)}px`,
|
|
346
|
-
shadowStyle,
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
function buildAggregateTokens(inspirations) {
|
|
350
|
-
const colors = aggregateColors(inspirations);
|
|
351
|
-
const typography = aggregateTypography(inspirations);
|
|
352
|
-
const components = aggregateComponents(inspirations);
|
|
353
|
-
const motion = aggregateMotion(inspirations);
|
|
354
|
-
const transitionValues = motion.flatMap((token) => {
|
|
355
|
-
if (asAnimationToken(token)) {
|
|
356
|
-
if (!token.timing) {
|
|
357
|
-
return [];
|
|
189
|
+
// Durations: prefer structured, fall back to raw
|
|
190
|
+
let gotDuration = false;
|
|
191
|
+
for (const t of m.transitions ?? []) {
|
|
192
|
+
if (t.duration && t.duration !== '0s') {
|
|
193
|
+
durationCount.set(t.duration, (durationCount.get(t.duration) ?? 0) + m.count);
|
|
194
|
+
gotDuration = true;
|
|
358
195
|
}
|
|
359
|
-
return [`${token.motionIntent} ${token.timing.duration}ms ${token.timing.easing}`];
|
|
360
196
|
}
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
197
|
+
for (const a of m.animations ?? []) {
|
|
198
|
+
if (a.duration && a.duration !== '0s') {
|
|
199
|
+
durationCount.set(a.duration, (durationCount.get(a.duration) ?? 0) + m.count);
|
|
200
|
+
gotDuration = true;
|
|
201
|
+
}
|
|
364
202
|
}
|
|
365
|
-
if (
|
|
366
|
-
|
|
203
|
+
if (!gotDuration && m.transition && m.transition !== 'none' && m.transition !== 'all') {
|
|
204
|
+
const durMatch = m.transition.match(/([\d.]+)(m?s)\b/);
|
|
205
|
+
if (durMatch) {
|
|
206
|
+
durationCount.set(`${durMatch[1]}${durMatch[2]}`, (durationCount.get(`${durMatch[1]}${durMatch[2]}`) ?? 0) + m.count);
|
|
207
|
+
}
|
|
367
208
|
}
|
|
368
|
-
return values;
|
|
369
|
-
});
|
|
370
|
-
const spacingValues = components.flatMap((component) => [
|
|
371
|
-
component.styles.padding,
|
|
372
|
-
component.styles.margin,
|
|
373
|
-
component.styles.gap,
|
|
374
|
-
component.styles.rowGap,
|
|
375
|
-
component.styles.columnGap,
|
|
376
|
-
].filter(Boolean));
|
|
377
|
-
const cssVariableCount = inspirations.reduce((count, inspiration) => (count + Object.keys(inspiration.analysis.cssVariables).length), 0);
|
|
378
|
-
return {
|
|
379
|
-
colors: colors.map((color) => color.hex),
|
|
380
|
-
fontFamilies: unique(typography.map((token) => token.fontFamily.toLowerCase())),
|
|
381
|
-
fontSizes: unique(typography.map((token) => token.fontSize)),
|
|
382
|
-
transitions: transitionValues,
|
|
383
|
-
spacingValues,
|
|
384
|
-
cssVariableCount,
|
|
385
|
-
framework: null,
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
function extractFirstJsonObject(raw) {
|
|
389
|
-
const trimmed = raw.trim();
|
|
390
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
391
|
-
return trimmed;
|
|
392
|
-
}
|
|
393
|
-
const start = raw.indexOf('{');
|
|
394
|
-
const end = raw.lastIndexOf('}');
|
|
395
|
-
if (start >= 0 && end > start) {
|
|
396
|
-
return raw.slice(start, end + 1);
|
|
397
209
|
}
|
|
398
|
-
|
|
210
|
+
const topEasing = [...easingCount.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 'ease';
|
|
211
|
+
const durations = [...durationCount.entries()]
|
|
212
|
+
.sort((a, b) => b[1] - a[1])
|
|
213
|
+
.slice(0, 4)
|
|
214
|
+
.map(([d]) => d);
|
|
215
|
+
const durationMs = durations.map((d) => {
|
|
216
|
+
const match = d.match(/([\d.]+)(s|ms)/);
|
|
217
|
+
if (!match)
|
|
218
|
+
return 300;
|
|
219
|
+
return match[2] === 's' ? parseFloat(match[1]) * 1000 : parseFloat(match[1]);
|
|
220
|
+
});
|
|
221
|
+
// Filter out animation loops (>10s) for intensity classification
|
|
222
|
+
const uiDurationMs = durationMs.filter((ms) => ms <= 10_000);
|
|
223
|
+
const avgDuration = uiDurationMs.length > 0
|
|
224
|
+
? uiDurationMs.reduce((a, b) => a + b, 0) / uiDurationMs.length
|
|
225
|
+
: 0;
|
|
226
|
+
let intensity;
|
|
227
|
+
if (motion.length <= 3 && avgDuration < 200)
|
|
228
|
+
intensity = 'subtle';
|
|
229
|
+
else if (motion.length <= 10 && avgDuration < 400)
|
|
230
|
+
intensity = 'moderate';
|
|
231
|
+
else
|
|
232
|
+
intensity = 'expressive';
|
|
233
|
+
return { easing: topEasing, durations, intensity };
|
|
399
234
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
235
|
+
function deriveComponentPreference(components) {
|
|
236
|
+
const radiusValues = [];
|
|
237
|
+
const shadowValues = [];
|
|
238
|
+
for (const comp of components) {
|
|
239
|
+
const br = comp.styles['border-radius'];
|
|
240
|
+
if (br && br !== '0px')
|
|
241
|
+
radiusValues.push(br);
|
|
242
|
+
const bs = comp.styles['box-shadow'];
|
|
243
|
+
if (bs && bs !== 'none')
|
|
244
|
+
shadowValues.push(bs);
|
|
245
|
+
}
|
|
246
|
+
const radiusCounts = new Map();
|
|
247
|
+
for (const r of radiusValues) {
|
|
248
|
+
radiusCounts.set(r, (radiusCounts.get(r) ?? 0) + 1);
|
|
249
|
+
}
|
|
250
|
+
const topRadius = [...radiusCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? '0px';
|
|
251
|
+
let shadowStyle = 'none';
|
|
252
|
+
if (shadowValues.length > 0) {
|
|
253
|
+
const hasBigShadow = shadowValues.some((s) => {
|
|
254
|
+
const nums = s.match(/(\d+)px/g);
|
|
255
|
+
return nums && nums.some((n) => parseInt(n) > 20);
|
|
256
|
+
});
|
|
257
|
+
if (hasBigShadow)
|
|
258
|
+
shadowStyle = 'dramatic';
|
|
259
|
+
else if (shadowValues.length > components.length * 0.3)
|
|
260
|
+
shadowStyle = 'elevated';
|
|
261
|
+
else
|
|
262
|
+
shadowStyle = 'subtle';
|
|
403
263
|
}
|
|
404
|
-
const prompt = [
|
|
405
|
-
'Return strict JSON with keys: narrative (string), principles (string[]).',
|
|
406
|
-
'narrative should be 2-3 sentences and concise.',
|
|
407
|
-
'principles should contain 3-6 short imperative statements.',
|
|
408
|
-
'',
|
|
409
|
-
`Persona: ${input.profile.persona.name} — "${input.profile.persona.tagline}"`,
|
|
410
|
-
`Palette: ${input.profile.color.palette.map((entry) => `${entry.hex} (${entry.role})`).join(', ')}`,
|
|
411
|
-
`Typography: primary=${input.profile.typography.primaryFont}, secondary=${input.profile.typography.secondaryFont ?? 'none'}`,
|
|
412
|
-
`Spacing: base=${input.profile.spacing.baseUnit}, scale=${input.profile.spacing.scale.join(', ')}`,
|
|
413
|
-
`Motion: intensity=${input.profile.motion.intensity}, durations=${input.profile.motion.durations.join(', ')}, easing=${input.profile.motion.easing}`,
|
|
414
|
-
`Shape: radius=${input.profile.components.borderRadius}, shadow=${input.profile.components.shadowStyle}`,
|
|
415
|
-
].join('\n');
|
|
416
|
-
const content = await runDesignLlm({
|
|
417
|
-
llm: input.llm,
|
|
418
|
-
system: 'You are a design systems editor. Output JSON only.',
|
|
419
|
-
userContent: prompt,
|
|
420
|
-
temperature: 0.3,
|
|
421
|
-
});
|
|
422
|
-
const parsed = JSON.parse(extractFirstJsonObject(content));
|
|
423
264
|
return {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
428
|
-
.filter((value) => value.length > 0)
|
|
429
|
-
.slice(0, 6)
|
|
430
|
-
: undefined,
|
|
265
|
+
cherryPicks: [],
|
|
266
|
+
borderRadius: topRadius,
|
|
267
|
+
shadowStyle,
|
|
431
268
|
};
|
|
432
269
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
for (const
|
|
439
|
-
|
|
270
|
+
// ── Conflict detection ─────────────────────────────────────
|
|
271
|
+
function detectConflicts(inspirations) {
|
|
272
|
+
const conflicts = [];
|
|
273
|
+
// Font conflict: different primaries across sources
|
|
274
|
+
const fontsBySource = new Map();
|
|
275
|
+
for (const ins of inspirations) {
|
|
276
|
+
const topFont = ins.analysis.typography
|
|
277
|
+
.sort((a, b) => b.count - a.count)[0]?.fontFamily;
|
|
278
|
+
if (topFont)
|
|
279
|
+
fontsBySource.set(ins.name, topFont);
|
|
280
|
+
}
|
|
281
|
+
const uniqueFonts = [...new Set(fontsBySource.values())];
|
|
282
|
+
if (uniqueFonts.length > 2) {
|
|
283
|
+
conflicts.push({
|
|
284
|
+
dimension: 'typography',
|
|
285
|
+
description: `Sources use ${uniqueFonts.length} different primary fonts: ${uniqueFonts.slice(0, 4).join(', ')}`,
|
|
286
|
+
options: uniqueFonts.slice(0, 4),
|
|
287
|
+
resolved: false,
|
|
288
|
+
});
|
|
440
289
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
290
|
+
// Color temperature conflict
|
|
291
|
+
const huesBySource = [];
|
|
292
|
+
for (const ins of inspirations) {
|
|
293
|
+
const hues = ins.analysis.colors
|
|
294
|
+
.filter((c) => c.hex.length === 7)
|
|
295
|
+
.slice(0, 5)
|
|
296
|
+
.map((c) => hueFromHex(c.hex));
|
|
297
|
+
if (hues.length > 0)
|
|
298
|
+
huesBySource.push(hues);
|
|
299
|
+
}
|
|
300
|
+
if (huesBySource.length >= 2) {
|
|
301
|
+
const avgHues = huesBySource.map((hs) => hs.reduce((a, b) => a + b, 0) / hs.length);
|
|
302
|
+
const hueSpread = Math.max(...avgHues) - Math.min(...avgHues);
|
|
303
|
+
if (hueSpread > 120) {
|
|
304
|
+
conflicts.push({
|
|
305
|
+
dimension: 'color',
|
|
306
|
+
description: `Color palettes span ${Math.round(hueSpread)}° of hue — sources have different color temperatures`,
|
|
307
|
+
options: ['warm bias', 'cool bias', 'mixed palette'],
|
|
308
|
+
resolved: false,
|
|
309
|
+
});
|
|
445
310
|
}
|
|
446
|
-
return {
|
|
447
|
-
...conflict,
|
|
448
|
-
resolved: prior.resolved,
|
|
449
|
-
resolvedByDecisionId: prior.resolvedByDecisionId,
|
|
450
|
-
};
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
async function analyzeUrl(options) {
|
|
454
|
-
if (options.headed) {
|
|
455
|
-
const result = await theatricalScan(options.url);
|
|
456
|
-
return result.analysis;
|
|
457
311
|
}
|
|
458
|
-
|
|
459
|
-
const screenshotPath = path.join(os.tmpdir(), `${sessionName}.png`);
|
|
460
|
-
return captureDesignFromUrl({
|
|
461
|
-
url: options.url,
|
|
462
|
-
sessionName,
|
|
463
|
-
screenshotPath,
|
|
464
|
-
workingDir: options.rootDir,
|
|
465
|
-
journeySteps: 1,
|
|
466
|
-
responsiveViewports: [{ label: 'desktop', width: 1440, height: 1200 }],
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
function normalizeUrls(urls) {
|
|
470
|
-
const normalized = urls
|
|
471
|
-
.map((url) => url.trim())
|
|
472
|
-
.filter((url) => url.length > 0)
|
|
473
|
-
.map((url) => {
|
|
474
|
-
if (!looksLikeUrl(url)) {
|
|
475
|
-
throw new Error(`Invalid URL/domain input: ${url}`);
|
|
476
|
-
}
|
|
477
|
-
return normalizeToUrl(url);
|
|
478
|
-
});
|
|
479
|
-
return unique(normalized);
|
|
312
|
+
return conflicts;
|
|
480
313
|
}
|
|
314
|
+
// ── Main builder ───────────────────────────────────────────
|
|
481
315
|
export async function buildTasteProfile(options) {
|
|
482
|
-
const urls =
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
projectId: options.projectId,
|
|
489
|
-
projectName: options.projectName,
|
|
490
|
-
});
|
|
491
|
-
await saveDatabase(options.rootDir, db);
|
|
492
|
-
const previewAnalyses = [];
|
|
493
|
-
for (const url of urls) {
|
|
494
|
-
previewAnalyses.push(await analyzeUrl({
|
|
495
|
-
rootDir: options.rootDir,
|
|
496
|
-
url,
|
|
497
|
-
headed: Boolean(options.headed),
|
|
498
|
-
}));
|
|
499
|
-
}
|
|
500
|
-
for (const url of urls) {
|
|
316
|
+
const { rootDir, projectId, projectName, urls, headed = false, llm } = options;
|
|
317
|
+
const db = await loadDatabase(rootDir);
|
|
318
|
+
const project = ensureProject(db, { projectId, projectName });
|
|
319
|
+
// Step 1-2: Scan + ingest each URL
|
|
320
|
+
for (const rawUrl of urls) {
|
|
321
|
+
const url = looksLikeUrl(rawUrl) ? normalizeToUrl(rawUrl) : rawUrl;
|
|
501
322
|
await ingestInspiration({
|
|
502
|
-
rootDir
|
|
323
|
+
rootDir,
|
|
503
324
|
project: project.id,
|
|
504
|
-
projectName:
|
|
325
|
+
projectName: project.name,
|
|
505
326
|
url,
|
|
506
|
-
|
|
507
|
-
|
|
327
|
+
tags: ['taste-source'],
|
|
328
|
+
skipVisuals: true,
|
|
329
|
+
headed,
|
|
330
|
+
llm,
|
|
508
331
|
});
|
|
509
332
|
}
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
333
|
+
// Step 3: Reload to get all inspirations
|
|
334
|
+
const freshDb = await loadDatabase(rootDir);
|
|
335
|
+
const freshProject = freshDb.projects.find((p) => p.id === project.id);
|
|
336
|
+
if (!freshProject)
|
|
337
|
+
throw new Error(`Project not found after ingest: ${project.id}`);
|
|
338
|
+
const inspirations = freshProject.inspirations;
|
|
339
|
+
if (inspirations.length === 0) {
|
|
340
|
+
throw new Error('No inspirations found — nothing to build taste from');
|
|
514
341
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
.
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
transitions: [...acc.transitions, ...tokens.transitions],
|
|
535
|
-
spacingValues: [...acc.spacingValues, ...tokens.spacingValues],
|
|
536
|
-
cssVariableCount: acc.cssVariableCount + tokens.cssVariableCount,
|
|
537
|
-
framework: null,
|
|
538
|
-
}), {
|
|
539
|
-
colors: [],
|
|
540
|
-
fontFamilies: [],
|
|
541
|
-
fontSizes: [],
|
|
542
|
-
transitions: [],
|
|
543
|
-
spacingValues: [],
|
|
544
|
-
cssVariableCount: 0,
|
|
545
|
-
framework: null,
|
|
546
|
-
});
|
|
547
|
-
const aggregateTokens = {
|
|
548
|
-
colors: unique([...aggregateTokensFromProject.colors, ...aggregateTokensFromPreview.colors]),
|
|
549
|
-
fontFamilies: unique([...aggregateTokensFromProject.fontFamilies, ...aggregateTokensFromPreview.fontFamilies]),
|
|
550
|
-
fontSizes: unique([...aggregateTokensFromProject.fontSizes, ...aggregateTokensFromPreview.fontSizes]),
|
|
551
|
-
transitions: [...aggregateTokensFromProject.transitions, ...aggregateTokensFromPreview.transitions],
|
|
552
|
-
spacingValues: [...aggregateTokensFromProject.spacingValues, ...aggregateTokensFromPreview.spacingValues],
|
|
553
|
-
cssVariableCount: aggregateTokensFromProject.cssVariableCount + aggregateTokensFromPreview.cssVariableCount,
|
|
554
|
-
framework: null,
|
|
342
|
+
// Step 4: Aggregate
|
|
343
|
+
const colors = aggregateColors(inspirations);
|
|
344
|
+
const typography = aggregateTypography(inspirations);
|
|
345
|
+
const components = aggregateComponents(inspirations);
|
|
346
|
+
const motion = aggregateMotion(inspirations);
|
|
347
|
+
// Step 5: Derive preferences
|
|
348
|
+
const colorPref = deriveColorPreference(colors, inspirations);
|
|
349
|
+
const typographyPref = deriveTypographyPreference(typography);
|
|
350
|
+
const spacingPref = deriveSpacingPreference(components);
|
|
351
|
+
const motionPref = deriveMotionPreference(motion);
|
|
352
|
+
const componentPref = deriveComponentPreference(components);
|
|
353
|
+
// Step 6: Score + persona
|
|
354
|
+
const mergedAnalysis = {
|
|
355
|
+
colors,
|
|
356
|
+
typography,
|
|
357
|
+
components: components.map(({ count, ...rest }) => rest),
|
|
358
|
+
motion: motion.map(({ count, ...rest }) => rest),
|
|
359
|
+
layout: inspirations.flatMap((i) => i.analysis.layout).slice(0, 50),
|
|
360
|
+
cssVariables: {},
|
|
555
361
|
};
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
const
|
|
362
|
+
const tokens = designAnalysisToScanTokens(mergedAnalysis);
|
|
363
|
+
const score = computeScore(tokens);
|
|
364
|
+
const persona = assignPersona(score);
|
|
365
|
+
// Step 7: Detect conflicts
|
|
366
|
+
const conflicts = detectConflicts(inspirations);
|
|
367
|
+
// Step 8: LLM narrative
|
|
368
|
+
let narrative;
|
|
369
|
+
let principles;
|
|
370
|
+
if (llm) {
|
|
371
|
+
try {
|
|
372
|
+
const enrichment = await enrichWithLlm({
|
|
373
|
+
llm,
|
|
374
|
+
sourceName: `Taste profile for ${project.name}`,
|
|
375
|
+
analysis: mergedAnalysis,
|
|
376
|
+
});
|
|
377
|
+
narrative = enrichment.summary;
|
|
378
|
+
principles = enrichment.designPrinciples;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// LLM enrichment is optional
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// Step 9: Build + save
|
|
385
|
+
const existing = await loadTasteProfile(rootDir, project.id);
|
|
386
|
+
const version = existing ? existing.version + 1 : 1;
|
|
561
387
|
const profile = {
|
|
562
388
|
id: project.id,
|
|
563
|
-
name:
|
|
564
|
-
version
|
|
565
|
-
sourceInspirationIds:
|
|
566
|
-
sourceUrls:
|
|
389
|
+
name: project.name,
|
|
390
|
+
version,
|
|
391
|
+
sourceInspirationIds: inspirations.map((i) => i.id),
|
|
392
|
+
sourceUrls: inspirations.filter((i) => i.url).map((i) => i.url),
|
|
567
393
|
persona,
|
|
568
|
-
aggregateScore,
|
|
569
|
-
color,
|
|
570
|
-
typography,
|
|
571
|
-
spacing,
|
|
572
|
-
motion,
|
|
573
|
-
components:
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
principles: previous?.principles,
|
|
581
|
-
createdAt: previous?.createdAt ?? now,
|
|
582
|
-
updatedAt: now,
|
|
394
|
+
aggregateScore: score,
|
|
395
|
+
color: colorPref,
|
|
396
|
+
typography: typographyPref,
|
|
397
|
+
spacing: spacingPref,
|
|
398
|
+
motion: motionPref,
|
|
399
|
+
components: componentPref,
|
|
400
|
+
decisions: existing?.decisions ?? [],
|
|
401
|
+
conflicts,
|
|
402
|
+
narrative,
|
|
403
|
+
principles,
|
|
404
|
+
createdAt: existing?.createdAt ?? nowIso(),
|
|
405
|
+
updatedAt: nowIso(),
|
|
583
406
|
};
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
llm: options.llm,
|
|
587
|
-
profile,
|
|
588
|
-
});
|
|
589
|
-
profile.narrative = narrative.narrative ?? profile.narrative;
|
|
590
|
-
profile.principles = narrative.principles ?? profile.principles;
|
|
591
|
-
}
|
|
592
|
-
catch {
|
|
593
|
-
// LLM enrichment is optional for taste profile building.
|
|
594
|
-
}
|
|
595
|
-
await saveTasteProfile(options.rootDir, profile);
|
|
407
|
+
await saveTasteProfile(rootDir, profile);
|
|
408
|
+
// Step 10: Return
|
|
596
409
|
return profile;
|
|
597
410
|
}
|
|
598
411
|
//# sourceMappingURL=taste.js.map
|