qr-codes / source /src.js
evalstate's picture
evalstate HF Staff
Add logo sizing and optional editable header to preview and PNG/SVG exports
faa32d1 verified
Raw
History Blame Contribute Delete
9.1 kB
import QRCode from 'qrcode';
import './style.css';
const initial = 'https://huggingface.co/datasets/evalstate/mcp-clients';
const themes = [
{id:'phosphor',name:'Green phosphor',code:'P1',accent:'#a7f878',ink:'#123822',paper:'#edfadf',description:'Late nights. Green screens. Pure signal.'},
{id:'amber',name:'Amber console',code:'P2',accent:'#ffbd69',ink:'#492b0d',paper:'#fff0d4',description:'A warm glow from a different era.'},
{id:'ice',name:'Arctic terminal',code:'P3',accent:'#91d9ff',ink:'#12304a',paper:'#e9f6ff',description:'Cold pixels. A very clear connection.'}
];
let current=themes[0], svg='', revision=0;
let logoData='';
const logo = new Image();
const response = await fetch('/hf-logo-pirate.png');
const blob = await response.blob();
logoData = await new Promise(resolve=>{const reader=new FileReader();reader.onload=()=>resolve(reader.result);reader.readAsDataURL(blob)});
logo.src=logoData; await logo.decode();
document.querySelector('#app').innerHTML=`
<header><a class="wordmark" href="./"><span class="mark">▥</span> SIGNAL<span class="slash">/</span><span class="muted">QR STUDIO</span></a><span class="online"><i></i> LOCAL ENCODING · NO TRACKING</span></header>
<main><div class="eyebrow">UTILITY_001 / LINKS → PIXELS</div><h1>Your link.<br>A little more <em>signal.</em></h1><p class="intro">QR codes with an old-school soul. Pick a frequency,<br class="desktop"> drop in a link, and send it out into the world.</p>
<div class="workspace"><section class="controls"><div class="section-heading"><span>01 / INPUT</span><span class="muted">HTTP(S)</span></div><label for="url">Where are we going?</label><div class="input-shell"><span>›</span><input id="url" type="url" spellcheck="false" aria-describedby="error" value="${initial}"></div><p id="error" role="status" class="hint">Static by design. Your URL, directly encoded.</p>
<div class="section-heading theme-heading">02 / FREQUENCY</div><div class="themes">${themes.map((t,i)=>`<button class="theme ${i===0?'selected':''}" data-theme="${t.id}" aria-pressed="${i===0}" style="--swatch:${t.accent}"><span class="swatch"></span><span>${t.name}<small>${t.code} / ${['CLASSIC','WARM','COOL'][i]}</small></span><span class="radio"></span></button>`).join('')}</div>
<div class="logo-row"><div><label for="logo">Pirate on board</label><p>Hugging Face logo, front and center.</p></div><input id="logo" type="checkbox" checked role="switch"></div>
<div class="size-control"><label for="logo-size">Logo size <output id="size-value">17%</output></label><input id="logo-size" type="range" min="12" max="26" value="17" step="1"><p class="hint">Larger logos cover more data. Test with your phone before printing.</p></div>
<div class="logo-row"><div><label for="include-header">Include header</label><p>Shown in the preview and both downloads.</p></div><input id="include-header" type="checkbox" checked role="switch"></div><label class="header-label" for="header-text">Header text</label><input id="header-text" type="text" maxlength="40" value="HF / TRANSMISSION"><div class="note"><span>↳</span><p>Built for the real world.<br><span class="muted">High error correction. A clean quiet zone.<br>No redirects, accounts, or expiration.</span></p></div></section>
<section class="preview-panel"><div class="preview-bar"><span><i></i> LIVE PREVIEW</span><span id="preview-code">P1 / PHOSPHOR</span></div><div class="preview-stage"><div class="print-card"><div class="card-top"><span id="header-preview">HF / TRANSMISSION</span></div><div id="qr" aria-label="Generated QR code"></div></div></div><div class="preview-caption"><span id="description">${current.description}</span><span>ECC / H</span></div></section></div>
<div class="export-row"><div><span class="ready" id="status">● SIGNAL READY</span><p>Take your pixels with you.</p></div><div class="actions"><button id="svg">↓ Download SVG</button><button class="primary" id="png">↓ Download PNG <span>↗</span></button></div></div>
<footer><span>MADE OF PIXELS. BUILT FOR PEOPLE.</span><span>STATIC QR / OPEN DESTINATIONS <span class="cursor">▮</span></span></footer></main>`;
const input=document.querySelector('#url');
function validURL(value){try {const u=new URL(value);return ['http:','https:'].includes(u.protocol)&&!!u.hostname;}catch{return false}}
function update(){
revision++;
const includeHeader=document.querySelector("#include-header").checked;
document.querySelector(".card-top").hidden=!includeHeader;
document.querySelector("#header-preview").textContent=document.querySelector("#header-text").value;
document.querySelector("#header-text").disabled=!includeHeader;
document.querySelector("#size-value").textContent=document.querySelector("#logo-size").value+"%";
document.querySelector("#logo-size").disabled=!document.querySelector("#logo").checked;
const value=input.value.trim();
const valid=validURL(value);
document.querySelector('#error').textContent=valid?'Static by design. Your URL, directly encoded.':'Enter a complete URL starting with https:// or http://.';
input.setAttribute('aria-invalid',String(!valid));
document.querySelectorAll('.actions button').forEach(b=>b.disabled=!valid);
if(!valid){svg='';document.querySelector('#qr').innerHTML='<p class="empty">AWAITING VALID URL</p>';document.querySelector('#status').textContent='○ AWAITING INPUT';return}
try {
const qr=QRCode.create(value,{errorCorrectionLevel:'H'});const n=qr.modules.size, size=n+8;
// A modest centered logo; all finder/timing patterns and the 4-module quiet zone remain intact.
const logoSize=Math.floor(n*Number(document.querySelector("#logo-size").value)/100),start=(size-logoSize)/2;
let path='';for(let y=0;y<n;y++)for(let x=0;x<n;x++)if(qr.modules.get(y,x))path+=`M${x+4} ${y+4}h1v1h-1z`;
const withLogo=document.querySelector('#logo').checked;
svg=`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" fill="${current.paper}"/><path d="${path}" fill="${current.ink}"/>${withLogo?`<rect x="${start-.6}" y="${start-.6}" width="${logoSize+1.2}" height="${logoSize+1.2}" rx=".7" fill="${current.paper}"/><image x="${start}" y="${start}" width="${logoSize}" height="${logoSize}" xlink:href="${logoData}"/>`:''}</svg>`;
document.querySelector('#qr').innerHTML=svg;document.querySelector('#status').textContent='● SIGNAL READY';
}catch{svg='';document.querySelector('#qr').innerHTML='<p class="empty">URL TOO LONG</p>';document.querySelector('#error').textContent='This URL exceeds QR capacity. Try a shorter URL.';document.querySelector('#status').textContent='○ INPUT TOO LONG';document.querySelectorAll('.actions button').forEach(b=>b.disabled=true)}
}
function save(blob,ext){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`signal-${current.id}.${ext}`;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
function exportSVG(){
if(!document.querySelector('#include-header').checked)return svg;
const text=document.querySelector('#header-text').value.replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&apos;'}[c]));
const nested=svg.replace('width="1024" height="1024"','x="0" y="112" width="1024" height="1024"');
return `<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1136" viewBox="0 0 1024 1136"><rect width="1024" height="1136" fill="${current.paper}"/><text x="64" y="68" font-family="monospace" font-size="28" fill="${current.ink}">${text}</text><path d="M64 104H960" stroke="${current.ink}" stroke-opacity=".3"/>${nested}</svg>`;
}
document.querySelector('#svg').onclick=()=>{if(svg)save(new Blob([exportSVG()],{type:'image/svg+xml'}),'svg')};
document.querySelector('#png').onclick=async()=>{if(!svg)return;const version=revision;const image=new Image();image.src=URL.createObjectURL(new Blob([exportSVG()],{type:'image/svg+xml'}));try{await image.decode();if(version!==revision)return;const canvas=document.createElement('canvas');canvas.width=1024;canvas.height=image.naturalHeight;canvas.getContext('2d').drawImage(image,0,0);canvas.toBlob(blob=>{if(blob)save(blob,'png')},'image/png')}finally{URL.revokeObjectURL(image.src)}};
for(const id of ['logo-size','include-header','header-text'])document.getElementById(id).addEventListener('input',update);
input.addEventListener('input',update);document.querySelector('#logo').onchange=update;
document.querySelectorAll('.theme').forEach(button=>button.onclick=()=>{current=themes.find(t=>t.id===button.dataset.theme);document.documentElement.style.setProperty('--accent',current.accent);document.documentElement.style.setProperty('--paper',current.paper);document.documentElement.style.setProperty('--ink',current.ink);document.querySelectorAll('.theme').forEach(b=>{b.classList.toggle('selected',b===button);b.setAttribute('aria-pressed',String(b===button))});document.querySelector('#preview-code').textContent=`${current.code} / ${current.id.toUpperCase()}`;document.querySelector('#description').textContent=current.description;update()});update();