summaryrefslogtreecommitdiff
path: root/src/engine/generator.ts
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-18 22:32:53 -0500
committerChristian Cleberg <[email protected]>2026-04-18 22:32:53 -0500
commit580e798e16346eb894eb899edac82d2efdf40adc (patch)
tree6dec3b7db678f781138b40d34da37d21ac8fbe7d /src/engine/generator.ts
downloadbrand-bench-580e798e16346eb894eb899edac82d2efdf40adc.tar.gz
brand-bench-580e798e16346eb894eb899edac82d2efdf40adc.tar.bz2
brand-bench-580e798e16346eb894eb899edac82d2efdf40adc.zip
initial commit
Diffstat (limited to 'src/engine/generator.ts')
-rw-r--r--src/engine/generator.ts957
1 files changed, 957 insertions, 0 deletions
diff --git a/src/engine/generator.ts b/src/engine/generator.ts
new file mode 100644
index 0000000..46ddaf7
--- /dev/null
+++ b/src/engine/generator.ts
@@ -0,0 +1,957 @@
+import type {
+ BrandInputs,
+ BrandOutputs,
+ ToneGuidance,
+ VisualDirection,
+ LogoConcept,
+ UsageExample,
+ ColorPalette,
+ ColorSwatch,
+ Typography,
+ TypographyToken,
+} from '../types';
+
+type CategoryType = 'developer' | 'creative' | 'product' | 'services' | 'personal' | 'general';
+
+interface GenContext {
+ name: string;
+ category: string;
+ purpose: string;
+ audience: string;
+ tone: string[];
+ avoid: string[];
+ notes: string;
+ catType: CategoryType;
+ hasTone: (t: string) => boolean;
+ hasAvoid: (a: string) => boolean;
+}
+
+// ── Helpers ──────────────────────────────────────────────────────────────────
+
+function detectCategory(raw: string): CategoryType {
+ const c = raw.toLowerCase();
+ if (/\b(dev|developer|tool|cli|sdk|api|library|lib|framework|plugin|compiler|linter|utility)\b/.test(c))
+ return 'developer';
+ if (/\b(design|creative|studio|agency|art|visual|branding|illustration|photography)\b/.test(c))
+ return 'creative';
+ if (/\b(saas|platform|service|app|software|product|startup|dashboard)\b/.test(c))
+ return 'product';
+ if (/\b(consult|advisory|freelance|firm|practice|coach|training)\b/.test(c))
+ return 'services';
+ if (/\b(personal|portfolio|blog|brand|me|writer|designer|maker|creator)\b/.test(c))
+ return 'personal';
+ return 'general';
+}
+
+function buildContext(inputs: BrandInputs): GenContext {
+ const catType = detectCategory(inputs.category);
+ const hasTone = (t: string) =>
+ inputs.tone.some(x => x.toLowerCase().includes(t.toLowerCase()));
+ const hasAvoid = (a: string) =>
+ inputs.avoid.some(x => x.toLowerCase().includes(a.toLowerCase()));
+
+ return {
+ name: inputs.name.trim() || 'Untitled',
+ category: inputs.category.trim() || 'project',
+ purpose: inputs.purpose.trim() || 'solve a specific problem',
+ audience: inputs.audience.trim() || 'its intended users',
+ tone: inputs.tone,
+ avoid: inputs.avoid,
+ notes: inputs.notes.trim(),
+ catType,
+ hasTone,
+ hasAvoid,
+ };
+}
+
+function pick<T>(arr: T[]): T {
+ return arr[Math.floor(Math.random() * arr.length)];
+}
+
+function pickN<T>(arr: T[], n: number): T[] {
+ const copy = [...arr];
+ const result: T[] = [];
+ while (result.length < n && copy.length > 0) {
+ const i = Math.floor(Math.random() * copy.length);
+ result.push(copy.splice(i, 1)[0]);
+ }
+ return result;
+}
+
+function cap(s: string): string {
+ return s.charAt(0).toUpperCase() + s.slice(1);
+}
+
+// ── Overview ─────────────────────────────────────────────────────────────────
+
+function generateOverview(ctx: GenContext): string {
+ const { name, category, purpose, audience, catType } = ctx;
+
+ const base = pick([
+ `${name} is a ${category} for ${audience}. ${cap(purpose)}.`,
+ `A ${category} built for ${audience}. ${name} is built to ${purpose}.`,
+ `${name} helps ${audience} ${purpose}.`,
+ `${name} is a ${category} built to ${purpose}. Made for ${audience}.`,
+ ]);
+
+ let suffix = '';
+ if (ctx.hasTone('minimal')) suffix = ' Nothing more.';
+ else if (ctx.hasTone('technical')) suffix = ' Stays out of the way while doing its job.';
+ else if (ctx.hasTone('calm')) suffix = ' Designed to reduce friction, not add to it.';
+ else if (ctx.hasTone('bold')) suffix = ' No compromises.';
+ else if (ctx.hasTone('warm')) suffix = ' Made with care.';
+ else if (catType === 'developer') suffix = ' Built to stay composable and predictable.';
+ else if (catType === 'creative') suffix = ' Work that speaks before the introduction.';
+
+ return base + suffix;
+}
+
+// ── Positioning ───────────────────────────────────────────────────────────────
+
+function generatePositioning(ctx: GenContext): string {
+ const { name, catType } = ctx;
+
+ const templates: Record<CategoryType, string[]> = {
+ developer: [
+ `${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.`,
+ `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.`,
+ `${name} is not a platform. It's a tool. You own the infrastructure; ${name} owns the workflow. That distinction matters.`,
+ ],
+ creative: [
+ `${name} is defined by its process as much as its output. The work should be recognizable without a logo — that's the goal.`,
+ `${name} occupies a deliberate position: between personal and professional, between output and craft. Not trying to be everything. Trying to be specific.`,
+ `There's no shortage of creative work. ${name} earns attention through quality and consistency, not volume or novelty.`,
+ ],
+ product: [
+ `${name} doesn't try to replace your existing workflow. It fits into it. The goal is to reduce friction in a specific, measurable place.`,
+ `The space ${name} plays in has no shortage of tools. What it offers is focus — one problem, done well, with clear boundaries.`,
+ `${name} is built for people who've tried the alternatives and found them too complicated, too expensive, or too broad.`,
+ ],
+ services: [
+ `${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.`,
+ `Clients come to ${name} when they need someone who has solved this problem before. The brand should communicate experience, not aspiration.`,
+ `${name} does one thing well and charges accordingly. Clarity of scope is the offer.`,
+ ],
+ personal: [
+ `${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.`,
+ `${name} doesn't try to appeal to everyone. It's built around a specific set of interests and a specific way of working.`,
+ `There are a lot of personal sites. ${name} is distinct by being specific — not broad, not aspirational, not trying to cover every base.`,
+ ],
+ general: [
+ `${name} occupies a clear position: built for a specific audience with a specific need. Not trying to be everything.`,
+ `${name} is not for everyone. That's intentional. Clarity of purpose is more valuable than breadth of appeal.`,
+ ],
+ };
+
+ return pick(templates[catType] ?? templates.general);
+}
+
+// ── Tone Guidance ─────────────────────────────────────────────────────────────
+
+function generateToneGuidance(ctx: GenContext): ToneGuidance {
+ const { name, purpose, tone, avoid, catType, hasTone } = ctx;
+
+ const voiceMap: Record<string, string> = {
+ minimal: 'Short sentences. No adjectives unless load-bearing. Direct.',
+ technical: 'Precise nouns, specific verbs. Write for experts. Avoid explaining what the reader already knows.',
+ calm: 'Measured pace. Confident statements. No urgency cues. Let the work speak.',
+ bold: 'Strong verbs. Active voice. Make a claim and stand behind it.',
+ warm: 'Approachable but not casual. Human without being informal.',
+ dry: 'Deadpan. Understate. Trust the reader to get it.',
+ focused: 'Stay on topic. One idea per sentence. Cut the rest.',
+ };
+
+ const catVoiceDefaults: Record<CategoryType, string> = {
+ developer: 'Write for engineers. Assume technical literacy. Specificity earns trust.',
+ creative: 'Lead with the work. Copy should serve the visual, not explain it.',
+ product: 'Clear over clever. Features earn their mention by solving something real.',
+ services: "Experience over enthusiasm. What you've done, not what you'll do.",
+ personal: 'First person where appropriate. Honest and considered.',
+ general: 'Clear, direct, grounded. Earn attention with specificity.',
+ };
+
+ const toneVoiceParts = tone.map(t => voiceMap[t.toLowerCase()]).filter(Boolean);
+ const voiceNotes =
+ toneVoiceParts.length > 0
+ ? toneVoiceParts.join(' ')
+ : catVoiceDefaults[catType] ?? catVoiceDefaults.general;
+
+ const avoidDefaults: Record<CategoryType, string[]> = {
+ developer: ['"seamlessly"', '"game-changing"', '"powerful" as an adjective', 'passive voice'],
+ creative: ['"unique"', '"innovative"', '"passion-driven"', 'agency-speak'],
+ product: ['"revolutionary"', '"disrupts"', '"leverage"', '"synergy"'],
+ services: ['"partner"', '"solutions"', '"holistic"', '"best-in-class"'],
+ personal: ['"journey"', '"passionate about"', '"thought leader"', '"excited to announce"'],
+ general: ['"world-class"', '"cutting-edge"', '"innovative"', '"paradigm"'],
+ };
+
+ const avoidList = [
+ ...avoid,
+ ...(avoidDefaults[catType] ?? avoidDefaults.general).filter(
+ a => !avoid.some(ua => ua.toLowerCase().includes(a.toLowerCase().replace(/"/g, '')))
+ ),
+ ];
+
+ const phrasesByTone: string[] = [];
+ if (hasTone('minimal')) phrasesByTone.push(`"${name}. ${cap(purpose)}."`);
+ if (hasTone('technical'))
+ phrasesByTone.push('"Typed, composable, deterministic."');
+ if (hasTone('calm'))
+ phrasesByTone.push('"Reliable tools, clearly documented, quietly maintained."');
+ if (hasTone('bold')) phrasesByTone.push('"Pick it up. It works. Put it down."');
+ if (hasTone('warm'))
+ phrasesByTone.push('"Made for people who care about their tools."');
+
+ const catPhrases: Record<CategoryType, string[]> = {
+ developer: [
+ '"One command. Done."',
+ '"Less ceremony. More output."',
+ '"Configure once. Forget about it."',
+ ],
+ creative: [
+ '"The work is the argument."',
+ '"No portfolio lorem ipsum."',
+ '"Good work, clearly presented."',
+ ],
+ product: [
+ '"It fits where you already work."',
+ '"No setup tax."',
+ '"Does one thing. Does it well."',
+ ],
+ services: [
+ '"You\'ve seen this problem before. So have we."',
+ '"Specific outcomes, clear process."',
+ '"We don\'t do vague."',
+ ],
+ personal: [
+ '"This is what I work on."',
+ '"Specific interests, honest opinions."',
+ '"No personal brand. Just work."',
+ ],
+ general: [
+ '"Built for a reason."',
+ '"Useful before impressive."',
+ '"Does what it says."',
+ ],
+ };
+
+ const allPhrases = [
+ ...phrasesByTone,
+ ...(catPhrases[catType] ?? catPhrases.general),
+ ];
+
+ return {
+ attributes: tone.length > 0 ? tone : ['direct', 'clear'],
+ voiceNotes,
+ avoidList,
+ examplePhrases: pickN(allPhrases, Math.min(4, allPhrases.length)),
+ };
+}
+
+// ── Titles ────────────────────────────────────────────────────────────────────
+
+function generateTitles(ctx: GenContext): string[] {
+ const { name, category, purpose, audience, hasTone } = ctx;
+
+ const purposeWords = purpose.split(/\s+/);
+ const coreVerb = purposeWords[0] ?? 'build';
+ const audienceShort = audience.split(/\s+/).slice(0, 3).join(' ');
+
+ // Distinct structural patterns — each must look meaningfully different
+ const variants: string[] = [
+ `${name} — ${category} for ${audienceShort}`,
+ `${name} / ${audience}`,
+ `${name}: ${cap(coreVerb)} without ceremony`,
+ `${name} for ${audienceShort}`,
+ `${name} — built to ${coreVerb}`,
+ ];
+
+ // Tone-specific variants
+ if (hasTone('minimal')) variants.push(`${name}.`);
+ if (hasTone('technical')) {
+ variants.push(`${name} — ${category} utility`);
+ variants.push(`${name} — ${coreVerb}, ship, repeat`);
+ }
+ if (hasTone('bold')) {
+ variants.push(`${name}. Built for ${audienceShort}.`);
+ variants.push(`${name}. No ceremony.`);
+ }
+ if (hasTone('calm')) variants.push(`${name} — a ${category} for ${audienceShort}`);
+
+ // Deduplicate and ensure first is always bare name
+ const pool = variants.filter(v => v !== name);
+ const extras = pickN(pool, 2);
+ return [name, ...extras];
+}
+
+// ── Subtitles ─────────────────────────────────────────────────────────────────
+
+function generateSubtitles(ctx: GenContext): string[] {
+ const { name, category, purpose, audience } = ctx;
+ const purposeShort = purpose.split(/\s+/).slice(0, 6).join(' ');
+
+ const all: string[] = [
+ `${cap(category)} for ${audience}.`,
+ `${cap(purpose)}.`,
+ `A ${category} built to ${purpose}.`,
+ `Built for ${audience} who need to ${purposeShort}.`,
+ `${name}: the ${category} for ${audience}.`,
+ `${cap(category)}. For ${audience}.`,
+ `${cap(purpose)}. No overhead.`,
+ `The ${category} for ${audience} who know what they need.`,
+ ];
+
+ return pickN(all, 3);
+}
+
+// ── Taglines ──────────────────────────────────────────────────────────────────
+
+function generateTaglines(ctx: GenContext): string[] {
+ const { category, purpose, audience, catType, hasTone } = ctx;
+
+ const purposeWords = purpose.split(/\s+/);
+ const coreVerb = purposeWords[0] ?? 'build';
+ // Take only the object noun (skip prepositions like "without", "for", "with")
+ const stopWords = new Set(['without', 'with', 'for', 'and', 'or', 'the', 'a', 'an']);
+ const coreNounWords = purposeWords.slice(1).filter(w => !stopWords.has(w.toLowerCase()));
+ const coreNoun = coreNounWords[0] || 'your work';
+ const audienceShort = audience.split(/\s+/).slice(0, 2).join(' ');
+
+ const catTaglines: Record<CategoryType, string[]> = {
+ developer: [
+ `${cap(coreVerb)}, ship, move on.`,
+ `Less boilerplate. More control.`,
+ `${cap(category)} that stays out of your way.`,
+ `One command. Done.`,
+ `Configure once. Forget about it.`,
+ `Less ceremony. More output.`,
+ `Built for ${audienceShort} who ship.`,
+ `${cap(coreNoun)}, no overhead.`,
+ ],
+ creative: [
+ `The work is the argument.`,
+ `Good work, clearly presented.`,
+ `No introduction needed.`,
+ `${cap(coreNoun)}, done properly.`,
+ `Craft over noise.`,
+ `Say less. Show more.`,
+ `Quality, consistently.`,
+ ],
+ product: [
+ `Does one thing. Does it well.`,
+ `Fits where you already work.`,
+ `No setup tax.`,
+ `Built for ${audienceShort}.`,
+ `${cap(coreVerb)} without the friction.`,
+ `One less problem.`,
+ `Useful before impressive.`,
+ ],
+ services: [
+ `You've seen this problem before. So have we.`,
+ `Specific outcomes. Clear process.`,
+ `Experience, not enthusiasm.`,
+ `We don't do vague.`,
+ `The result is the product.`,
+ `Built for the problem you actually have.`,
+ ],
+ personal: [
+ `This is what I work on.`,
+ `Specific interests. Honest opinions.`,
+ `Work, not performance.`,
+ `Making things that matter.`,
+ `No brand. Just work.`,
+ ],
+ general: [
+ `Built for a reason.`,
+ `Useful before impressive.`,
+ `Does what it says.`,
+ `${cap(coreVerb)} without the noise.`,
+ `For ${audienceShort} who know what they want.`,
+ ],
+ };
+
+ const toneTaglines: string[] = [];
+ if (hasTone('minimal')) toneTaglines.push(`${cap(coreVerb)}. Ship.`, `Simple by design.`);
+ if (hasTone('calm'))
+ toneTaglines.push(`Reliable tools, quietly maintained.`, `Steady. Dependable. Yours.`);
+ if (hasTone('bold'))
+ toneTaglines.push(`Pick it up. It works.`, `No compromises.`, `Built to be used.`);
+ if (hasTone('technical'))
+ toneTaglines.push(`Typed. Composable. Predictable.`, `Deterministic by design.`);
+
+ const pool = [...(catTaglines[catType] ?? catTaglines.general), ...toneTaglines];
+ return pickN(pool, 3);
+}
+
+// ── Visual Directions ─────────────────────────────────────────────────────────
+
+function generateVisualDirections(ctx: GenContext): VisualDirection[] {
+ const { catType, hasTone } = ctx;
+
+ const allDirections: VisualDirection[] = [];
+
+ if (catType === 'developer' || hasTone('technical') || hasTone('minimal')) {
+ allDirections.push(
+ {
+ id: 'terminal-minimal',
+ name: 'Terminal Minimal',
+ description:
+ 'Dark background, monospace type throughout, no ornament. Functional and uncompromising. Every element earns its place.',
+ palette: 'Near-black ground (#0d0d0d), off-white text (#e0e0e0), single muted accent (amber or green).',
+ typography: 'Monospace primary — JetBrains Mono or Iosevka. Consistent weight. No italic.',
+ references: 'htop, k9s, the Stripe CLI, Linear issue view.',
+ },
+ {
+ id: 'technical-document',
+ name: 'Technical Document',
+ description:
+ 'Off-white ground, dense information layout, RFC/spec aesthetic. Designed for reading, not scanning. Typography does all the work.',
+ palette: 'Warm white (#f5f3ef), dark text (#1a1a1a), minimal color — one functional accent only.',
+ typography: 'Sans-serif (Inter or similar) for body, mono for code. Tight line height. Strong hierarchy through size and weight alone.',
+ references: 'Stripe docs, Oxide Computer RFCs, GNU manpages reformatted.',
+ },
+ {
+ id: 'precision-interface',
+ name: 'Precision Interface',
+ description:
+ 'Neutral mid-range palette, strong grid, engineering-tool aesthetic. Balanced between document and application. Calm but capable.',
+ palette: 'Mid-grey ground (#f0f0f0 or #1c1c1c), charcoal type, restrained use of blue or slate as action color.',
+ typography: 'Sans-serif for UI, mono for data. Clear size differentiation. No decorative weight use.',
+ references: 'Figma sidebar, Retool, Zed editor, TablePlus.',
+ }
+ );
+ }
+
+ if (catType === 'creative' || catType === 'personal') {
+ allDirections.push(
+ {
+ id: 'editorial',
+ name: 'Editorial',
+ description:
+ 'Strong typographic hierarchy, restrained palette, print-design influences. Work foreground, everything else background.',
+ palette: 'Off-white or cream (#f7f4ef), near-black type, accent used once — a single warm or cool tone.',
+ typography: 'A good serif for display, neutral sans for body. Generous leading. No decorative fonts.',
+ references: 'Are.na, Typewolf, Emigre back catalog, Letterform Archive.',
+ },
+ {
+ id: 'quiet-studio',
+ name: 'Quiet Studio',
+ description:
+ 'Neutral and considered. Nothing decorative. Space used to direct attention, not fill it.',
+ palette: 'Warm white (#fafaf8) or deep neutral (#141414), type-only color use. No gradients.',
+ typography: 'One typeface family, two weights. Let leading and spacing create rhythm.',
+ references: 'Pentagram case studies, Swiss International Style, Muji product design.',
+ },
+ {
+ id: 'contemporary-craft',
+ name: 'Contemporary Craft',
+ description:
+ 'Tactile references — paper, grain, texture — applied with restraint. Warmth without nostalgia.',
+ palette: 'Off-white base with a warm paper tone, subtle texture overlays, earthy accent.',
+ typography: 'Mix: display serif + utility sans. Comfortable reading size. Generous margins.',
+ references: 'Oak Studio, Analog, Offscreen Magazine, Present & Correct.',
+ }
+ );
+ }
+
+ if (catType === 'product' || catType === 'services') {
+ allDirections.push(
+ {
+ id: 'focused-product',
+ name: 'Focused Product',
+ description:
+ 'Clean, professional, information-forward. Looks like it was built to be used, not to be admired. Trust through clarity.',
+ palette: 'White or light grey ground, dark neutral type, one brand color used only for primary actions.',
+ typography: 'Neutral sans-serif, systematic sizing, no personality — the product is the personality.',
+ references: 'Linear, Cron (v1), Vercel dashboard, Raycast.',
+ },
+ {
+ id: 'minimal-commerce',
+ name: 'Minimal Commerce',
+ description:
+ 'Premium restraint. No decorative elements. White space signals quality. Type-driven.',
+ palette: 'White ground, black type, one warm accent for selective emphasis.',
+ typography: 'A refined sans-serif. Large display size for key claims. Small, tracked caps for labels.',
+ references: 'Stripe marketing, Basecamp, Arc browser landing page.',
+ },
+ {
+ id: 'structured-trust',
+ name: 'Structured Trust',
+ description:
+ 'Grid-heavy, methodical, legible. Communicates that things are in order. More function, less flourish.',
+ palette: 'Light neutral ground, two text weights (body + emphasis), a contained accent color.',
+ typography: 'Professional sans-serif — GT Walsheim or Plus Jakarta or similar. Tight tracking for headings.',
+ references: 'Harvest app, FreshBooks, Notion, Loom landing page.',
+ }
+ );
+ }
+
+ if (allDirections.length === 0) {
+ allDirections.push(
+ {
+ id: 'type-forward',
+ name: 'Type Forward',
+ description:
+ 'Typography as the only design element. No illustration, no photography, no pattern. Words do everything.',
+ palette: 'Black and white, one optional accent. No gradients.',
+ typography: 'One great typeface. Multiple weights. Extreme size contrast. Nothing else needed.',
+ references: 'Early Bloomberg Businessweek covers, Helvetica film poster, The Economist.',
+ },
+ {
+ id: 'system-neutral',
+ name: 'System Neutral',
+ description:
+ 'Invisible design — system fonts, default spacing, no signature. The brand is in the content, not the container.',
+ palette: 'System defaults. One custom color — the brand color. Everything else inherited.',
+ typography: 'System UI stack. Optimized for the OS it runs on.',
+ references: 'HN, iA Writer, Pinboard, older Stripe.',
+ }
+ );
+ }
+
+ return pickN(allDirections, Math.min(3, allDirections.length));
+}
+
+// ── Logo Concepts ─────────────────────────────────────────────────────────────
+
+function generateLogoConcepts(ctx: GenContext): LogoConcept[] {
+ const { name, catType } = ctx;
+ const initial = name.charAt(0).toUpperCase();
+ const initials =
+ name
+ .split(/\s+/)
+ .slice(0, 2)
+ .map(w => w.charAt(0).toUpperCase())
+ .join('') || initial;
+
+ const concepts: LogoConcept[] = [];
+
+ if (catType === 'developer') {
+ concepts.push(
+ {
+ id: 'geometric-letterform',
+ title: 'Geometric Letterform',
+ concept: `The letter "${initial}" treated as a structural element — not styled, just precise. Think grid construction, not calligraphy.`,
+ mark: 'Monoweight geometric construction. Works at 16px and 1600px. No gradients, no effects.',
+ execution:
+ 'Build on a strict grid. Consider negative space as intentional, not leftover. Test at 16×16 favicon size first.',
+ },
+ {
+ id: 'wordmark-mono',
+ title: 'Wordmark in Mono',
+ concept: `"${name}" set in a monospace typeface, tracked slightly loose. The choice of mono is the signal.`,
+ mark: 'Wordmark only. No icon. The name is the mark.',
+ execution:
+ 'Try JetBrains Mono, Iosevka, or Commit Mono at medium weight. Adjust tracking. Optically align.',
+ },
+ {
+ id: 'abstract-structure',
+ title: 'Abstract Structure',
+ concept:
+ 'A geometric mark suggesting assembly, layering, or composition — aligned with the product metaphor.',
+ mark: 'Two or three simple shapes in precise relation. No ornamentation.',
+ execution:
+ 'Explore grid fragments, interlocking forms, or stacked bars. Test inversion on dark and light.',
+ }
+ );
+ } else if (catType === 'creative' || catType === 'personal') {
+ concepts.push(
+ {
+ id: 'custom-wordmark',
+ title: 'Custom Wordmark',
+ concept: `"${name}" as a custom letterform — not a font off the shelf, but drawn. The craft shows.`,
+ mark: 'Wordmark with subtle custom refinements: adjusted spacing, modified terminals, intentional details.',
+ execution:
+ 'Start with a base typeface. Modify key letterforms. The goal is invisible craft, not obvious customization.',
+ },
+ {
+ id: 'monogram',
+ title: 'Monogram',
+ concept: `"${initials}" as a tight, legible monogram. Simple enough to stamp, refined enough to scale up.`,
+ mark: 'Two letterforms in structural relation. Not overlapping decoratively — compositionally.',
+ execution:
+ 'Grid-align. Consider positive/negative figure-ground play. Must read clearly at 24px.',
+ }
+ );
+ } else {
+ concepts.push(
+ {
+ id: 'clean-wordmark',
+ title: 'Wordmark',
+ concept: `"${name}" set in a well-chosen typeface, thoughtfully spaced. No icon needed.`,
+ mark: 'Wordmark. The typeface selection and spacing carry the identity.',
+ execution:
+ 'Choose a typeface with character but not personality. Adjust tracking. Optically center.',
+ },
+ {
+ id: 'initial-mark',
+ title: `"${initial}" Mark`,
+ concept: `A standalone "${initial}" mark that works as a favicon, app icon, and small-scale identifier.`,
+ mark: 'Single letter, geometric or structured. Consistent weight with wordmark.',
+ execution:
+ 'Build on an 8-unit grid. Test at 16px, 32px, and 512px. Must work in one color.',
+ }
+ );
+ }
+
+ concepts.push({
+ id: 'symbol-plus-wordmark',
+ title: 'Symbol + Wordmark System',
+ concept:
+ 'A mark system: standalone symbol for small contexts, symbol + name for full contexts. Flexible.',
+ mark: 'Two formats: symbol alone, symbol left-aligned with wordmark right.',
+ execution:
+ 'Define the relationship (size ratio, spacing) precisely. Lock it. Never deviate. Test both formats in context.',
+ });
+
+ return pickN(concepts, 2);
+}
+
+// ── Usage Examples ────────────────────────────────────────────────────────────
+
+function generateUsageExamples(ctx: GenContext): UsageExample[] {
+ const { name, category, purpose, audience, catType } = ctx;
+ // Take a clean verb phrase for landing copy — stop before prepositions
+ const stopWords = new Set(['without', 'for', 'with', 'and', 'or', 'the', 'a', 'an', 'via', 'using']);
+ const purposeWords = purpose.split(/\s+/);
+ const heroWords: string[] = [];
+ for (const w of purposeWords) {
+ if (stopWords.has(w.toLowerCase()) && heroWords.length > 0) break;
+ heroWords.push(w);
+ }
+ const heroVerb = heroWords.join(' ') || purpose;
+
+ const examples: UsageExample[] = [
+ {
+ context: 'README header',
+ text: `# ${name}\n\n${cap(category)} for ${audience}. ${cap(purpose)}.`,
+ },
+ {
+ context: 'Landing page hero',
+ text: `${cap(heroVerb)} without ceremony.\n\n${name} is a ${category} built for ${audience} who need to ${purpose}.`,
+ },
+ {
+ context: 'Social / bio',
+ text: `Building ${name} — ${category} for ${audience}. ${cap(purpose)}, no overhead.`,
+ },
+ {
+ context: 'One-liner',
+ text: `${name}: ${category} built to ${purpose}.`,
+ },
+ ];
+
+ if (catType === 'developer') {
+ examples.push({
+ context: 'Package registry description',
+ text: `${name} is a ${category} for ${audience}. ${cap(purpose)}. No configuration required.`,
+ });
+ examples.push({
+ context: 'CLI help text intro',
+ text: `${name} — ${cap(purpose)}.`,
+ });
+ }
+
+ if (catType === 'creative' || catType === 'personal') {
+ examples.push({
+ context: 'Portfolio about line',
+ text: `${name} is the practice of ${audience.split(/\s+/).slice(0, 2).join(' ')} ${heroVerb}.`,
+ });
+ }
+
+ if (catType === 'product') {
+ examples.push({
+ context: 'App store description',
+ text: `${name} is a ${category} for ${audience}. It ${purpose} — without the complexity of larger platforms.`,
+ });
+ }
+
+ return examples.slice(0, 6);
+}
+
+// ── Constraints ───────────────────────────────────────────────────────────────
+
+function generateConstraints(ctx: GenContext): string[] {
+ const { tone, avoid, hasTone } = ctx;
+ const list: string[] = [];
+
+ if (tone.length > 0) {
+ list.push(`Tone: ${tone.join(', ')}.`);
+ }
+
+ if (avoid.length > 0) {
+ list.push(`Avoid: ${avoid.join(', ')}.`);
+ }
+
+ if (hasTone('minimal')) {
+ list.push('Headlines: 6 words maximum.');
+ list.push('Body copy: 2 sentences per paragraph maximum.');
+ }
+
+ if (hasTone('technical')) {
+ list.push('Assume reader has domain knowledge. Skip definitions.');
+ list.push('Prefer specific nouns over categorical ones (say the actual thing).');
+ }
+
+ if (hasTone('calm')) {
+ list.push('No urgency language ("act now", "limited time", "don\'t miss").');
+ list.push('No exclamation points.');
+ }
+
+ if (hasTone('bold')) {
+ list.push('Active voice always. No passive constructions.');
+ list.push('Every claim should be substantiable.');
+ }
+
+ list.push('No em dashes in casual contexts. Use a period or restructure.');
+ list.push('Spell out numbers under 10 in prose. Use numerals for data.');
+ list.push('One idea per sentence. Split if in doubt.');
+
+ return list;
+}
+
+// ── Typography ────────────────────────────────────────────────────────────────
+
+export const TYPE_SCALE: TypographyToken[] = [
+ { label: 'Display', size: '56px', weight: '700', lineHeight: '1.1', usage: 'Hero headlines, major landing sections' },
+ { label: 'Heading 1', size: '40px', weight: '700', lineHeight: '1.2', usage: 'Page titles, primary headers' },
+ { label: 'Heading 2', size: '28px', weight: '600', lineHeight: '1.25', usage: 'Section headers, card titles' },
+ { label: 'Heading 3', size: '20px', weight: '600', lineHeight: '1.3', usage: 'Sub-section headers, feature titles' },
+ { label: 'Body Large', size: '18px', weight: '400', lineHeight: '1.6', usage: 'Lead paragraphs, key descriptions' },
+ { label: 'Body', size: '16px', weight: '400', lineHeight: '1.65', usage: 'Default body copy' },
+ { label: 'Caption', size: '13px', weight: '400', lineHeight: '1.5', usage: 'Meta info, timestamps, helper text' },
+ { label: 'Label', size: '11px', weight: '600', lineHeight: '1.4', usage: 'UI labels, tags, overlines' },
+];
+
+type FontPair = { primary: string; secondary: string; mono: string; pairNote: string };
+
+const FONT_PAIRS: Record<string, FontPair[]> = {
+ developer: [
+ { primary: 'Geist', secondary: 'Geist', mono: 'Geist Mono', pairNote: 'Single-family system. Clean, neutral, interface-optimized.' },
+ { primary: 'Inter', secondary: 'Inter', mono: 'JetBrains Mono', pairNote: 'Inter for all UI copy; JetBrains Mono for code.' },
+ { primary: 'IBM Plex Sans', secondary: 'IBM Plex Sans', mono: 'IBM Plex Mono', pairNote: 'IBM Plex family — coherent, technical, widely legible.' },
+ ],
+ creative: [
+ { primary: 'Playfair Display', secondary: 'Lato', mono: 'Courier Prime', pairNote: 'High-contrast editorial serif for display; Lato for body.' },
+ { primary: 'Fraunces', secondary: 'DM Sans', mono: 'DM Mono', pairNote: 'Optical-size serif for headlines; DM Sans for body. Expressive and modern.' },
+ { primary: 'Cormorant Garamond', secondary: 'Nunito Sans', mono: 'Courier Prime', pairNote: 'Refined luxury serif for display; Nunito Sans for readable body.' },
+ ],
+ product: [
+ { primary: 'Plus Jakarta Sans', secondary: 'Plus Jakarta Sans', mono: 'DM Mono', pairNote: 'Jakarta Sans at varying weights; DM Mono for data and code.' },
+ { primary: 'Inter', secondary: 'Inter', mono: 'Fira Code', pairNote: 'Inter throughout — modern, neutral, excellent hinting.' },
+ { primary: 'Manrope', secondary: 'Manrope', mono: 'JetBrains Mono', pairNote: 'Geometric Manrope for all UI; JetBrains Mono for code blocks.' },
+ ],
+ services: [
+ { primary: 'Libre Baskerville', secondary: 'Source Sans 3', mono: 'Source Code Pro', pairNote: 'Baskerville for authority and trust; Source Sans for approachable body.' },
+ { primary: 'Merriweather', secondary: 'Open Sans', mono: 'Roboto Mono', pairNote: 'Merriweather for credibility; Open Sans keeps body warm.' },
+ { primary: 'Lora', secondary: 'Nunito Sans', mono: 'Courier Prime', pairNote: 'Lora brings warmth to headlines; Nunito Sans lightens the reading.' },
+ ],
+ personal: [
+ { primary: 'Lora', secondary: 'Nunito', mono: 'DM Mono', pairNote: 'Lora for expressive headlines; Nunito for friendly body copy.' },
+ { primary: 'DM Serif Display', secondary: 'DM Sans', mono: 'DM Mono', pairNote: 'Unified DM family. Serif display for character; sans for clarity.' },
+ { primary: 'Playfair Display', secondary: 'Source Sans 3', mono: 'Courier Prime', pairNote: 'Playfair adds personality; Source Sans 3 grounds body text.' },
+ ],
+ general: [
+ { primary: 'Inter', secondary: 'Inter', mono: 'JetBrains Mono', pairNote: 'Inter throughout with weight variation. Universal starting point.' },
+ { primary: 'Plus Jakarta Sans', secondary: 'Lora', mono: 'Fira Code', pairNote: 'Geometric sans for UI; Lora serif for long-form content.' },
+ { primary: 'Manrope', secondary: 'Merriweather', mono: 'Source Code Pro', pairNote: 'Friendly geometric sans paired with a trusted editorial serif.' },
+ ],
+};
+
+function generateTypography(ctx: GenContext): Typography {
+ const pairs = FONT_PAIRS[ctx.catType] ?? FONT_PAIRS.general;
+ return { ...pick(pairs), scale: TYPE_SCALE };
+}
+
+// ── Color Palette ─────────────────────────────────────────────────────────────
+
+function makeSwatches(entries: [string, string, string][]): ColorSwatch[] {
+ return entries.map(([name, hex, role], i) => ({ id: `s${i}`, name, hex, role }));
+}
+
+function generateColorPalette(ctx: GenContext): ColorPalette {
+ type SwatchEntry = [string, string, string]; // [name, hex, role]
+ type PaletteSet = SwatchEntry[][];
+
+ const palettes: Record<string, PaletteSet> = {
+ developer: [
+ [
+ ['Background', '#0d1117', 'background'],
+ ['Surface', '#161b22', 'neutral'],
+ ['Border', '#30363d', 'neutral'],
+ ['Primary', '#58a6ff', 'primary'],
+ ['Accent', '#3fb950', 'accent'],
+ ['Text', '#f0f6fc', 'text'],
+ ],
+ [
+ ['Background', '#0a0010', 'background'],
+ ['Surface', '#160025', 'neutral'],
+ ['Neutral', '#2d1f3d', 'neutral'],
+ ['Primary', '#7c3aed', 'primary'],
+ ['Accent', '#a78bfa', 'accent'],
+ ['Text', '#e2d9f3', 'text'],
+ ],
+ [
+ ['Background', '#0a0a0a', 'background'],
+ ['Surface', '#141414', 'neutral'],
+ ['Neutral', '#292929', 'neutral'],
+ ['Primary', '#e5e5e5', 'primary'],
+ ['Accent', '#c9a96e', 'accent'],
+ ['Text', '#f5f5f5', 'text'],
+ ],
+ ],
+ creative: [
+ [
+ ['Background', '#faf7f2', 'background'],
+ ['Surface', '#f5efe4', 'neutral'],
+ ['Neutral', '#e8d9c4', 'neutral'],
+ ['Primary', '#c07850', 'primary'],
+ ['Accent', '#4a7c5f', 'accent'],
+ ['Text', '#1a1410', 'text'],
+ ],
+ [
+ ['Background', '#0f0f0f', 'background'],
+ ['Surface', '#1a1a1a', 'neutral'],
+ ['Neutral', '#2e2e2e', 'neutral'],
+ ['Primary', '#ff6b35', 'primary'],
+ ['Accent', '#ffd700', 'accent'],
+ ['Text', '#f8f8f8', 'text'],
+ ],
+ [
+ ['Background', '#f8f4ef', 'background'],
+ ['Surface', '#efe9e0', 'neutral'],
+ ['Neutral', '#d4c4b0', 'neutral'],
+ ['Primary', '#8b5e3c', 'primary'],
+ ['Accent', '#6b8f71', 'accent'],
+ ['Text', '#2c2018', 'text'],
+ ],
+ ],
+ product: [
+ [
+ ['Background', '#fafbfc', 'background'],
+ ['Surface', '#f0f4f8', 'neutral'],
+ ['Neutral', '#d1dce8', 'neutral'],
+ ['Primary', '#2563eb', 'primary'],
+ ['Accent', '#7c3aed', 'accent'],
+ ['Text', '#1e293b', 'text'],
+ ],
+ [
+ ['Background', '#0f172a', 'background'],
+ ['Surface', '#1e293b', 'neutral'],
+ ['Neutral', '#334155', 'neutral'],
+ ['Primary', '#6366f1', 'primary'],
+ ['Accent', '#22d3ee', 'accent'],
+ ['Text', '#f1f5f9', 'text'],
+ ],
+ [
+ ['Background', '#f0fafa', 'background'],
+ ['Surface', '#e0f5f5', 'neutral'],
+ ['Neutral', '#b2dede', 'neutral'],
+ ['Primary', '#0d9488', 'primary'],
+ ['Accent', '#f59e0b', 'accent'],
+ ['Text', '#134e4a', 'text'],
+ ],
+ ],
+ services: [
+ [
+ ['Background', '#f8fafd', 'background'],
+ ['Surface', '#edf2fa', 'neutral'],
+ ['Neutral', '#ccd9ee', 'neutral'],
+ ['Primary', '#1d4ed8', 'primary'],
+ ['Accent', '#0f9e6e', 'accent'],
+ ['Text', '#1a2038', 'text'],
+ ],
+ [
+ ['Background', '#0c1421', 'background'],
+ ['Surface', '#152035', 'neutral'],
+ ['Neutral', '#243450', 'neutral'],
+ ['Primary', '#3b82f6', 'primary'],
+ ['Accent', '#34d399', 'accent'],
+ ['Text', '#f8fafc', 'text'],
+ ],
+ [
+ ['Background', '#faf8f5', 'background'],
+ ['Surface', '#f0ebe0', 'neutral'],
+ ['Neutral', '#d9cdb8', 'neutral'],
+ ['Primary', '#78523a', 'primary'],
+ ['Accent', '#2d6a4f', 'accent'],
+ ['Text', '#1c1410', 'text'],
+ ],
+ ],
+ personal: [
+ [
+ ['Background', '#fffef9', 'background'],
+ ['Surface', '#fdf8ee', 'neutral'],
+ ['Neutral', '#f0e6cc', 'neutral'],
+ ['Primary', '#c9a96e', 'primary'],
+ ['Accent', '#7c9e87', 'accent'],
+ ['Text', '#2d2d2d', 'text'],
+ ],
+ [
+ ['Background', '#fafafa', 'background'],
+ ['Surface', '#f5f5f5', 'neutral'],
+ ['Neutral', '#e5e5e5', 'neutral'],
+ ['Primary', '#171717', 'primary'],
+ ['Accent', '#737373', 'accent'],
+ ['Text', '#404040', 'text'],
+ ],
+ [
+ ['Background', '#f9f8ff', 'background'],
+ ['Surface', '#f0eeff', 'neutral'],
+ ['Neutral', '#ddd8f7', 'neutral'],
+ ['Primary', '#4f46e5', 'primary'],
+ ['Accent', '#ec4899', 'accent'],
+ ['Text', '#1e1b4b', 'text'],
+ ],
+ ],
+ general: [
+ [
+ ['Background', '#111111', 'background'],
+ ['Surface', '#1a1a1a', 'neutral'],
+ ['Neutral', '#2e2e2e', 'neutral'],
+ ['Primary', '#c9a96e', 'primary'],
+ ['Accent', '#6b8f71', 'accent'],
+ ['Text', '#dedede', 'text'],
+ ],
+ [
+ ['Background', '#ffffff', 'background'],
+ ['Surface', '#f5f5f5', 'neutral'],
+ ['Neutral', '#e0e0e0', 'neutral'],
+ ['Primary', '#1a1a1a', 'primary'],
+ ['Accent', '#3b82f6', 'accent'],
+ ['Text', '#333333', 'text'],
+ ],
+ [
+ ['Background', '#0f0f1a', 'background'],
+ ['Surface', '#1a1a2e', 'neutral'],
+ ['Neutral', '#252545', 'neutral'],
+ ['Primary', '#4f8ef7', 'primary'],
+ ['Accent', '#a78bfa', 'accent'],
+ ['Text', '#e2e8f0', 'text'],
+ ],
+ ],
+ };
+
+ const pool = palettes[ctx.catType] ?? palettes.general;
+ return { swatches: makeSwatches(pick(pool) as SwatchEntry[]) };
+}
+
+// ── Entry point ───────────────────────────────────────────────────────────────
+
+export function generate(inputs: BrandInputs): BrandOutputs {
+ const ctx = buildContext(inputs);
+
+ return {
+ overview: generateOverview(ctx),
+ positioning: generatePositioning(ctx),
+ tone: generateToneGuidance(ctx),
+ titles: generateTitles(ctx),
+ subtitles: generateSubtitles(ctx),
+ taglines: generateTaglines(ctx),
+ visualDirections: generateVisualDirections(ctx),
+ palette: generateColorPalette(ctx),
+ typography: generateTypography(ctx),
+ logoConcepts: generateLogoConcepts(ctx),
+ usageExamples: generateUsageExamples(ctx),
+ constraints: generateConstraints(ctx),
+ };
+}