analyzthis_design 2.5.2 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -7
- package/agents/cards/mood-board.md +7 -8
- package/agents/manifests/mood-board.json +13 -9
- package/agents/router.json +1 -1
- package/dist/README.md +19 -7
- package/dist/agents/cards/mood-board.md +7 -8
- package/dist/agents/manifests/mood-board.json +13 -9
- package/dist/agents/router.json +1 -1
- package/dist/bin/cli.js +177 -28
- package/dist/lib/install.js +1 -0
- package/dist/lib/mcp-server.js +167 -99
- package/dist/lib/moodboard-film.js +119 -0
- package/dist/lib/moodboard-view.js +426 -0
- package/dist/lib/moodboard.js +557 -80
- package/dist/skills/anuj/SKILL.md +2 -0
- package/dist/skills/arjun/SKILL.md +2 -0
- package/dist/skills/getting-started/SKILL.md +2 -2
- package/dist/skills/meera/SKILL.md +2 -0
- package/dist/skills/mood-board/SKILL.md +87 -106
- package/dist/skills/noor/SKILL.md +2 -0
- package/dist/skills/persona-orchestrator/references/lens.md +4 -4
- package/dist/skills/priya/SKILL.md +2 -0
- package/dist/skills/raj/SKILL.md +2 -0
- package/dist/skills/zara/SKILL.md +23 -24
- package/package.json +11 -3
- package/skills/anuj/SKILL.md +2 -0
- package/skills/arjun/SKILL.md +2 -0
- package/skills/getting-started/SKILL.md +2 -2
- package/skills/meera/SKILL.md +2 -0
- package/skills/mood-board/SKILL.md +87 -106
- package/skills/noor/SKILL.md +2 -0
- package/skills/persona-orchestrator/references/lens.md +4 -4
- package/skills/priya/SKILL.md +2 -0
- package/skills/raj/SKILL.md +2 -0
- package/skills/zara/SKILL.md +23 -24
package/dist/lib/moodboard.js
CHANGED
|
@@ -70,31 +70,217 @@ function persistBoard(board, workspaceDir) {
|
|
|
70
70
|
var projectId = board.project_id;
|
|
71
71
|
var bDir = boardDir(projectId, board.board_id);
|
|
72
72
|
board.updated_at = new Date().toISOString();
|
|
73
|
+
if (board.kind === 'ui') normalizeBoard(board);
|
|
73
74
|
writeJson(path.join(bDir, 'board.json'), board);
|
|
75
|
+
var htmlPath = '';
|
|
76
|
+
if (board.kind === 'ui') {
|
|
77
|
+
// Static snapshot opens straight from disk; `moodboard show` serves the
|
|
78
|
+
// clickable version.
|
|
79
|
+
htmlPath = path.join(bDir, 'index.html');
|
|
80
|
+
try { fs.writeFileSync(htmlPath, require('./moodboard-view').renderBoardHtml(board, { interactive: false })); }
|
|
81
|
+
catch (e) { htmlPath = ''; }
|
|
82
|
+
}
|
|
74
83
|
var ws = workspaceDir || defaultWorkspaceDir();
|
|
75
84
|
ensureDir(ws);
|
|
76
85
|
var workspacePath = path.join(ws, board.board_id + '.json');
|
|
77
86
|
try { fs.writeFileSync(workspacePath, JSON.stringify(board, null, 2)); }
|
|
78
87
|
catch (e) { /* best effort */ }
|
|
79
|
-
return { board: board, boardDir: bDir, workspacePath: workspacePath };
|
|
88
|
+
return { board: board, boardDir: bDir, workspacePath: workspacePath, htmlPath: htmlPath };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Board v3: sections, element tiles, user decisions ──────────────────────
|
|
92
|
+
|
|
93
|
+
// Section targets add up to roughly 20-24 tiles. A board only uses the
|
|
94
|
+
// sections named in its brief, so a dashboard can drop first_screen.
|
|
95
|
+
var SECTIONS = [
|
|
96
|
+
{ id: 'first_screen', label: 'First screen', min: 3, max: 5 },
|
|
97
|
+
{ id: 'typography', label: 'Typography', min: 3, max: 4 },
|
|
98
|
+
{ id: 'color', label: 'Colour and contrast', min: 3, max: 4 },
|
|
99
|
+
{ id: 'components', label: 'Components and density', min: 6, max: 8 },
|
|
100
|
+
{ id: 'imagery', label: 'Imagery and icons', min: 2, max: 4 },
|
|
101
|
+
{ id: 'motion', label: 'Motion and interaction', min: 1, max: 3 },
|
|
102
|
+
];
|
|
103
|
+
var SECTION_IDS = SECTIONS.map(function(s) { return s.id; });
|
|
104
|
+
var TILE_TARGET = { min: 20, max: 24, sites: 6 };
|
|
105
|
+
var USER_DECISIONS = ['keep', 'reject', 'maybe'];
|
|
106
|
+
var JURY = ['arjun', 'meera', 'priya', 'noor'];
|
|
107
|
+
var IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
|
|
108
|
+
|
|
109
|
+
function clip(value, max) {
|
|
110
|
+
return String(value == null ? '' : value).slice(0, max);
|
|
80
111
|
}
|
|
81
112
|
|
|
82
|
-
function
|
|
113
|
+
function isHttpUrl(value) {
|
|
114
|
+
try {
|
|
115
|
+
var u = new URL(String(value));
|
|
116
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function siteOf(url) {
|
|
123
|
+
try { return new URL(url).hostname.replace(/^www\./, ''); }
|
|
124
|
+
catch (e) { return ''; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A tile is identified by where it came from plus what element it is, so a
|
|
128
|
+
// rejected "pricing card on linear.app" cannot sneak back in a later round.
|
|
129
|
+
function tileKey(pageUrl, element) {
|
|
130
|
+
var u = String(pageUrl || '').replace(/[?#].*$/, '').replace(/\/+$/, '').toLowerCase();
|
|
131
|
+
return u + '|' + String(element || '').trim().toLowerCase();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function defaultBrief(task, brief) {
|
|
135
|
+
brief = brief || {};
|
|
136
|
+
var sections = (brief.sections || []).filter(function(s) { return SECTION_IDS.indexOf(s) !== -1; });
|
|
137
|
+
return {
|
|
138
|
+
job: clip(brief.job || task, 240),
|
|
139
|
+
adjectives: (brief.adjectives || []).map(function(a) { return clip(a, 40); }).filter(Boolean).slice(0, 3),
|
|
140
|
+
not_this: clip(brief.not_this || '', 200),
|
|
141
|
+
sections: sections.length ? sections : SECTION_IDS.slice(),
|
|
142
|
+
confirmed_by_user: !!brief.confirmed_by_user,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sanitizeTokens(tokens) {
|
|
147
|
+
tokens = tokens || {};
|
|
148
|
+
var hex = /^#[0-9a-f]{3,8}$/i;
|
|
149
|
+
return {
|
|
150
|
+
colors: (tokens.colors || []).map(function(c) { return String(c).trim(); })
|
|
151
|
+
.filter(function(c) { return hex.test(c) || /^rgba?\([\d\s.,%]+\)$/i.test(c); }).slice(0, 8),
|
|
152
|
+
fonts: (tokens.fonts || []).map(function(f) { return clip(f, 60).trim(); }).filter(Boolean).slice(0, 4),
|
|
153
|
+
radius: clip(tokens.radius || '', 24),
|
|
154
|
+
spacing: clip(tokens.spacing || '', 40),
|
|
155
|
+
font_size: clip(tokens.font_size || '', 24),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function defaultScoutBrief(task, round, constraints) {
|
|
160
|
+
var c = constraints && typeof constraints === 'object' ? constraints : { veto: constraints || '' };
|
|
83
161
|
var lines = [
|
|
84
162
|
'Zara Scout Pass, round ' + (round || 1) + '.',
|
|
85
|
-
'Job: ' +
|
|
86
|
-
'Find 3-5 UI frames. Search Google Images, Dribbble, or Pinterest (or live product pages). Open them. Do not invent URLs. Do not generate AI comps.',
|
|
87
|
-
'Lens: Peak-End, first 2 seconds. Same job? Peak feeling? One steal, one refuse. Concept shots allowed if you say they are concept. Never sell a Dribbble hero as shipped product.',
|
|
88
|
-
'Skip generic AI-purple heroes and cinematic GSAP landings unless this job is a marketing page.',
|
|
89
|
-
'For each frame write: steal (one thing), refuse (one thing), peak (one first impression).',
|
|
90
|
-
'Then stop. The jury (Arjun, Noor, Priya, Meera) votes. You do not score your own picks.',
|
|
163
|
+
'Job: ' + clip(task, 240),
|
|
91
164
|
];
|
|
92
|
-
if (
|
|
93
|
-
|
|
165
|
+
if (c.adjectives && c.adjectives.length) lines.push('Feel: ' + c.adjectives.join(', ') + (c.not_this ? '. Not this: ' + c.not_this : ''));
|
|
166
|
+
if (c.userNote || (c.kept && c.kept.length) || (c.rejected && c.rejected.length)) {
|
|
167
|
+
lines.push('USER DECISIONS — these outrank every persona, including you:');
|
|
168
|
+
if (c.userNote) lines.push(' User asked: ' + clip(c.userNote, 500));
|
|
169
|
+
if (c.kept && c.kept.length) lines.push(' Kept (build around these): ' + c.kept.join('; '));
|
|
170
|
+
if (c.rejected && c.rejected.length) lines.push(' Rejected (never bring back): ' + c.rejected.join('; '));
|
|
94
171
|
}
|
|
172
|
+
if (c.missing && c.missing.length) lines.push('Sections still thin: ' + c.missing.join(', ') + '. Fill these first.');
|
|
173
|
+
lines.push(
|
|
174
|
+
'Build an element collage: ' + TILE_TARGET.min + '-' + TILE_TARGET.max + ' tiles from at least ' + TILE_TARGET.sites + ' different sites. Crop one element per tile (a card, a table header, a filter bar, a button group, a type pairing). Not whole pages.',
|
|
175
|
+
'Hunt on real pages: live product sites, Mobbin, Dribbble, Pinterest, Awwwards, Google Images. Open each page in the browser. Never invent URLs. Never generate AI comps.',
|
|
176
|
+
'Capture: browser_navigate to the page, browser_take_screenshot with ref=<element selector> and a filename, then run the capture script (moodboard capture-script) with Runtime.evaluate to read real colours, fonts, radius, spacing.',
|
|
177
|
+
'Every tile needs: page_url, image_path, section (' + SECTION_IDS.join(' | ') + '), element, tokens, steal, refuse, peak.',
|
|
178
|
+
'Lens: Peak-End, first 2 seconds. Concept shots are allowed if you say so. Never sell a Dribbble shot as shipped product. Skip AI-purple heroes and cinematic scroll-hijack on daily working screens.',
|
|
179
|
+
'Then stop. Arjun, Noor, Priya and Meera vote as advice. The user decides.'
|
|
180
|
+
);
|
|
181
|
+
if (c.veto) lines.push('Jury notes (advice only): ' + clip(c.veto, 500));
|
|
95
182
|
return lines.join('\n');
|
|
96
183
|
}
|
|
97
184
|
|
|
185
|
+
// Older boards stored Zara's picks as `frames`. Move them into `tiles` once.
|
|
186
|
+
function normalizeBoard(board) {
|
|
187
|
+
if (!board) return board;
|
|
188
|
+
if (!board.brief) board.brief = defaultBrief(board.task, null);
|
|
189
|
+
if (!board.tiles) {
|
|
190
|
+
board.tiles = (board.frames || []).map(function(f, idx) {
|
|
191
|
+
return {
|
|
192
|
+
id: f.id || generateBoardId(),
|
|
193
|
+
n: idx + 1,
|
|
194
|
+
section: SECTION_IDS.indexOf(f.surface) !== -1 ? f.surface : 'components',
|
|
195
|
+
element: f.title || 'Frame',
|
|
196
|
+
site: siteOf(f.url),
|
|
197
|
+
page_url: f.url || '',
|
|
198
|
+
image: '',
|
|
199
|
+
image_url: f.url || '',
|
|
200
|
+
tokens: sanitizeTokens(null),
|
|
201
|
+
steal: f.steal || '', refuse: f.refuse || '', peak: f.peak || '',
|
|
202
|
+
round: f.round || 1,
|
|
203
|
+
source: f.source || 'zara_scout',
|
|
204
|
+
added_at: f.added_at || new Date().toISOString(),
|
|
205
|
+
votes: {},
|
|
206
|
+
user: null,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
delete board.frames;
|
|
211
|
+
if (!board.decisions) board.decisions = [];
|
|
212
|
+
if (!board.blocked) board.blocked = [];
|
|
213
|
+
board.version = 3;
|
|
214
|
+
return board;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function findTile(board, ref) {
|
|
218
|
+
var asNum = parseInt(ref, 10);
|
|
219
|
+
for (var i = 0; i < board.tiles.length; i++) {
|
|
220
|
+
var t = board.tiles[i];
|
|
221
|
+
if (t.id === ref || (String(asNum) === String(ref) && t.n === asNum)) return t;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// The user's decision is final. Persona votes only produce a suggestion
|
|
227
|
+
// while the user has not decided.
|
|
228
|
+
function tileStatus(tile) {
|
|
229
|
+
if (tile.user && USER_DECISIONS.indexOf(tile.user.decision) !== -1) {
|
|
230
|
+
return { status: tile.user.decision, by: 'user' };
|
|
231
|
+
}
|
|
232
|
+
var keep = 0, skip = 0;
|
|
233
|
+
Object.keys(tile.votes || {}).forEach(function(p) {
|
|
234
|
+
if (tile.votes[p].vote === 'keep') keep++;
|
|
235
|
+
else if (tile.votes[p].vote === 'skip') skip++;
|
|
236
|
+
});
|
|
237
|
+
if (!keep && !skip) return { status: 'undecided', by: 'none' };
|
|
238
|
+
return { status: keep >= skip ? 'suggested_keep' : 'suggested_skip', by: 'personas', keep: keep, skip: skip };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function computeDirection(board) {
|
|
242
|
+
var kept = board.tiles.filter(function(t) { return tileStatus(t).status === 'keep'; });
|
|
243
|
+
var rejected = board.tiles.filter(function(t) { return tileStatus(t).status === 'reject'; });
|
|
244
|
+
var palette = [];
|
|
245
|
+
var fonts = [];
|
|
246
|
+
kept.forEach(function(t) {
|
|
247
|
+
(t.tokens.colors || []).forEach(function(c) { if (palette.indexOf(c.toLowerCase()) === -1) palette.push(c.toLowerCase()); });
|
|
248
|
+
(t.tokens.fonts || []).forEach(function(f) { if (fonts.indexOf(f) === -1) fonts.push(f); });
|
|
249
|
+
});
|
|
250
|
+
var bySection = {};
|
|
251
|
+
board.brief.sections.forEach(function(s) { bySection[s] = 0; });
|
|
252
|
+
kept.forEach(function(t) { if (bySection[t.section] != null) bySection[t.section]++; });
|
|
253
|
+
var overruled = kept.filter(function(t) {
|
|
254
|
+
var skips = Object.keys(t.votes || {}).filter(function(p) { return t.votes[p].vote === 'skip'; });
|
|
255
|
+
return skips.length > 0;
|
|
256
|
+
}).map(function(t) { return t.n; });
|
|
257
|
+
return {
|
|
258
|
+
kept: kept.map(function(t) {
|
|
259
|
+
return { n: t.n, section: t.section, element: t.element, site: t.site, page_url: t.page_url, steal: t.steal, tokens: t.tokens, note: (t.user && t.user.note) || '' };
|
|
260
|
+
}),
|
|
261
|
+
rejected: rejected.map(function(t) {
|
|
262
|
+
return { n: t.n, element: t.element, site: t.site, page_url: t.page_url, note: (t.user && t.user.note) || t.refuse || '' };
|
|
263
|
+
}),
|
|
264
|
+
palette: palette.slice(0, 10),
|
|
265
|
+
fonts: fonts.slice(0, 4),
|
|
266
|
+
by_section: bySection,
|
|
267
|
+
missing_sections: Object.keys(bySection).filter(function(s) { return !bySection[s]; }),
|
|
268
|
+
overruled_personas_on: overruled,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function scoutConstraints(board, userNote) {
|
|
273
|
+
var dir = computeDirection(board);
|
|
274
|
+
return {
|
|
275
|
+
adjectives: board.brief.adjectives,
|
|
276
|
+
not_this: board.brief.not_this,
|
|
277
|
+
userNote: userNote || '',
|
|
278
|
+
kept: dir.kept.map(function(k) { return '#' + k.n + ' ' + k.element + ' (' + k.site + ')' + (k.note ? ' — ' + k.note : ''); }),
|
|
279
|
+
rejected: dir.rejected.map(function(r) { return r.element + ' (' + r.site + ')' + (r.note ? ' — ' + r.note : ''); }),
|
|
280
|
+
missing: dir.missing_sections,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
98
284
|
// ─── Source discovery for mood references ───────────────────────────────────
|
|
99
285
|
|
|
100
286
|
// Known hosts that expose an image or a design article we can fetch as text.
|
|
@@ -394,7 +580,10 @@ async function buildMoodBoard(opts) {
|
|
|
394
580
|
|
|
395
581
|
var refs = [];
|
|
396
582
|
var userRefs = opts.userReferences || [];
|
|
397
|
-
|
|
583
|
+
// The visual element board is the default. `--kind mood` keeps the older
|
|
584
|
+
// link-and-tag board.
|
|
585
|
+
var kind = opts.kind === 'mood' ? 'mood' : 'ui';
|
|
586
|
+
var brief = defaultBrief(task, opts.brief);
|
|
398
587
|
var discover = kind === 'ui' ? !!(opts.urls && opts.urls.length) : opts.discover !== false;
|
|
399
588
|
|
|
400
589
|
if (discover) {
|
|
@@ -435,7 +624,6 @@ async function buildMoodBoard(opts) {
|
|
|
435
624
|
created_at: new Date().toISOString(),
|
|
436
625
|
updated_at: new Date().toISOString(),
|
|
437
626
|
references: refs,
|
|
438
|
-
frames: [],
|
|
439
627
|
design_system_pack: dsPack,
|
|
440
628
|
deliberation: null,
|
|
441
629
|
synthesis: null,
|
|
@@ -443,12 +631,18 @@ async function buildMoodBoard(opts) {
|
|
|
443
631
|
scout: kind === 'ui' ? {
|
|
444
632
|
persona: 'zara',
|
|
445
633
|
round: 1,
|
|
446
|
-
max_rounds: 3,
|
|
447
634
|
status: 'awaiting_scout',
|
|
448
|
-
brief: defaultScoutBrief(task, 1,
|
|
635
|
+
brief: defaultScoutBrief(task, 1, { adjectives: brief.adjectives, not_this: brief.not_this }),
|
|
449
636
|
last_veto: '',
|
|
450
637
|
} : null,
|
|
451
638
|
};
|
|
639
|
+
if (kind === 'ui') {
|
|
640
|
+
board.brief = brief;
|
|
641
|
+
board.tiles = [];
|
|
642
|
+
board.decisions = [];
|
|
643
|
+
board.blocked = [];
|
|
644
|
+
board.version = 3;
|
|
645
|
+
}
|
|
452
646
|
|
|
453
647
|
return persistBoard(board, workspaceDir);
|
|
454
648
|
}
|
|
@@ -476,38 +670,25 @@ async function critiqueMoodBoard(opts) {
|
|
|
476
670
|
if (!board) throw new Error('Board not found: ' + boardId);
|
|
477
671
|
|
|
478
672
|
var isUi = board.kind === 'ui';
|
|
479
|
-
|
|
673
|
+
if (isUi) normalizeBoard(board);
|
|
674
|
+
var experts = opts.experts || (isUi ? JURY.slice() : ['arjun', 'meera', 'priya', 'zara', 'noor']);
|
|
480
675
|
var task = opts.task || board.task;
|
|
481
676
|
|
|
482
|
-
if (isUi &&
|
|
483
|
-
var need = new Error('Zara has not scouted yet. Add
|
|
677
|
+
if (isUi && !board.tiles.length) {
|
|
678
|
+
var need = new Error('Zara has not scouted yet. Add tiles, then critique.');
|
|
484
679
|
need.code = 'NEED_SCOUT';
|
|
485
680
|
throw need;
|
|
486
681
|
}
|
|
487
682
|
|
|
488
683
|
var refsSummary = isUi
|
|
489
|
-
? board
|
|
490
|
-
return '[' + (idx + 1) + '] ' + f.title + ' round=' + (f.round || 1) +
|
|
491
|
-
' steal=' + (f.steal || '') + ' refuse=' + (f.refuse || '') +
|
|
492
|
-
' peak=' + (f.peak || '') + ' — ' + (f.url || '').slice(0, 120);
|
|
493
|
-
}).join('\n')
|
|
684
|
+
? juryTileLines(board)
|
|
494
685
|
: board.references.map(function(r, idx) {
|
|
495
686
|
return '[' + (idx + 1) + '] ' + r.title + ' (' + r.type + ') tags=' + (r.tags || []).join(',') + ' — ' + (r.url || r.description || '').slice(0, 120);
|
|
496
687
|
}).join('\n');
|
|
497
688
|
|
|
498
689
|
var dsText = formatDsPack(board.design_system_pack);
|
|
499
690
|
|
|
500
|
-
var critiqueTask = isUi ? [
|
|
501
|
-
'UI image-board jury for: ' + task,
|
|
502
|
-
'Zara scouted these frames. She does not vote. KEEP or SKIP each frame with one because-sentence.',
|
|
503
|
-
'Do not reward cinematic marketing motion on a daily working surface.',
|
|
504
|
-
'',
|
|
505
|
-
'Frames:\n' + refsSummary,
|
|
506
|
-
'',
|
|
507
|
-
dsText,
|
|
508
|
-
'',
|
|
509
|
-
'Use the Honeycomb rigor matrix. Rank the best 2-3 frames. Verdict SHIP only if the set fits the job. REVISE or BLOCK sends Zara to scout again.'
|
|
510
|
-
].join('\n') : [
|
|
691
|
+
var critiqueTask = isUi ? juryPrompt(board, dsText) : [
|
|
511
692
|
'Mood-board critique for: ' + task,
|
|
512
693
|
'',
|
|
513
694
|
'References:\n' + refsSummary,
|
|
@@ -539,63 +720,192 @@ async function critiqueMoodBoard(opts) {
|
|
|
539
720
|
return { board: saved.board, boardDir: saved.boardDir, workspacePath: saved.workspacePath, orchestratorResult: result };
|
|
540
721
|
}
|
|
541
722
|
|
|
723
|
+
// The jury's verdict is advice. It never closes the board or sends Zara back
|
|
724
|
+
// on its own — the board waits for the user.
|
|
542
725
|
function applyJuryOutcome(board, synthesis) {
|
|
543
726
|
var verdict = String((synthesis && synthesis.verdict) || '').toUpperCase();
|
|
544
727
|
var scout = board.scout || {};
|
|
545
|
-
var maxRounds = scout.max_rounds || 3;
|
|
546
728
|
var notes = '';
|
|
547
729
|
if (synthesis && synthesis.summary) notes = synthesis.summary;
|
|
548
730
|
else if (synthesis && synthesis.top3 && synthesis.top3.join) notes = synthesis.top3.join('; ');
|
|
549
731
|
else notes = verdict;
|
|
732
|
+
scout.status = 'awaiting_user';
|
|
733
|
+
scout.jury_verdict = verdict;
|
|
734
|
+
scout.last_veto = clip(notes, 600);
|
|
735
|
+
board.scout = scout;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function juryTileLines(board) {
|
|
739
|
+
return board.tiles.map(function(t) {
|
|
740
|
+
return '#' + t.n + ' [' + t.section + '] ' + t.element + ' — ' + t.site + (t.video ? ' (scroll film)' : '') + ' round=' + t.round +
|
|
741
|
+
'\n steal=' + t.steal + ' | refuse=' + t.refuse + ' | peak=' + t.peak +
|
|
742
|
+
(t.tokens.colors.length ? '\n colours=' + t.tokens.colors.join(' ') : '') +
|
|
743
|
+
(t.tokens.fonts.length ? ' fonts=' + t.tokens.fonts.join(', ') : '') +
|
|
744
|
+
(t.user ? '\n USER ALREADY DECIDED: ' + t.user.decision + (t.user.note ? ' — ' + t.user.note : '') + ' (do not argue with this)' : '');
|
|
745
|
+
}).join('\n');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function juryPrompt(board, dsText) {
|
|
749
|
+
return [
|
|
750
|
+
'UI element-board jury for: ' + board.task,
|
|
751
|
+
'You are Arjun, Noor, Priya and Meera. Zara scouted these tiles and does not vote. Your votes are advice shown to the user; the user decides.',
|
|
752
|
+
'Vote KEEP or SKIP on every tile with one because-sentence. Do not reward cinematic marketing motion on a daily working surface.',
|
|
753
|
+
'',
|
|
754
|
+
'Tiles:\n' + juryTileLines(board),
|
|
755
|
+
'',
|
|
756
|
+
dsText || '',
|
|
757
|
+
'',
|
|
758
|
+
'Return votes as JSON: {"votes":[{"tile":<n>,"persona":"arjun","vote":"keep|skip","because":"..."}],"verdict":"SHIP|REVISE|BLOCK","summary":"..."}',
|
|
759
|
+
'Then call analyzthis_mood_board with action=votes, board=' + board.board_id + ', and that JSON.'
|
|
760
|
+
].join('\n');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function loadUiBoard(opts) {
|
|
764
|
+
var board = getBoard(opts);
|
|
765
|
+
if (board.kind !== 'ui') throw new Error('This belongs on a UI element board (kind ui).');
|
|
766
|
+
return normalizeBoard(board);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Writes base64 screenshot data (inline, or a saved CDP Page.captureScreenshot
|
|
770
|
+
// response file) into the board's assets folder.
|
|
771
|
+
function saveBase64Image(bDir, tileId, data, label) {
|
|
772
|
+
if (!data || typeof data !== 'string') throw new Error('No screenshot data in ' + label);
|
|
773
|
+
var buf = Buffer.from(data.replace(/^data:image\/\w+;base64,/, ''), 'base64');
|
|
774
|
+
if (buf.length > 8 * 1024 * 1024) throw new Error('Screenshot is larger than 8 MB: ' + label);
|
|
775
|
+
var isPng = buf.slice(0, 4).toString('hex') === '89504e47';
|
|
776
|
+
var isJpg = buf.slice(0, 2).toString('hex') === 'ffd8';
|
|
777
|
+
if (!isPng && !isJpg) throw new Error('Screenshot data is not a PNG or JPEG: ' + label);
|
|
778
|
+
var name = tileId + (isPng ? '.png' : '.jpg');
|
|
779
|
+
ensureDir(path.join(bDir, 'assets'));
|
|
780
|
+
fs.writeFileSync(path.join(bDir, 'assets', name), buf);
|
|
781
|
+
return { image: 'assets/' + name, image_url: '' };
|
|
782
|
+
}
|
|
550
783
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
scout.brief = 'Max scout rounds reached. Raj or the designer picks from the frames on this board.\n' + scout.last_veto;
|
|
558
|
-
} else {
|
|
559
|
-
scout.round = (scout.round || 1) + 1;
|
|
560
|
-
scout.status = 'awaiting_scout';
|
|
561
|
-
scout.last_veto = String(notes).slice(0, 600);
|
|
562
|
-
scout.brief = defaultScoutBrief(board.task, scout.round, scout.last_veto);
|
|
784
|
+
function resolveImage(board, bDir, tileId, tile) {
|
|
785
|
+
if (tile.image_base64) return saveBase64Image(bDir, tileId, tile.image_base64, 'image_base64');
|
|
786
|
+
if (tile.image_path && path.extname(String(tile.image_path)).toLowerCase() === '.json') {
|
|
787
|
+
var jsonPath = path.resolve(String(tile.image_path));
|
|
788
|
+
var raw = loadJson(jsonPath);
|
|
789
|
+
return saveBase64Image(bDir, tileId, raw && (raw.data || (raw.result && raw.result.data)), jsonPath);
|
|
563
790
|
}
|
|
564
|
-
|
|
791
|
+
if (tile.image_path) {
|
|
792
|
+
var src = path.resolve(String(tile.image_path));
|
|
793
|
+
var ext = path.extname(src).toLowerCase();
|
|
794
|
+
if (IMAGE_EXTS.indexOf(ext) === -1) throw new Error('Image must be one of ' + IMAGE_EXTS.join(', ') + ': ' + src);
|
|
795
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isFile()) throw new Error('Image file not found: ' + src);
|
|
796
|
+
if (fs.statSync(src).size > 8 * 1024 * 1024) throw new Error('Image is larger than 8 MB: ' + src);
|
|
797
|
+
var name = tileId + ext;
|
|
798
|
+
ensureDir(path.join(bDir, 'assets'));
|
|
799
|
+
fs.copyFileSync(src, path.join(bDir, 'assets', name));
|
|
800
|
+
return { image: 'assets/' + name, image_url: '' };
|
|
801
|
+
}
|
|
802
|
+
if (tile.image_url && isHttpUrl(tile.image_url)) return { image: '', image_url: String(tile.image_url) };
|
|
803
|
+
throw new Error('Each tile needs image_path (a saved crop) or an http(s) image_url.');
|
|
565
804
|
}
|
|
566
805
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
var
|
|
571
|
-
|
|
572
|
-
if (
|
|
806
|
+
// Copies a scroll film (WebM/MP4, checked by file signature, not extension).
|
|
807
|
+
function resolveVideo(bDir, tileId, videoPath) {
|
|
808
|
+
if (!videoPath) return '';
|
|
809
|
+
var src = path.resolve(String(videoPath));
|
|
810
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isFile()) throw new Error('Video file not found: ' + src);
|
|
811
|
+
if (fs.statSync(src).size > 40 * 1024 * 1024) throw new Error('Video is larger than 40 MB: ' + src);
|
|
812
|
+
var head = Buffer.alloc(12);
|
|
813
|
+
var fd = fs.openSync(src, 'r');
|
|
814
|
+
fs.readSync(fd, head, 0, 12, 0);
|
|
815
|
+
fs.closeSync(fd);
|
|
816
|
+
var ext = head.slice(0, 4).toString('hex') === '1a45dfa3' ? '.webm' : head.slice(4, 8).toString('latin1') === 'ftyp' ? '.mp4' : '';
|
|
817
|
+
if (!ext) throw new Error('Video must be WebM or MP4: ' + src);
|
|
818
|
+
ensureDir(path.join(bDir, 'assets'));
|
|
819
|
+
fs.copyFileSync(src, path.join(bDir, 'assets', tileId + ext));
|
|
820
|
+
return 'assets/' + tileId + ext;
|
|
821
|
+
}
|
|
573
822
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
823
|
+
// Up to four waypoint stills shown as a strip under a film.
|
|
824
|
+
function resolveStills(board, bDir, tileId, stills) {
|
|
825
|
+
return (Array.isArray(stills) ? stills : []).slice(0, 4).map(function(p, i) {
|
|
826
|
+
return resolveImage(board, bDir, tileId + '-' + (i + 1), { image_path: p }).image;
|
|
827
|
+
});
|
|
828
|
+
}
|
|
578
829
|
|
|
579
|
-
|
|
830
|
+
// Adds Zara's (or the user's) element crops. Every tile must say where it
|
|
831
|
+
// came from; tiles the user already rejected are refused.
|
|
832
|
+
function addTiles(opts) {
|
|
833
|
+
opts = opts || {};
|
|
834
|
+
var incoming = opts.tiles || (opts.tile ? [opts.tile] : []);
|
|
835
|
+
if (!opts.boardId || !incoming.length) throw new Error('--board and at least one tile are required');
|
|
836
|
+
var board = loadUiBoard(opts);
|
|
837
|
+
var bDir = boardDir(board.project_id, board.board_id);
|
|
580
838
|
var round = (board.scout && board.scout.round) || 1;
|
|
839
|
+
var added = [];
|
|
840
|
+
var skipped = [];
|
|
581
841
|
for (var i = 0; i < incoming.length; i++) {
|
|
582
|
-
var
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
842
|
+
var t = incoming[i] || {};
|
|
843
|
+
var pageUrl = t.page_url || t.url || '';
|
|
844
|
+
var element = clip(t.element || t.title || '', 120).trim();
|
|
845
|
+
if (!isHttpUrl(pageUrl)) { skipped.push({ element: element, reason: 'missing or invalid page_url' }); continue; }
|
|
846
|
+
if (!element) { skipped.push({ page_url: pageUrl, reason: 'missing element name' }); continue; }
|
|
847
|
+
var key = tileKey(pageUrl, element);
|
|
848
|
+
if (board.blocked.indexOf(key) !== -1) { skipped.push({ element: element, reason: 'user rejected this before' }); continue; }
|
|
849
|
+
if (board.tiles.some(function(x) { return tileKey(x.page_url, x.element) === key; })) {
|
|
850
|
+
skipped.push({ element: element, reason: 'already on the board' });
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
var id = generateBoardId();
|
|
854
|
+
var img, video, stills;
|
|
855
|
+
try {
|
|
856
|
+
// A film may arrive without a poster: its first still becomes the poster.
|
|
857
|
+
img = resolveImage(board, bDir, id, (t.image_path || t.image_base64 || t.image_url) ? t : { image_path: (t.stills || [])[0] });
|
|
858
|
+
video = resolveVideo(bDir, id, t.video_path);
|
|
859
|
+
stills = resolveStills(board, bDir, id, t.stills);
|
|
860
|
+
} catch (e) { skipped.push({ element: element, reason: e.message }); continue; }
|
|
861
|
+
var tile = {
|
|
862
|
+
id: id,
|
|
863
|
+
n: board.tiles.reduce(function(m, x) { return Math.max(m, x.n || 0); }, 0) + 1,
|
|
864
|
+
section: SECTION_IDS.indexOf(t.section) !== -1 ? t.section : 'components',
|
|
865
|
+
element: element,
|
|
866
|
+
site: siteOf(pageUrl),
|
|
867
|
+
page_url: String(pageUrl),
|
|
868
|
+
image: img.image,
|
|
869
|
+
image_url: img.image_url,
|
|
870
|
+
video: video,
|
|
871
|
+
stills: stills,
|
|
872
|
+
tokens: sanitizeTokens(t.tokens),
|
|
873
|
+
steal: clip(t.steal, 200),
|
|
874
|
+
refuse: clip(t.refuse, 200),
|
|
875
|
+
peak: clip(t.peak, 200),
|
|
876
|
+
concept: !!t.concept,
|
|
593
877
|
round: round,
|
|
878
|
+
source: t.source === 'user' ? 'user' : 'zara_scout',
|
|
594
879
|
added_at: new Date().toISOString(),
|
|
595
|
-
|
|
880
|
+
votes: {},
|
|
881
|
+
user: null,
|
|
882
|
+
};
|
|
883
|
+
board.tiles.push(tile);
|
|
884
|
+
added.push(tile);
|
|
596
885
|
}
|
|
597
|
-
if (board.scout) board.scout.status = 'awaiting_jury';
|
|
598
|
-
|
|
886
|
+
if (board.scout && added.length) board.scout.status = 'awaiting_jury';
|
|
887
|
+
var saved = persistBoard(board, opts.workspaceDir);
|
|
888
|
+
saved.added = added;
|
|
889
|
+
saved.skipped = skipped;
|
|
890
|
+
return saved;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Older `frames` input (url/title) is still accepted and becomes tiles.
|
|
894
|
+
function addFrames(opts) {
|
|
895
|
+
opts = opts || {};
|
|
896
|
+
var frames = opts.frames || (opts.frame ? [opts.frame] : []);
|
|
897
|
+
return addTiles({
|
|
898
|
+
project: opts.project,
|
|
899
|
+
boardId: opts.boardId,
|
|
900
|
+
workspaceDir: opts.workspaceDir,
|
|
901
|
+
tiles: frames.map(function(f) {
|
|
902
|
+
return {
|
|
903
|
+
page_url: f.page_url || f.url, element: f.element || f.title, section: f.section || f.surface,
|
|
904
|
+
image_path: f.image_path, image_url: f.image_url || f.url,
|
|
905
|
+
steal: f.steal, refuse: f.refuse, peak: f.peak, tokens: f.tokens,
|
|
906
|
+
};
|
|
907
|
+
}),
|
|
908
|
+
});
|
|
599
909
|
}
|
|
600
910
|
|
|
601
911
|
function getBoard(opts) {
|
|
@@ -608,17 +918,124 @@ function getBoard(opts) {
|
|
|
608
918
|
return board;
|
|
609
919
|
}
|
|
610
920
|
|
|
611
|
-
|
|
921
|
+
// Persona votes are stored per tile as advice. Zara may not vote on tiles she
|
|
922
|
+
// scouted, and no persona vote can change a tile the user has decided.
|
|
923
|
+
function recordPersonaVotes(opts) {
|
|
612
924
|
opts = opts || {};
|
|
613
|
-
var board =
|
|
614
|
-
|
|
615
|
-
var
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
925
|
+
var board = loadUiBoard(opts);
|
|
926
|
+
var votes = opts.votes || [];
|
|
927
|
+
var ignored = [];
|
|
928
|
+
for (var i = 0; i < votes.length; i++) {
|
|
929
|
+
var v = votes[i] || {};
|
|
930
|
+
var persona = String(v.persona || '').toLowerCase();
|
|
931
|
+
var vote = String(v.vote || '').toLowerCase();
|
|
932
|
+
var tile = findTile(board, v.tile);
|
|
933
|
+
if (!tile) { ignored.push({ tile: v.tile, reason: 'unknown tile' }); continue; }
|
|
934
|
+
if (JURY.indexOf(persona) === -1) { ignored.push({ tile: v.tile, persona: persona, reason: 'not on the jury' }); continue; }
|
|
935
|
+
if (vote !== 'keep' && vote !== 'skip') { ignored.push({ tile: v.tile, reason: 'vote must be keep or skip' }); continue; }
|
|
936
|
+
tile.votes[persona] = { vote: vote, because: clip(v.because, 240), at: new Date().toISOString() };
|
|
937
|
+
}
|
|
938
|
+
var synthesis = { verdict: String(opts.verdict || '').toUpperCase(), summary: clip(opts.summary, 1000), top3: opts.top3 || [] };
|
|
620
939
|
applyJuryOutcome(board, synthesis);
|
|
621
940
|
board.synthesis = synthesis;
|
|
941
|
+
var saved = persistBoard(board, opts.workspaceDir);
|
|
942
|
+
saved.ignored = ignored;
|
|
943
|
+
return saved;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function recordJuryVerdict(opts) {
|
|
947
|
+
return recordPersonaVotes(opts);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// The user's choice on a tile is final. It is written to an append-only
|
|
951
|
+
// ledger, and a rejection blocks that element from ever being re-added.
|
|
952
|
+
function recordUserDecision(opts) {
|
|
953
|
+
opts = opts || {};
|
|
954
|
+
var board = loadUiBoard(opts);
|
|
955
|
+
var tile = findTile(board, opts.tile);
|
|
956
|
+
if (!tile) throw new Error('Tile not found: ' + opts.tile);
|
|
957
|
+
var decision = String(opts.decision || '').toLowerCase();
|
|
958
|
+
if (decision !== 'clear' && USER_DECISIONS.indexOf(decision) === -1) {
|
|
959
|
+
throw new Error('Decision must be keep, reject, maybe, or clear.');
|
|
960
|
+
}
|
|
961
|
+
var key = tileKey(tile.page_url, tile.element);
|
|
962
|
+
var now = new Date().toISOString();
|
|
963
|
+
tile.user = decision === 'clear' ? null : { decision: decision, note: clip(opts.note, 500), at: now };
|
|
964
|
+
board.blocked = board.blocked.filter(function(k) { return k !== key; });
|
|
965
|
+
if (decision === 'reject') board.blocked.push(key);
|
|
966
|
+
board.decisions.push({ tile_id: tile.id, n: tile.n, decision: decision, note: clip(opts.note, 500), at: now, by: 'user' });
|
|
967
|
+
return persistBoard(board, opts.workspaceDir);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// User-requested rounds have no cap. Zara gets the user's notes, keeps and
|
|
971
|
+
// rejections as the top constraints.
|
|
972
|
+
function requestRescout(opts) {
|
|
973
|
+
opts = opts || {};
|
|
974
|
+
var board = loadUiBoard(opts);
|
|
975
|
+
board.scout = board.scout || { persona: 'zara', round: 1 };
|
|
976
|
+
board.scout.round = (board.scout.round || 1) + 1;
|
|
977
|
+
board.scout.status = 'awaiting_scout';
|
|
978
|
+
board.scout.user_note = clip(opts.note, 500);
|
|
979
|
+
board.scout.brief = defaultScoutBrief(board.task, board.scout.round, scoutConstraints(board, opts.note));
|
|
980
|
+
board.decisions.push({ decision: 'rescout', note: clip(opts.note, 500), at: new Date().toISOString(), by: 'user' });
|
|
981
|
+
return persistBoard(board, opts.workspaceDir);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Locking writes the user's direction into the session so every persona
|
|
985
|
+
// reads it before designing.
|
|
986
|
+
function lockDirection(opts) {
|
|
987
|
+
opts = opts || {};
|
|
988
|
+
var board = loadUiBoard(opts);
|
|
989
|
+
var dir = computeDirection(board);
|
|
990
|
+
if (!dir.kept.length) throw new Error('Keep at least one tile before locking the direction.');
|
|
991
|
+
var now = new Date().toISOString();
|
|
992
|
+
board.direction = dir;
|
|
993
|
+
board.direction.locked_at = now;
|
|
994
|
+
board.direction.note = clip(opts.note, 500);
|
|
995
|
+
board.scout = board.scout || {};
|
|
996
|
+
board.scout.status = 'done';
|
|
997
|
+
board.decisions.push({ decision: 'lock', note: clip(opts.note, 500), at: now, by: 'user' });
|
|
998
|
+
var saved = persistBoard(board, opts.workspaceDir);
|
|
999
|
+
try {
|
|
1000
|
+
session.update({
|
|
1001
|
+
project: board.project_id,
|
|
1002
|
+
patch: {
|
|
1003
|
+
design_direction: {
|
|
1004
|
+
board_id: board.board_id,
|
|
1005
|
+
task: board.task,
|
|
1006
|
+
locked_at: now,
|
|
1007
|
+
locked_by: 'user',
|
|
1008
|
+
rule: 'User-locked mood board. Outranks every persona opinion. Do not reintroduce rejected elements.',
|
|
1009
|
+
adjectives: board.brief.adjectives,
|
|
1010
|
+
not_this: board.brief.not_this,
|
|
1011
|
+
note: board.direction.note,
|
|
1012
|
+
palette: dir.palette,
|
|
1013
|
+
fonts: dir.fonts,
|
|
1014
|
+
kept: dir.kept,
|
|
1015
|
+
rejected: dir.rejected,
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
saved.sessionError = e.message;
|
|
1021
|
+
}
|
|
1022
|
+
return saved;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function setBrief(opts) {
|
|
1026
|
+
opts = opts || {};
|
|
1027
|
+
var board = loadUiBoard(opts);
|
|
1028
|
+
var next = defaultBrief(board.task, {
|
|
1029
|
+
job: opts.job || board.brief.job,
|
|
1030
|
+
adjectives: opts.adjectives || board.brief.adjectives,
|
|
1031
|
+
not_this: opts.not_this != null ? opts.not_this : board.brief.not_this,
|
|
1032
|
+
sections: opts.sections || board.brief.sections,
|
|
1033
|
+
confirmed_by_user: opts.confirmed_by_user != null ? opts.confirmed_by_user : board.brief.confirmed_by_user,
|
|
1034
|
+
});
|
|
1035
|
+
board.brief = next;
|
|
1036
|
+
if (board.scout) {
|
|
1037
|
+
board.scout.brief = defaultScoutBrief(board.task, board.scout.round || 1, scoutConstraints(board, board.scout.user_note));
|
|
1038
|
+
}
|
|
622
1039
|
return persistBoard(board, opts.workspaceDir);
|
|
623
1040
|
}
|
|
624
1041
|
|
|
@@ -682,16 +1099,76 @@ function listBoards(projectId) {
|
|
|
682
1099
|
.map(function(d) { return d.name; });
|
|
683
1100
|
}
|
|
684
1101
|
|
|
1102
|
+
// ─── Token capture script (run in the page via CDP Runtime.evaluate) ─────────
|
|
1103
|
+
|
|
1104
|
+
// Reads the element's real computed styles plus its children's, so the board
|
|
1105
|
+
// shows the site's actual colours and fonts instead of guesses.
|
|
1106
|
+
// `text` narrows the match to the first element whose visible text contains
|
|
1107
|
+
// it, because a bare `h1` often hits the logo instead of the headline.
|
|
1108
|
+
function captureScript(selector, text) {
|
|
1109
|
+
var sel = JSON.stringify(String(selector || 'body'));
|
|
1110
|
+
var txt = JSON.stringify(String(text || '').toLowerCase());
|
|
1111
|
+
return [
|
|
1112
|
+
'(function () {',
|
|
1113
|
+
' var all = Array.prototype.slice.call(document.querySelectorAll(' + sel + '));',
|
|
1114
|
+
' function area(el) { var r = el.getBoundingClientRect(); return r.width * r.height; }',
|
|
1115
|
+
' var hits = all.filter(function (el) { return area(el) > 0 && (!' + txt + ' || el.textContent.toLowerCase().indexOf(' + txt + ') !== -1); });',
|
|
1116
|
+
' var root = ' + txt + ' ? hits.sort(function (a, b) { return area(a) - area(b); })[0] : hits[0];',
|
|
1117
|
+
' if (!root) return { error: "No visible element matches " + ' + sel + ' + (' + txt + ' ? " with text " + ' + txt + ' : "") };',
|
|
1118
|
+
' function hex(c) {',
|
|
1119
|
+
' var m = String(c).match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?/);',
|
|
1120
|
+
' if (!m || m[4] === "0") return null;',
|
|
1121
|
+
' return "#" + [m[1], m[2], m[3]].map(function (n) { return ("0" + parseInt(n, 10).toString(16)).slice(-2); }).join("");',
|
|
1122
|
+
' }',
|
|
1123
|
+
' var counts = {}, fonts = [];',
|
|
1124
|
+
' var nodes = [root].concat(Array.prototype.slice.call(root.querySelectorAll("*"), 0, 300));',
|
|
1125
|
+
' nodes.forEach(function (el) {',
|
|
1126
|
+
' var s = getComputedStyle(el);',
|
|
1127
|
+
' [s.color, s.backgroundColor, s.borderTopColor].forEach(function (c) { var h = hex(c); if (h) counts[h] = (counts[h] || 0) + 1; });',
|
|
1128
|
+
' var f = s.fontFamily.split(",")[0].replace(/["\']/g, "").trim();',
|
|
1129
|
+
' if (f && fonts.indexOf(f) === -1) fonts.push(f);',
|
|
1130
|
+
' });',
|
|
1131
|
+
// Capturing off-screen areas repeats content, so bring the element into
|
|
1132
|
+
// view and keep the crop within one viewport.
|
|
1133
|
+
' root.scrollIntoView({ block: "start" });',
|
|
1134
|
+
' var rs = getComputedStyle(root), r = root.getBoundingClientRect(), pad = 8;',
|
|
1135
|
+
' return {',
|
|
1136
|
+
' colors: Object.keys(counts).sort(function (a, b) { return counts[b] - counts[a]; }).slice(0, 8),',
|
|
1137
|
+
' fonts: fonts.slice(0, 4),',
|
|
1138
|
+
' radius: rs.borderRadius,',
|
|
1139
|
+
' spacing: rs.padding,',
|
|
1140
|
+
' font_size: rs.fontSize,',
|
|
1141
|
+
' size: Math.round(r.width) + "x" + Math.round(r.height),',
|
|
1142
|
+
' clip: { x: Math.max(0, r.left + scrollX - pad), y: Math.max(0, r.top + scrollY - pad), width: Math.ceil(r.width + pad * 2), height: Math.min(innerHeight, Math.ceil(r.height + pad * 2)), scale: 1 }',
|
|
1143
|
+
' };',
|
|
1144
|
+
'})()',
|
|
1145
|
+
].join('\n');
|
|
1146
|
+
}
|
|
1147
|
+
|
|
685
1148
|
module.exports = {
|
|
686
1149
|
buildMoodBoard: buildMoodBoard,
|
|
687
1150
|
critiqueMoodBoard: critiqueMoodBoard,
|
|
688
1151
|
addUserReferenceAndRerun: addUserReferenceAndRerun,
|
|
689
1152
|
addFrames: addFrames,
|
|
1153
|
+
addTiles: addTiles,
|
|
690
1154
|
getBoard: getBoard,
|
|
1155
|
+
loadUiBoard: loadUiBoard,
|
|
691
1156
|
recordJuryVerdict: recordJuryVerdict,
|
|
1157
|
+
recordPersonaVotes: recordPersonaVotes,
|
|
1158
|
+
recordUserDecision: recordUserDecision,
|
|
1159
|
+
requestRescout: requestRescout,
|
|
1160
|
+
lockDirection: lockDirection,
|
|
1161
|
+
setBrief: setBrief,
|
|
1162
|
+
computeDirection: computeDirection,
|
|
1163
|
+
tileStatus: tileStatus,
|
|
1164
|
+
juryPrompt: juryPrompt,
|
|
1165
|
+
captureScript: captureScript,
|
|
1166
|
+
SECTIONS: SECTIONS,
|
|
1167
|
+
TILE_TARGET: TILE_TARGET,
|
|
692
1168
|
defaultScoutBrief: defaultScoutBrief,
|
|
693
1169
|
listBoards: listBoards,
|
|
694
1170
|
boardDir: boardDir,
|
|
1171
|
+
siteOf: siteOf,
|
|
695
1172
|
formatDsPack: formatDsPack,
|
|
696
1173
|
tagHeuristics: tagHeuristics,
|
|
697
1174
|
};
|