krz/brand-bench

A dynamic brand documentation generator with Ollama integration.

clone: git clone https://gitbay.org/krz/brand-bench.git

main: src/lib/export.ts · raw

  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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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 &amp; 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}