From 5c3f8dc7ca3865a73f6189040ab011f1ba73459e Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 23 May 2026 18:46:49 +0000 Subject: [PATCH] Add app.js --- app.js | 454 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 454 insertions(+) create mode 100644 app.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..9482790 --- /dev/null +++ b/app.js @@ -0,0 +1,454 @@ +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();