@stacklist-app/brandkit 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/engine.js ADDED
@@ -0,0 +1,847 @@
1
+ /**
2
+ * Brandkit Rendering Engine
3
+ * Loads config.json → bootstraps theme → renders all sections → wires interactivity
4
+ */
5
+ (async function () {
6
+ /* ================================================================
7
+ 0. Load config
8
+ ================================================================ */
9
+ var res = await fetch('./config.json');
10
+ var config = await res.json();
11
+ init(config);
12
+
13
+ function init(cfg) {
14
+ var copyFormat = localStorage.getItem('brandkit-copy-format') || 'hex';
15
+
16
+ /* ==============================================================
17
+ HTML escape helper — prevents XSS from config values
18
+ ============================================================== */
19
+ function esc(s) {
20
+ return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;')
21
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
22
+ }
23
+
24
+ /* ==============================================================
25
+ BOOTSTRAP — inject fonts + CSS variables before any rendering
26
+ ============================================================== */
27
+ function bootstrap() {
28
+ // Inject Google Fonts
29
+ if (cfg.fonts) {
30
+ var families = [];
31
+ if (cfg.fonts.display && cfg.fonts.display.googleImport) families.push(cfg.fonts.display.googleImport);
32
+ if (cfg.fonts.body && cfg.fonts.body.googleImport) families.push(cfg.fonts.body.googleImport);
33
+ if (families.length) {
34
+ var link = document.createElement('link');
35
+ link.rel = 'stylesheet';
36
+ link.href = 'https://fonts.googleapis.com/css2?family=' + families.join('&family=') + '&display=swap';
37
+ document.head.appendChild(link);
38
+ }
39
+ }
40
+
41
+ // Inject CSS custom properties from theme
42
+ if (cfg.theme) {
43
+ var vars = [];
44
+ var keys = Object.keys(cfg.theme);
45
+ for (var i = 0; i < keys.length; i++) {
46
+ vars.push(' ' + keys[i] + ': ' + cfg.theme[keys[i]] + ';');
47
+ }
48
+ // Add font variables from config
49
+ if (cfg.fonts) {
50
+ if (cfg.fonts.display) vars.push(" --font-display: '" + cfg.fonts.display.family + "', sans-serif;");
51
+ if (cfg.fonts.body) vars.push(" --font-body: '" + cfg.fonts.body.family + "', sans-serif;");
52
+ }
53
+ var style = document.createElement('style');
54
+ style.setAttribute('data-brandkit-theme', '');
55
+ style.textContent = ':root {\n' + vars.join('\n') + '\n}';
56
+ document.head.appendChild(style);
57
+ }
58
+
59
+ // Set page title
60
+ if (cfg.brand && cfg.brand.displayName) {
61
+ document.title = cfg.brand.displayName + ' \u2014 Brand Guide';
62
+ }
63
+ }
64
+
65
+ /* ==============================================================
66
+ 1. Toast
67
+ ============================================================== */
68
+ function toast(message) {
69
+ var container = document.getElementById('toast-container');
70
+ if (!container) return;
71
+ var el = document.createElement('div');
72
+ el.className = 'toast';
73
+ el.innerHTML =
74
+ '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">' +
75
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>' +
76
+ '</svg>' +
77
+ message;
78
+ container.appendChild(el);
79
+ setTimeout(function () {
80
+ el.classList.add('out');
81
+ setTimeout(function () { el.remove(); }, 200);
82
+ }, 2000);
83
+ }
84
+
85
+ async function copyText(text) {
86
+ try {
87
+ await navigator.clipboard.writeText(text);
88
+ toast('Copied: ' + (text.length > 50 ? text.slice(0, 47) + '...' : text));
89
+ } catch (_) {
90
+ toast('Copy failed \u2014 try from localhost or HTTPS');
91
+ }
92
+ }
93
+
94
+ /* ==============================================================
95
+ 2. Render navigation
96
+ ============================================================== */
97
+ function renderNav() {
98
+ var navEl = document.getElementById('nav');
99
+ if (!navEl || !cfg.nav) return;
100
+ navEl.innerHTML = cfg.nav.map(function (group) {
101
+ var header = '<li class="nav-group">' + esc(group.group) + '</li>';
102
+ var items = group.items.map(function (item) {
103
+ return '<li class="nav-group-items"><a href="#' + esc(item.id) + '">' + esc(item.label) + '</a></li>';
104
+ }).join('');
105
+ return header + items;
106
+ }).join('');
107
+ }
108
+
109
+ /* ==============================================================
110
+ 3. Render colors
111
+ ============================================================== */
112
+ function renderColorGrid(colors, containerId) {
113
+ var container = document.getElementById(containerId);
114
+ if (!container) return;
115
+ if (!colors || !colors.length) {
116
+ container.innerHTML = '<div class="empty-state">No colors configured</div>';
117
+ return;
118
+ }
119
+ container.innerHTML = colors.map(function (c) {
120
+ // Field aliases for resilience
121
+ var name = c.name || c.label || '';
122
+ var hex = c.hex || '';
123
+ var oklch = c.oklch || '';
124
+ var cssVar = c.cssVar || c.token || '';
125
+ var role = c.role || c.usage || '';
126
+ var isLight = c.light || (hex && parseInt(hex.slice(1), 16) > 0xAAAAAA);
127
+ var valueDisplay = oklch ? (esc(hex) + ' \u00B7 ' + esc(oklch)) : esc(hex);
128
+ return (
129
+ '<div class="color-card">' +
130
+ '<div class="color-swatch copyable ' + (isLight ? 'has-border' : '') + '"' +
131
+ ' style="background:' + esc(hex) + ';"' +
132
+ ' data-hex="' + esc(hex) + '"' +
133
+ ' data-oklch="' + esc(oklch) + '"' +
134
+ ' data-css-var="' + esc(cssVar) + '"' +
135
+ ' tabindex="0" role="button" aria-label="Copy ' + esc(name) + ' color value">' +
136
+ '<div class="copy-hint"><span>Click to copy</span></div>' +
137
+ '</div>' +
138
+ '<div class="color-name">' + esc(name) + '</div>' +
139
+ '<div class="color-value copyable" data-copy="' + esc(hex) + '">' + valueDisplay + '</div>' +
140
+ '<div class="color-role">' + esc(role) + '</div>' +
141
+ '</div>'
142
+ );
143
+ }).join('');
144
+ }
145
+
146
+ function renderColors() {
147
+ if (!cfg.colors) return;
148
+ // Support both { label, items } objects and plain arrays
149
+ var groups = ['brand', 'neutrals', 'semantic'];
150
+ groups.forEach(function (key) {
151
+ var group = cfg.colors[key];
152
+ if (!group) return;
153
+ var items = group.items || group;
154
+ var label = group.label || key.charAt(0).toUpperCase() + key.slice(1);
155
+ // Set the group label if the container's preceding label exists
156
+ var labelEl = document.getElementById('colors-' + key + '-label');
157
+ if (labelEl) labelEl.textContent = label;
158
+ renderColorGrid(items, 'colors-' + key);
159
+ });
160
+ }
161
+
162
+ /* ==============================================================
163
+ 4. Render gradients
164
+ ============================================================== */
165
+ function renderGradients() {
166
+ var stopsContainer = document.getElementById('gradient-stops');
167
+ if (!stopsContainer || !cfg.gradients || !cfg.gradients.length) return;
168
+ var brand = cfg.gradients[0];
169
+ if (brand && brand.stops && brand.stops.length) {
170
+ stopsContainer.innerHTML = brand.stops.map(function (s) {
171
+ return (
172
+ '<div class="gradient-stop">' +
173
+ '<div class="gradient-stop-dot" style="background:' + s.color + ';"></div>' +
174
+ s.name + ' \u00B7 ' + s.position +
175
+ '</div>'
176
+ );
177
+ }).join('');
178
+ }
179
+ }
180
+
181
+ /* ==============================================================
182
+ 5. Render logos
183
+ ============================================================== */
184
+ function renderLogos() {
185
+ var grid = document.getElementById('logos-grid');
186
+ if (!grid) return;
187
+ if (!cfg.logos || !cfg.logos.length) {
188
+ grid.innerHTML = '<div class="empty-state">No logos configured. Add logo files to the logos/ directory and update config.json.</div>';
189
+ return;
190
+ }
191
+
192
+ var downloadIcon =
193
+ '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">' +
194
+ '<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v12m0 0l-4-4m4 4l4-4M4 18h16"/>' +
195
+ '</svg>';
196
+
197
+ grid.innerHTML = cfg.logos.map(function (logo, idx) {
198
+ // Determine background class
199
+ var bgClass = '';
200
+ var bgStyle = '';
201
+ if (logo.background === 'light') {
202
+ bgClass = 'on-light';
203
+ bgStyle = 'background:#FFFFFF; border:1px solid var(--mist);';
204
+ } else if (logo.background === 'dark') {
205
+ bgClass = 'on-dark';
206
+ bgStyle = 'background:var(--ink);';
207
+ } else if (logo.background === 'gradient') {
208
+ bgClass = 'on-gradient';
209
+ bgStyle = 'background:var(--gradient-brand);';
210
+ }
211
+
212
+ // Available formats
213
+ var formats = Object.keys(logo.variants);
214
+ var firstFormat = formats[0] || 'svg';
215
+
216
+ // Preview image: use svg first, then png, then jpg
217
+ var previewSrc = logo.variants.svg || logo.variants.png || logo.variants.jpg || '';
218
+
219
+ // Format toggle buttons
220
+ var formatToggles = formats.map(function (fmt, fi) {
221
+ return (
222
+ '<button class="logo-format-btn' + (fi === 0 ? ' active' : '') + '"' +
223
+ ' data-logo-idx="' + idx + '"' +
224
+ ' data-format="' + fmt + '">' +
225
+ fmt.toUpperCase() +
226
+ '</button>'
227
+ );
228
+ }).join('');
229
+
230
+ // Size picker (only visible for raster formats)
231
+ var sizeOptions = (cfg.logoSizes || []).map(function (s) {
232
+ return '<option value="' + (s.width || '') + '">' + s.label + '</option>';
233
+ }).join('');
234
+
235
+ var isFirstRaster = (firstFormat !== 'svg');
236
+
237
+ var sizePicker =
238
+ '<select class="logo-size-picker" data-logo-idx="' + idx + '"' +
239
+ (isFirstRaster ? '' : ' style="display:none;"') + '>' +
240
+ sizeOptions +
241
+ '</select>';
242
+
243
+ return (
244
+ '<div class="logo-card ' + bgClass + '" style="' + bgStyle + '" data-logo-idx="' + idx + '">' +
245
+ '<img src="' + previewSrc + '" alt="' + logo.name + '">' +
246
+ '<div class="logo-name">' + logo.name + '</div>' +
247
+ '<div class="logo-description">' + (logo.description || '') + '</div>' +
248
+ '<div class="logo-controls">' +
249
+ '<div class="logo-format-toggle">' + formatToggles + '</div>' +
250
+ sizePicker +
251
+ '<button class="logo-download-btn" data-logo-idx="' + idx + '">' +
252
+ downloadIcon + ' Download' +
253
+ '</button>' +
254
+ '</div>' +
255
+ '</div>'
256
+ );
257
+ }).join('');
258
+
259
+ // Wire up format toggles
260
+ grid.addEventListener('click', function (e) {
261
+ var fmtBtn = e.target.closest('.logo-format-btn');
262
+ if (fmtBtn) {
263
+ var card = fmtBtn.closest('.logo-card');
264
+ card.querySelectorAll('.logo-format-btn').forEach(function (b) {
265
+ b.classList.toggle('active', b === fmtBtn);
266
+ });
267
+ var picker = card.querySelector('.logo-size-picker');
268
+ if (picker) {
269
+ picker.style.display = (fmtBtn.dataset.format === 'svg') ? 'none' : '';
270
+ }
271
+ return;
272
+ }
273
+
274
+ // Download button
275
+ var dlBtn = e.target.closest('.logo-download-btn');
276
+ if (dlBtn) {
277
+ var logoIndex = parseInt(dlBtn.dataset.logoIdx, 10);
278
+ var logoData = cfg.logos[logoIndex];
279
+ var cardEl = dlBtn.closest('.logo-card');
280
+
281
+ var activeBtn = cardEl.querySelector('.logo-format-btn.active');
282
+ var format = activeBtn ? activeBtn.dataset.format : Object.keys(logoData.variants)[0];
283
+
284
+ var filePath = logoData.variants[format];
285
+ if (!filePath) return;
286
+
287
+ var slug = cfg.brand.name || 'brand';
288
+ var namePart = logoData.name.toLowerCase()
289
+ .replace(/[^a-z0-9]+/g, '-')
290
+ .replace(/(^-|-$)/g, '');
291
+
292
+ if (format === 'svg') {
293
+ triggerDirectDownload(filePath, slug + '-' + namePart + '.svg');
294
+ return;
295
+ }
296
+
297
+ var sizePicker2 = cardEl.querySelector('.logo-size-picker');
298
+ var targetWidth = sizePicker2 ? parseInt(sizePicker2.value, 10) : NaN;
299
+
300
+ if (!targetWidth || isNaN(targetWidth)) {
301
+ var ext = format === 'png' ? '.png' : '.jpg';
302
+ triggerDirectDownload(filePath, slug + '-' + namePart + ext);
303
+ } else {
304
+ resizeAndDownload(filePath, format, targetWidth, slug + '-' + namePart + '-' + targetWidth + 'px');
305
+ }
306
+ }
307
+ });
308
+ }
309
+
310
+ function triggerDirectDownload(href, filename) {
311
+ var a = document.createElement('a');
312
+ a.href = href;
313
+ a.download = filename;
314
+ document.body.appendChild(a);
315
+ a.click();
316
+ document.body.removeChild(a);
317
+ }
318
+
319
+ function resizeAndDownload(src, format, targetWidth, filenameBase) {
320
+ var img = new Image();
321
+ img.crossOrigin = 'anonymous';
322
+ img.onload = function () {
323
+ var ratio = img.naturalHeight / img.naturalWidth;
324
+ var targetHeight = Math.round(targetWidth * ratio);
325
+ var canvas = document.createElement('canvas');
326
+ canvas.width = targetWidth;
327
+ canvas.height = targetHeight;
328
+ var ctx = canvas.getContext('2d');
329
+ ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
330
+ var mimeType = format === 'png' ? 'image/png' : 'image/jpeg';
331
+ var ext = format === 'png' ? '.png' : '.jpg';
332
+ canvas.toBlob(function (blob) {
333
+ if (!blob) return;
334
+ var url = URL.createObjectURL(blob);
335
+ triggerDirectDownload(url, filenameBase + ext);
336
+ setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
337
+ }, mimeType, 0.92);
338
+ };
339
+ img.onerror = function () {
340
+ toast('Failed to load image for resize');
341
+ };
342
+ img.src = src;
343
+ }
344
+
345
+ /* ==============================================================
346
+ 6. Render typography
347
+ ============================================================== */
348
+ function renderTypography() {
349
+ var typeScale = document.getElementById('type-scale');
350
+ if (!typeScale) return;
351
+ if (!cfg.typography || !cfg.typography.length) {
352
+ typeScale.innerHTML = '<div class="empty-state">No type scale configured</div>';
353
+ return;
354
+ }
355
+
356
+ typeScale.innerHTML = cfg.typography.map(function (t) {
357
+ var family = t.font === 'display'
358
+ ? cfg.fonts.display.family
359
+ : cfg.fonts.body.family;
360
+
361
+ var specParts = [t.size, t.weight, t.tracking !== '0' ? t.tracking : null, t.leading ? t.leading + ' leading' : null, family].filter(Boolean);
362
+
363
+ var displaySize = Math.min(parseInt(t.size, 10), 48);
364
+ var inlineStyle =
365
+ "font-family:'" + family + "',sans-serif;" +
366
+ 'font-size:' + displaySize + 'px;' +
367
+ 'font-weight:' + t.weight + ';' +
368
+ 'letter-spacing:' + t.tracking + ';' +
369
+ 'line-height:' + t.leading + ';' +
370
+ (t.uppercase ? 'text-transform:uppercase;' : '') +
371
+ (t.name === 'Body SM' ? 'color:var(--graphite);' : '') +
372
+ (t.name === 'Caption' || t.name === 'Overline' ? 'color:var(--slate);' : '');
373
+
374
+ return (
375
+ '<div class="type-row">' +
376
+ '<span class="type-row-label">' + t.name + '</span>' +
377
+ '<span class="type-row-sample" style="' + inlineStyle + '">' + t.sample + '</span>' +
378
+ '<span class="type-row-spec">' + specParts.join(' \u00B7 ') + '</span>' +
379
+ '</div>'
380
+ );
381
+ }).join('');
382
+ }
383
+
384
+ /* ==============================================================
385
+ 7. Render voice
386
+ ============================================================== */
387
+ function renderVoice() {
388
+ var voiceGrid = document.getElementById('voice-grid');
389
+ if (!voiceGrid || !cfg.voice) return;
390
+
391
+ voiceGrid.innerHTML =
392
+ '<div class="voice-card do">' +
393
+ '<div class="voice-card-label">' + cfg.brand.displayName + ' says</div>' +
394
+ cfg.voice.do.map(function (v) {
395
+ return '<div class="voice-example">' + v + '</div>';
396
+ }).join('') +
397
+ '</div>' +
398
+ '<div class="voice-card dont">' +
399
+ '<div class="voice-card-label">' + cfg.brand.displayName + ' never says</div>' +
400
+ cfg.voice.dont.map(function (v) {
401
+ return '<div class="voice-example">' + v + '</div>';
402
+ }).join('') +
403
+ '</div>';
404
+ }
405
+
406
+ /* ==============================================================
407
+ 8. Render components
408
+ ============================================================== */
409
+ function renderComponents() {
410
+ var container = document.getElementById('components-content');
411
+ if (!container) return;
412
+ if (!cfg.components) {
413
+ container.innerHTML = '<div class="empty-state">No component patterns configured</div>';
414
+ return;
415
+ }
416
+
417
+ var html = '';
418
+
419
+ // Buttons
420
+ if (cfg.components.buttons) {
421
+ cfg.components.buttons.forEach(function (group) {
422
+ html += '<div class="component-label">' + group.variant + '</div>';
423
+ group.items.forEach(function (item) {
424
+ html += '<div class="component-row">';
425
+ item.sizes.forEach(function (size, i) {
426
+ html +=
427
+ '<button class="btn ' + item.class + ' btn-' + size + '">' +
428
+ item.labels[i] +
429
+ '</button>';
430
+ });
431
+ html += '</div>';
432
+ });
433
+ });
434
+ }
435
+
436
+ // Cards
437
+ if (cfg.components.cards) {
438
+ html += '<div class="component-label" style="margin-top:28px;">Cards \u2014 Light Background</div>';
439
+ html += '<div class="card-demo-grid">';
440
+ cfg.components.cards.forEach(function (card) {
441
+ var cardTitle = card.title || card.label || '';
442
+ var cardDesc = card.description || '';
443
+ var cardTag = card.tag || '';
444
+ html +=
445
+ '<div class="card-demo">' +
446
+ '<h4>' + cardTitle + '</h4>' +
447
+ '<p>' + cardDesc + '</p>' +
448
+ (cardTag ? '<span class="tag">' + cardTag + '</span>' : '') +
449
+ '</div>';
450
+ });
451
+ html += '</div>';
452
+ }
453
+
454
+ // Stats
455
+ if (cfg.components.stats) {
456
+ html += '<div class="component-label" style="margin-top:28px;">Stats \u2014 Dark Background</div>';
457
+ html += '<div class="dark-card-demo">';
458
+ html += '<div class="dark-section-label">By the numbers</div>';
459
+ html += '<div class="dark-card-demo-grid">';
460
+ cfg.components.stats.forEach(function (stat) {
461
+ html +=
462
+ '<div class="dark-card">' +
463
+ '<h4>' + stat.value + '</h4>' +
464
+ '<p>' + stat.label + '</p>' +
465
+ '</div>';
466
+ });
467
+ html += '</div></div>';
468
+ }
469
+
470
+ container.innerHTML = html;
471
+ }
472
+
473
+ /* ==============================================================
474
+ 9. Render spacing
475
+ ============================================================== */
476
+ function renderSpacing() {
477
+ var scale = document.getElementById('spacing-scale');
478
+ if (!scale) return;
479
+ var data = cfg.spacing || [
480
+ { px: 4, token: 'space-1' }, { px: 8, token: 'space-2' }, { px: 12, token: 'space-3' },
481
+ { px: 16, token: 'space-4' }, { px: 24, token: 'space-6' }, { px: 32, token: 'space-8' },
482
+ { px: 48, token: 'space-12' }, { px: 64, token: 'space-16' }, { px: 96, token: 'space-24' }
483
+ ];
484
+ scale.innerHTML = data.map(function (s) {
485
+ return (
486
+ '<div class="spacing-row">' +
487
+ '<span class="spacing-label">' + s.px + 'px</span>' +
488
+ '<div class="spacing-bar" style="width:' + Math.min(s.px * 4, 400) + 'px;">' +
489
+ '<span class="spacing-value">' + s.token + '</span>' +
490
+ '</div>' +
491
+ '</div>'
492
+ );
493
+ }).join('');
494
+ }
495
+
496
+ /* ==============================================================
497
+ 10. Render accessibility
498
+ ============================================================== */
499
+ function renderAccessibility() {
500
+ var grid = document.getElementById('a11y-grid');
501
+ if (!grid) return;
502
+ if (!cfg.accessibility || !cfg.accessibility.length) {
503
+ grid.innerHTML = '<div class="empty-state">No accessibility pairs configured</div>';
504
+ return;
505
+ }
506
+
507
+ grid.innerHTML = cfg.accessibility.map(function (a) {
508
+ // Field aliases
509
+ var bg = a.bg || a.background || '#FFFFFF';
510
+ var fg = a.fg || a.foreground || '#000000';
511
+ var bgName = a.bgName || '';
512
+ var fgName = a.fgName || '';
513
+ var rating = a.rating || a.level || '';
514
+ var ratio = a.ratio || '';
515
+ // Split "pair" field if bgName/fgName missing
516
+ if (!bgName && !fgName && a.pair) {
517
+ var parts = a.pair.split(' on ');
518
+ if (parts.length === 2) { fgName = parts[0]; bgName = parts[1]; }
519
+ }
520
+
521
+ var ratingClass =
522
+ (rating === 'AAA' || rating === 'AA') ? 'pass' :
523
+ rating === 'AA Large' ? 'large' : 'fail';
524
+ var border = a.border ? 'border:1px solid var(--mist);' : '';
525
+ var textSize = a.largeText ? 'font-size:20px;font-weight:600;' : '';
526
+
527
+ return (
528
+ '<div class="a11y-card" style="background:' + bg + ';color:' + fg + ';' + border + '">' +
529
+ '<div class="a11y-text" style="' + textSize + '">' + fgName + ' on ' + bgName + '</div>' +
530
+ '<div class="a11y-meta">' +
531
+ '<span class="a11y-ratio">' + ratio + '</span>' +
532
+ '<span class="a11y-badge ' + ratingClass + '">' + rating + '</span>' +
533
+ '</div>' +
534
+ '</div>'
535
+ );
536
+ }).join('');
537
+ }
538
+
539
+ /* ==============================================================
540
+ 11. Render CSS variables
541
+ ============================================================== */
542
+ function renderCSSVars() {
543
+ var codeBlock = document.getElementById('code-block');
544
+ if (!codeBlock) return;
545
+ if (!cfg.cssVariables || !cfg.cssVariables.length) {
546
+ codeBlock.innerHTML = '<span class="code-section">/* No CSS variables configured */</span>';
547
+ return;
548
+ }
549
+
550
+ codeBlock.innerHTML = cfg.cssVariables.map(function (section) {
551
+ var sectionName = section.section || section.name || 'Variables';
552
+ var vars = section.vars || [];
553
+ var sectionComment = '<span class="code-section">/* \u2500\u2500 ' + sectionName + ' \u2500\u2500 */</span>';
554
+ var lines = vars.map(function (v) {
555
+ // Field aliases
556
+ var prop = v.prop || v.var || v.name || '';
557
+ var value = v.value || '';
558
+ var comment = v.comment || v.usage || '';
559
+ var copyVal = prop + ': ' + value + ';';
560
+ var commentHtml = comment ? ' <span class="token-comment">/* ' + comment + ' */</span>' : '';
561
+ return (
562
+ '<span class="code-line" data-copy="' + copyVal.replace(/"/g, '&quot;') + '">' +
563
+ '<span class="token-prop">' + prop + '</span>: ' +
564
+ '<span class="token-value">' + value + '</span>;' +
565
+ commentHtml +
566
+ '</span>'
567
+ );
568
+ }).join('\n');
569
+ return sectionComment + '\n' + lines;
570
+ }).join('\n\n');
571
+ }
572
+
573
+ /* ==============================================================
574
+ 12. Render hierarchy (from config)
575
+ ============================================================== */
576
+ function renderHierarchy() {
577
+ var container = document.getElementById('hierarchy-content');
578
+ if (!container) return;
579
+ if (!cfg.hierarchy || !cfg.hierarchy.length) {
580
+ container.innerHTML = '<div class="empty-state">No text hierarchy configured</div>';
581
+ return;
582
+ }
583
+
584
+ var demoHtml = '<div class="hierarchy-demo">';
585
+ cfg.hierarchy.forEach(function (h) {
586
+ demoHtml += '<p class="' + h.class + '">' + h.description + '</p>';
587
+ });
588
+ demoHtml += '</div>';
589
+
590
+ var labelsHtml = '<div class="hierarchy-labels">';
591
+ cfg.hierarchy.forEach(function (h) {
592
+ labelsHtml +=
593
+ '<div class="hierarchy-label">' +
594
+ '<div class="hierarchy-dot" style="background: var(' + h.colorVar + ');"></div> ' +
595
+ h.colorName + ': ' + h.hex +
596
+ '</div>';
597
+ });
598
+ labelsHtml += '</div>';
599
+
600
+ container.innerHTML = demoHtml + labelsHtml;
601
+ }
602
+
603
+ /* ==============================================================
604
+ 13. Render section intros (from config)
605
+ ============================================================== */
606
+ function renderSectionIntros() {
607
+ if (!cfg.sections) return;
608
+ var mapping = {
609
+ 'gradients': 'section-intro-gradients',
610
+ 'logos': 'section-intro-logos',
611
+ 'components': 'section-intro-components',
612
+ 'spacing': 'section-intro-spacing',
613
+ 'variables': 'section-intro-variables'
614
+ };
615
+ var keys = Object.keys(mapping);
616
+ for (var i = 0; i < keys.length; i++) {
617
+ var el = document.getElementById(mapping[keys[i]]);
618
+ if (el && cfg.sections[keys[i]]) {
619
+ el.textContent = cfg.sections[keys[i]];
620
+ }
621
+ }
622
+ // Gradient text demo
623
+ var gradTextEl = document.getElementById('gradient-text-demo');
624
+ if (gradTextEl && cfg.sections.gradientTextDemo) {
625
+ gradTextEl.textContent = cfg.sections.gradientTextDemo;
626
+ }
627
+ }
628
+
629
+ /* ==============================================================
630
+ 14. Copy to clipboard (initCopy)
631
+ ============================================================== */
632
+ function initCopy() {
633
+ document.addEventListener('click', function (e) {
634
+ // Color swatch
635
+ var swatch = e.target.closest('.color-swatch.copyable');
636
+ if (swatch) {
637
+ var value = swatch.dataset[copyFormat] || swatch.dataset.hex;
638
+ copyText(value);
639
+ return;
640
+ }
641
+
642
+ // Color value text
643
+ var colorVal = e.target.closest('.color-value.copyable');
644
+ if (colorVal) {
645
+ copyText(colorVal.dataset.copy || colorVal.textContent.trim());
646
+ return;
647
+ }
648
+
649
+ // Gradient display
650
+ var gradientCopy = e.target.closest('.gradient-display.copyable');
651
+ if (gradientCopy) {
652
+ copyText(gradientCopy.dataset.copy);
653
+ return;
654
+ }
655
+
656
+ // Code lines
657
+ var codeLine = e.target.closest('.code-line');
658
+ if (codeLine) {
659
+ copyText(codeLine.dataset.copy);
660
+ return;
661
+ }
662
+ });
663
+ }
664
+
665
+ /* ==============================================================
666
+ 15. Copy format bar
667
+ ============================================================== */
668
+ function initFormatBar() {
669
+ var bar = document.querySelector('.copy-format-bar');
670
+ if (!bar) return;
671
+
672
+ // Restore saved format
673
+ bar.querySelectorAll('button').forEach(function (b) {
674
+ b.classList.toggle('active', b.dataset.format === copyFormat);
675
+ });
676
+
677
+ bar.addEventListener('click', function (e) {
678
+ var btn = e.target.closest('button');
679
+ if (!btn) return;
680
+ copyFormat = btn.dataset.format;
681
+ localStorage.setItem('brandkit-copy-format', copyFormat);
682
+ bar.querySelectorAll('button').forEach(function (b) {
683
+ b.classList.toggle('active', b === btn);
684
+ });
685
+ });
686
+ }
687
+
688
+ /* ==============================================================
689
+ 16. Section navigation (IntersectionObserver)
690
+ ============================================================== */
691
+ function initNav() {
692
+ var sections = document.querySelectorAll('.section[id]');
693
+ var navLinks = document.querySelectorAll('#nav a');
694
+
695
+ if (!sections.length || !navLinks.length) return;
696
+
697
+ var observer = new IntersectionObserver(function (entries) {
698
+ entries.forEach(function (entry) {
699
+ if (entry.isIntersecting) {
700
+ navLinks.forEach(function (link) {
701
+ link.classList.toggle('active', link.getAttribute('href') === '#' + entry.target.id);
702
+ });
703
+ }
704
+ });
705
+ }, { rootMargin: '-20% 0px -70% 0px' });
706
+
707
+ sections.forEach(function (section) { observer.observe(section); });
708
+
709
+ navLinks.forEach(function (link) {
710
+ link.addEventListener('click', function (e) {
711
+ e.preventDefault();
712
+ var target = document.querySelector(link.getAttribute('href'));
713
+ if (target) target.scrollIntoView({ behavior: 'smooth' });
714
+ });
715
+ });
716
+ }
717
+
718
+ /* ==============================================================
719
+ 17. Type tester
720
+ ============================================================== */
721
+ function initTypeTester() {
722
+ var testerFont = document.getElementById('type-tester-font');
723
+ var testerInput = document.getElementById('type-tester-input');
724
+ if (!testerFont || !testerInput) return;
725
+
726
+ testerFont.addEventListener('change', function () {
727
+ testerInput.style.fontFamily = "'" + testerFont.value + "', sans-serif";
728
+ });
729
+ }
730
+
731
+ /* ==============================================================
732
+ 18. Render shell (header, intro, footer, misc)
733
+ ============================================================== */
734
+ function renderShell() {
735
+ // Header
736
+ var wordmark = document.getElementById('header-wordmark');
737
+ if (wordmark) wordmark.textContent = cfg.brand.name;
738
+ var meta = document.getElementById('header-meta');
739
+ if (meta) meta.innerHTML = 'Web Style Guide v' + cfg.brand.version + '<br>' + cfg.brand.date;
740
+
741
+ // Intro
742
+ var intro = document.getElementById('intro');
743
+ if (intro) intro.textContent = cfg.brand.description;
744
+
745
+ // Voice intro
746
+ var voiceIntro = document.getElementById('voice-intro');
747
+ if (voiceIntro && cfg.voice) voiceIntro.textContent = cfg.voice.description;
748
+
749
+ // Footer
750
+ var footer = document.getElementById('footer');
751
+ if (footer) footer.innerHTML =
752
+ '<span>' + cfg.brand.url + ' \u00B7 ' + cfg.brand.byline + '</span>' +
753
+ '<span>Web Style Guide v' + cfg.brand.version + ' \u00B7 ' + cfg.brand.date + '</span>';
754
+
755
+ // Typography specimens — read descriptions from config
756
+ var typeDisplayName = document.getElementById('type-display-name');
757
+ if (typeDisplayName && cfg.fonts) typeDisplayName.textContent = cfg.fonts.display.family;
758
+ var typeDisplayDesc = document.getElementById('type-display-desc');
759
+ if (typeDisplayDesc && cfg.fonts && cfg.fonts.display.description) {
760
+ typeDisplayDesc.textContent = cfg.fonts.display.description;
761
+ }
762
+
763
+ var typeBodyName = document.getElementById('type-body-name');
764
+ if (typeBodyName && cfg.fonts) typeBodyName.textContent = cfg.fonts.body.family;
765
+ var typeBodyDesc = document.getElementById('type-body-desc');
766
+ if (typeBodyDesc && cfg.fonts && cfg.fonts.body.description) {
767
+ typeBodyDesc.textContent = cfg.fonts.body.description;
768
+ }
769
+
770
+ // Type tester font options
771
+ var testerFont = document.getElementById('type-tester-font');
772
+ var testerInput = document.getElementById('type-tester-input');
773
+ if (testerFont && cfg.fonts) {
774
+ testerFont.innerHTML =
775
+ '<option value="' + cfg.fonts.display.family + '">' + cfg.fonts.display.family + '</option>' +
776
+ '<option value="' + cfg.fonts.body.family + '">' + cfg.fonts.body.family + '</option>';
777
+ if (testerInput) {
778
+ testerInput.style.fontFamily = "'" + cfg.fonts.display.family + "', sans-serif";
779
+ }
780
+ }
781
+
782
+ // Gradient data-copy attributes, inline styles, and labels
783
+ var gradBrand = document.getElementById('gradient-brand-copy');
784
+ if (gradBrand && cfg.gradients && cfg.gradients[0]) {
785
+ gradBrand.dataset.copy = cfg.gradients[0].css;
786
+ gradBrand.style.background = cfg.gradients[0].css;
787
+ }
788
+ var gradBrandLabel = document.getElementById('gradient-brand-label');
789
+ if (gradBrandLabel && cfg.gradients && cfg.gradients[0]) {
790
+ var g0desc = cfg.gradients[0].description || cfg.gradients[0].usage || '';
791
+ gradBrandLabel.textContent = cfg.gradients[0].name + ' Gradient \u00B7 135\u00B0 \u00B7 ' + g0desc;
792
+ }
793
+
794
+ var gradSubtle = document.getElementById('gradient-subtle-copy');
795
+ if (gradSubtle && cfg.gradients && cfg.gradients[1]) {
796
+ gradSubtle.dataset.copy = cfg.gradients[1].css;
797
+ gradSubtle.style.background = cfg.gradients[1].css;
798
+ }
799
+ var gradSubtleLabel = document.getElementById('gradient-subtle-label');
800
+ if (gradSubtleLabel && cfg.gradients && cfg.gradients[1]) {
801
+ var g1desc = cfg.gradients[1].description || cfg.gradients[1].usage || '';
802
+ gradSubtleLabel.textContent = cfg.gradients[1].name + ' Gradient \u00B7 135\u00B0 \u00B7 ' + g1desc;
803
+ }
804
+
805
+ // Gradient usage do/don't
806
+ var gradUsage = document.getElementById('gradient-usage');
807
+ if (gradUsage && cfg.gradientUsage) {
808
+ gradUsage.innerHTML =
809
+ '<div class="gradient-usage-card do">' +
810
+ '<h4>Use gradient for</h4>' +
811
+ '<ul>' + cfg.gradientUsage.do.map(function (item) { return '<li>' + item + '</li>'; }).join('') + '</ul>' +
812
+ '</div>' +
813
+ '<div class="gradient-usage-card dont">' +
814
+ "<h4>Don't use gradient for</h4>" +
815
+ '<ul>' + cfg.gradientUsage.dont.map(function (item) { return '<li>' + item + '</li>'; }).join('') + '</ul>' +
816
+ '</div>';
817
+ }
818
+
819
+ // Sidebar brand name
820
+ var sidebarBrand = document.querySelector('.sidebar-brand');
821
+ if (sidebarBrand) sidebarBrand.textContent = 'brandkit';
822
+ }
823
+
824
+ /* ==============================================================
825
+ Execute all
826
+ ============================================================== */
827
+ bootstrap();
828
+ renderShell();
829
+ renderNav();
830
+ renderSectionIntros();
831
+ renderColors();
832
+ renderGradients();
833
+ renderLogos();
834
+ renderTypography();
835
+ renderHierarchy();
836
+ renderVoice();
837
+ renderComponents();
838
+ renderSpacing();
839
+ renderAccessibility();
840
+ renderCSSVars();
841
+
842
+ initCopy();
843
+ initFormatBar();
844
+ initNav();
845
+ initTypeTester();
846
+ }
847
+ })();