krz/brand-bench
A dynamic brand documentation generator with Ollama integration.
clone: git clone https://gitbay.org/krz/brand-bench.git
1import type { BrandInputs, BrandOutputs } from '../types';
2
3export function toMarkdown(inputs: BrandInputs, outputs: BrandOutputs): string {
4 const lines: string[] = [];
5
6 lines.push(`# Brand Package — ${inputs.name || 'Untitled'}`);
7 lines.push('');
8 lines.push(`*Generated ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}*`);
9 lines.push('');
10 lines.push('---');
11 lines.push('');
12
13 lines.push('## Overview');
14 lines.push('');
15 lines.push(outputs.overview);
16 lines.push('');
17
18 lines.push('## Positioning');
19 lines.push('');
20 lines.push(outputs.positioning);
21 lines.push('');
22
23 lines.push('## Tone & Voice');
24 lines.push('');
25 lines.push(`**Attributes:** ${outputs.tone.attributes.join(', ')}`);
26 lines.push('');
27 lines.push(`**Voice:** ${outputs.tone.voiceNotes}`);
28 lines.push('');
29 if (outputs.tone.avoidList.length > 0) {
30 lines.push(`**Avoid:** ${outputs.tone.avoidList.join(', ')}`);
31 lines.push('');
32 }
33 if (outputs.tone.examplePhrases.length > 0) {
34 lines.push('**Example phrases:**');
35 outputs.tone.examplePhrases.forEach(p => lines.push(`- ${p}`));
36 lines.push('');
37 }
38
39 lines.push('## Messaging');
40 lines.push('');
41 lines.push('### Titles');
42 outputs.titles.forEach((t, i) => lines.push(`${i + 1}. ${t}`));
43 lines.push('');
44 lines.push('### Subtitles');
45 outputs.subtitles.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
46 lines.push('');
47 lines.push('### Taglines');
48 outputs.taglines.forEach((t, i) => lines.push(`${i + 1}. ${t}`));
49 lines.push('');
50
51 lines.push('## Color Palette');
52 lines.push('');
53 outputs.palette.swatches.forEach(s => {
54 lines.push(`- **${s.name}** — \`${s.hex}\` *(${s.role})*`);
55 });
56 lines.push('');
57
58 lines.push('## Typography');
59 lines.push('');
60 lines.push(`**Primary:** ${outputs.typography.primary}`);
61 if (outputs.typography.secondary !== outputs.typography.primary)
62 lines.push(`**Secondary:** ${outputs.typography.secondary}`);
63 lines.push(`**Monospace:** ${outputs.typography.mono}`);
64 lines.push('');
65 lines.push(`*${outputs.typography.pairNote}*`);
66 lines.push('');
67 lines.push('| Style | Size | Weight | Usage |');
68 lines.push('|-------|------|--------|-------|');
69 outputs.typography.scale.forEach(t => {
70 lines.push(`| ${t.label} | ${t.size} | ${t.weight} | ${t.usage} |`);
71 });
72 lines.push('');
73
74 lines.push('## Visual Direction');
75 lines.push('');
76 outputs.visualDirections.forEach(dir => {
77 lines.push(`### ${dir.name}`);
78 lines.push('');
79 lines.push(dir.description);
80 lines.push('');
81 lines.push(`**Palette:** ${dir.palette}`);
82 lines.push('');
83 lines.push(`**Typography:** ${dir.typography}`);
84 lines.push('');
85 lines.push(`**References:** ${dir.references}`);
86 lines.push('');
87 });
88
89 lines.push('## Logo Concepts');
90 lines.push('');
91 outputs.logoConcepts.forEach(lc => {
92 lines.push(`### ${lc.title}`);
93 lines.push('');
94 lines.push(lc.concept);
95 lines.push('');
96 lines.push(`**Mark:** ${lc.mark}`);
97 lines.push('');
98 lines.push(`**Execution:** ${lc.execution}`);
99 lines.push('');
100 });
101
102 lines.push('## Usage Examples');
103 lines.push('');
104 outputs.usageExamples.forEach(ex => {
105 lines.push(`### ${ex.context}`);
106 lines.push('');
107 lines.push('```');
108 lines.push(ex.text);
109 lines.push('```');
110 lines.push('');
111 });
112
113 lines.push('## Constraints');
114 lines.push('');
115 outputs.constraints.forEach(c => lines.push(`- ${c}`));
116 lines.push('');
117
118 lines.push('---');
119 lines.push('');
120 lines.push('*Brand Workbench*');
121
122 return lines.join('\n');
123}
124
125export function toJSON(inputs: BrandInputs, outputs: BrandOutputs): string {
126 return JSON.stringify(
127 {
128 project: {
129 name: inputs.name,
130 category: inputs.category,
131 purpose: inputs.purpose,
132 audience: inputs.audience,
133 },
134 brand: outputs,
135 meta: {
136 generated: new Date().toISOString(),
137 version: '1.0',
138 },
139 },
140 null,
141 2
142 );
143}
144
145export function toHTML(inputs: BrandInputs, outputs: BrandOutputs): string {
146 const date = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
147 const esc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
148
149 const swatchesHtml = outputs.palette.swatches.map(s => `
150 <div class="swatch">
151 <div class="swatch-color" style="background:${esc(s.hex)}"></div>
152 <div class="swatch-name">${esc(s.name)}</div>
153 <div class="swatch-hex">${esc(s.hex)}</div>
154 </div>`).join('');
155
156 const scaleRows = outputs.typography.scale.map(t => `
157 <tr><td>${esc(t.label)}</td><td>${esc(t.size)}</td><td>${esc(t.weight)}</td><td>${esc(t.usage)}</td></tr>`).join('');
158
159 const dirsHtml = outputs.visualDirections.map(d => `
160 <div class="card">
161 <h3>${esc(d.name)}</h3>
162 <p>${esc(d.description)}</p>
163 <dl>
164 <dt>Palette</dt><dd>${esc(d.palette)}</dd>
165 <dt>Typography</dt><dd>${esc(d.typography)}</dd>
166 <dt>References</dt><dd>${esc(d.references)}</dd>
167 </dl>
168 </div>`).join('');
169
170 const logosHtml = outputs.logoConcepts.map(l => `
171 <div class="card">
172 <h3>${esc(l.title)}</h3>
173 <p>${esc(l.concept)}</p>
174 <dl>
175 <dt>Mark</dt><dd>${esc(l.mark)}</dd>
176 <dt>Execution</dt><dd>${esc(l.execution)}</dd>
177 </dl>
178 </div>`).join('');
179
180 const usageHtml = outputs.usageExamples.map(ex => `
181 <div class="usage-item">
182 <div class="usage-label">${esc(ex.context)}</div>
183 <pre>${esc(ex.text)}</pre>
184 </div>`).join('');
185
186 const constraintsHtml = outputs.constraints.map(c => `<li>${esc(c)}</li>`).join('\n');
187
188 const secondaryRow = outputs.typography.secondary !== outputs.typography.primary
189 ? `<tr><td>Secondary</td><td>${esc(outputs.typography.secondary)}</td></tr>` : '';
190
191 return `<!DOCTYPE html>
192<html lang="en">
193<head>
194<meta charset="UTF-8">
195<meta name="viewport" content="width=device-width,initial-scale=1">
196<title>${esc(inputs.name || 'Brand')} — Brand Guidelines</title>
197<style>
198 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
199 html { font-size: 15px; }
200 body { font-family: ui-sans-serif, -apple-system, system-ui, sans-serif; color: #1a1a1a; background: #fff; line-height: 1.6; }
201 .guide { max-width: 860px; margin: 0 auto; padding: 60px 40px 100px; }
202 .guide-header { border-bottom: 2px solid #111; padding-bottom: 24px; margin-bottom: 48px; }
203 .guide-header h1 { font-size: 36px; font-weight: 700; letter-spacing: -0.02em; }
204 .guide-header .meta { font-size: 13px; color: #777; margin-top: 6px; }
205 h2 { font-size: 11px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: #999; margin-bottom: 20px; padding-bottom: 8px; border-bottom: 1px solid #e5e5e5; }
206 section { margin-bottom: 56px; }
207 p { color: #333; margin-bottom: 12px; }
208 .font-table, .scale-table { width: 100%; border-collapse: collapse; font-size: 14px; margin-bottom: 16px; }
209 .font-table td, .scale-table td, .scale-table th { padding: 8px 12px; border-bottom: 1px solid #eee; text-align: left; }
210 .scale-table th { font-size: 11px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.06em; }
211 .font-table td:first-child { color: #999; width: 120px; }
212 .pair-note { font-size: 13px; color: #777; font-style: italic; margin-top: 8px; }
213 .swatches { display: flex; flex-wrap: wrap; gap: 12px; }
214 .swatch { width: 100px; }
215 .swatch-color { width: 100px; height: 64px; border-radius: 6px; border: 1px solid rgba(0,0,0,.08); margin-bottom: 6px; }
216 .swatch-name { font-size: 12px; font-weight: 500; color: #333; }
217 .swatch-hex { font-size: 11px; font-family: ui-monospace, monospace; color: #777; }
218 .card { border: 1px solid #e5e5e5; border-radius: 8px; padding: 20px; margin-bottom: 16px; }
219 .card h3 { font-size: 15px; font-weight: 600; margin-bottom: 8px; }
220 .card p { font-size: 14px; color: #555; margin-bottom: 12px; }
221 dl { display: grid; grid-template-columns: 100px 1fr; gap: 4px 16px; font-size: 13px; }
222 dt { color: #999; font-weight: 500; }
223 dd { color: #444; }
224 .usage-item { margin-bottom: 20px; }
225 .usage-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #999; margin-bottom: 6px; }
226 pre { background: #f5f5f5; border-radius: 6px; padding: 14px 16px; font-size: 13px; font-family: ui-monospace, monospace; white-space: pre-wrap; color: #333; line-height: 1.6; }
227 ul { padding-left: 20px; }
228 li { font-size: 14px; color: #444; margin-bottom: 6px; }
229 .tone-pills { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
230 .tone-pill { background: #f0f0f0; border-radius: 4px; padding: 3px 10px; font-size: 12px; color: #555; }
231 .tone-row { font-size: 14px; color: #444; margin-bottom: 8px; }
232 .tone-row strong { color: #999; display: inline-block; min-width: 100px; }
233 .taglines ol, .titles ol { padding-left: 20px; }
234 .taglines li, .titles li { font-size: 15px; color: #333; margin-bottom: 8px; }
235</style>
236</head>
237<body>
238<div class="guide">
239 <div class="guide-header">
240 <h1>${esc(inputs.name || 'Brand')} Guidelines</h1>
241 <div class="meta">Generated ${date}${inputs.category ? ` · ${esc(inputs.category)}` : ''}${inputs.audience ? ` · ${esc(inputs.audience)}` : ''}</div>
242 </div>
243
244 <section>
245 <h2>Overview</h2>
246 <p>${esc(outputs.overview)}</p>
247 </section>
248
249 <section>
250 <h2>Positioning</h2>
251 <p>${esc(outputs.positioning)}</p>
252 </section>
253
254 <section>
255 <h2>Tone & Voice</h2>
256 <div class="tone-pills">${outputs.tone.attributes.map(a => `<span class="tone-pill">${esc(a)}</span>`).join('')}</div>
257 <div class="tone-row"><strong>Voice</strong>${esc(outputs.tone.voiceNotes)}</div>
258 ${outputs.tone.avoidList.length ? `<div class="tone-row"><strong>Avoid</strong>${esc(outputs.tone.avoidList.join(', '))}</div>` : ''}
259 </section>
260
261 <section>
262 <h2>Messaging</h2>
263 <div class="titles">
264 <p><strong>Titles</strong></p>
265 <ol>${outputs.titles.map(t => `<li>${esc(t)}</li>`).join('')}</ol>
266 </div>
267 <br>
268 <div class="taglines">
269 <p><strong>Taglines</strong></p>
270 <ol>${outputs.taglines.map(t => `<li>${esc(t)}</li>`).join('')}</ol>
271 </div>
272 </section>
273
274 <section>
275 <h2>Color Palette</h2>
276 <div class="swatches">${swatchesHtml}</div>
277 </section>
278
279 <section>
280 <h2>Typography</h2>
281 <table class="font-table">
282 <tr><td>Primary</td><td>${esc(outputs.typography.primary)}</td></tr>
283 ${secondaryRow}
284 <tr><td>Monospace</td><td>${esc(outputs.typography.mono)}</td></tr>
285 </table>
286 <p class="pair-note">${esc(outputs.typography.pairNote)}</p>
287 <br>
288 <table class="scale-table">
289 <thead><tr><th>Style</th><th>Size</th><th>Weight</th><th>Usage</th></tr></thead>
290 <tbody>${scaleRows}</tbody>
291 </table>
292 </section>
293
294 <section>
295 <h2>Visual Direction</h2>
296 ${dirsHtml}
297 </section>
298
299 <section>
300 <h2>Logo Concepts</h2>
301 ${logosHtml}
302 </section>
303
304 <section>
305 <h2>Usage Examples</h2>
306 ${usageHtml}
307 </section>
308
309 <section>
310 <h2>Constraints</h2>
311 <ul>${constraintsHtml}</ul>
312 </section>
313</div>
314</body>
315</html>`;
316}
317
318export function downloadFile(content: string, filename: string, mimeType: string): void {
319 const blob = new Blob([content], { type: mimeType });
320 const url = URL.createObjectURL(blob);
321 const a = document.createElement('a');
322 a.href = url;
323 a.download = filename;
324 a.click();
325 URL.revokeObjectURL(url);
326}