const button = document.getElementById('cycleButton'); const fontName = document.getElementById('fontName'); const fontSize = document.getElementById('fontSize'); const fontWeight = document.getElementById('fontWeight'); const letterSpacing = document.getElementById('letterSpacing'); const fontSizeValue = document.getElementById('fontSizeValue'); const fontWeightValue = document.getElementById('fontWeightValue'); const letterSpacingValue = document.getElementById('letterSpacingValue'); const sizeGuide = document.getElementById('sizeGuide'); const weightGuide = document.getElementById('weightGuide'); const spacingGuide = document.getElementById('spacingGuide'); const debugFontSize = document.getElementById('debugFontSize'); const debugWeight = document.getElementById('debugWeight'); const debugLetterSpacing = document.getElementById('debugLetterSpacing'); const debugGuide = document.getElementById('debugGuide'); const debugFamily = document.getElementById('debugFamily'); const debugCategory = document.getElementById('debugCategory'); const editableFields = document.querySelectorAll('.editable'); const transformInputs = document.querySelectorAll('input[name="transform"]'); const loadedFonts = new Map(); let current = 0; let activeEditable = document.querySelector('.editable'); const fonts = [ { name: 'Satoshi', category: 'Sans', slug: 'satoshi' }, { name: 'General Sans', category: 'Sans', slug: 'general-sans' }, { name: 'Switzer', category: 'Sans', slug: 'switzer' }, { name: 'Cabinet Grotesk', category: 'Sans', slug: 'cabinet-grotesk' }, { name: 'Ranade', category: 'Sans', slug: 'ranade' }, { name: 'Clash Display', category: 'Display', slug: 'clash-display' }, { name: 'Stardom', category: 'Display', slug: 'stardom' }, { name: 'Melodrama', category: 'Display', slug: 'melodrama' }, { name: 'Panchang', category: 'Display', slug: 'panchang' }, { name: 'Telma', category: 'Display', slug: 'telma' } ]; async function fetchAllFontshareFonts() { try { const response = await fetch('https://api.fontshare.com/v2/fonts'); if (!response.ok) { return; } const data = await response.json(); if (!Array.isArray(data)) { return; } data.forEach((font) => { if (!font?.slug || !font?.family) { return; } if ( fonts.some( (entry) => entry.slug === font.slug ) ) { return; } fonts.push({ name: font.family, category: font.styles?.[0]?.name || 'Sans', slug: font.slug }); }); } catch { return; } } function buildFontUrl(font) { return `https://api.fontshare.com/v2/css?f[]=${font.slug}@300,400,500,600,700,800&display=swap`; } function injectFont(font) { if (loadedFonts.has(font.slug)) { return loadedFonts.get(font.slug); } const promise = new Promise((resolve, reject) => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = buildFontUrl(font); link.onload = resolve; link.onerror = reject; document.head.appendChild(link); }); loadedFonts.set(font.slug, promise); return promise; } function updateLiveValues() { fontSizeValue.textContent = `${fontSize.value}px`; fontWeightValue.textContent = fontWeight.value; letterSpacingValue.textContent = `${letterSpacing.value}px`; debugFontSize.textContent = `${fontSize.value}px`; debugWeight.textContent = fontWeight.value; debugLetterSpacing.textContent = `${letterSpacing.value}px`; } function setGuide(node, min, max, rangeMin, rangeMax) { const left = ((min - rangeMin) / (rangeMax - rangeMin)) * 100; const width = ((max - min) / (rangeMax - rangeMin)) * 100; node.style.left = `${left}%`; node.style.width = `${width}%`; } function updateSmartGuides() { const text = activeEditable?.textContent?.trim() || ''; const size = Number(fontSize.value); const weight = Number(fontWeight.value); const spacing = Number(letterSpacing.value); const activeFont = fonts[current]; const [sizeMin, sizeMax] = SmartGuides.getOptimalSize(text.length); const [weightMin, weightMax] = SmartGuides.getOptimalWeight( activeFont.category, size ); const [spacingMin, spacingMax] = SmartGuides.getOptimalSpacing( size, weight ); setGuide(sizeGuide, sizeMin, sizeMax, 8, 320); setGuide(weightGuide, weightMin, weightMax, 100, 900); setGuide(spacingGuide, spacingMin, spacingMax, -12, 40); debugGuide.textContent = SmartGuides.getGuideLabel( size, weight, spacing ); } function getTransformValue() { return ( document.querySelector( 'input[name="transform"]:checked' )?.value || 'none' ); } function applyStyles() { const size = `${fontSize.value}px`; const weight = fontWeight.value; const spacing = `${letterSpacing.value}px`; const transform = getTransformValue(); editableFields.forEach((field) => { field.style.fontSize = size; field.style.fontWeight = weight; field.style.letterSpacing = spacing; field.style.textTransform = transform; }); updateLiveValues(); updateSmartGuides(); } async function applyFont(index) { const font = fonts[index]; if (!font) { return; } try { await injectFont(font); document.documentElement.style.setProperty( '--active-font', `"${font.name}",system-ui,sans-serif` ); fontName.textContent = `${font.name} · ${font.category}`; debugFamily.textContent = font.name; debugCategory.textContent = font.category; } catch { document.documentElement.style.setProperty( '--active-font', 'system-ui,sans-serif' ); fontName.textContent = `Fallback · System`; } updateSmartGuides(); } async function cycleFont() { current = (current + 1) % fonts.length; await applyFont(current); } function bindEditable(field) { field.addEventListener('focus', () => { activeEditable = field; updateSmartGuides(); }); field.addEventListener('input', () => { updateSmartGuides(); }); } function bindEvents() { button.addEventListener('click', cycleFont); fontSize.addEventListener('input', applyStyles); fontWeight.addEventListener('input', applyStyles); letterSpacing.addEventListener('input', applyStyles); transformInputs.forEach((input) => { input.addEventListener('change', applyStyles); }); editableFields.forEach(bindEditable); } async function init() { bindEvents(); updateLiveValues(); applyStyles(); await fetchAllFontshareFonts(); await applyFont(current); } // v26 const fontUpload = document.getElementById( 'fontUpload' ); const customFonts = new Map(); function sanitizeFontName( value ) { return value .replace(/\.[^/.]+$/, '') .replace(/[^a-z0-9]/gi, '-'); } async function loadLocalFont( file ) { if (!file) { return; } const extension = file.name .split('.') .pop() ?.toLowerCase(); const supported = ['ttf','otf','woff','woff2']; if ( !supported.includes( extension ) ) { return; } const family = sanitizeFontName( file.name ); const objectUrl = URL.createObjectURL( file ); try { const fontFace = new FontFace( family, `url(${objectUrl})` ); await fontFace.load(); document.fonts.add( fontFace ); customFonts.set( family, objectUrl ); document.documentElement.style.setProperty( '--active-font', `"${family}",system-ui,sans-serif` ); fontName.textContent = `${family} · Local`; debugFamily.textContent = family; debugCategory.textContent = 'Local'; requestAnimationFrame( () => { applyStyles(); updateSmartGuides(); } ); } catch { document.documentElement.style.setProperty( '--active-font', 'system-ui,sans-serif' ); fontName.textContent = 'Fallback · System'; } } fontUpload.addEventListener( 'change', async (event) => { const file = event.target.files?.[0]; await loadLocalFont( file ); event.target.value = ''; } ); const controlsPane = document.querySelector( '.controls-pane' ); const controlsToggle = document.getElementById( 'controlsToggle' ); let minimized = false; function syncControlsState() { controlsPane.classList.toggle( 'minimized', minimized ); controlsToggle.textContent = minimized ? '+' : '✕'; } controlsToggle.addEventListener( 'click', () => { minimized = !minimized; syncControlsState(); } ); syncControlsState(); init();