From 81dc2c211dba62284c4bc84009cebc6c84be9aa5 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 23 May 2026 20:39:35 +0000 Subject: [PATCH] deploy 1779568775 --- .gitignore | 1 + .woodpecker.yml | 46 +++++ LICENSE | 18 ++ README.md | 3 + app.js | 454 +++++++++++++++++++++++++++++++++++++++++++ index.html | 468 ++++++++++++++++++++++++++++++++++++++++++++ style.css | 506 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1496 insertions(+) create mode 100644 .gitignore create mode 100644 .woodpecker.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.js create mode 100644 index.html create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a694c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +LICENSE \ No newline at end of file diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..0369214 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,46 @@ +steps: + build: + image: alpine + + commands: + - apk add --no-cache rsync + - rm -rf site + - mkdir -p site + - rsync -a --exclude site --exclude .git ./ site/ + + deploy: + image: alpine/git + + secrets: + - source: pages_token + target: pages_token + + commands: + - echo "$pages_token" | wc -c + + - git config --global user.name admin + - git config --global user.email admin@backend-3.com + + - git clone https://admin:${pages_token}@git.backend-3.com/admin/font-design.git deploy + + - cd deploy + + - git checkout gh-pages || git checkout --orphan gh-pages + + - find . -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} + + + - cp -r ../site/. . + + - git add . + + - git commit -m "deploy $(date +%s)" || true + + - git push origin gh-pages --force + + depends_on: + - build + + when: + - event: push + - event: manual + - branch: main \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8080cbc --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 admin + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e739bf3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# font-design + +font library / design ui \ No newline at end of file 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(); diff --git a/index.html b/index.html new file mode 100644 index 0000000..5d992e1 --- /dev/null +++ b/index.html @@ -0,0 +1,468 @@ + + + + + + + + + +font prev + + + + + + + + +
+ +
+ +
+ +
+ +
+Hero +
+ +
+Satoshi +
+ +
+ +
+ +
+Display +
+ +
+Modern display typography. +
+ +
+ +
+ +
+Headline +
+ +
+Refined geometric details with clean spacing. +
+ +
+ +
+ +
+Body +
+ +
+Satoshi is a modernist sans serif. +
+ +
+ +
+ +
+Caption +
+ +
+Edit text inline and cycle fonts. +
+ +
+ +
+ +
+ +
+ +
+ +
+Initializing... +
+ +
+ + + + + +
+ + + +
+ +
+ +
+ +
+ + + +
+96px +
+ +
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +
+700 +
+ +
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +
+0px +
+ +
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +
+ + + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + + +Family + + + +Satoshi + + + + + + + + + +Category + + + +Sans + + + + + + + + + +Font Size + + + +96px + + + + + + + + + +Weight + + + +700 + + + + + + + + + +Spacing + + + +0px + + + + + + + + + +Guide + + + +Balanced + + + + +
+ +
+ + + + + + + + diff --git a/style.css b/style.css new file mode 100644 index 0000000..2cccd6c --- /dev/null +++ b/style.css @@ -0,0 +1,506 @@ +:root { + --bg: #f5f5f7; + --surface: rgba(255,255,255,0.06); + --surface-hover: rgba(255,255,255,0.12); + --border: rgba(255,255,255,0.08); + --text: #111111; + --muted: #6d6d73; + --active-font: system-ui,sans-serif; + --radius: 20px; + --radius-sm: 14px; + --control-height: 46px; + --panel-width: 380px; + --panel-padding: 16px; +} + +*, +*::before, +*::after { + box-sizing: border-box; + min-width: 0; +} + +html { + width: 100%; + min-height: 100%; + overflow-x: hidden; + text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; +} + +body { + margin: 0; + min-height: 100dvh; + overflow-x: hidden; + overscroll-behavior-y: none; + background: var(--bg); + color: var(--text); + font: + 500 14px/1.4 + system-ui, + sans-serif; + -webkit-font-smoothing: antialiased; + text-rendering: geometricPrecision; + padding: + env(safe-area-inset-top) + env(safe-area-inset-right) + calc(76px + env(safe-area-inset-bottom)) + env(safe-area-inset-left); +} + +button, +input, +textarea, +select { + font: inherit; +} + +button, +input[type="range"], +.radio, +.font-upload { + -webkit-tap-highlight-color: transparent; +} + +.row, +.row * { + font-family: var(--active-font) !important; +} + +.app { + width: 100%; + min-height: 100dvh; +} + +.preview-pane { + width: 100%; + min-height: calc(100dvh - 100px); + display: flex; + align-items: center; + justify-content: center; + padding-right: calc(var(--panel-width) + 40px); +} + +.controls-pane { + position: fixed; + top: 20px; + right: 20px; + width: min( + var(--panel-width), + calc(100vw - 32px) + ); + max-height: calc(100dvh - 40px); + z-index: 9998; + overflow: hidden auto; + border-radius: 28px; + border: 1px solid rgba(255,255,255,0.18); + background: + linear-gradient( + 180deg, + rgba(255,255,255,0.18), + rgba(255,255,255,0.08) + ); + backdrop-filter: blur(40px) saturate(180%); + -webkit-backdrop-filter: blur(40px) saturate(180%); + box-shadow: + 0 0px 0px rgba(0,0,0,0.16), + inset 0 0px 0 rgba(255,255,255,0.16); + scrollbar-width: none; + transition: + width 180ms ease, + max-height 180ms ease, + transform 180ms ease; +} + +.controls-pane::-webkit-scrollbar { + display: none; +} + +.controls-pane.minimized { + width: 210px; + max-height: 72px; +} + +.controls-pane.minimized .controls { + opacity: 0; + pointer-events: none; + transform: translateY(-6px); +} + +.controls-pane.minimized .top-actions { + display: none; +} + +.top { + display: flex; + align-items: center; + gap: 10px; + padding: var(--panel-padding); +} + +.top-actions { + flex: 1; + display: flex; + gap: 8px; +} + +.controls { + display: grid; + gap: 12px; + padding: + 0 + var(--panel-padding) + var(--panel-padding); + transition: + opacity 160ms ease, + transform 160ms ease; +} + +.control, +.row, +.radio, +button, +.font-upload, +.pill { + border: 1px solid var(--border); + background: var(--surface); +} + +.control, +.row { + border-radius: var(--radius); +} + +.control { + display: grid; + gap: 12px; + padding: 14px; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + +.row { + padding: 20px; +} + +.grid { + width: 100%; + display: grid; + gap: 12px; +} + +.top-actions button, +.top-actions .font-upload, +.controls-toggle { + height: var(--control-height); +} + +button, +.font-upload, +.controls-toggle, +.radio { + appearance: none; + border-radius: var(--radius-sm); + transition: + transform 140ms ease, + background 140ms ease; +} + +button, +.font-upload { + padding: 0 14px; + cursor: pointer; + font-size: 12px; + font-weight: 700; +} + +button:hover, +.font-upload:hover, +.radio:hover, +.controls-toggle:hover { + background: var(--surface-hover); +} + +button:active, +.font-upload:active, +.radio:active, +.controls-toggle:active { + transform: scale(0.985); +} + +.font-upload { + display: inline-flex; + align-items: center; + justify-content: center; + user-select: none; +} + +.font-upload input { + display: none; +} + +.controls-toggle { + width: 46px; + min-width: 46px; + display: grid; + place-items: center; + cursor: pointer; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + +.pill { + min-height: 42px; + padding: 0 14px; + border-radius: 999px; + display: inline-flex; + align-items: center; + color: var(--muted); + font-size: 12px; + font-weight: 700; + white-space: nowrap; +} + +.control-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.control label, +.label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); +} + +.label { + margin-bottom: 10px; +} + +.live-value { + font-size: 11px; + font-weight: 700; +} + +.slider-wrap { + position: relative; + display: flex; + align-items: center; +} + +.smart-guide { + position: absolute; + top: 50%; + left: 0; + height: 8px; + border-radius: 999px; + background: #111111; + opacity: 0.14; + transform: translateY(-50%); + pointer-events: none; +} + +input[type="range"] { + width: 100%; + appearance: none; + height: 8px; + border-radius: 999px; + background: rgba(0,0,0,0.08); +} + +input[type="range"]::-webkit-slider-thumb { + appearance: none; + width: 16px; + height: 16px; + border-radius: 999px; + border: none; + background: #111111; +} + +input[type="range"]::-moz-range-thumb { + width: 16px; + height: 16px; + border: none; + border-radius: 999px; + background: #111111; +} + +.radio-group { + display: grid; + gap: 8px; +} + +.radio { + min-height: 42px; + display: flex; + align-items: center; + gap: 10px; + padding: 0 12px; + cursor: pointer; +} + +.radio span { + flex: 1; + font-size: 12px; + font-weight: 600; +} + +.radio input { + appearance: none; + width: 14px; + height: 14px; + border-radius: 999px; + border: 1px solid rgba(0,0,0,0.2); +} + +.radio input:checked { + background: #111111; + border-color: #111111; +} + +.editable { + width: 100%; + outline: none; + background: transparent; + overflow-wrap: anywhere; + caret-color: #111111; +} + +.hero { + font-size: clamp(72px,12vw,180px); + line-height: 0.9; + letter-spacing: -0.08em; + font-weight: 700; +} + +.xl { + font-size: clamp(36px,5vw,68px); + line-height: 1; + letter-spacing: -0.05em; + font-weight: 600; +} + +.lg { + font-size: clamp(24px,3vw,42px); + line-height: 1.08; + letter-spacing: -0.035em; + font-weight: 600; +} + +.md { + font-size: clamp(16px,2vw,22px); + line-height: 1.5; + letter-spacing: -0.015em; + font-weight: 500; +} + +.sm { + font-size: clamp(13px,1.6vw,16px); + line-height: 1.7; + font-weight: 500; + color: var(--muted); +} + +.debug-banner { + position: fixed; + inset: + auto + 0 + 0; + z-index: 9999; + border-top: 1px solid rgba(0,0,0,0.06); + background: rgba(255,255,255,0.5); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); +} + +.debug-track { + min-height: 52px; + display: flex; + align-items: center; + gap: 14px; + overflow-x: auto; + scrollbar-width: none; + padding: + 0 + 14px + env(safe-area-inset-bottom); +} + +.debug-track::-webkit-scrollbar { + display: none; +} + +.debug-item { + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.debug-key, +.debug-value { + font: + 600 10px/1 + ui-monospace, + monospace; +} + +.debug-key { + opacity: 0.45; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.debug-divider { + width: 1px; + height: 10px; + background: rgba(0,0,0,0.08); +} + +@media (max-width: 1180px) { + .preview-pane { + padding-right: 0; + } + + .controls-pane { + top: auto; + right: 14px; + bottom: 14px; + left: 14px; + width: auto; + max-height: 72dvh; + } + + .controls-pane.minimized { + width: auto; + } +} + +@media (max-width: 640px) { + .top { + flex-wrap: wrap; + } + + .top-actions { + width: 100%; + display: grid; + grid-template-columns: repeat(2,minmax(0,1fr)); + } + + .row { + padding: 16px; + } + + .hero { + font-size: clamp(52px,14vw,110px); + } + + .xl { + font-size: clamp(28px,8vw,56px); + } +}