krz/brand-bench
A dynamic brand documentation generator with Ollama integration.
clone: git clone https://gitbay.org/krz/brand-bench.git
main: src/engine/generator.ts · raw
1import type {
2 BrandInputs,
3 BrandOutputs,
4 ToneGuidance,
5 VisualDirection,
6 LogoConcept,
7 UsageExample,
8 ColorPalette,
9 ColorSwatch,
10 Typography,
11 TypographyToken,
12} from '../types';
13
14type CategoryType = 'developer' | 'creative' | 'product' | 'services' | 'personal' | 'general';
15
16interface GenContext {
17 name: string;
18 category: string;
19 purpose: string;
20 audience: string;
21 tone: string[];
22 avoid: string[];
23 notes: string;
24 catType: CategoryType;
25 hasTone: (t: string) => boolean;
26 hasAvoid: (a: string) => boolean;
27}
28
29// ── Helpers ──────────────────────────────────────────────────────────────────
30
31function detectCategory(raw: string): CategoryType {
32 const c = raw.toLowerCase();
33 if (/\b(dev|developer|tool|cli|sdk|api|library|lib|framework|plugin|compiler|linter|utility)\b/.test(c))
34 return 'developer';
35 if (/\b(design|creative|studio|agency|art|visual|branding|illustration|photography)\b/.test(c))
36 return 'creative';
37 if (/\b(saas|platform|service|app|software|product|startup|dashboard)\b/.test(c))
38 return 'product';
39 if (/\b(consult|advisory|freelance|firm|practice|coach|training)\b/.test(c))
40 return 'services';
41 if (/\b(personal|portfolio|blog|brand|me|writer|designer|maker|creator)\b/.test(c))
42 return 'personal';
43 return 'general';
44}
45
46function buildContext(inputs: BrandInputs): GenContext {
47 const catType = detectCategory(inputs.category);
48 const hasTone = (t: string) =>
49 inputs.tone.some(x => x.toLowerCase().includes(t.toLowerCase()));
50 const hasAvoid = (a: string) =>
51 inputs.avoid.some(x => x.toLowerCase().includes(a.toLowerCase()));
52
53 return {
54 name: inputs.name.trim() || 'Untitled',
55 category: inputs.category.trim() || 'project',
56 purpose: inputs.purpose.trim() || 'solve a specific problem',
57 audience: inputs.audience.trim() || 'its intended users',
58 tone: inputs.tone,
59 avoid: inputs.avoid,
60 notes: inputs.notes.trim(),
61 catType,
62 hasTone,
63 hasAvoid,
64 };
65}
66
67function pick<T>(arr: T[]): T {
68 return arr[Math.floor(Math.random() * arr.length)];
69}
70
71function pickN<T>(arr: T[], n: number): T[] {
72 const copy = [...arr];
73 const result: T[] = [];
74 while (result.length < n && copy.length > 0) {
75 const i = Math.floor(Math.random() * copy.length);
76 result.push(copy.splice(i, 1)[0]);
77 }
78 return result;
79}
80
81function cap(s: string): string {
82 return s.charAt(0).toUpperCase() + s.slice(1);
83}
84
85// ── Overview ─────────────────────────────────────────────────────────────────
86
87function generateOverview(ctx: GenContext): string {
88 const { name, category, purpose, audience, catType } = ctx;
89
90 const base = pick([
91 `${name} is a ${category} for ${audience}. ${cap(purpose)}.`,
92 `A ${category} built for ${audience}. ${name} is built to ${purpose}.`,
93 `${name} helps ${audience} ${purpose}.`,
94 `${name} is a ${category} built to ${purpose}. Made for ${audience}.`,
95 ]);
96
97 let suffix = '';
98 if (ctx.hasTone('minimal')) suffix = ' Nothing more.';
99 else if (ctx.hasTone('technical')) suffix = ' Stays out of the way while doing its job.';
100 else if (ctx.hasTone('calm')) suffix = ' Designed to reduce friction, not add to it.';
101 else if (ctx.hasTone('bold')) suffix = ' No compromises.';
102 else if (ctx.hasTone('warm')) suffix = ' Made with care.';
103 else if (catType === 'developer') suffix = ' Built to stay composable and predictable.';
104 else if (catType === 'creative') suffix = ' Work that speaks before the introduction.';
105
106 return base + suffix;
107}
108
109// ── Positioning ───────────────────────────────────────────────────────────────
110
111function generatePositioning(ctx: GenContext): string {
112 const { name, catType } = ctx;
113
114 const templates: Record<CategoryType, string[]> = {
115 developer: [
116 `${name} sits in the space between "build it yourself" and "platform lock-in." It's a workflow layer, not an abstraction. You keep control; ${name} handles the repetitive parts.`,
117 `Most tools in this space try to do too much. ${name} doesn't. It handles exactly what it promises — then stays out of your way.`,
118 `${name} is not a platform. It's a tool. You own the infrastructure; ${name} owns the workflow. That distinction matters.`,
119 ],
120 creative: [
121 `${name} is defined by its process as much as its output. The work should be recognizable without a logo — that's the goal.`,
122 `${name} occupies a deliberate position: between personal and professional, between output and craft. Not trying to be everything. Trying to be specific.`,
123 `There's no shortage of creative work. ${name} earns attention through quality and consistency, not volume or novelty.`,
124 ],
125 product: [
126 `${name} doesn't try to replace your existing workflow. It fits into it. The goal is to reduce friction in a specific, measurable place.`,
127 `The space ${name} plays in has no shortage of tools. What it offers is focus — one problem, done well, with clear boundaries.`,
128 `${name} is built for people who've tried the alternatives and found them too complicated, too expensive, or too broad.`,
129 ],
130 services: [
131 `${name} is not a generalist practice. It works on a specific type of problem with a specific type of client. That specificity is the positioning.`,
132 `Clients come to ${name} when they need someone who has solved this problem before. The brand should communicate experience, not aspiration.`,
133 `${name} does one thing well and charges accordingly. Clarity of scope is the offer.`,
134 ],
135 personal: [
136 `${name} is a deliberate presence — not a portfolio, not a platform, not a personal brand in the marketing sense. A clear point of view, consistently expressed.`,
137 `${name} doesn't try to appeal to everyone. It's built around a specific set of interests and a specific way of working.`,
138 `There are a lot of personal sites. ${name} is distinct by being specific — not broad, not aspirational, not trying to cover every base.`,
139 ],
140 general: [
141 `${name} occupies a clear position: built for a specific audience with a specific need. Not trying to be everything.`,
142 `${name} is not for everyone. That's intentional. Clarity of purpose is more valuable than breadth of appeal.`,
143 ],
144 };
145
146 return pick(templates[catType] ?? templates.general);
147}
148
149// ── Tone Guidance ─────────────────────────────────────────────────────────────
150
151function generateToneGuidance(ctx: GenContext): ToneGuidance {
152 const { name, purpose, tone, avoid, catType, hasTone } = ctx;
153
154 const voiceMap: Record<string, string> = {
155 minimal: 'Short sentences. No adjectives unless load-bearing. Direct.',
156 technical: 'Precise nouns, specific verbs. Write for experts. Avoid explaining what the reader already knows.',
157 calm: 'Measured pace. Confident statements. No urgency cues. Let the work speak.',
158 bold: 'Strong verbs. Active voice. Make a claim and stand behind it.',
159 warm: 'Approachable but not casual. Human without being informal.',
160 dry: 'Deadpan. Understate. Trust the reader to get it.',
161 focused: 'Stay on topic. One idea per sentence. Cut the rest.',
162 };
163
164 const catVoiceDefaults: Record<CategoryType, string> = {
165 developer: 'Write for engineers. Assume technical literacy. Specificity earns trust.',
166 creative: 'Lead with the work. Copy should serve the visual, not explain it.',
167 product: 'Clear over clever. Features earn their mention by solving something real.',
168 services: "Experience over enthusiasm. What you've done, not what you'll do.",
169 personal: 'First person where appropriate. Honest and considered.',
170 general: 'Clear, direct, grounded. Earn attention with specificity.',
171 };
172
173 const toneVoiceParts = tone.map(t => voiceMap[t.toLowerCase()]).filter(Boolean);
174 const voiceNotes =
175 toneVoiceParts.length > 0
176 ? toneVoiceParts.join(' ')
177 : catVoiceDefaults[catType] ?? catVoiceDefaults.general;
178
179 const avoidDefaults: Record<CategoryType, string[]> = {
180 developer: ['"seamlessly"', '"game-changing"', '"powerful" as an adjective', 'passive voice'],
181 creative: ['"unique"', '"innovative"', '"passion-driven"', 'agency-speak'],
182 product: ['"revolutionary"', '"disrupts"', '"leverage"', '"synergy"'],
183 services: ['"partner"', '"solutions"', '"holistic"', '"best-in-class"'],
184 personal: ['"journey"', '"passionate about"', '"thought leader"', '"excited to announce"'],
185 general: ['"world-class"', '"cutting-edge"', '"innovative"', '"paradigm"'],
186 };
187
188 const avoidList = [
189 ...avoid,
190 ...(avoidDefaults[catType] ?? avoidDefaults.general).filter(
191 a => !avoid.some(ua => ua.toLowerCase().includes(a.toLowerCase().replace(/"/g, '')))
192 ),
193 ];
194
195 const phrasesByTone: string[] = [];
196 if (hasTone('minimal')) phrasesByTone.push(`"${name}. ${cap(purpose)}."`);
197 if (hasTone('technical'))
198 phrasesByTone.push('"Typed, composable, deterministic."');
199 if (hasTone('calm'))
200 phrasesByTone.push('"Reliable tools, clearly documented, quietly maintained."');
201 if (hasTone('bold')) phrasesByTone.push('"Pick it up. It works. Put it down."');
202 if (hasTone('warm'))
203 phrasesByTone.push('"Made for people who care about their tools."');
204
205 const catPhrases: Record<CategoryType, string[]> = {
206 developer: [
207 '"One command. Done."',
208 '"Less ceremony. More output."',
209 '"Configure once. Forget about it."',
210 ],
211 creative: [
212 '"The work is the argument."',
213 '"No portfolio lorem ipsum."',
214 '"Good work, clearly presented."',
215 ],
216 product: [
217 '"It fits where you already work."',
218 '"No setup tax."',
219 '"Does one thing. Does it well."',
220 ],
221 services: [
222 '"You\'ve seen this problem before. So have we."',
223 '"Specific outcomes, clear process."',
224 '"We don\'t do vague."',
225 ],
226 personal: [
227 '"This is what I work on."',
228 '"Specific interests, honest opinions."',
229 '"No personal brand. Just work."',
230 ],
231 general: [
232 '"Built for a reason."',
233 '"Useful before impressive."',
234 '"Does what it says."',
235 ],
236 };
237
238 const allPhrases = [
239 ...phrasesByTone,
240 ...(catPhrases[catType] ?? catPhrases.general),
241 ];
242
243 return {
244 attributes: tone.length > 0 ? tone : ['direct', 'clear'],
245 voiceNotes,
246 avoidList,
247 examplePhrases: pickN(allPhrases, Math.min(4, allPhrases.length)),
248 };
249}
250
251// ── Titles ────────────────────────────────────────────────────────────────────
252
253function generateTitles(ctx: GenContext): string[] {
254 const { name, category, purpose, audience, hasTone } = ctx;
255
256 const purposeWords = purpose.split(/\s+/);
257 const coreVerb = purposeWords[0] ?? 'build';
258 const audienceShort = audience.split(/\s+/).slice(0, 3).join(' ');
259
260 // Distinct structural patterns — each must look meaningfully different
261 const variants: string[] = [
262 `${name} — ${category} for ${audienceShort}`,
263 `${name} / ${audience}`,
264 `${name}: ${cap(coreVerb)} without ceremony`,
265 `${name} for ${audienceShort}`,
266 `${name} — built to ${coreVerb}`,
267 ];
268
269 // Tone-specific variants
270 if (hasTone('minimal')) variants.push(`${name}.`);
271 if (hasTone('technical')) {
272 variants.push(`${name} — ${category} utility`);
273 variants.push(`${name} — ${coreVerb}, ship, repeat`);
274 }
275 if (hasTone('bold')) {
276 variants.push(`${name}. Built for ${audienceShort}.`);
277 variants.push(`${name}. No ceremony.`);
278 }
279 if (hasTone('calm')) variants.push(`${name} — a ${category} for ${audienceShort}`);
280
281 // Deduplicate and ensure first is always bare name
282 const pool = variants.filter(v => v !== name);
283 const extras = pickN(pool, 2);
284 return [name, ...extras];
285}
286
287// ── Subtitles ─────────────────────────────────────────────────────────────────
288
289function generateSubtitles(ctx: GenContext): string[] {
290 const { name, category, purpose, audience } = ctx;
291 const purposeShort = purpose.split(/\s+/).slice(0, 6).join(' ');
292
293 const all: string[] = [
294 `${cap(category)} for ${audience}.`,
295 `${cap(purpose)}.`,
296 `A ${category} built to ${purpose}.`,
297 `Built for ${audience} who need to ${purposeShort}.`,
298 `${name}: the ${category} for ${audience}.`,
299 `${cap(category)}. For ${audience}.`,
300 `${cap(purpose)}. No overhead.`,
301 `The ${category} for ${audience} who know what they need.`,
302 ];
303
304 return pickN(all, 3);
305}
306
307// ── Taglines ──────────────────────────────────────────────────────────────────
308
309function generateTaglines(ctx: GenContext): string[] {
310 const { category, purpose, audience, catType, hasTone } = ctx;
311
312 const purposeWords = purpose.split(/\s+/);
313 const coreVerb = purposeWords[0] ?? 'build';
314 // Take only the object noun (skip prepositions like "without", "for", "with")
315 const stopWords = new Set(['without', 'with', 'for', 'and', 'or', 'the', 'a', 'an']);
316 const coreNounWords = purposeWords.slice(1).filter(w => !stopWords.has(w.toLowerCase()));
317 const coreNoun = coreNounWords[0] || 'your work';
318 const audienceShort = audience.split(/\s+/).slice(0, 2).join(' ');
319
320 const catTaglines: Record<CategoryType, string[]> = {
321 developer: [
322 `${cap(coreVerb)}, ship, move on.`,
323 `Less boilerplate. More control.`,
324 `${cap(category)} that stays out of your way.`,
325 `One command. Done.`,
326 `Configure once. Forget about it.`,
327 `Less ceremony. More output.`,
328 `Built for ${audienceShort} who ship.`,
329 `${cap(coreNoun)}, no overhead.`,
330 ],
331 creative: [
332 `The work is the argument.`,
333 `Good work, clearly presented.`,
334 `No introduction needed.`,
335 `${cap(coreNoun)}, done properly.`,
336 `Craft over noise.`,
337 `Say less. Show more.`,
338 `Quality, consistently.`,
339 ],
340 product: [
341 `Does one thing. Does it well.`,
342 `Fits where you already work.`,
343 `No setup tax.`,
344 `Built for ${audienceShort}.`,
345 `${cap(coreVerb)} without the friction.`,
346 `One less problem.`,
347 `Useful before impressive.`,
348 ],
349 services: [
350 `You've seen this problem before. So have we.`,
351 `Specific outcomes. Clear process.`,
352 `Experience, not enthusiasm.`,
353 `We don't do vague.`,
354 `The result is the product.`,
355 `Built for the problem you actually have.`,
356 ],
357 personal: [
358 `This is what I work on.`,
359 `Specific interests. Honest opinions.`,
360 `Work, not performance.`,
361 `Making things that matter.`,
362 `No brand. Just work.`,
363 ],
364 general: [
365 `Built for a reason.`,
366 `Useful before impressive.`,
367 `Does what it says.`,
368 `${cap(coreVerb)} without the noise.`,
369 `For ${audienceShort} who know what they want.`,
370 ],
371 };
372
373 const toneTaglines: string[] = [];
374 if (hasTone('minimal')) toneTaglines.push(`${cap(coreVerb)}. Ship.`, `Simple by design.`);
375 if (hasTone('calm'))
376 toneTaglines.push(`Reliable tools, quietly maintained.`, `Steady. Dependable. Yours.`);
377 if (hasTone('bold'))
378 toneTaglines.push(`Pick it up. It works.`, `No compromises.`, `Built to be used.`);
379 if (hasTone('technical'))
380 toneTaglines.push(`Typed. Composable. Predictable.`, `Deterministic by design.`);
381
382 const pool = [...(catTaglines[catType] ?? catTaglines.general), ...toneTaglines];
383 return pickN(pool, 3);
384}
385
386// ── Visual Directions ─────────────────────────────────────────────────────────
387
388function generateVisualDirections(ctx: GenContext): VisualDirection[] {
389 const { catType, hasTone } = ctx;
390
391 const allDirections: VisualDirection[] = [];
392
393 if (catType === 'developer' || hasTone('technical') || hasTone('minimal')) {
394 allDirections.push(
395 {
396 id: 'terminal-minimal',
397 name: 'Terminal Minimal',
398 description:
399 'Dark background, monospace type throughout, no ornament. Functional and uncompromising. Every element earns its place.',
400 palette: 'Near-black ground (#0d0d0d), off-white text (#e0e0e0), single muted accent (amber or green).',
401 typography: 'Monospace primary — JetBrains Mono or Iosevka. Consistent weight. No italic.',
402 references: 'htop, k9s, the Stripe CLI, Linear issue view.',
403 },
404 {
405 id: 'technical-document',
406 name: 'Technical Document',
407 description:
408 'Off-white ground, dense information layout, RFC/spec aesthetic. Designed for reading, not scanning. Typography does all the work.',
409 palette: 'Warm white (#f5f3ef), dark text (#1a1a1a), minimal color — one functional accent only.',
410 typography: 'Sans-serif (Inter or similar) for body, mono for code. Tight line height. Strong hierarchy through size and weight alone.',
411 references: 'Stripe docs, Oxide Computer RFCs, GNU manpages reformatted.',
412 },
413 {
414 id: 'precision-interface',
415 name: 'Precision Interface',
416 description:
417 'Neutral mid-range palette, strong grid, engineering-tool aesthetic. Balanced between document and application. Calm but capable.',
418 palette: 'Mid-grey ground (#f0f0f0 or #1c1c1c), charcoal type, restrained use of blue or slate as action color.',
419 typography: 'Sans-serif for UI, mono for data. Clear size differentiation. No decorative weight use.',
420 references: 'Figma sidebar, Retool, Zed editor, TablePlus.',
421 }
422 );
423 }
424
425 if (catType === 'creative' || catType === 'personal') {
426 allDirections.push(
427 {
428 id: 'editorial',
429 name: 'Editorial',
430 description:
431 'Strong typographic hierarchy, restrained palette, print-design influences. Work foreground, everything else background.',
432 palette: 'Off-white or cream (#f7f4ef), near-black type, accent used once — a single warm or cool tone.',
433 typography: 'A good serif for display, neutral sans for body. Generous leading. No decorative fonts.',
434 references: 'Are.na, Typewolf, Emigre back catalog, Letterform Archive.',
435 },
436 {
437 id: 'quiet-studio',
438 name: 'Quiet Studio',
439 description:
440 'Neutral and considered. Nothing decorative. Space used to direct attention, not fill it.',
441 palette: 'Warm white (#fafaf8) or deep neutral (#141414), type-only color use. No gradients.',
442 typography: 'One typeface family, two weights. Let leading and spacing create rhythm.',
443 references: 'Pentagram case studies, Swiss International Style, Muji product design.',
444 },
445 {
446 id: 'contemporary-craft',
447 name: 'Contemporary Craft',
448 description:
449 'Tactile references — paper, grain, texture — applied with restraint. Warmth without nostalgia.',
450 palette: 'Off-white base with a warm paper tone, subtle texture overlays, earthy accent.',
451 typography: 'Mix: display serif + utility sans. Comfortable reading size. Generous margins.',
452 references: 'Oak Studio, Analog, Offscreen Magazine, Present & Correct.',
453 }
454 );
455 }
456
457 if (catType === 'product' || catType === 'services') {
458 allDirections.push(
459 {
460 id: 'focused-product',
461 name: 'Focused Product',
462 description:
463 'Clean, professional, information-forward. Looks like it was built to be used, not to be admired. Trust through clarity.',
464 palette: 'White or light grey ground, dark neutral type, one brand color used only for primary actions.',
465 typography: 'Neutral sans-serif, systematic sizing, no personality — the product is the personality.',
466 references: 'Linear, Cron (v1), Vercel dashboard, Raycast.',
467 },
468 {
469 id: 'minimal-commerce',
470 name: 'Minimal Commerce',
471 description:
472 'Premium restraint. No decorative elements. White space signals quality. Type-driven.',
473 palette: 'White ground, black type, one warm accent for selective emphasis.',
474 typography: 'A refined sans-serif. Large display size for key claims. Small, tracked caps for labels.',
475 references: 'Stripe marketing, Basecamp, Arc browser landing page.',
476 },
477 {
478 id: 'structured-trust',
479 name: 'Structured Trust',
480 description:
481 'Grid-heavy, methodical, legible. Communicates that things are in order. More function, less flourish.',
482 palette: 'Light neutral ground, two text weights (body + emphasis), a contained accent color.',
483 typography: 'Professional sans-serif — GT Walsheim or Plus Jakarta or similar. Tight tracking for headings.',
484 references: 'Harvest app, FreshBooks, Notion, Loom landing page.',
485 }
486 );
487 }
488
489 if (allDirections.length === 0) {
490 allDirections.push(
491 {
492 id: 'type-forward',
493 name: 'Type Forward',
494 description:
495 'Typography as the only design element. No illustration, no photography, no pattern. Words do everything.',
496 palette: 'Black and white, one optional accent. No gradients.',
497 typography: 'One great typeface. Multiple weights. Extreme size contrast. Nothing else needed.',
498 references: 'Early Bloomberg Businessweek covers, Helvetica film poster, The Economist.',
499 },
500 {
501 id: 'system-neutral',
502 name: 'System Neutral',
503 description:
504 'Invisible design — system fonts, default spacing, no signature. The brand is in the content, not the container.',
505 palette: 'System defaults. One custom color — the brand color. Everything else inherited.',
506 typography: 'System UI stack. Optimized for the OS it runs on.',
507 references: 'HN, iA Writer, Pinboard, older Stripe.',
508 }
509 );
510 }
511
512 return pickN(allDirections, Math.min(3, allDirections.length));
513}
514
515// ── Logo Concepts ─────────────────────────────────────────────────────────────
516
517function generateLogoConcepts(ctx: GenContext): LogoConcept[] {
518 const { name, catType } = ctx;
519 const initial = name.charAt(0).toUpperCase();
520 const initials =
521 name
522 .split(/\s+/)
523 .slice(0, 2)
524 .map(w => w.charAt(0).toUpperCase())
525 .join('') || initial;
526
527 const concepts: LogoConcept[] = [];
528
529 if (catType === 'developer') {
530 concepts.push(
531 {
532 id: 'geometric-letterform',
533 title: 'Geometric Letterform',
534 concept: `The letter "${initial}" treated as a structural element — not styled, just precise. Think grid construction, not calligraphy.`,
535 mark: 'Monoweight geometric construction. Works at 16px and 1600px. No gradients, no effects.',
536 execution:
537 'Build on a strict grid. Consider negative space as intentional, not leftover. Test at 16×16 favicon size first.',
538 },
539 {
540 id: 'wordmark-mono',
541 title: 'Wordmark in Mono',
542 concept: `"${name}" set in a monospace typeface, tracked slightly loose. The choice of mono is the signal.`,
543 mark: 'Wordmark only. No icon. The name is the mark.',
544 execution:
545 'Try JetBrains Mono, Iosevka, or Commit Mono at medium weight. Adjust tracking. Optically align.',
546 },
547 {
548 id: 'abstract-structure',
549 title: 'Abstract Structure',
550 concept:
551 'A geometric mark suggesting assembly, layering, or composition — aligned with the product metaphor.',
552 mark: 'Two or three simple shapes in precise relation. No ornamentation.',
553 execution:
554 'Explore grid fragments, interlocking forms, or stacked bars. Test inversion on dark and light.',
555 }
556 );
557 } else if (catType === 'creative' || catType === 'personal') {
558 concepts.push(
559 {
560 id: 'custom-wordmark',
561 title: 'Custom Wordmark',
562 concept: `"${name}" as a custom letterform — not a font off the shelf, but drawn. The craft shows.`,
563 mark: 'Wordmark with subtle custom refinements: adjusted spacing, modified terminals, intentional details.',
564 execution:
565 'Start with a base typeface. Modify key letterforms. The goal is invisible craft, not obvious customization.',
566 },
567 {
568 id: 'monogram',
569 title: 'Monogram',
570 concept: `"${initials}" as a tight, legible monogram. Simple enough to stamp, refined enough to scale up.`,
571 mark: 'Two letterforms in structural relation. Not overlapping decoratively — compositionally.',
572 execution:
573 'Grid-align. Consider positive/negative figure-ground play. Must read clearly at 24px.',
574 }
575 );
576 } else {
577 concepts.push(
578 {
579 id: 'clean-wordmark',
580 title: 'Wordmark',
581 concept: `"${name}" set in a well-chosen typeface, thoughtfully spaced. No icon needed.`,
582 mark: 'Wordmark. The typeface selection and spacing carry the identity.',
583 execution:
584 'Choose a typeface with character but not personality. Adjust tracking. Optically center.',
585 },
586 {
587 id: 'initial-mark',
588 title: `"${initial}" Mark`,
589 concept: `A standalone "${initial}" mark that works as a favicon, app icon, and small-scale identifier.`,
590 mark: 'Single letter, geometric or structured. Consistent weight with wordmark.',
591 execution:
592 'Build on an 8-unit grid. Test at 16px, 32px, and 512px. Must work in one color.',
593 }
594 );
595 }
596
597 concepts.push({
598 id: 'symbol-plus-wordmark',
599 title: 'Symbol + Wordmark System',
600 concept:
601 'A mark system: standalone symbol for small contexts, symbol + name for full contexts. Flexible.',
602 mark: 'Two formats: symbol alone, symbol left-aligned with wordmark right.',
603 execution:
604 'Define the relationship (size ratio, spacing) precisely. Lock it. Never deviate. Test both formats in context.',
605 });
606
607 return pickN(concepts, 2);
608}
609
610// ── Usage Examples ────────────────────────────────────────────────────────────
611
612function generateUsageExamples(ctx: GenContext): UsageExample[] {
613 const { name, category, purpose, audience, catType } = ctx;
614 // Take a clean verb phrase for landing copy — stop before prepositions
615 const stopWords = new Set(['without', 'for', 'with', 'and', 'or', 'the', 'a', 'an', 'via', 'using']);
616 const purposeWords = purpose.split(/\s+/);
617 const heroWords: string[] = [];
618 for (const w of purposeWords) {
619 if (stopWords.has(w.toLowerCase()) && heroWords.length > 0) break;
620 heroWords.push(w);
621 }
622 const heroVerb = heroWords.join(' ') || purpose;
623
624 const examples: UsageExample[] = [
625 {
626 context: 'README header',
627 text: `# ${name}\n\n${cap(category)} for ${audience}. ${cap(purpose)}.`,
628 },
629 {
630 context: 'Landing page hero',
631 text: `${cap(heroVerb)} without ceremony.\n\n${name} is a ${category} built for ${audience} who need to ${purpose}.`,
632 },
633 {
634 context: 'Social / bio',
635 text: `Building ${name} — ${category} for ${audience}. ${cap(purpose)}, no overhead.`,
636 },
637 {
638 context: 'One-liner',
639 text: `${name}: ${category} built to ${purpose}.`,
640 },
641 ];
642
643 if (catType === 'developer') {
644 examples.push({
645 context: 'Package registry description',
646 text: `${name} is a ${category} for ${audience}. ${cap(purpose)}. No configuration required.`,
647 });
648 examples.push({
649 context: 'CLI help text intro',
650 text: `${name} — ${cap(purpose)}.`,
651 });
652 }
653
654 if (catType === 'creative' || catType === 'personal') {
655 examples.push({
656 context: 'Portfolio about line',
657 text: `${name} is the practice of ${audience.split(/\s+/).slice(0, 2).join(' ')} ${heroVerb}.`,
658 });
659 }
660
661 if (catType === 'product') {
662 examples.push({
663 context: 'App store description',
664 text: `${name} is a ${category} for ${audience}. It ${purpose} — without the complexity of larger platforms.`,
665 });
666 }
667
668 return examples.slice(0, 6);
669}
670
671// ── Constraints ───────────────────────────────────────────────────────────────
672
673function generateConstraints(ctx: GenContext): string[] {
674 const { tone, avoid, hasTone } = ctx;
675 const list: string[] = [];
676
677 if (tone.length > 0) {
678 list.push(`Tone: ${tone.join(', ')}.`);
679 }
680
681 if (avoid.length > 0) {
682 list.push(`Avoid: ${avoid.join(', ')}.`);
683 }
684
685 if (hasTone('minimal')) {
686 list.push('Headlines: 6 words maximum.');
687 list.push('Body copy: 2 sentences per paragraph maximum.');
688 }
689
690 if (hasTone('technical')) {
691 list.push('Assume reader has domain knowledge. Skip definitions.');
692 list.push('Prefer specific nouns over categorical ones (say the actual thing).');
693 }
694
695 if (hasTone('calm')) {
696 list.push('No urgency language ("act now", "limited time", "don\'t miss").');
697 list.push('No exclamation points.');
698 }
699
700 if (hasTone('bold')) {
701 list.push('Active voice always. No passive constructions.');
702 list.push('Every claim should be substantiable.');
703 }
704
705 list.push('No em dashes in casual contexts. Use a period or restructure.');
706 list.push('Spell out numbers under 10 in prose. Use numerals for data.');
707 list.push('One idea per sentence. Split if in doubt.');
708
709 return list;
710}
711
712// ── Typography ────────────────────────────────────────────────────────────────
713
714export const TYPE_SCALE: TypographyToken[] = [
715 { label: 'Display', size: '56px', weight: '700', lineHeight: '1.1', usage: 'Hero headlines, major landing sections' },
716 { label: 'Heading 1', size: '40px', weight: '700', lineHeight: '1.2', usage: 'Page titles, primary headers' },
717 { label: 'Heading 2', size: '28px', weight: '600', lineHeight: '1.25', usage: 'Section headers, card titles' },
718 { label: 'Heading 3', size: '20px', weight: '600', lineHeight: '1.3', usage: 'Sub-section headers, feature titles' },
719 { label: 'Body Large', size: '18px', weight: '400', lineHeight: '1.6', usage: 'Lead paragraphs, key descriptions' },
720 { label: 'Body', size: '16px', weight: '400', lineHeight: '1.65', usage: 'Default body copy' },
721 { label: 'Caption', size: '13px', weight: '400', lineHeight: '1.5', usage: 'Meta info, timestamps, helper text' },
722 { label: 'Label', size: '11px', weight: '600', lineHeight: '1.4', usage: 'UI labels, tags, overlines' },
723];
724
725type FontPair = { primary: string; secondary: string; mono: string; pairNote: string };
726
727const FONT_PAIRS: Record<string, FontPair[]> = {
728 developer: [
729 { primary: 'Geist', secondary: 'Geist', mono: 'Geist Mono', pairNote: 'Single-family system. Clean, neutral, interface-optimized.' },
730 { primary: 'Inter', secondary: 'Inter', mono: 'JetBrains Mono', pairNote: 'Inter for all UI copy; JetBrains Mono for code.' },
731 { primary: 'IBM Plex Sans', secondary: 'IBM Plex Sans', mono: 'IBM Plex Mono', pairNote: 'IBM Plex family — coherent, technical, widely legible.' },
732 ],
733 creative: [
734 { primary: 'Playfair Display', secondary: 'Lato', mono: 'Courier Prime', pairNote: 'High-contrast editorial serif for display; Lato for body.' },
735 { primary: 'Fraunces', secondary: 'DM Sans', mono: 'DM Mono', pairNote: 'Optical-size serif for headlines; DM Sans for body. Expressive and modern.' },
736 { primary: 'Cormorant Garamond', secondary: 'Nunito Sans', mono: 'Courier Prime', pairNote: 'Refined luxury serif for display; Nunito Sans for readable body.' },
737 ],
738 product: [
739 { primary: 'Plus Jakarta Sans', secondary: 'Plus Jakarta Sans', mono: 'DM Mono', pairNote: 'Jakarta Sans at varying weights; DM Mono for data and code.' },
740 { primary: 'Inter', secondary: 'Inter', mono: 'Fira Code', pairNote: 'Inter throughout — modern, neutral, excellent hinting.' },
741 { primary: 'Manrope', secondary: 'Manrope', mono: 'JetBrains Mono', pairNote: 'Geometric Manrope for all UI; JetBrains Mono for code blocks.' },
742 ],
743 services: [
744 { primary: 'Libre Baskerville', secondary: 'Source Sans 3', mono: 'Source Code Pro', pairNote: 'Baskerville for authority and trust; Source Sans for approachable body.' },
745 { primary: 'Merriweather', secondary: 'Open Sans', mono: 'Roboto Mono', pairNote: 'Merriweather for credibility; Open Sans keeps body warm.' },
746 { primary: 'Lora', secondary: 'Nunito Sans', mono: 'Courier Prime', pairNote: 'Lora brings warmth to headlines; Nunito Sans lightens the reading.' },
747 ],
748 personal: [
749 { primary: 'Lora', secondary: 'Nunito', mono: 'DM Mono', pairNote: 'Lora for expressive headlines; Nunito for friendly body copy.' },
750 { primary: 'DM Serif Display', secondary: 'DM Sans', mono: 'DM Mono', pairNote: 'Unified DM family. Serif display for character; sans for clarity.' },
751 { primary: 'Playfair Display', secondary: 'Source Sans 3', mono: 'Courier Prime', pairNote: 'Playfair adds personality; Source Sans 3 grounds body text.' },
752 ],
753 general: [
754 { primary: 'Inter', secondary: 'Inter', mono: 'JetBrains Mono', pairNote: 'Inter throughout with weight variation. Universal starting point.' },
755 { primary: 'Plus Jakarta Sans', secondary: 'Lora', mono: 'Fira Code', pairNote: 'Geometric sans for UI; Lora serif for long-form content.' },
756 { primary: 'Manrope', secondary: 'Merriweather', mono: 'Source Code Pro', pairNote: 'Friendly geometric sans paired with a trusted editorial serif.' },
757 ],
758};
759
760function generateTypography(ctx: GenContext): Typography {
761 const pairs = FONT_PAIRS[ctx.catType] ?? FONT_PAIRS.general;
762 return { ...pick(pairs), scale: TYPE_SCALE };
763}
764
765// ── Color Palette ─────────────────────────────────────────────────────────────
766
767function makeSwatches(entries: [string, string, string][]): ColorSwatch[] {
768 return entries.map(([name, hex, role], i) => ({ id: `s${i}`, name, hex, role }));
769}
770
771function generateColorPalette(ctx: GenContext): ColorPalette {
772 type SwatchEntry = [string, string, string]; // [name, hex, role]
773 type PaletteSet = SwatchEntry[][];
774
775 const palettes: Record<string, PaletteSet> = {
776 developer: [
777 [
778 ['Background', '#0d1117', 'background'],
779 ['Surface', '#161b22', 'neutral'],
780 ['Border', '#30363d', 'neutral'],
781 ['Primary', '#58a6ff', 'primary'],
782 ['Accent', '#3fb950', 'accent'],
783 ['Text', '#f0f6fc', 'text'],
784 ],
785 [
786 ['Background', '#0a0010', 'background'],
787 ['Surface', '#160025', 'neutral'],
788 ['Neutral', '#2d1f3d', 'neutral'],
789 ['Primary', '#7c3aed', 'primary'],
790 ['Accent', '#a78bfa', 'accent'],
791 ['Text', '#e2d9f3', 'text'],
792 ],
793 [
794 ['Background', '#0a0a0a', 'background'],
795 ['Surface', '#141414', 'neutral'],
796 ['Neutral', '#292929', 'neutral'],
797 ['Primary', '#e5e5e5', 'primary'],
798 ['Accent', '#c9a96e', 'accent'],
799 ['Text', '#f5f5f5', 'text'],
800 ],
801 ],
802 creative: [
803 [
804 ['Background', '#faf7f2', 'background'],
805 ['Surface', '#f5efe4', 'neutral'],
806 ['Neutral', '#e8d9c4', 'neutral'],
807 ['Primary', '#c07850', 'primary'],
808 ['Accent', '#4a7c5f', 'accent'],
809 ['Text', '#1a1410', 'text'],
810 ],
811 [
812 ['Background', '#0f0f0f', 'background'],
813 ['Surface', '#1a1a1a', 'neutral'],
814 ['Neutral', '#2e2e2e', 'neutral'],
815 ['Primary', '#ff6b35', 'primary'],
816 ['Accent', '#ffd700', 'accent'],
817 ['Text', '#f8f8f8', 'text'],
818 ],
819 [
820 ['Background', '#f8f4ef', 'background'],
821 ['Surface', '#efe9e0', 'neutral'],
822 ['Neutral', '#d4c4b0', 'neutral'],
823 ['Primary', '#8b5e3c', 'primary'],
824 ['Accent', '#6b8f71', 'accent'],
825 ['Text', '#2c2018', 'text'],
826 ],
827 ],
828 product: [
829 [
830 ['Background', '#fafbfc', 'background'],
831 ['Surface', '#f0f4f8', 'neutral'],
832 ['Neutral', '#d1dce8', 'neutral'],
833 ['Primary', '#2563eb', 'primary'],
834 ['Accent', '#7c3aed', 'accent'],
835 ['Text', '#1e293b', 'text'],
836 ],
837 [
838 ['Background', '#0f172a', 'background'],
839 ['Surface', '#1e293b', 'neutral'],
840 ['Neutral', '#334155', 'neutral'],
841 ['Primary', '#6366f1', 'primary'],
842 ['Accent', '#22d3ee', 'accent'],
843 ['Text', '#f1f5f9', 'text'],
844 ],
845 [
846 ['Background', '#f0fafa', 'background'],
847 ['Surface', '#e0f5f5', 'neutral'],
848 ['Neutral', '#b2dede', 'neutral'],
849 ['Primary', '#0d9488', 'primary'],
850 ['Accent', '#f59e0b', 'accent'],
851 ['Text', '#134e4a', 'text'],
852 ],
853 ],
854 services: [
855 [
856 ['Background', '#f8fafd', 'background'],
857 ['Surface', '#edf2fa', 'neutral'],
858 ['Neutral', '#ccd9ee', 'neutral'],
859 ['Primary', '#1d4ed8', 'primary'],
860 ['Accent', '#0f9e6e', 'accent'],
861 ['Text', '#1a2038', 'text'],
862 ],
863 [
864 ['Background', '#0c1421', 'background'],
865 ['Surface', '#152035', 'neutral'],
866 ['Neutral', '#243450', 'neutral'],
867 ['Primary', '#3b82f6', 'primary'],
868 ['Accent', '#34d399', 'accent'],
869 ['Text', '#f8fafc', 'text'],
870 ],
871 [
872 ['Background', '#faf8f5', 'background'],
873 ['Surface', '#f0ebe0', 'neutral'],
874 ['Neutral', '#d9cdb8', 'neutral'],
875 ['Primary', '#78523a', 'primary'],
876 ['Accent', '#2d6a4f', 'accent'],
877 ['Text', '#1c1410', 'text'],
878 ],
879 ],
880 personal: [
881 [
882 ['Background', '#fffef9', 'background'],
883 ['Surface', '#fdf8ee', 'neutral'],
884 ['Neutral', '#f0e6cc', 'neutral'],
885 ['Primary', '#c9a96e', 'primary'],
886 ['Accent', '#7c9e87', 'accent'],
887 ['Text', '#2d2d2d', 'text'],
888 ],
889 [
890 ['Background', '#fafafa', 'background'],
891 ['Surface', '#f5f5f5', 'neutral'],
892 ['Neutral', '#e5e5e5', 'neutral'],
893 ['Primary', '#171717', 'primary'],
894 ['Accent', '#737373', 'accent'],
895 ['Text', '#404040', 'text'],
896 ],
897 [
898 ['Background', '#f9f8ff', 'background'],
899 ['Surface', '#f0eeff', 'neutral'],
900 ['Neutral', '#ddd8f7', 'neutral'],
901 ['Primary', '#4f46e5', 'primary'],
902 ['Accent', '#ec4899', 'accent'],
903 ['Text', '#1e1b4b', 'text'],
904 ],
905 ],
906 general: [
907 [
908 ['Background', '#111111', 'background'],
909 ['Surface', '#1a1a1a', 'neutral'],
910 ['Neutral', '#2e2e2e', 'neutral'],
911 ['Primary', '#c9a96e', 'primary'],
912 ['Accent', '#6b8f71', 'accent'],
913 ['Text', '#dedede', 'text'],
914 ],
915 [
916 ['Background', '#ffffff', 'background'],
917 ['Surface', '#f5f5f5', 'neutral'],
918 ['Neutral', '#e0e0e0', 'neutral'],
919 ['Primary', '#1a1a1a', 'primary'],
920 ['Accent', '#3b82f6', 'accent'],
921 ['Text', '#333333', 'text'],
922 ],
923 [
924 ['Background', '#0f0f1a', 'background'],
925 ['Surface', '#1a1a2e', 'neutral'],
926 ['Neutral', '#252545', 'neutral'],
927 ['Primary', '#4f8ef7', 'primary'],
928 ['Accent', '#a78bfa', 'accent'],
929 ['Text', '#e2e8f0', 'text'],
930 ],
931 ],
932 };
933
934 const pool = palettes[ctx.catType] ?? palettes.general;
935 return { swatches: makeSwatches(pick(pool) as SwatchEntry[]) };
936}
937
938// ── Entry point ───────────────────────────────────────────────────────────────
939
940export function generate(inputs: BrandInputs): BrandOutputs {
941 const ctx = buildContext(inputs);
942
943 return {
944 overview: generateOverview(ctx),
945 positioning: generatePositioning(ctx),
946 tone: generateToneGuidance(ctx),
947 titles: generateTitles(ctx),
948 subtitles: generateSubtitles(ctx),
949 taglines: generateTaglines(ctx),
950 visualDirections: generateVisualDirections(ctx),
951 palette: generateColorPalette(ctx),
952 typography: generateTypography(ctx),
953 logoConcepts: generateLogoConcepts(ctx),
954 usageExamples: generateUsageExamples(ctx),
955 constraints: generateConstraints(ctx),
956 };
957}